Fix notification timestamp
[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         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) (count int64, err error)
48         UnLike(ctx context.Context, client io.Writer, c *model.Client, id string) (count int64, err error)
49         Retweet(ctx context.Context, client io.Writer, c *model.Client, id string) (count int64, err error)
50         UnRetweet(ctx context.Context, client io.Writer, c *model.Client, id string) (count int64, 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, "home")
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, "error")
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, "signin")
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.RetweetedByID = statuses[i].ID
286                         statuses[i].Reblog.ThreadInNewTab = c.Session.Settings.ThreadInNewTab
287                         statuses[i].Reblog.MaskNSFW = c.Session.Settings.MaskNSFW
288                 }
289         }
290
291         if len(maxID) > 0 && len(statuses) > 0 {
292                 hasPrev = true
293                 prevLink = fmt.Sprintf("/timeline/$s?min_id=%s", timelineType, statuses[0].ID)
294         }
295         if len(minID) > 0 && len(pg.MinID) > 0 {
296                 newStatuses, err := c.GetTimelineHome(ctx, &mastodon.Pagination{MinID: pg.MinID, Limit: 20})
297                 if err != nil {
298                         return err
299                 }
300                 newStatusesLen := len(newStatuses)
301                 if newStatusesLen == 20 {
302                         hasPrev = true
303                         prevLink = fmt.Sprintf("/timeline/%s?min_id=%s", timelineType, pg.MinID)
304                 } else {
305                         i := 20 - newStatusesLen - 1
306                         if len(statuses) > i {
307                                 hasPrev = true
308                                 prevLink = fmt.Sprintf("/timeline/%s?min_id=%s", timelineType, statuses[i].ID)
309                         }
310                 }
311         }
312         if len(pg.MaxID) > 0 {
313                 hasNext = true
314                 nextLink = fmt.Sprintf("/timeline/%s?max_id=%s", timelineType, pg.MaxID)
315         }
316
317         postContext := model.PostContext{
318                 DefaultVisibility: c.Session.Settings.DefaultVisibility,
319                 Formats:           svc.postFormats,
320         }
321
322         commonData, err := svc.getCommonData(ctx, client, c, timelineType+" timeline ")
323         if err != nil {
324                 return
325         }
326
327         data := &renderer.TimelineData{
328                 Title:       title,
329                 Statuses:    statuses,
330                 HasNext:     hasNext,
331                 NextLink:    nextLink,
332                 HasPrev:     hasPrev,
333                 PrevLink:    prevLink,
334                 PostContext: postContext,
335                 CommonData:  commonData,
336         }
337
338         err = svc.renderer.RenderTimelinePage(ctx, client, data)
339         if err != nil {
340                 return
341         }
342
343         return
344 }
345
346 func (svc *service) ServeThreadPage(ctx context.Context, client io.Writer, c *model.Client, id string, reply bool) (err error) {
347         status, err := c.GetStatus(ctx, id)
348         if err != nil {
349                 return
350         }
351
352         u, err := c.GetAccountCurrentUser(ctx)
353         if err != nil {
354                 return
355         }
356
357         var postContext model.PostContext
358         if reply {
359                 var content string
360                 if u.ID != status.Account.ID {
361                         content += "@" + status.Account.Acct + " "
362                 }
363                 for i := range status.Mentions {
364                         if status.Mentions[i].ID != u.ID && status.Mentions[i].ID != status.Account.ID {
365                                 content += "@" + status.Mentions[i].Acct + " "
366                         }
367                 }
368
369                 var visibility string
370                 if c.Session.Settings.CopyScope {
371                         s, err := c.GetStatus(ctx, id)
372                         if err != nil {
373                                 return err
374                         }
375                         visibility = s.Visibility
376                 } else {
377                         visibility = c.Session.Settings.DefaultVisibility
378                 }
379
380                 postContext = model.PostContext{
381                         DefaultVisibility: visibility,
382                         Formats:           svc.postFormats,
383                         ReplyContext: &model.ReplyContext{
384                                 InReplyToID:   id,
385                                 InReplyToName: status.Account.Acct,
386                                 ReplyContent:  content,
387                         },
388                 }
389         }
390
391         context, err := c.GetStatusContext(ctx, id)
392         if err != nil {
393                 return
394         }
395
396         statuses := append(append(context.Ancestors, status), context.Descendants...)
397
398         replyMap := make(map[string][]mastodon.ReplyInfo)
399
400         for i := range statuses {
401                 statuses[i].ShowReplies = true
402                 statuses[i].ReplyMap = replyMap
403                 statuses[i].MaskNSFW = c.Session.Settings.MaskNSFW
404                 addToReplyMap(replyMap, statuses[i].InReplyToID, statuses[i].ID, i+1)
405         }
406
407         commonData, err := svc.getCommonData(ctx, client, c, "post by "+status.Account.DisplayName)
408         if err != nil {
409                 return
410         }
411
412         data := &renderer.ThreadData{
413                 Statuses:    statuses,
414                 PostContext: postContext,
415                 ReplyMap:    replyMap,
416                 CommonData:  commonData,
417         }
418
419         err = svc.renderer.RenderThreadPage(ctx, client, data)
420         if err != nil {
421                 return
422         }
423
424         return
425 }
426
427 func (svc *service) ServeNotificationPage(ctx context.Context, client io.Writer, c *model.Client, maxID string, minID string) (err error) {
428         var hasNext bool
429         var nextLink string
430
431         var pg = mastodon.Pagination{
432                 MaxID: maxID,
433                 MinID: minID,
434                 Limit: 20,
435         }
436
437         notifications, err := c.GetNotifications(ctx, &pg)
438         if err != nil {
439                 return
440         }
441
442         var unreadCount int
443         for i := range notifications {
444                 if notifications[i].Status != nil {
445                         notifications[i].Status.CreatedAt = notifications[i].CreatedAt
446                         notifications[i].Status.MaskNSFW = c.Session.Settings.MaskNSFW
447                         switch notifications[i].Type {
448                         case "reblog", "favourite":
449                                 notifications[i].Status.HideAccountInfo = true
450                         }
451                 }
452                 if notifications[i].Pleroma != nil && notifications[i].Pleroma.IsSeen {
453                         unreadCount++
454                 }
455         }
456
457         if unreadCount > 0 {
458                 err := c.ReadNotifications(ctx, notifications[0].ID)
459                 if err != nil {
460                         return err
461                 }
462         }
463
464         if len(pg.MaxID) > 0 {
465                 hasNext = true
466                 nextLink = "/notifications?max_id=" + pg.MaxID
467         }
468
469         commonData, err := svc.getCommonData(ctx, client, c, "notifications")
470         if err != nil {
471                 return
472         }
473
474         data := &renderer.NotificationData{
475                 Notifications: notifications,
476                 HasNext:       hasNext,
477                 NextLink:      nextLink,
478                 CommonData:    commonData,
479         }
480         err = svc.renderer.RenderNotificationPage(ctx, client, data)
481         if err != nil {
482                 return
483         }
484
485         return
486 }
487
488 func (svc *service) ServeUserPage(ctx context.Context, client io.Writer, c *model.Client, id string, maxID string, minID string) (err error) {
489         user, err := c.GetAccount(ctx, id)
490         if err != nil {
491                 return
492         }
493
494         var hasNext bool
495         var nextLink string
496
497         var pg = mastodon.Pagination{
498                 MaxID: maxID,
499                 MinID: minID,
500                 Limit: 20,
501         }
502
503         statuses, err := c.GetAccountStatuses(ctx, id, &pg)
504         if err != nil {
505                 return
506         }
507
508         for i := range statuses {
509                 statuses[i].MaskNSFW = c.Session.Settings.MaskNSFW
510                 if statuses[i].Reblog != nil {
511                         statuses[i].Reblog.MaskNSFW = c.Session.Settings.MaskNSFW
512                 }
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
533         err = svc.renderer.RenderUserPage(ctx, client, data)
534         if err != nil {
535                 return
536         }
537
538         return
539 }
540
541 func (svc *service) ServeAboutPage(ctx context.Context, client io.Writer, c *model.Client) (err error) {
542         commonData, err := svc.getCommonData(ctx, client, c, "about")
543         if err != nil {
544                 return
545         }
546
547         data := &renderer.AboutData{
548                 CommonData: commonData,
549         }
550         err = svc.renderer.RenderAboutPage(ctx, client, data)
551         if err != nil {
552                 return
553         }
554
555         return
556 }
557
558 func (svc *service) ServeEmojiPage(ctx context.Context, client io.Writer, c *model.Client) (err error) {
559         commonData, err := svc.getCommonData(ctx, client, c, "emojis")
560         if err != nil {
561                 return
562         }
563
564         emojis, err := c.GetInstanceEmojis(ctx)
565         if err != nil {
566                 return
567         }
568
569         data := &renderer.EmojiData{
570                 Emojis:     emojis,
571                 CommonData: commonData,
572         }
573
574         err = svc.renderer.RenderEmojiPage(ctx, client, data)
575         if err != nil {
576                 return
577         }
578
579         return
580 }
581
582 func (svc *service) ServeLikedByPage(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
583         likers, err := c.GetFavouritedBy(ctx, id, nil)
584         if err != nil {
585                 return
586         }
587
588         commonData, err := svc.getCommonData(ctx, client, c, "likes")
589         if err != nil {
590                 return
591         }
592
593         data := &renderer.LikedByData{
594                 CommonData: commonData,
595                 Users:      likers,
596         }
597
598         err = svc.renderer.RenderLikedByPage(ctx, client, data)
599         if err != nil {
600                 return
601         }
602
603         return
604 }
605
606 func (svc *service) ServeRetweetedByPage(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
607         retweeters, err := c.GetRebloggedBy(ctx, id, nil)
608         if err != nil {
609                 return
610         }
611
612         commonData, err := svc.getCommonData(ctx, client, c, "retweets")
613         if err != nil {
614                 return
615         }
616
617         data := &renderer.RetweetedByData{
618                 CommonData: commonData,
619                 Users:      retweeters,
620         }
621
622         err = svc.renderer.RenderRetweetedByPage(ctx, client, data)
623         if err != nil {
624                 return
625         }
626
627         return
628 }
629
630 func (svc *service) ServeFollowingPage(ctx context.Context, client io.Writer, c *model.Client, id string, maxID string, minID string) (err error) {
631         var hasNext bool
632         var nextLink string
633
634         var pg = mastodon.Pagination{
635                 MaxID: maxID,
636                 MinID: minID,
637                 Limit: 20,
638         }
639
640         followings, err := c.GetAccountFollowing(ctx, id, &pg)
641         if err != nil {
642                 return
643         }
644
645         if len(followings) == 20 && len(pg.MaxID) > 0 {
646                 hasNext = true
647                 nextLink = "/following/" + id + "?max_id=" + pg.MaxID
648         }
649
650         commonData, err := svc.getCommonData(ctx, client, c, "following")
651         if err != nil {
652                 return
653         }
654
655         data := &renderer.FollowingData{
656                 CommonData: commonData,
657                 Users:      followings,
658                 HasNext:    hasNext,
659                 NextLink:   nextLink,
660         }
661
662         err = svc.renderer.RenderFollowingPage(ctx, client, data)
663         if err != nil {
664                 return
665         }
666
667         return
668 }
669
670 func (svc *service) ServeFollowersPage(ctx context.Context, client io.Writer, c *model.Client, id string, maxID string, minID string) (err error) {
671         var hasNext bool
672         var nextLink string
673
674         var pg = mastodon.Pagination{
675                 MaxID: maxID,
676                 MinID: minID,
677                 Limit: 20,
678         }
679
680         followers, err := c.GetAccountFollowers(ctx, id, &pg)
681         if err != nil {
682                 return
683         }
684
685         if len(followers) == 20 && len(pg.MaxID) > 0 {
686                 hasNext = true
687                 nextLink = "/followers/" + id + "?max_id=" + pg.MaxID
688         }
689
690         commonData, err := svc.getCommonData(ctx, client, c, "followers")
691         if err != nil {
692                 return
693         }
694
695         data := &renderer.FollowersData{
696                 CommonData: commonData,
697                 Users:      followers,
698                 HasNext:    hasNext,
699                 NextLink:   nextLink,
700         }
701
702         err = svc.renderer.RenderFollowersPage(ctx, client, data)
703         if err != nil {
704                 return
705         }
706
707         return
708 }
709
710 func (svc *service) ServeSearchPage(ctx context.Context, client io.Writer, c *model.Client, q string, qType string, offset int) (err error) {
711         var hasNext bool
712         var nextLink string
713
714         results, err := c.Search(ctx, q, qType, 20, true, offset)
715         if err != nil {
716                 return
717         }
718
719         switch qType {
720         case "accounts":
721                 hasNext = len(results.Accounts) == 20
722         case "statuses":
723                 hasNext = len(results.Statuses) == 20
724                 for i := range results.Statuses {
725                         results.Statuses[i].MaskNSFW = c.Session.Settings.MaskNSFW
726                 }
727
728         }
729
730         if hasNext {
731                 offset += 20
732                 nextLink = fmt.Sprintf("/search?q=%s&type=%s&offset=%d", q, qType, offset)
733         }
734
735         var title = "search"
736         if len(q) > 0 {
737                 title += " \"" + q + "\""
738         }
739         commonData, err := svc.getCommonData(ctx, client, c, title)
740         if err != nil {
741                 return
742         }
743
744         data := &renderer.SearchData{
745                 CommonData: commonData,
746                 Q:          q,
747                 Type:       qType,
748                 Users:      results.Accounts,
749                 Statuses:   results.Statuses,
750                 HasNext:    hasNext,
751                 NextLink:   nextLink,
752         }
753
754         err = svc.renderer.RenderSearchPage(ctx, client, data)
755         if err != nil {
756                 return
757         }
758
759         return
760 }
761
762 func (svc *service) ServeSettingsPage(ctx context.Context, client io.Writer, c *model.Client) (err error) {
763         commonData, err := svc.getCommonData(ctx, client, c, "settings")
764         if err != nil {
765                 return
766         }
767
768         data := &renderer.SettingsData{
769                 CommonData: commonData,
770                 Settings:   &c.Session.Settings,
771         }
772
773         err = svc.renderer.RenderSettingsPage(ctx, client, data)
774         if err != nil {
775                 return
776         }
777
778         return
779 }
780
781 func (svc *service) SaveSettings(ctx context.Context, client io.Writer, c *model.Client, settings *model.Settings) (err error) {
782         session, err := svc.sessionRepo.Get(c.Session.ID)
783         if err != nil {
784                 return
785         }
786
787         session.Settings = *settings
788         err = svc.sessionRepo.Add(session)
789         if err != nil {
790                 return
791         }
792
793         return
794 }
795
796 func (svc *service) getCommonData(ctx context.Context, client io.Writer, c *model.Client, title string) (data *renderer.CommonData, err error) {
797         data = new(renderer.CommonData)
798
799         data.HeaderData = &renderer.HeaderData{
800                 Title:             title + " - " + svc.clientName,
801                 NotificationCount: 0,
802                 CustomCSS:         svc.customCSS,
803         }
804
805         if c != nil && c.Session.IsLoggedIn() {
806                 notifications, err := c.GetNotifications(ctx, nil)
807                 if err != nil {
808                         return nil, err
809                 }
810
811                 var notificationCount int
812                 for i := range notifications {
813                         if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen {
814                                 notificationCount++
815                         }
816                 }
817
818                 u, err := c.GetAccountCurrentUser(ctx)
819                 if err != nil {
820                         return nil, err
821                 }
822
823                 data.NavbarData = &renderer.NavbarData{
824                         User:              u,
825                         NotificationCount: notificationCount,
826                 }
827
828                 data.HeaderData.NotificationCount = notificationCount
829                 data.HeaderData.FluorideMode = c.Session.Settings.FluorideMode
830         }
831
832         return
833 }
834
835 func (svc *service) Like(ctx context.Context, client io.Writer, c *model.Client, id string) (count int64, err error) {
836         s, err := c.Favourite(ctx, id)
837         if err != nil {
838                 return
839         }
840         count = s.FavouritesCount
841         return
842 }
843
844 func (svc *service) UnLike(ctx context.Context, client io.Writer, c *model.Client, id string) (count int64, err error) {
845         s, err := c.Unfavourite(ctx, id)
846         if err != nil {
847                 return
848         }
849         count = s.FavouritesCount
850         return
851 }
852
853 func (svc *service) Retweet(ctx context.Context, client io.Writer, c *model.Client, id string) (count int64, err error) {
854         s, err := c.Reblog(ctx, id)
855         if err != nil {
856                 return
857         }
858         if s.Reblog != nil {
859                 count = s.Reblog.ReblogsCount
860         }
861         return
862 }
863
864 func (svc *service) UnRetweet(ctx context.Context, client io.Writer, c *model.Client, id string) (count int64, err error) {
865         s, err := c.Unreblog(ctx, id)
866         if err != nil {
867                 return
868         }
869         count = s.ReblogsCount
870         return
871 }
872
873 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) {
874         var mediaIds []string
875         for _, f := range files {
876                 a, err := c.UploadMediaFromMultipartFileHeader(ctx, f)
877                 if err != nil {
878                         return "", err
879                 }
880                 mediaIds = append(mediaIds, a.ID)
881         }
882
883         tweet := &mastodon.Toot{
884                 Status:      content,
885                 InReplyToID: replyToID,
886                 MediaIDs:    mediaIds,
887                 ContentType: format,
888                 Visibility:  visibility,
889                 Sensitive:   isNSFW,
890         }
891
892         s, err := c.PostStatus(ctx, tweet)
893         if err != nil {
894                 return
895         }
896
897         return s.ID, nil
898 }
899
900 func (svc *service) Follow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
901         _, err = c.AccountFollow(ctx, id)
902         return
903 }
904
905 func (svc *service) UnFollow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
906         _, err = c.AccountUnfollow(ctx, id)
907         return
908 }
909
910 func addToReplyMap(m map[string][]mastodon.ReplyInfo, key interface{}, val string, number int) {
911         if key == nil {
912                 return
913         }
914
915         keyStr, ok := key.(string)
916         if !ok {
917                 return
918         }
919         _, ok = m[keyStr]
920         if !ok {
921                 m[keyStr] = []mastodon.ReplyInfo{}
922         }
923
924         m[keyStr] = append(m[keyStr], mastodon.ReplyInfo{val, number})
925 }