Refactor everything
[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 "local":
215                 statuses, err = c.GetTimelinePublic(ctx, true, &pg)
216                 title = "Local Timeline"
217         case "twkn":
218                 statuses, err = c.GetTimelinePublic(ctx, false, &pg)
219                 title = "The Whole Known Network"
220         }
221         if err != nil {
222                 return err
223         }
224
225         for i := range statuses {
226                 if statuses[i].Reblog != nil {
227                         statuses[i].Reblog.RetweetedByID = statuses[i].ID
228                 }
229         }
230
231         if len(maxID) > 0 && len(statuses) > 0 {
232                 prevLink = fmt.Sprintf("/timeline/%s?min_id=%s", tType,
233                         statuses[0].ID)
234         }
235
236         if len(minID) > 0 && len(pg.MinID) > 0 {
237                 newPg := &mastodon.Pagination{MinID: pg.MinID, Limit: 20}
238                 newStatuses, err := c.GetTimelineHome(ctx, newPg)
239                 if err != nil {
240                         return err
241                 }
242                 newLen := len(newStatuses)
243                 if newLen == 20 {
244                         prevLink = fmt.Sprintf("/timeline/%s?min_id=%s",
245                                 tType, pg.MinID)
246                 } else {
247                         i := 20 - newLen - 1
248                         if len(statuses) > i {
249                                 prevLink = fmt.Sprintf("/timeline/%s?min_id=%s",
250                                         tType, statuses[i].ID)
251                         }
252                 }
253         }
254
255         if len(pg.MaxID) > 0 {
256                 nextLink = fmt.Sprintf("/timeline/%s?max_id=%s", tType, pg.MaxID)
257         }
258
259         postContext := model.PostContext{
260                 DefaultVisibility: c.Session.Settings.DefaultVisibility,
261                 Formats:           svc.postFormats,
262         }
263
264         commonData, err := svc.getCommonData(ctx, c, tType+" timeline ")
265         if err != nil {
266                 return
267         }
268
269         data := &renderer.TimelineData{
270                 Title:       title,
271                 Statuses:    statuses,
272                 NextLink:    nextLink,
273                 PrevLink:    prevLink,
274                 PostContext: postContext,
275                 CommonData:  commonData,
276         }
277
278         rCtx := getRendererContext(c)
279         return svc.renderer.RenderTimelinePage(rCtx, c.Writer, 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                 addToReplyMap(replies, statuses[i].InReplyToID, statuses[i].ID, i+1)
344         }
345
346         commonData, err := svc.getCommonData(ctx, c, "post by "+status.Account.DisplayName)
347         if err != nil {
348                 return
349         }
350
351         data := &renderer.ThreadData{
352                 Statuses:    statuses,
353                 PostContext: postContext,
354                 ReplyMap:    replies,
355                 CommonData:  commonData,
356         }
357
358         rCtx := getRendererContext(c)
359         return svc.renderer.RenderThreadPage(rCtx, c.Writer, data)
360 }
361
362 func (svc *service) ServeLikedByPage(ctx context.Context, c *model.Client,
363         id string) (err error) {
364
365         likers, err := c.GetFavouritedBy(ctx, id, nil)
366         if err != nil {
367                 return
368         }
369
370         commonData, err := svc.getCommonData(ctx, c, "likes")
371         if err != nil {
372                 return
373         }
374
375         data := &renderer.LikedByData{
376                 CommonData: commonData,
377                 Users:      likers,
378         }
379
380         rCtx := getRendererContext(c)
381         return svc.renderer.RenderLikedByPage(rCtx, c.Writer, data)
382 }
383
384 func (svc *service) ServeRetweetedByPage(ctx context.Context, c *model.Client,
385         id string) (err error) {
386
387         retweeters, err := c.GetRebloggedBy(ctx, id, nil)
388         if err != nil {
389                 return
390         }
391
392         commonData, err := svc.getCommonData(ctx, c, "retweets")
393         if err != nil {
394                 return
395         }
396
397         data := &renderer.RetweetedByData{
398                 CommonData: commonData,
399                 Users:      retweeters,
400         }
401
402         rCtx := getRendererContext(c)
403         return svc.renderer.RenderRetweetedByPage(rCtx, c.Writer, data)
404 }
405
406 func (svc *service) ServeFollowingPage(ctx context.Context, c *model.Client,
407         id string, maxID string, minID string) (err error) {
408
409         var nextLink string
410         var pg = mastodon.Pagination{
411                 MaxID: maxID,
412                 MinID: minID,
413                 Limit: 20,
414         }
415
416         followings, err := c.GetAccountFollowing(ctx, id, &pg)
417         if err != nil {
418                 return
419         }
420
421         if len(followings) == 20 && len(pg.MaxID) > 0 {
422                 nextLink = "/following/" + id + "?max_id=" + pg.MaxID
423         }
424
425         commonData, err := svc.getCommonData(ctx, c, "following")
426         if err != nil {
427                 return
428         }
429
430         data := &renderer.FollowingData{
431                 CommonData: commonData,
432                 Users:      followings,
433                 NextLink:   nextLink,
434         }
435
436         rCtx := getRendererContext(c)
437         return svc.renderer.RenderFollowingPage(rCtx, c.Writer, data)
438 }
439
440 func (svc *service) ServeFollowersPage(ctx context.Context, c *model.Client,
441         id string, maxID string, minID string) (err error) {
442
443         var nextLink string
444         var pg = mastodon.Pagination{
445                 MaxID: maxID,
446                 MinID: minID,
447                 Limit: 20,
448         }
449
450         followers, err := c.GetAccountFollowers(ctx, id, &pg)
451         if err != nil {
452                 return
453         }
454
455         if len(followers) == 20 && len(pg.MaxID) > 0 {
456                 nextLink = "/followers/" + id + "?max_id=" + pg.MaxID
457         }
458
459         commonData, err := svc.getCommonData(ctx, c, "followers")
460         if err != nil {
461                 return
462         }
463
464         data := &renderer.FollowersData{
465                 CommonData: commonData,
466                 Users:      followers,
467                 NextLink:   nextLink,
468         }
469         rCtx := getRendererContext(c)
470         return svc.renderer.RenderFollowersPage(rCtx, c.Writer, data)
471 }
472
473 func (svc *service) ServeNotificationPage(ctx context.Context, c *model.Client,
474         maxID string, minID string) (err error) {
475
476         var nextLink string
477         var unreadCount int
478         var pg = mastodon.Pagination{
479                 MaxID: maxID,
480                 MinID: minID,
481                 Limit: 20,
482         }
483
484         notifications, err := c.GetNotifications(ctx, &pg)
485         if err != nil {
486                 return
487         }
488
489         for i := range notifications {
490                 if notifications[i].Status != nil {
491                         notifications[i].Status.CreatedAt = notifications[i].CreatedAt
492                         switch notifications[i].Type {
493                         case "reblog", "favourite":
494                                 notifications[i].Status.HideAccountInfo = true
495                         }
496                 }
497                 if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen {
498                         unreadCount++
499                 }
500         }
501
502         if unreadCount > 0 {
503                 err := c.ReadNotifications(ctx, notifications[0].ID)
504                 if err != nil {
505                         return err
506                 }
507         }
508
509         if len(pg.MaxID) > 0 {
510                 nextLink = "/notifications?max_id=" + pg.MaxID
511         }
512
513         commonData, err := svc.getCommonData(ctx, c, "notifications")
514         if err != nil {
515                 return
516         }
517
518         data := &renderer.NotificationData{
519                 Notifications: notifications,
520                 NextLink:      nextLink,
521                 CommonData:    commonData,
522         }
523         rCtx := getRendererContext(c)
524         return svc.renderer.RenderNotificationPage(rCtx, c.Writer, data)
525 }
526
527 func (svc *service) ServeUserPage(ctx context.Context, c *model.Client,
528         id string, maxID string, minID string) (err error) {
529
530         var nextLink string
531
532         var pg = mastodon.Pagination{
533                 MaxID: maxID,
534                 MinID: minID,
535                 Limit: 20,
536         }
537
538         user, err := c.GetAccount(ctx, id)
539         if err != nil {
540                 return
541         }
542
543         statuses, err := c.GetAccountStatuses(ctx, id, &pg)
544         if err != nil {
545                 return
546         }
547
548         if len(pg.MaxID) > 0 {
549                 nextLink = "/user/" + id + "?max_id=" + pg.MaxID
550         }
551
552         commonData, err := svc.getCommonData(ctx, c, user.DisplayName)
553         if err != nil {
554                 return
555         }
556
557         data := &renderer.UserData{
558                 User:       user,
559                 Statuses:   statuses,
560                 NextLink:   nextLink,
561                 CommonData: commonData,
562         }
563         rCtx := getRendererContext(c)
564         return svc.renderer.RenderUserPage(rCtx, c.Writer, data)
565 }
566
567 func (svc *service) ServeAboutPage(ctx context.Context, c *model.Client) (err error) {
568         commonData, err := svc.getCommonData(ctx, c, "about")
569         if err != nil {
570                 return
571         }
572
573         data := &renderer.AboutData{
574                 CommonData: commonData,
575         }
576
577         rCtx := getRendererContext(c)
578         return svc.renderer.RenderAboutPage(rCtx, c.Writer, data)
579 }
580
581 func (svc *service) ServeEmojiPage(ctx context.Context, c *model.Client) (err error) {
582         commonData, err := svc.getCommonData(ctx, c, "emojis")
583         if err != nil {
584                 return
585         }
586
587         emojis, err := c.GetInstanceEmojis(ctx)
588         if err != nil {
589                 return
590         }
591
592         data := &renderer.EmojiData{
593                 Emojis:     emojis,
594                 CommonData: commonData,
595         }
596
597         rCtx := getRendererContext(c)
598         return svc.renderer.RenderEmojiPage(rCtx, c.Writer, data)
599 }
600
601 func (svc *service) ServeSearchPage(ctx context.Context, c *model.Client,
602         q string, qType string, offset int) (err error) {
603
604         var nextLink string
605         var title = "search"
606
607         results, err := c.Search(ctx, q, qType, 20, true, offset)
608         if err != nil {
609                 return
610         }
611
612         if (qType == "accounts" && len(results.Accounts) == 20) ||
613                 (qType == "statuses" && len(results.Statuses) == 20) {
614                 offset += 20
615                 nextLink = fmt.Sprintf("/search?q=%s&type=%s&offset=%d", q, qType, offset)
616         }
617
618         if len(q) > 0 {
619                 title += " \"" + q + "\""
620         }
621
622         commonData, err := svc.getCommonData(ctx, c, title)
623         if err != nil {
624                 return
625         }
626
627         data := &renderer.SearchData{
628                 CommonData: commonData,
629                 Q:          q,
630                 Type:       qType,
631                 Users:      results.Accounts,
632                 Statuses:   results.Statuses,
633                 NextLink:   nextLink,
634         }
635
636         rCtx := getRendererContext(c)
637         return svc.renderer.RenderSearchPage(rCtx, c.Writer, data)
638 }
639
640 func (svc *service) ServeSettingsPage(ctx context.Context, c *model.Client) (err error) {
641         commonData, err := svc.getCommonData(ctx, c, "settings")
642         if err != nil {
643                 return
644         }
645
646         data := &renderer.SettingsData{
647                 CommonData: commonData,
648                 Settings:   &c.Session.Settings,
649         }
650
651         rCtx := getRendererContext(c)
652         return svc.renderer.RenderSettingsPage(rCtx, c.Writer, data)
653 }
654
655 func (svc *service) NewSession(ctx context.Context, instance string) (
656         redirectUrl string, sessionID string, err error) {
657
658         var instanceURL string
659         if strings.HasPrefix(instance, "https://") {
660                 instanceURL = instance
661                 instance = strings.TrimPrefix(instance, "https://")
662         } else {
663                 instanceURL = "https://" + instance
664         }
665
666         sessionID, err = util.NewSessionID()
667         if err != nil {
668                 return
669         }
670
671         csrfToken, err := util.NewCSRFToken()
672         if err != nil {
673                 return
674         }
675
676         session := model.Session{
677                 ID:             sessionID,
678                 InstanceDomain: instance,
679                 CSRFToken:      csrfToken,
680                 Settings:       *model.NewSettings(),
681         }
682
683         err = svc.sessionRepo.Add(session)
684         if err != nil {
685                 return
686         }
687
688         app, err := svc.appRepo.Get(instance)
689         if err != nil {
690                 if err != model.ErrAppNotFound {
691                         return
692                 }
693
694                 mastoApp, err := mastodon.RegisterApp(ctx, &mastodon.AppConfig{
695                         Server:       instanceURL,
696                         ClientName:   svc.clientName,
697                         Scopes:       svc.clientScope,
698                         Website:      svc.clientWebsite,
699                         RedirectURIs: svc.clientWebsite + "/oauth_callback",
700                 })
701                 if err != nil {
702                         return "", "", err
703                 }
704
705                 app = model.App{
706                         InstanceDomain: instance,
707                         InstanceURL:    instanceURL,
708                         ClientID:       mastoApp.ClientID,
709                         ClientSecret:   mastoApp.ClientSecret,
710                 }
711
712                 err = svc.appRepo.Add(app)
713                 if err != nil {
714                         return "", "", err
715                 }
716         }
717
718         u, err := url.Parse("/oauth/authorize")
719         if err != nil {
720                 return
721         }
722
723         q := make(url.Values)
724         q.Set("scope", "read write follow")
725         q.Set("client_id", app.ClientID)
726         q.Set("response_type", "code")
727         q.Set("redirect_uri", svc.clientWebsite+"/oauth_callback")
728         u.RawQuery = q.Encode()
729
730         redirectUrl = instanceURL + u.String()
731
732         return
733 }
734
735 func (svc *service) Signin(ctx context.Context, c *model.Client,
736         sessionID string, code string) (token string, err error) {
737
738         if len(code) < 1 {
739                 err = errInvalidArgument
740                 return
741         }
742
743         err = c.AuthenticateToken(ctx, code, svc.clientWebsite+"/oauth_callback")
744         if err != nil {
745                 return
746         }
747         token = c.GetAccessToken(ctx)
748
749         return
750 }
751
752 func (svc *service) Post(ctx context.Context, c *model.Client, content string,
753         replyToID string, format string, visibility string, isNSFW bool,
754         files []*multipart.FileHeader) (id string, err error) {
755
756         var mediaIDs []string
757         for _, f := range files {
758                 a, err := c.UploadMediaFromMultipartFileHeader(ctx, f)
759                 if err != nil {
760                         return "", err
761                 }
762                 mediaIDs = append(mediaIDs, a.ID)
763         }
764
765         tweet := &mastodon.Toot{
766                 Status:      content,
767                 InReplyToID: replyToID,
768                 MediaIDs:    mediaIDs,
769                 ContentType: format,
770                 Visibility:  visibility,
771                 Sensitive:   isNSFW,
772         }
773
774         s, err := c.PostStatus(ctx, tweet)
775         if err != nil {
776                 return
777         }
778
779         return s.ID, nil
780 }
781
782 func (svc *service) Like(ctx context.Context, c *model.Client, id string) (
783         count int64, err error) {
784         s, err := c.Favourite(ctx, id)
785         if err != nil {
786                 return
787         }
788         count = s.FavouritesCount
789         return
790 }
791
792 func (svc *service) UnLike(ctx context.Context, c *model.Client, id string) (
793         count int64, err error) {
794         s, err := c.Unfavourite(ctx, id)
795         if err != nil {
796                 return
797         }
798         count = s.FavouritesCount
799         return
800 }
801
802 func (svc *service) Retweet(ctx context.Context, c *model.Client, id string) (
803         count int64, err error) {
804         s, err := c.Reblog(ctx, id)
805         if err != nil {
806                 return
807         }
808         if s.Reblog != nil {
809                 count = s.Reblog.ReblogsCount
810         }
811         return
812 }
813
814 func (svc *service) UnRetweet(ctx context.Context, c *model.Client, id string) (
815         count int64, err error) {
816         s, err := c.Unreblog(ctx, id)
817         if err != nil {
818                 return
819         }
820         count = s.ReblogsCount
821         return
822 }
823
824 func (svc *service) Follow(ctx context.Context, c *model.Client, id string) (err error) {
825         _, err = c.AccountFollow(ctx, id)
826         return
827 }
828
829 func (svc *service) UnFollow(ctx context.Context, c *model.Client, id string) (err error) {
830         _, err = c.AccountUnfollow(ctx, id)
831         return
832 }
833
834 func (svc *service) SaveSettings(ctx context.Context, c *model.Client,
835         settings *model.Settings) (err error) {
836
837         session, err := svc.sessionRepo.Get(c.Session.ID)
838         if err != nil {
839                 return
840         }
841
842         session.Settings = *settings
843         return svc.sessionRepo.Add(session)
844 }