fluoride: Fix syntax and formatting
[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                 isDirect := status.Visibility == "direct" 
317                 if isDirect || c.Session.Settings.CopyScope {
318                         visibility = status.Visibility
319                 } else {
320                         visibility = c.Session.Settings.DefaultVisibility
321                 }
322
323                 postContext = model.PostContext{
324                         DefaultVisibility: visibility,
325                         Formats:           svc.postFormats,
326                         ReplyContext: &model.ReplyContext{
327                                 InReplyToID:     id,
328                                 InReplyToName:   status.Account.Acct,
329                                 ReplyContent:    content,
330                                 ForceVisibility: isDirect,
331                         },
332                         DarkMode: c.Session.Settings.DarkMode,
333                 }
334         }
335
336         context, err := c.GetStatusContext(ctx, id)
337         if err != nil {
338                 return
339         }
340
341         statuses := append(append(context.Ancestors, status), context.Descendants...)
342         replies := make(map[string][]mastodon.ReplyInfo)
343         idNumbers := make(map[string]int)
344
345         for i := range statuses {
346                 statuses[i].ShowReplies = true
347
348                 statuses[i].IDNumbers = idNumbers
349                 idNumbers[statuses[i].ID] = i + 1
350
351                 statuses[i].IDReplies = replies
352                 addToReplyMap(replies, statuses[i].InReplyToID, statuses[i].ID, i+1)
353         }
354
355         commonData := svc.getCommonData(c, "post by "+status.Account.DisplayName)
356         data := &renderer.ThreadData{
357                 Statuses:    statuses,
358                 PostContext: postContext,
359                 ReplyMap:    replies,
360                 CommonData:  commonData,
361         }
362
363         rCtx := getRendererContext(c)
364         return svc.renderer.Render(rCtx, c.Writer, renderer.ThreadPage, data)
365 }
366
367 func (svc *service) ServeLikedByPage(c *model.Client, id string) (err error) {
368         likers, err := c.GetFavouritedBy(ctx, id, nil)
369         if err != nil {
370                 return
371         }
372
373         commonData := svc.getCommonData(c, "likes")
374         data := &renderer.LikedByData{
375                 CommonData: commonData,
376                 Users:      likers,
377         }
378
379         rCtx := getRendererContext(c)
380         return svc.renderer.Render(rCtx, c.Writer, renderer.LikedByPage, data)
381 }
382
383 func (svc *service) ServeRetweetedByPage(c *model.Client, id string) (err error) {
384         retweeters, err := c.GetRebloggedBy(ctx, id, nil)
385         if err != nil {
386                 return
387         }
388
389         commonData := svc.getCommonData(c, "retweets")
390         data := &renderer.RetweetedByData{
391                 CommonData: commonData,
392                 Users:      retweeters,
393         }
394
395         rCtx := getRendererContext(c)
396         return svc.renderer.Render(rCtx, c.Writer, renderer.RetweetedByPage, data)
397 }
398
399 func (svc *service) ServeNotificationPage(c *model.Client, maxID string,
400         minID string) (err error) {
401
402         var nextLink string
403         var unreadCount int
404         var readID string
405         var pg = mastodon.Pagination{
406                 MaxID: maxID,
407                 MinID: minID,
408                 Limit: 20,
409         }
410
411         notifications, err := c.GetNotifications(ctx, &pg)
412         if err != nil {
413                 return
414         }
415
416         for i := range notifications {
417                 if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen {
418                         unreadCount++
419                 }
420         }
421
422         if unreadCount > 0 {
423                 readID = notifications[0].ID
424         }
425
426         if len(notifications) == 20 && len(pg.MaxID) > 0 {
427                 nextLink = "/notifications?max_id=" + pg.MaxID
428         }
429
430         commonData := svc.getCommonData(c, "notifications")
431         commonData.AutoRefresh = c.Session.Settings.AutoRefreshNotifications
432         commonData.Target = "main"
433         commonData.Count = unreadCount
434         data := &renderer.NotificationData{
435                 Notifications: notifications,
436                 UnreadCount:   unreadCount,
437                 ReadID:        readID,
438                 NextLink:      nextLink,
439                 CommonData:    commonData,
440         }
441         rCtx := getRendererContext(c)
442         return svc.renderer.Render(rCtx, c.Writer, renderer.NotificationPage, data)
443 }
444
445 func (svc *service) ServeUserPage(c *model.Client, id string, pageType string,
446         maxID string, minID string) (err error) {
447
448         var nextLink string
449         var statuses []*mastodon.Status
450         var users []*mastodon.Account
451         var pg = mastodon.Pagination{
452                 MaxID: maxID,
453                 MinID: minID,
454                 Limit: 20,
455         }
456
457         user, err := c.GetAccount(ctx, id)
458         if err != nil {
459                 return
460         }
461
462         switch pageType {
463         case "":
464                 statuses, err = c.GetAccountStatuses(ctx, id, false, &pg)
465                 if err != nil {
466                         return
467                 }
468                 if len(statuses) == 20 && len(pg.MaxID) > 0 {
469                         nextLink = fmt.Sprintf("/user/%s?max_id=%s", id,
470                                 pg.MaxID)
471                 }
472         case "following":
473                 users, err = c.GetAccountFollowing(ctx, id, &pg)
474                 if err != nil {
475                         return
476                 }
477                 if len(users) == 20 && len(pg.MaxID) > 0 {
478                         nextLink = fmt.Sprintf("/user/%s/following?max_id=%s",
479                                 id, pg.MaxID)
480                 }
481         case "followers":
482                 users, err = c.GetAccountFollowers(ctx, id, &pg)
483                 if err != nil {
484                         return
485                 }
486                 if len(users) == 20 && len(pg.MaxID) > 0 {
487                         nextLink = fmt.Sprintf("/user/%s/followers?max_id=%s",
488                                 id, pg.MaxID)
489                 }
490         case "media":
491                 statuses, err = c.GetAccountStatuses(ctx, id, true, &pg)
492                 if err != nil {
493                         return
494                 }
495                 if len(statuses) == 20 && len(pg.MaxID) > 0 {
496                         nextLink = fmt.Sprintf("/user/%s/media?max_id=%s",
497                                 id, pg.MaxID)
498                 }
499         default:
500                 return errInvalidArgument
501         }
502
503         commonData := svc.getCommonData(c, user.DisplayName)
504         data := &renderer.UserData{
505                 User:       user,
506                 IsCurrent:  c.Session.UserID == user.ID,
507                 Type:       pageType,
508                 Users:      users,
509                 Statuses:   statuses,
510                 NextLink:   nextLink,
511                 CommonData: commonData,
512         }
513         rCtx := getRendererContext(c)
514         return svc.renderer.Render(rCtx, c.Writer, renderer.UserPage, data)
515 }
516
517 func (svc *service) ServeUserSearchPage(c *model.Client,
518         id string, q string, offset int) (err error) {
519
520         var nextLink string
521         var title = "search"
522
523         user, err := c.GetAccount(ctx, id)
524         if err != nil {
525                 return
526         }
527
528         results, err := c.Search(ctx, q, "statuses", 20, true, offset, id)
529         if err != nil {
530                 return
531         }
532
533         if len(results.Statuses) == 20 {
534                 offset += 20
535                 nextLink = fmt.Sprintf("/usersearch/%s?q=%s&offset=%d", id, q, offset)
536         }
537
538         if len(q) > 0 {
539                 title += " \"" + q + "\""
540         }
541
542         commonData := svc.getCommonData(c, title)
543         data := &renderer.UserSearchData{
544                 CommonData: commonData,
545                 User:       user,
546                 Q:          q,
547                 Statuses:   results.Statuses,
548                 NextLink:   nextLink,
549         }
550
551         rCtx := getRendererContext(c)
552         return svc.renderer.Render(rCtx, c.Writer, renderer.UserSearchPage, data)
553 }
554
555 func (svc *service) ServeAboutPage(c *model.Client) (err error) {
556         commonData := svc.getCommonData(c, "about")
557         data := &renderer.AboutData{
558                 CommonData: commonData,
559         }
560
561         rCtx := getRendererContext(c)
562         return svc.renderer.Render(rCtx, c.Writer, renderer.AboutPage, data)
563 }
564
565 func (svc *service) ServeEmojiPage(c *model.Client) (err error) {
566         emojis, err := c.GetInstanceEmojis(ctx)
567         if err != nil {
568                 return
569         }
570
571         commonData := svc.getCommonData(c, "emojis")
572         data := &renderer.EmojiData{
573                 Emojis:     emojis,
574                 CommonData: commonData,
575         }
576
577         rCtx := getRendererContext(c)
578         return svc.renderer.Render(rCtx, c.Writer, renderer.EmojiPage, data)
579 }
580
581 func (svc *service) ServeSearchPage(c *model.Client,
582         q string, qType string, offset int) (err error) {
583
584         var nextLink string
585         var title = "search"
586
587         results, err := c.Search(ctx, q, qType, 20, true, offset, "")
588         if err != nil {
589                 return
590         }
591
592         if (qType == "accounts" && len(results.Accounts) == 20) ||
593                 (qType == "statuses" && len(results.Statuses) == 20) {
594                 offset += 20
595                 nextLink = fmt.Sprintf("/search?q=%s&type=%s&offset=%d", q, qType, offset)
596         }
597
598         if len(q) > 0 {
599                 title += " \"" + q + "\""
600         }
601
602         commonData := svc.getCommonData(c, title)
603         data := &renderer.SearchData{
604                 CommonData: commonData,
605                 Q:          q,
606                 Type:       qType,
607                 Users:      results.Accounts,
608                 Statuses:   results.Statuses,
609                 NextLink:   nextLink,
610         }
611
612         rCtx := getRendererContext(c)
613         return svc.renderer.Render(rCtx, c.Writer, renderer.SearchPage, data)
614 }
615
616 func (svc *service) ServeSettingsPage(c *model.Client) (err error) {
617         commonData := svc.getCommonData(c, "settings")
618         data := &renderer.SettingsData{
619                 CommonData: commonData,
620                 Settings:   &c.Session.Settings,
621         }
622
623         rCtx := getRendererContext(c)
624         return svc.renderer.Render(rCtx, c.Writer, renderer.SettingsPage, data)
625 }
626
627 func (svc *service) SingleInstance() (instance string, ok bool) {
628         if len(svc.singleInstance) > 0 {
629                 instance = svc.singleInstance
630                 ok = true
631         }
632         return
633 }
634
635 func (svc *service) NewSession(instance string) (
636         redirectUrl string, sessionID string, err error) {
637
638         var instanceURL string
639         if strings.HasPrefix(instance, "https://") {
640                 instanceURL = instance
641                 instance = strings.TrimPrefix(instance, "https://")
642         } else {
643                 instanceURL = "https://" + instance
644         }
645
646         sessionID, err = util.NewSessionID()
647         if err != nil {
648                 return
649         }
650
651         csrfToken, err := util.NewCSRFToken()
652         if err != nil {
653                 return
654         }
655
656         session := model.Session{
657                 ID:             sessionID,
658                 InstanceDomain: instance,
659                 CSRFToken:      csrfToken,
660                 Settings:       *model.NewSettings(),
661         }
662
663         err = svc.sessionRepo.Add(session)
664         if err != nil {
665                 return
666         }
667
668         app, err := svc.appRepo.Get(instance)
669         if err != nil {
670                 if err != model.ErrAppNotFound {
671                         return
672                 }
673
674                 mastoApp, err := mastodon.RegisterApp(ctx, &mastodon.AppConfig{
675                         Server:       instanceURL,
676                         ClientName:   svc.clientName,
677                         Scopes:       svc.clientScope,
678                         Website:      svc.clientWebsite,
679                         RedirectURIs: svc.clientWebsite + "/oauth_callback",
680                 })
681                 if err != nil {
682                         return "", "", err
683                 }
684
685                 app = model.App{
686                         InstanceDomain: instance,
687                         InstanceURL:    instanceURL,
688                         ClientID:       mastoApp.ClientID,
689                         ClientSecret:   mastoApp.ClientSecret,
690                 }
691
692                 err = svc.appRepo.Add(app)
693                 if err != nil {
694                         return "", "", err
695                 }
696         }
697
698         u, err := url.Parse("/oauth/authorize")
699         if err != nil {
700                 return
701         }
702
703         q := make(url.Values)
704         q.Set("scope", "read write follow")
705         q.Set("client_id", app.ClientID)
706         q.Set("response_type", "code")
707         q.Set("redirect_uri", svc.clientWebsite+"/oauth_callback")
708         u.RawQuery = q.Encode()
709
710         redirectUrl = instanceURL + u.String()
711
712         return
713 }
714
715 func (svc *service) Signin(c *model.Client, sessionID string,
716         code string) (token string, userID string, err error) {
717
718         if len(code) < 1 {
719                 err = errInvalidArgument
720                 return
721         }
722
723         err = c.AuthenticateToken(ctx, code, svc.clientWebsite+"/oauth_callback")
724         if err != nil {
725                 return
726         }
727         token = c.GetAccessToken(ctx)
728
729         u, err := c.GetAccountCurrentUser(ctx)
730         if err != nil {
731                 return
732         }
733         userID = u.ID
734
735         return
736 }
737
738 func (svc *service) Signout(c *model.Client) (err error) {
739         svc.sessionRepo.Remove(c.Session.ID)
740         return
741 }
742
743 func (svc *service) Post(c *model.Client, content string,
744         replyToID string, format string, visibility string, isNSFW bool,
745         files []*multipart.FileHeader) (id string, err error) {
746
747         var mediaIDs []string
748         for _, f := range files {
749                 a, err := c.UploadMediaFromMultipartFileHeader(ctx, f)
750                 if err != nil {
751                         return "", err
752                 }
753                 mediaIDs = append(mediaIDs, a.ID)
754         }
755
756         tweet := &mastodon.Toot{
757                 Status:      content,
758                 InReplyToID: replyToID,
759                 MediaIDs:    mediaIDs,
760                 ContentType: format,
761                 Visibility:  visibility,
762                 Sensitive:   isNSFW,
763         }
764
765         s, err := c.PostStatus(ctx, tweet)
766         if err != nil {
767                 return
768         }
769
770         return s.ID, nil
771 }
772
773 func (svc *service) Like(c *model.Client, id string) (count int64, err error) {
774         s, err := c.Favourite(ctx, id)
775         if err != nil {
776                 return
777         }
778         count = s.FavouritesCount
779         return
780 }
781
782 func (svc *service) UnLike(c *model.Client, id string) (count int64, err error) {
783         s, err := c.Unfavourite(ctx, id)
784         if err != nil {
785                 return
786         }
787         count = s.FavouritesCount
788         return
789 }
790
791 func (svc *service) Retweet(c *model.Client, id string) (count int64, err error) {
792         s, err := c.Reblog(ctx, id)
793         if err != nil {
794                 return
795         }
796         if s.Reblog != nil {
797                 count = s.Reblog.ReblogsCount
798         }
799         return
800 }
801
802 func (svc *service) UnRetweet(c *model.Client, id string) (
803         count int64, err error) {
804         s, err := c.Unreblog(ctx, id)
805         if err != nil {
806                 return
807         }
808         count = s.ReblogsCount
809         return
810 }
811
812 func (svc *service) Vote(c *model.Client, id string, choices []string) (err error) {
813         _, err = c.Vote(ctx, id, choices)
814         if err != nil {
815                 return
816         }
817         return
818 }
819
820 func (svc *service) Follow(c *model.Client, id string, reblogs *bool) (err error) {
821         _, err = c.AccountFollow(ctx, id, reblogs)
822         return
823 }
824
825 func (svc *service) UnFollow(c *model.Client, id string) (err error) {
826         _, err = c.AccountUnfollow(ctx, id)
827         return
828 }
829
830 func (svc *service) Mute(c *model.Client, id string) (err error) {
831         _, err = c.AccountMute(ctx, id)
832         return
833 }
834
835 func (svc *service) UnMute(c *model.Client, id string) (err error) {
836         _, err = c.AccountUnmute(ctx, id)
837         return
838 }
839
840 func (svc *service) Block(c *model.Client, id string) (err error) {
841         _, err = c.AccountBlock(ctx, id)
842         return
843 }
844
845 func (svc *service) UnBlock(c *model.Client, id string) (err error) {
846         _, err = c.AccountUnblock(ctx, id)
847         return
848 }
849
850 func (svc *service) Subscribe(c *model.Client, id string) (err error) {
851         _, err = c.Subscribe(ctx, id)
852         return
853 }
854
855 func (svc *service) UnSubscribe(c *model.Client, id string) (err error) {
856         _, err = c.UnSubscribe(ctx, id)
857         return
858 }
859
860 func (svc *service) SaveSettings(c *model.Client, s *model.Settings) (err error) {
861         session, err := svc.sessionRepo.Get(c.Session.ID)
862         if err != nil {
863                 return
864         }
865
866         session.Settings = *s
867         return svc.sessionRepo.Add(session)
868 }
869
870 func (svc *service) MuteConversation(c *model.Client, id string) (err error) {
871         _, err = c.MuteConversation(ctx, id)
872         return
873 }
874
875 func (svc *service) UnMuteConversation(c *model.Client, id string) (err error) {
876         _, err = c.UnmuteConversation(ctx, id)
877         return
878 }
879
880 func (svc *service) Delete(c *model.Client, id string) (err error) {
881         return c.DeleteStatus(ctx, id)
882 }
883
884 func (svc *service) ReadNotifications(c *model.Client, maxID string) (err error) {
885         return c.ReadNotifications(ctx, maxID)
886 }