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