0ef8f094f89c269c5f24cc03fab41065c1152a2a
[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/model"
12         "bloat/renderer"
13         "bloat/util"
14         "mastodon"
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         ServeFollowingPage(ctx context.Context, c *model.Client, id string, maxID string, minID string) (err error)
29         ServeFollowersPage(ctx context.Context, c *model.Client, id string, maxID string, minID 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, maxID string, minID string) (err error)
32         ServeAboutPage(ctx context.Context, c *model.Client) (err error)
33         ServeEmojiPage(ctx context.Context, c *model.Client) (err error)
34         ServeSearchPage(ctx context.Context, c *model.Client, q string, qType 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) ServeFollowingPage(ctx context.Context, c *model.Client,
410         id string, maxID string, minID string) (err error) {
411
412         var nextLink string
413         var pg = mastodon.Pagination{
414                 MaxID: maxID,
415                 MinID: minID,
416                 Limit: 20,
417         }
418
419         followings, err := c.GetAccountFollowing(ctx, id, &pg)
420         if err != nil {
421                 return
422         }
423
424         if len(followings) == 20 && len(pg.MaxID) > 0 {
425                 nextLink = "/following/" + id + "?max_id=" + pg.MaxID
426         }
427
428         commonData, err := svc.getCommonData(ctx, c, "following")
429         if err != nil {
430                 return
431         }
432
433         data := &renderer.FollowingData{
434                 CommonData: commonData,
435                 Users:      followings,
436                 NextLink:   nextLink,
437         }
438
439         rCtx := getRendererContext(c)
440         return svc.renderer.RenderFollowingPage(rCtx, c.Writer, data)
441 }
442
443 func (svc *service) ServeFollowersPage(ctx context.Context, c *model.Client,
444         id string, maxID string, minID string) (err error) {
445
446         var nextLink string
447         var pg = mastodon.Pagination{
448                 MaxID: maxID,
449                 MinID: minID,
450                 Limit: 20,
451         }
452
453         followers, err := c.GetAccountFollowers(ctx, id, &pg)
454         if err != nil {
455                 return
456         }
457
458         if len(followers) == 20 && len(pg.MaxID) > 0 {
459                 nextLink = "/followers/" + id + "?max_id=" + pg.MaxID
460         }
461
462         commonData, err := svc.getCommonData(ctx, c, "followers")
463         if err != nil {
464                 return
465         }
466
467         data := &renderer.FollowersData{
468                 CommonData: commonData,
469                 Users:      followers,
470                 NextLink:   nextLink,
471         }
472         rCtx := getRendererContext(c)
473         return svc.renderer.RenderFollowersPage(rCtx, c.Writer, data)
474 }
475
476 func (svc *service) ServeNotificationPage(ctx context.Context, c *model.Client,
477         maxID string, minID string) (err error) {
478
479         var nextLink string
480         var unreadCount int
481         var pg = mastodon.Pagination{
482                 MaxID: maxID,
483                 MinID: minID,
484                 Limit: 20,
485         }
486
487         notifications, err := c.GetNotifications(ctx, &pg)
488         if err != nil {
489                 return
490         }
491
492         for i := range notifications {
493                 if notifications[i].Status != nil {
494                         notifications[i].Status.CreatedAt = notifications[i].CreatedAt
495                         switch notifications[i].Type {
496                         case "reblog", "favourite":
497                                 notifications[i].Status.HideAccountInfo = true
498                         }
499                 }
500                 if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen {
501                         unreadCount++
502                 }
503         }
504
505         if unreadCount > 0 {
506                 err := c.ReadNotifications(ctx, notifications[0].ID)
507                 if err != nil {
508                         return err
509                 }
510         }
511
512         if len(pg.MaxID) > 0 {
513                 nextLink = "/notifications?max_id=" + pg.MaxID
514         }
515
516         commonData, err := svc.getCommonData(ctx, c, "notifications")
517         if err != nil {
518                 return
519         }
520
521         data := &renderer.NotificationData{
522                 Notifications: notifications,
523                 NextLink:      nextLink,
524                 CommonData:    commonData,
525         }
526         rCtx := getRendererContext(c)
527         return svc.renderer.RenderNotificationPage(rCtx, c.Writer, data)
528 }
529
530 func (svc *service) ServeUserPage(ctx context.Context, c *model.Client,
531         id string, maxID string, minID string) (err error) {
532
533         var nextLink string
534
535         var pg = mastodon.Pagination{
536                 MaxID: maxID,
537                 MinID: minID,
538                 Limit: 20,
539         }
540
541         user, err := c.GetAccount(ctx, id)
542         if err != nil {
543                 return
544         }
545
546         statuses, err := c.GetAccountStatuses(ctx, id, &pg)
547         if err != nil {
548                 return
549         }
550
551         if len(pg.MaxID) > 0 {
552                 nextLink = "/user/" + id + "?max_id=" + pg.MaxID
553         }
554
555         commonData, err := svc.getCommonData(ctx, c, user.DisplayName)
556         if err != nil {
557                 return
558         }
559
560         data := &renderer.UserData{
561                 User:       user,
562                 Statuses:   statuses,
563                 NextLink:   nextLink,
564                 CommonData: commonData,
565         }
566         rCtx := getRendererContext(c)
567         return svc.renderer.RenderUserPage(rCtx, c.Writer, data)
568 }
569
570 func (svc *service) ServeAboutPage(ctx context.Context, c *model.Client) (err error) {
571         commonData, err := svc.getCommonData(ctx, c, "about")
572         if err != nil {
573                 return
574         }
575
576         data := &renderer.AboutData{
577                 CommonData: commonData,
578         }
579
580         rCtx := getRendererContext(c)
581         return svc.renderer.RenderAboutPage(rCtx, c.Writer, data)
582 }
583
584 func (svc *service) ServeEmojiPage(ctx context.Context, c *model.Client) (err error) {
585         commonData, err := svc.getCommonData(ctx, c, "emojis")
586         if err != nil {
587                 return
588         }
589
590         emojis, err := c.GetInstanceEmojis(ctx)
591         if err != nil {
592                 return
593         }
594
595         data := &renderer.EmojiData{
596                 Emojis:     emojis,
597                 CommonData: commonData,
598         }
599
600         rCtx := getRendererContext(c)
601         return svc.renderer.RenderEmojiPage(rCtx, c.Writer, data)
602 }
603
604 func (svc *service) ServeSearchPage(ctx context.Context, c *model.Client,
605         q string, qType string, offset int) (err error) {
606
607         var nextLink string
608         var title = "search"
609
610         results, err := c.Search(ctx, q, qType, 20, true, offset)
611         if err != nil {
612                 return
613         }
614
615         if (qType == "accounts" && len(results.Accounts) == 20) ||
616                 (qType == "statuses" && len(results.Statuses) == 20) {
617                 offset += 20
618                 nextLink = fmt.Sprintf("/search?q=%s&type=%s&offset=%d", q, qType, offset)
619         }
620
621         if len(q) > 0 {
622                 title += " \"" + q + "\""
623         }
624
625         commonData, err := svc.getCommonData(ctx, c, title)
626         if err != nil {
627                 return
628         }
629
630         data := &renderer.SearchData{
631                 CommonData: commonData,
632                 Q:          q,
633                 Type:       qType,
634                 Users:      results.Accounts,
635                 Statuses:   results.Statuses,
636                 NextLink:   nextLink,
637         }
638
639         rCtx := getRendererContext(c)
640         return svc.renderer.RenderSearchPage(rCtx, c.Writer, data)
641 }
642
643 func (svc *service) ServeSettingsPage(ctx context.Context, c *model.Client) (err error) {
644         commonData, err := svc.getCommonData(ctx, c, "settings")
645         if err != nil {
646                 return
647         }
648
649         data := &renderer.SettingsData{
650                 CommonData: commonData,
651                 Settings:   &c.Session.Settings,
652         }
653
654         rCtx := getRendererContext(c)
655         return svc.renderer.RenderSettingsPage(rCtx, c.Writer, data)
656 }
657
658 func (svc *service) NewSession(ctx context.Context, instance string) (
659         redirectUrl string, sessionID string, err error) {
660
661         var instanceURL string
662         if strings.HasPrefix(instance, "https://") {
663                 instanceURL = instance
664                 instance = strings.TrimPrefix(instance, "https://")
665         } else {
666                 instanceURL = "https://" + instance
667         }
668
669         sessionID, err = util.NewSessionID()
670         if err != nil {
671                 return
672         }
673
674         csrfToken, err := util.NewCSRFToken()
675         if err != nil {
676                 return
677         }
678
679         session := model.Session{
680                 ID:             sessionID,
681                 InstanceDomain: instance,
682                 CSRFToken:      csrfToken,
683                 Settings:       *model.NewSettings(),
684         }
685
686         err = svc.sessionRepo.Add(session)
687         if err != nil {
688                 return
689         }
690
691         app, err := svc.appRepo.Get(instance)
692         if err != nil {
693                 if err != model.ErrAppNotFound {
694                         return
695                 }
696
697                 mastoApp, err := mastodon.RegisterApp(ctx, &mastodon.AppConfig{
698                         Server:       instanceURL,
699                         ClientName:   svc.clientName,
700                         Scopes:       svc.clientScope,
701                         Website:      svc.clientWebsite,
702                         RedirectURIs: svc.clientWebsite + "/oauth_callback",
703                 })
704                 if err != nil {
705                         return "", "", err
706                 }
707
708                 app = model.App{
709                         InstanceDomain: instance,
710                         InstanceURL:    instanceURL,
711                         ClientID:       mastoApp.ClientID,
712                         ClientSecret:   mastoApp.ClientSecret,
713                 }
714
715                 err = svc.appRepo.Add(app)
716                 if err != nil {
717                         return "", "", err
718                 }
719         }
720
721         u, err := url.Parse("/oauth/authorize")
722         if err != nil {
723                 return
724         }
725
726         q := make(url.Values)
727         q.Set("scope", "read write follow")
728         q.Set("client_id", app.ClientID)
729         q.Set("response_type", "code")
730         q.Set("redirect_uri", svc.clientWebsite+"/oauth_callback")
731         u.RawQuery = q.Encode()
732
733         redirectUrl = instanceURL + u.String()
734
735         return
736 }
737
738 func (svc *service) Signin(ctx context.Context, c *model.Client,
739         sessionID string, code string) (token string, err error) {
740
741         if len(code) < 1 {
742                 err = errInvalidArgument
743                 return
744         }
745
746         err = c.AuthenticateToken(ctx, code, svc.clientWebsite+"/oauth_callback")
747         if err != nil {
748                 return
749         }
750         token = c.GetAccessToken(ctx)
751
752         return
753 }
754
755 func (svc *service) Post(ctx context.Context, c *model.Client, content string,
756         replyToID string, format string, visibility string, isNSFW bool,
757         files []*multipart.FileHeader) (id string, err error) {
758
759         var mediaIDs []string
760         for _, f := range files {
761                 a, err := c.UploadMediaFromMultipartFileHeader(ctx, f)
762                 if err != nil {
763                         return "", err
764                 }
765                 mediaIDs = append(mediaIDs, a.ID)
766         }
767
768         tweet := &mastodon.Toot{
769                 Status:      content,
770                 InReplyToID: replyToID,
771                 MediaIDs:    mediaIDs,
772                 ContentType: format,
773                 Visibility:  visibility,
774                 Sensitive:   isNSFW,
775         }
776
777         s, err := c.PostStatus(ctx, tweet)
778         if err != nil {
779                 return
780         }
781
782         return s.ID, nil
783 }
784
785 func (svc *service) Like(ctx context.Context, c *model.Client, id string) (
786         count int64, err error) {
787         s, err := c.Favourite(ctx, id)
788         if err != nil {
789                 return
790         }
791         count = s.FavouritesCount
792         return
793 }
794
795 func (svc *service) UnLike(ctx context.Context, c *model.Client, id string) (
796         count int64, err error) {
797         s, err := c.Unfavourite(ctx, id)
798         if err != nil {
799                 return
800         }
801         count = s.FavouritesCount
802         return
803 }
804
805 func (svc *service) Retweet(ctx context.Context, c *model.Client, id string) (
806         count int64, err error) {
807         s, err := c.Reblog(ctx, id)
808         if err != nil {
809                 return
810         }
811         if s.Reblog != nil {
812                 count = s.Reblog.ReblogsCount
813         }
814         return
815 }
816
817 func (svc *service) UnRetweet(ctx context.Context, c *model.Client, id string) (
818         count int64, err error) {
819         s, err := c.Unreblog(ctx, id)
820         if err != nil {
821                 return
822         }
823         count = s.ReblogsCount
824         return
825 }
826
827 func (svc *service) Follow(ctx context.Context, c *model.Client, id string) (err error) {
828         _, err = c.AccountFollow(ctx, id)
829         return
830 }
831
832 func (svc *service) UnFollow(ctx context.Context, c *model.Client, id string) (err error) {
833         _, err = c.AccountUnfollow(ctx, id)
834         return
835 }
836
837 func (svc *service) SaveSettings(ctx context.Context, c *model.Client,
838         settings *model.Settings) (err error) {
839
840         session, err := svc.sessionRepo.Get(c.Session.ID)
841         if err != nil {
842                 return
843         }
844
845         session.Settings = *settings
846         return svc.sessionRepo.Add(session)
847 }