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