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