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