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