Show status numbers
[bloat] / service / service.go
1 package service
2
3 import (
4         "context"
5         "errors"
6         "fmt"
7         "mime/multipart"
8         "net/url"
9         "strings"
10
11         "bloat/mastodon"
12         "bloat/model"
13         "bloat/renderer"
14         "bloat/util"
15 )
16
17 var (
18         ctx                = context.Background()
19         errInvalidArgument = errors.New("invalid argument")
20 )
21
22 type Service interface {
23         ServeErrorPage(c *model.Client, err error)
24         ServeSigninPage(c *model.Client) (err error)
25         ServeRootPage(c *model.Client) (err error)
26         ServeNavPage(c *model.Client) (err error)
27         ServeTimelinePage(c *model.Client, tType string, maxID string,
28                 minID string) (err error)
29         ServeThreadPage(c *model.Client, id string, reply bool) (err error)
30         ServeLikedByPage(c *model.Client, id string) (err error)
31         ServeRetweetedByPage(c *model.Client, id string) (err error)
32         ServeNotificationPage(c *model.Client, maxID string, minID string) (err error)
33         ServeUserPage(c *model.Client, id string, pageType string, maxID string,
34                 minID string) (err error)
35         ServeAboutPage(c *model.Client) (err error)
36         ServeEmojiPage(c *model.Client) (err error)
37         ServeSearchPage(c *model.Client, q string, qType string, offset int) (err error)
38         ServeUserSearchPage(c *model.Client, id string, q string, offset int) (err error)
39         ServeSettingsPage(c *model.Client) (err error)
40         SingleInstance() (instance string, ok bool)
41         NewSession(instance string) (redirectUrl string, sessionID string, err error)
42         Signin(c *model.Client, sessionID string, code string) (token string,
43                 userID string, err error)
44         Signout(c *model.Client) (err error)
45         Post(c *model.Client, content string, replyToID string, format string, visibility string,
46                 isNSFW bool, files []*multipart.FileHeader) (id string, err error)
47         Like(c *model.Client, id string) (count int64, err error)
48         UnLike(c *model.Client, id string) (count int64, err error)
49         Retweet(c *model.Client, id string) (count int64, err error)
50         UnRetweet(c *model.Client, id string) (count int64, err error)
51         Vote(c *model.Client, id string, choices []string) (err error)
52         Follow(c *model.Client, id string, reblogs *bool) (err error)
53         UnFollow(c *model.Client, id string) (err error)
54         Mute(c *model.Client, id string) (err error)
55         UnMute(c *model.Client, id string) (err error)
56         Block(c *model.Client, id string) (err error)
57         UnBlock(c *model.Client, id string) (err error)
58         Subscribe(c *model.Client, id string) (err error)
59         UnSubscribe(c *model.Client, id string) (err error)
60         SaveSettings(c *model.Client, settings *model.Settings) (err error)
61         MuteConversation(c *model.Client, id string) (err error)
62         UnMuteConversation(c *model.Client, id string) (err error)
63         Delete(c *model.Client, id string) (err error)
64         ReadNotifications(c *model.Client, maxID string) (err error)
65 }
66
67 type service struct {
68         clientName     string
69         clientScope    string
70         clientWebsite  string
71         customCSS      string
72         postFormats    []model.PostFormat
73         renderer       renderer.Renderer
74         sessionRepo    model.SessionRepo
75         appRepo        model.AppRepo
76         singleInstance string
77 }
78
79 func NewService(clientName string,
80         clientScope string,
81         clientWebsite string,
82         customCSS string,
83         postFormats []model.PostFormat,
84         renderer renderer.Renderer,
85         sessionRepo model.SessionRepo,
86         appRepo model.AppRepo,
87         singleInstance string,
88 ) Service {
89         return &service{
90                 clientName:     clientName,
91                 clientScope:    clientScope,
92                 clientWebsite:  clientWebsite,
93                 customCSS:      customCSS,
94                 postFormats:    postFormats,
95                 renderer:       renderer,
96                 sessionRepo:    sessionRepo,
97                 appRepo:        appRepo,
98                 singleInstance: singleInstance,
99         }
100 }
101
102 func getRendererContext(c *model.Client) *renderer.Context {
103         var settings model.Settings
104         var session model.Session
105         if c != nil {
106                 settings = c.Session.Settings
107                 session = c.Session
108         } else {
109                 settings = *model.NewSettings()
110         }
111         return &renderer.Context{
112                 HideAttachments: settings.HideAttachments,
113                 MaskNSFW:        settings.MaskNSFW,
114                 ThreadInNewTab:  settings.ThreadInNewTab,
115                 FluorideMode:    settings.FluorideMode,
116                 DarkMode:        settings.DarkMode,
117                 CSRFToken:       session.CSRFToken,
118                 UserID:          session.UserID,
119         }
120 }
121
122 func addToReplyMap(m map[string][]mastodon.ReplyInfo, key interface{},
123         val string, number int) {
124         if key == nil {
125                 return
126         }
127
128         keyStr, ok := key.(string)
129         if !ok {
130                 return
131         }
132
133         _, ok = m[keyStr]
134         if !ok {
135                 m[keyStr] = []mastodon.ReplyInfo{}
136         }
137
138         m[keyStr] = append(m[keyStr], mastodon.ReplyInfo{val, number})
139 }
140
141 func (svc *service) getCommonData(c *model.Client,
142         title string) (data *renderer.CommonData) {
143
144         data = &renderer.CommonData{
145                 Title:     title + " - " + svc.clientName,
146                 CustomCSS: svc.customCSS,
147         }
148         if c != nil && c.Session.IsLoggedIn() {
149                 data.CSRFToken = c.Session.CSRFToken
150         }
151         return
152 }
153
154 func (svc *service) ServeErrorPage(c *model.Client, err error) {
155         var errStr string
156         if err != nil {
157                 errStr = err.Error()
158         }
159
160         commonData := svc.getCommonData(nil, "error")
161         data := &renderer.ErrorData{
162                 CommonData: commonData,
163                 Error:      errStr,
164         }
165
166         rCtx := getRendererContext(c)
167         svc.renderer.Render(rCtx, c.Writer, renderer.ErrorPage, data)
168 }
169
170 func (svc *service) ServeSigninPage(c *model.Client) (err error) {
171         commonData := svc.getCommonData(nil, "signin")
172         data := &renderer.SigninData{
173                 CommonData: commonData,
174         }
175
176         rCtx := getRendererContext(nil)
177         return svc.renderer.Render(rCtx, c.Writer, renderer.SigninPage, data)
178 }
179
180 func (svc *service) ServeRootPage(c *model.Client) (err error) {
181         data := &renderer.RootData{
182                 Title: svc.clientName,
183         }
184
185         rCtx := getRendererContext(c)
186         return svc.renderer.Render(rCtx, c.Writer, renderer.RootPage, data)
187 }
188
189 func (svc *service) ServeNavPage(c *model.Client) (err error) {
190         u, err := c.GetAccountCurrentUser(ctx)
191         if err != nil {
192                 return
193         }
194
195         postContext := model.PostContext{
196                 DefaultVisibility: c.Session.Settings.DefaultVisibility,
197                 Formats:           svc.postFormats,
198         }
199
200         commonData := svc.getCommonData(c, "Nav")
201         commonData.Target = "main"
202         data := &renderer.NavData{
203                 User:        u,
204                 CommonData:  commonData,
205                 PostContext: postContext,
206         }
207
208         rCtx := getRendererContext(c)
209         return svc.renderer.Render(rCtx, c.Writer, renderer.NavPage, data)
210 }
211
212 func (svc *service) ServeTimelinePage(c *model.Client, tType string,
213         maxID string, minID string) (err error) {
214
215         var nextLink, prevLink, title string
216         var statuses []*mastodon.Status
217         var pg = mastodon.Pagination{
218                 MaxID: maxID,
219                 MinID: minID,
220                 Limit: 20,
221         }
222
223         switch tType {
224         default:
225                 return errInvalidArgument
226         case "home":
227                 statuses, err = c.GetTimelineHome(ctx, &pg)
228                 title = "Timeline"
229         case "direct":
230                 statuses, err = c.GetTimelineDirect(ctx, &pg)
231                 title = "Local Timeline"
232         case "local":
233                 statuses, err = c.GetTimelinePublic(ctx, true, &pg)
234                 title = "Local Timeline"
235         case "twkn":
236                 statuses, err = c.GetTimelinePublic(ctx, false, &pg)
237                 title = "The Whole Known Network"
238         }
239         if err != nil {
240                 return err
241         }
242
243         for i := range statuses {
244                 if statuses[i].Reblog != nil {
245                         statuses[i].Reblog.RetweetedByID = statuses[i].ID
246                 }
247         }
248
249         if len(maxID) > 0 && len(statuses) > 0 {
250                 prevLink = fmt.Sprintf("/timeline/%s?min_id=%s", tType,
251                         statuses[0].ID)
252         }
253
254         if len(minID) > 0 && len(pg.MinID) > 0 {
255                 newPg := &mastodon.Pagination{MinID: pg.MinID, Limit: 20}
256                 newStatuses, err := c.GetTimelineHome(ctx, newPg)
257                 if err != nil {
258                         return err
259                 }
260                 newLen := len(newStatuses)
261                 if newLen == 20 {
262                         prevLink = fmt.Sprintf("/timeline/%s?min_id=%s",
263                                 tType, pg.MinID)
264                 } else {
265                         i := 20 - newLen - 1
266                         if len(statuses) > i {
267                                 prevLink = fmt.Sprintf("/timeline/%s?min_id=%s",
268                                         tType, statuses[i].ID)
269                         }
270                 }
271         }
272
273         if len(pg.MaxID) > 0 {
274                 nextLink = fmt.Sprintf("/timeline/%s?max_id=%s", tType, pg.MaxID)
275         }
276
277         commonData := svc.getCommonData(c, tType+" timeline ")
278         data := &renderer.TimelineData{
279                 Title:      title,
280                 Statuses:   statuses,
281                 NextLink:   nextLink,
282                 PrevLink:   prevLink,
283                 CommonData: commonData,
284         }
285
286         rCtx := getRendererContext(c)
287         return svc.renderer.Render(rCtx, c.Writer, renderer.TimelinePage, data)
288 }
289
290 func (svc *service) ServeThreadPage(c *model.Client, id string, reply bool) (err error) {
291         var postContext model.PostContext
292
293         status, err := c.GetStatus(ctx, id)
294         if err != nil {
295                 return
296         }
297
298         u, err := c.GetAccountCurrentUser(ctx)
299         if err != nil {
300                 return
301         }
302
303         if reply {
304                 var content string
305                 var visibility string
306                 if u.ID != status.Account.ID {
307                         content += "@" + status.Account.Acct + " "
308                 }
309                 for i := range status.Mentions {
310                         if status.Mentions[i].ID != u.ID &&
311                                 status.Mentions[i].ID != status.Account.ID {
312                                 content += "@" + status.Mentions[i].Acct + " "
313                         }
314                 }
315
316                 if c.Session.Settings.CopyScope {
317                         s, err := c.GetStatus(ctx, id)
318                         if err != nil {
319                                 return err
320                         }
321                         visibility = s.Visibility
322                 } else {
323                         visibility = c.Session.Settings.DefaultVisibility
324                 }
325
326                 postContext = model.PostContext{
327                         DefaultVisibility: visibility,
328                         Formats:           svc.postFormats,
329                         ReplyContext: &model.ReplyContext{
330                                 InReplyToID:   id,
331                                 InReplyToName: status.Account.Acct,
332                                 ReplyContent:  content,
333                         },
334                         DarkMode: c.Session.Settings.DarkMode,
335                 }
336         }
337
338         context, err := c.GetStatusContext(ctx, id)
339         if err != nil {
340                 return
341         }
342
343         statuses := append(append(context.Ancestors, status), context.Descendants...)
344         replies := make(map[string][]mastodon.ReplyInfo)
345         idNumbers := make(map[string]int)
346
347         for i := range statuses {
348                 statuses[i].ShowReplies = true
349
350                 statuses[i].IDNumbers = idNumbers
351                 idNumbers[statuses[i].ID] = i + 1
352
353                 statuses[i].IDReplies = replies
354                 addToReplyMap(replies, statuses[i].InReplyToID, statuses[i].ID, i + 1)
355         }
356
357         commonData := svc.getCommonData(c, "post by "+status.Account.DisplayName)
358         data := &renderer.ThreadData{
359                 Statuses:    statuses,
360                 PostContext: postContext,
361                 ReplyMap:    replies,
362                 CommonData:  commonData,
363         }
364
365         rCtx := getRendererContext(c)
366         return svc.renderer.Render(rCtx, c.Writer, renderer.ThreadPage, data)
367 }
368
369 func (svc *service) ServeLikedByPage(c *model.Client, id string) (err error) {
370         likers, err := c.GetFavouritedBy(ctx, id, nil)
371         if err != nil {
372                 return
373         }
374
375         commonData := svc.getCommonData(c, "likes")
376         data := &renderer.LikedByData{
377                 CommonData: commonData,
378                 Users:      likers,
379         }
380
381         rCtx := getRendererContext(c)
382         return svc.renderer.Render(rCtx, c.Writer, renderer.LikedByPage, data)
383 }
384
385 func (svc *service) ServeRetweetedByPage(c *model.Client, id string) (err error) {
386         retweeters, err := c.GetRebloggedBy(ctx, id, nil)
387         if err != nil {
388                 return
389         }
390
391         commonData := svc.getCommonData(c, "retweets")
392         data := &renderer.RetweetedByData{
393                 CommonData: commonData,
394                 Users:      retweeters,
395         }
396
397         rCtx := getRendererContext(c)
398         return svc.renderer.Render(rCtx, c.Writer, renderer.RetweetedByPage, data)
399 }
400
401 func (svc *service) ServeNotificationPage(c *model.Client, maxID string,
402         minID string) (err error) {
403
404         var nextLink string
405         var unreadCount int
406         var readID string
407         var pg = mastodon.Pagination{
408                 MaxID: maxID,
409                 MinID: minID,
410                 Limit: 20,
411         }
412
413         notifications, err := c.GetNotifications(ctx, &pg)
414         if err != nil {
415                 return
416         }
417
418         for i := range notifications {
419                 if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen {
420                         unreadCount++
421                 }
422         }
423
424         if unreadCount > 0 {
425                 readID = notifications[0].ID
426         }
427
428         if len(notifications) == 20 && len(pg.MaxID) > 0 {
429                 nextLink = "/notifications?max_id=" + pg.MaxID
430         }
431
432         commonData := svc.getCommonData(c, "notifications")
433         commonData.AutoRefresh = c.Session.Settings.AutoRefreshNotifications
434         commonData.Target = "main"
435         commonData.Count = unreadCount
436         data := &renderer.NotificationData{
437                 Notifications: notifications,
438                 UnreadCount:   unreadCount,
439                 ReadID:        readID,
440                 NextLink:      nextLink,
441                 CommonData:    commonData,
442         }
443         rCtx := getRendererContext(c)
444         return svc.renderer.Render(rCtx, c.Writer, renderer.NotificationPage, data)
445 }
446
447 func (svc *service) ServeUserPage(c *model.Client, id string, pageType string,
448         maxID string, minID string) (err error) {
449
450         var nextLink string
451         var statuses []*mastodon.Status
452         var users []*mastodon.Account
453         var pg = mastodon.Pagination{
454                 MaxID: maxID,
455                 MinID: minID,
456                 Limit: 20,
457         }
458
459         user, err := c.GetAccount(ctx, id)
460         if err != nil {
461                 return
462         }
463
464         switch pageType {
465         case "":
466                 statuses, err = c.GetAccountStatuses(ctx, id, false, &pg)
467                 if err != nil {
468                         return
469                 }
470                 if len(statuses) == 20 && len(pg.MaxID) > 0 {
471                         nextLink = fmt.Sprintf("/user/%s?max_id=%s", id,
472                                 pg.MaxID)
473                 }
474         case "following":
475                 users, err = c.GetAccountFollowing(ctx, id, &pg)
476                 if err != nil {
477                         return
478                 }
479                 if len(users) == 20 && len(pg.MaxID) > 0 {
480                         nextLink = fmt.Sprintf("/user/%s/following?max_id=%s",
481                                 id, pg.MaxID)
482                 }
483         case "followers":
484                 users, err = c.GetAccountFollowers(ctx, id, &pg)
485                 if err != nil {
486                         return
487                 }
488                 if len(users) == 20 && len(pg.MaxID) > 0 {
489                         nextLink = fmt.Sprintf("/user/%s/followers?max_id=%s",
490                                 id, pg.MaxID)
491                 }
492         case "media":
493                 statuses, err = c.GetAccountStatuses(ctx, id, true, &pg)
494                 if err != nil {
495                         return
496                 }
497                 if len(statuses) == 20 && len(pg.MaxID) > 0 {
498                         nextLink = fmt.Sprintf("/user/%s/media?max_id=%s",
499                                 id, pg.MaxID)
500                 }
501         default:
502                 return errInvalidArgument
503         }
504
505         commonData := svc.getCommonData(c, user.DisplayName)
506         data := &renderer.UserData{
507                 User:       user,
508                 IsCurrent:  c.Session.UserID == user.ID,
509                 Type:       pageType,
510                 Users:      users,
511                 Statuses:   statuses,
512                 NextLink:   nextLink,
513                 CommonData: commonData,
514         }
515         rCtx := getRendererContext(c)
516         return svc.renderer.Render(rCtx, c.Writer, renderer.UserPage, data)
517 }
518
519 func (svc *service) ServeUserSearchPage(c *model.Client,
520         id string, q string, offset int) (err error) {
521
522         var nextLink string
523         var title = "search"
524
525         user, err := c.GetAccount(ctx, id)
526         if err != nil {
527                 return
528         }
529
530         results, err := c.Search(ctx, q, "statuses", 20, true, offset, id)
531         if err != nil {
532                 return
533         }
534
535         if len(results.Statuses) == 20 {
536                 offset += 20
537                 nextLink = fmt.Sprintf("/usersearch/%s?q=%s&offset=%d", id, q, offset)
538         }
539
540         if len(q) > 0 {
541                 title += " \"" + q + "\""
542         }
543
544         commonData := svc.getCommonData(c, title)
545         data := &renderer.UserSearchData{
546                 CommonData: commonData,
547                 User:       user,
548                 Q:          q,
549                 Statuses:   results.Statuses,
550                 NextLink:   nextLink,
551         }
552
553         rCtx := getRendererContext(c)
554         return svc.renderer.Render(rCtx, c.Writer, renderer.UserSearchPage, data)
555 }
556
557 func (svc *service) ServeAboutPage(c *model.Client) (err error) {
558         commonData := svc.getCommonData(c, "about")
559         data := &renderer.AboutData{
560                 CommonData: commonData,
561         }
562
563         rCtx := getRendererContext(c)
564         return svc.renderer.Render(rCtx, c.Writer, renderer.AboutPage, data)
565 }
566
567 func (svc *service) ServeEmojiPage(c *model.Client) (err error) {
568         emojis, err := c.GetInstanceEmojis(ctx)
569         if err != nil {
570                 return
571         }
572
573         commonData := svc.getCommonData(c, "emojis")
574         data := &renderer.EmojiData{
575                 Emojis:     emojis,
576                 CommonData: commonData,
577         }
578
579         rCtx := getRendererContext(c)
580         return svc.renderer.Render(rCtx, c.Writer, renderer.EmojiPage, data)
581 }
582
583 func (svc *service) ServeSearchPage(c *model.Client,
584         q string, qType string, offset int) (err error) {
585
586         var nextLink string
587         var title = "search"
588
589         results, err := c.Search(ctx, q, qType, 20, true, offset, "")
590         if err != nil {
591                 return
592         }
593
594         if (qType == "accounts" && len(results.Accounts) == 20) ||
595                 (qType == "statuses" && len(results.Statuses) == 20) {
596                 offset += 20
597                 nextLink = fmt.Sprintf("/search?q=%s&type=%s&offset=%d", q, qType, offset)
598         }
599
600         if len(q) > 0 {
601                 title += " \"" + q + "\""
602         }
603
604         commonData := svc.getCommonData(c, title)
605         data := &renderer.SearchData{
606                 CommonData: commonData,
607                 Q:          q,
608                 Type:       qType,
609                 Users:      results.Accounts,
610                 Statuses:   results.Statuses,
611                 NextLink:   nextLink,
612         }
613
614         rCtx := getRendererContext(c)
615         return svc.renderer.Render(rCtx, c.Writer, renderer.SearchPage, data)
616 }
617
618 func (svc *service) ServeSettingsPage(c *model.Client) (err error) {
619         commonData := svc.getCommonData(c, "settings")
620         data := &renderer.SettingsData{
621                 CommonData: commonData,
622                 Settings:   &c.Session.Settings,
623         }
624
625         rCtx := getRendererContext(c)
626         return svc.renderer.Render(rCtx, c.Writer, renderer.SettingsPage, data)
627 }
628
629 func (svc *service) SingleInstance() (instance string, ok bool) {
630         if len(svc.singleInstance) > 0 {
631                 instance = svc.singleInstance
632                 ok = true
633         }
634         return
635 }
636
637 func (svc *service) NewSession(instance string) (
638         redirectUrl string, sessionID string, err error) {
639
640         var instanceURL string
641         if strings.HasPrefix(instance, "https://") {
642                 instanceURL = instance
643                 instance = strings.TrimPrefix(instance, "https://")
644         } else {
645                 instanceURL = "https://" + instance
646         }
647
648         sessionID, err = util.NewSessionID()
649         if err != nil {
650                 return
651         }
652
653         csrfToken, err := util.NewCSRFToken()
654         if err != nil {
655                 return
656         }
657
658         session := model.Session{
659                 ID:             sessionID,
660                 InstanceDomain: instance,
661                 CSRFToken:      csrfToken,
662                 Settings:       *model.NewSettings(),
663         }
664
665         err = svc.sessionRepo.Add(session)
666         if err != nil {
667                 return
668         }
669
670         app, err := svc.appRepo.Get(instance)
671         if err != nil {
672                 if err != model.ErrAppNotFound {
673                         return
674                 }
675
676                 mastoApp, err := mastodon.RegisterApp(ctx, &mastodon.AppConfig{
677                         Server:       instanceURL,
678                         ClientName:   svc.clientName,
679                         Scopes:       svc.clientScope,
680                         Website:      svc.clientWebsite,
681                         RedirectURIs: svc.clientWebsite + "/oauth_callback",
682                 })
683                 if err != nil {
684                         return "", "", err
685                 }
686
687                 app = model.App{
688                         InstanceDomain: instance,
689                         InstanceURL:    instanceURL,
690                         ClientID:       mastoApp.ClientID,
691                         ClientSecret:   mastoApp.ClientSecret,
692                 }
693
694                 err = svc.appRepo.Add(app)
695                 if err != nil {
696                         return "", "", err
697                 }
698         }
699
700         u, err := url.Parse("/oauth/authorize")
701         if err != nil {
702                 return
703         }
704
705         q := make(url.Values)
706         q.Set("scope", "read write follow")
707         q.Set("client_id", app.ClientID)
708         q.Set("response_type", "code")
709         q.Set("redirect_uri", svc.clientWebsite+"/oauth_callback")
710         u.RawQuery = q.Encode()
711
712         redirectUrl = instanceURL + u.String()
713
714         return
715 }
716
717 func (svc *service) Signin(c *model.Client, sessionID string,
718         code string) (token string, userID string, err error) {
719
720         if len(code) < 1 {
721                 err = errInvalidArgument
722                 return
723         }
724
725         err = c.AuthenticateToken(ctx, code, svc.clientWebsite+"/oauth_callback")
726         if err != nil {
727                 return
728         }
729         token = c.GetAccessToken(ctx)
730
731         u, err := c.GetAccountCurrentUser(ctx)
732         if err != nil {
733                 return
734         }
735         userID = u.ID
736
737         return
738 }
739
740 func (svc *service) Signout(c *model.Client) (err error) {
741         svc.sessionRepo.Remove(c.Session.ID)
742         return
743 }
744
745 func (svc *service) Post(c *model.Client, content string,
746         replyToID string, format string, visibility string, isNSFW bool,
747         files []*multipart.FileHeader) (id string, err error) {
748
749         var mediaIDs []string
750         for _, f := range files {
751                 a, err := c.UploadMediaFromMultipartFileHeader(ctx, f)
752                 if err != nil {
753                         return "", err
754                 }
755                 mediaIDs = append(mediaIDs, a.ID)
756         }
757
758         tweet := &mastodon.Toot{
759                 Status:      content,
760                 InReplyToID: replyToID,
761                 MediaIDs:    mediaIDs,
762                 ContentType: format,
763                 Visibility:  visibility,
764                 Sensitive:   isNSFW,
765         }
766
767         s, err := c.PostStatus(ctx, tweet)
768         if err != nil {
769                 return
770         }
771
772         return s.ID, nil
773 }
774
775 func (svc *service) Like(c *model.Client, id string) (count int64, err error) {
776         s, err := c.Favourite(ctx, id)
777         if err != nil {
778                 return
779         }
780         count = s.FavouritesCount
781         return
782 }
783
784 func (svc *service) UnLike(c *model.Client, id string) (count int64, err error) {
785         s, err := c.Unfavourite(ctx, id)
786         if err != nil {
787                 return
788         }
789         count = s.FavouritesCount
790         return
791 }
792
793 func (svc *service) Retweet(c *model.Client, id string) (count int64, err error) {
794         s, err := c.Reblog(ctx, id)
795         if err != nil {
796                 return
797         }
798         if s.Reblog != nil {
799                 count = s.Reblog.ReblogsCount
800         }
801         return
802 }
803
804 func (svc *service) UnRetweet(c *model.Client, id string) (
805         count int64, err error) {
806         s, err := c.Unreblog(ctx, id)
807         if err != nil {
808                 return
809         }
810         count = s.ReblogsCount
811         return
812 }
813
814 func (svc *service) Vote(c *model.Client, id string, choices []string) (err error) {
815         _, err = c.Vote(ctx, id, choices)
816         if err != nil {
817                 return
818         }
819         return
820 }
821
822 func (svc *service) Follow(c *model.Client, id string, reblogs *bool) (err error) {
823         _, err = c.AccountFollow(ctx, id, reblogs)
824         return
825 }
826
827 func (svc *service) UnFollow(c *model.Client, id string) (err error) {
828         _, err = c.AccountUnfollow(ctx, id)
829         return
830 }
831
832 func (svc *service) Mute(c *model.Client, id string) (err error) {
833         _, err = c.AccountMute(ctx, id)
834         return
835 }
836
837 func (svc *service) UnMute(c *model.Client, id string) (err error) {
838         _, err = c.AccountUnmute(ctx, id)
839         return
840 }
841
842 func (svc *service) Block(c *model.Client, id string) (err error) {
843         _, err = c.AccountBlock(ctx, id)
844         return
845 }
846
847 func (svc *service) UnBlock(c *model.Client, id string) (err error) {
848         _, err = c.AccountUnblock(ctx, id)
849         return
850 }
851
852 func (svc *service) Subscribe(c *model.Client, id string) (err error) {
853         _, err = c.Subscribe(ctx, id)
854         return
855 }
856
857 func (svc *service) UnSubscribe(c *model.Client, id string) (err error) {
858         _, err = c.UnSubscribe(ctx, id)
859         return
860 }
861
862 func (svc *service) SaveSettings(c *model.Client, s *model.Settings) (err error) {
863         session, err := svc.sessionRepo.Get(c.Session.ID)
864         if err != nil {
865                 return
866         }
867
868         session.Settings = *s
869         return svc.sessionRepo.Add(session)
870 }
871
872 func (svc *service) MuteConversation(c *model.Client, id string) (err error) {
873         _, err = c.MuteConversation(ctx, id)
874         return
875 }
876
877 func (svc *service) UnMuteConversation(c *model.Client, id string) (err error) {
878         _, err = c.UnmuteConversation(ctx, id)
879         return
880 }
881
882 func (svc *service) Delete(c *model.Client, id string) (err error) {
883         return c.DeleteStatus(ctx, id)
884 }
885
886 func (svc *service) ReadNotifications(c *model.Client, maxID string) (err error) {
887         return c.ReadNotifications(ctx, maxID)
888 }