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