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