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