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