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