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