Add page titles
[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.MaskNSFW = c.Session.Settings.MaskNSFW
446                         switch notifications[i].Type {
447                         case "reblog", "favourite":
448                                 notifications[i].Status.HideAccountInfo = true
449                         }
450                 }
451                 if notifications[i].Pleroma != nil && notifications[i].Pleroma.IsSeen {
452                         unreadCount++
453                 }
454         }
455
456         if unreadCount > 0 {
457                 err := c.ReadNotifications(ctx, notifications[0].ID)
458                 if err != nil {
459                         return err
460                 }
461         }
462
463         if len(pg.MaxID) > 0 {
464                 hasNext = true
465                 nextLink = "/notifications?max_id=" + pg.MaxID
466         }
467
468         commonData, err := svc.getCommonData(ctx, client, c, "notifications")
469         if err != nil {
470                 return
471         }
472
473         data := &renderer.NotificationData{
474                 Notifications: notifications,
475                 HasNext:       hasNext,
476                 NextLink:      nextLink,
477                 CommonData:    commonData,
478         }
479         err = svc.renderer.RenderNotificationPage(ctx, client, data)
480         if err != nil {
481                 return
482         }
483
484         return
485 }
486
487 func (svc *service) ServeUserPage(ctx context.Context, client io.Writer, c *model.Client, id string, maxID string, minID string) (err error) {
488         user, err := c.GetAccount(ctx, id)
489         if err != nil {
490                 return
491         }
492
493         var hasNext bool
494         var nextLink string
495
496         var pg = mastodon.Pagination{
497                 MaxID: maxID,
498                 MinID: minID,
499                 Limit: 20,
500         }
501
502         statuses, err := c.GetAccountStatuses(ctx, id, &pg)
503         if err != nil {
504                 return
505         }
506
507         for i := range statuses {
508                 statuses[i].MaskNSFW = c.Session.Settings.MaskNSFW
509                 if statuses[i].Reblog != nil {
510                         statuses[i].Reblog.MaskNSFW = c.Session.Settings.MaskNSFW
511                 }
512         }
513
514         if len(pg.MaxID) > 0 {
515                 hasNext = true
516                 nextLink = "/user/" + id + "?max_id=" + pg.MaxID
517         }
518
519         commonData, err := svc.getCommonData(ctx, client, c, user.DisplayName)
520         if err != nil {
521                 return
522         }
523
524         data := &renderer.UserData{
525                 User:       user,
526                 Statuses:   statuses,
527                 HasNext:    hasNext,
528                 NextLink:   nextLink,
529                 CommonData: commonData,
530         }
531
532         err = svc.renderer.RenderUserPage(ctx, client, data)
533         if err != nil {
534                 return
535         }
536
537         return
538 }
539
540 func (svc *service) ServeAboutPage(ctx context.Context, client io.Writer, c *model.Client) (err error) {
541         commonData, err := svc.getCommonData(ctx, client, c, "about")
542         if err != nil {
543                 return
544         }
545
546         data := &renderer.AboutData{
547                 CommonData: commonData,
548         }
549         err = svc.renderer.RenderAboutPage(ctx, client, data)
550         if err != nil {
551                 return
552         }
553
554         return
555 }
556
557 func (svc *service) ServeEmojiPage(ctx context.Context, client io.Writer, c *model.Client) (err error) {
558         commonData, err := svc.getCommonData(ctx, client, c, "emojis")
559         if err != nil {
560                 return
561         }
562
563         emojis, err := c.GetInstanceEmojis(ctx)
564         if err != nil {
565                 return
566         }
567
568         data := &renderer.EmojiData{
569                 Emojis:     emojis,
570                 CommonData: commonData,
571         }
572
573         err = svc.renderer.RenderEmojiPage(ctx, client, data)
574         if err != nil {
575                 return
576         }
577
578         return
579 }
580
581 func (svc *service) ServeLikedByPage(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
582         likers, err := c.GetFavouritedBy(ctx, id, nil)
583         if err != nil {
584                 return
585         }
586
587         commonData, err := svc.getCommonData(ctx, client, c, "likes")
588         if err != nil {
589                 return
590         }
591
592         data := &renderer.LikedByData{
593                 CommonData: commonData,
594                 Users:      likers,
595         }
596
597         err = svc.renderer.RenderLikedByPage(ctx, client, data)
598         if err != nil {
599                 return
600         }
601
602         return
603 }
604
605 func (svc *service) ServeRetweetedByPage(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
606         retweeters, err := c.GetRebloggedBy(ctx, id, nil)
607         if err != nil {
608                 return
609         }
610
611         commonData, err := svc.getCommonData(ctx, client, c, "retweets")
612         if err != nil {
613                 return
614         }
615
616         data := &renderer.RetweetedByData{
617                 CommonData: commonData,
618                 Users:      retweeters,
619         }
620
621         err = svc.renderer.RenderRetweetedByPage(ctx, client, data)
622         if err != nil {
623                 return
624         }
625
626         return
627 }
628
629 func (svc *service) ServeFollowingPage(ctx context.Context, client io.Writer, c *model.Client, id string, maxID string, minID string) (err error) {
630         var hasNext bool
631         var nextLink string
632
633         var pg = mastodon.Pagination{
634                 MaxID: maxID,
635                 MinID: minID,
636                 Limit: 20,
637         }
638
639         followings, err := c.GetAccountFollowing(ctx, id, &pg)
640         if err != nil {
641                 return
642         }
643
644         if len(followings) == 20 && len(pg.MaxID) > 0 {
645                 hasNext = true
646                 nextLink = "/following/" + id + "?max_id=" + pg.MaxID
647         }
648
649         commonData, err := svc.getCommonData(ctx, client, c, "following")
650         if err != nil {
651                 return
652         }
653
654         data := &renderer.FollowingData{
655                 CommonData: commonData,
656                 Users:      followings,
657                 HasNext:    hasNext,
658                 NextLink:   nextLink,
659         }
660
661         err = svc.renderer.RenderFollowingPage(ctx, client, data)
662         if err != nil {
663                 return
664         }
665
666         return
667 }
668
669 func (svc *service) ServeFollowersPage(ctx context.Context, client io.Writer, c *model.Client, id string, maxID string, minID string) (err error) {
670         var hasNext bool
671         var nextLink string
672
673         var pg = mastodon.Pagination{
674                 MaxID: maxID,
675                 MinID: minID,
676                 Limit: 20,
677         }
678
679         followers, err := c.GetAccountFollowers(ctx, id, &pg)
680         if err != nil {
681                 return
682         }
683
684         if len(followers) == 20 && len(pg.MaxID) > 0 {
685                 hasNext = true
686                 nextLink = "/followers/" + id + "?max_id=" + pg.MaxID
687         }
688
689         commonData, err := svc.getCommonData(ctx, client, c, "followers")
690         if err != nil {
691                 return
692         }
693
694         data := &renderer.FollowersData{
695                 CommonData: commonData,
696                 Users:      followers,
697                 HasNext:    hasNext,
698                 NextLink:   nextLink,
699         }
700
701         err = svc.renderer.RenderFollowersPage(ctx, client, data)
702         if err != nil {
703                 return
704         }
705
706         return
707 }
708
709 func (svc *service) ServeSearchPage(ctx context.Context, client io.Writer, c *model.Client, q string, qType string, offset int) (err error) {
710         var hasNext bool
711         var nextLink string
712
713         results, err := c.Search(ctx, q, qType, 20, true, offset)
714         if err != nil {
715                 return
716         }
717
718         switch qType {
719         case "accounts":
720                 hasNext = len(results.Accounts) == 20
721         case "statuses":
722                 hasNext = len(results.Statuses) == 20
723                 for i := range results.Statuses {
724                         results.Statuses[i].MaskNSFW = c.Session.Settings.MaskNSFW
725                 }
726
727         }
728
729         if hasNext {
730                 offset += 20
731                 nextLink = fmt.Sprintf("/search?q=%s&type=%s&offset=%d", q, qType, offset)
732         }
733
734         var title = "search"
735         if len(q) > 0 {
736                 title += " \"" + q + "\""
737         }
738         commonData, err := svc.getCommonData(ctx, client, c, title)
739         if err != nil {
740                 return
741         }
742
743         data := &renderer.SearchData{
744                 CommonData: commonData,
745                 Q:          q,
746                 Type:       qType,
747                 Users:      results.Accounts,
748                 Statuses:   results.Statuses,
749                 HasNext:    hasNext,
750                 NextLink:   nextLink,
751         }
752
753         err = svc.renderer.RenderSearchPage(ctx, client, data)
754         if err != nil {
755                 return
756         }
757
758         return
759 }
760
761 func (svc *service) ServeSettingsPage(ctx context.Context, client io.Writer, c *model.Client) (err error) {
762         commonData, err := svc.getCommonData(ctx, client, c, "settings")
763         if err != nil {
764                 return
765         }
766
767         data := &renderer.SettingsData{
768                 CommonData: commonData,
769                 Settings:   &c.Session.Settings,
770         }
771
772         err = svc.renderer.RenderSettingsPage(ctx, client, data)
773         if err != nil {
774                 return
775         }
776
777         return
778 }
779
780 func (svc *service) SaveSettings(ctx context.Context, client io.Writer, c *model.Client, settings *model.Settings) (err error) {
781         session, err := svc.sessionRepo.Get(c.Session.ID)
782         if err != nil {
783                 return
784         }
785
786         session.Settings = *settings
787         err = svc.sessionRepo.Add(session)
788         if err != nil {
789                 return
790         }
791
792         return
793 }
794
795 func (svc *service) getCommonData(ctx context.Context, client io.Writer, c *model.Client, title string) (data *renderer.CommonData, err error) {
796         data = new(renderer.CommonData)
797
798         data.HeaderData = &renderer.HeaderData{
799                 Title:             title + " - " + svc.clientName,
800                 NotificationCount: 0,
801                 CustomCSS:         svc.customCSS,
802         }
803
804         if c != nil && c.Session.IsLoggedIn() {
805                 notifications, err := c.GetNotifications(ctx, nil)
806                 if err != nil {
807                         return nil, err
808                 }
809
810                 var notificationCount int
811                 for i := range notifications {
812                         if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen {
813                                 notificationCount++
814                         }
815                 }
816
817                 u, err := c.GetAccountCurrentUser(ctx)
818                 if err != nil {
819                         return nil, err
820                 }
821
822                 data.NavbarData = &renderer.NavbarData{
823                         User:              u,
824                         NotificationCount: notificationCount,
825                 }
826
827                 data.HeaderData.NotificationCount = notificationCount
828                 data.HeaderData.FluorideMode = c.Session.Settings.FluorideMode
829         }
830
831         return
832 }
833
834 func (svc *service) Like(ctx context.Context, client io.Writer, c *model.Client, id string) (count int64, err error) {
835         s, err := c.Favourite(ctx, id)
836         if err != nil {
837                 return
838         }
839         count = s.FavouritesCount
840         return
841 }
842
843 func (svc *service) UnLike(ctx context.Context, client io.Writer, c *model.Client, id string) (count int64, err error) {
844         s, err := c.Unfavourite(ctx, id)
845         if err != nil {
846                 return
847         }
848         count = s.FavouritesCount
849         return
850 }
851
852 func (svc *service) Retweet(ctx context.Context, client io.Writer, c *model.Client, id string) (count int64, err error) {
853         s, err := c.Reblog(ctx, id)
854         if err != nil {
855                 return
856         }
857         if s.Reblog != nil {
858                 count = s.Reblog.ReblogsCount
859         }
860         return
861 }
862
863 func (svc *service) UnRetweet(ctx context.Context, client io.Writer, c *model.Client, id string) (count int64, err error) {
864         s, err := c.Unreblog(ctx, id)
865         if err != nil {
866                 return
867         }
868         count = s.ReblogsCount
869         return
870 }
871
872 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) {
873         var mediaIds []string
874         for _, f := range files {
875                 a, err := c.UploadMediaFromMultipartFileHeader(ctx, f)
876                 if err != nil {
877                         return "", err
878                 }
879                 mediaIds = append(mediaIds, a.ID)
880         }
881
882         tweet := &mastodon.Toot{
883                 Status:      content,
884                 InReplyToID: replyToID,
885                 MediaIDs:    mediaIds,
886                 ContentType: format,
887                 Visibility:  visibility,
888                 Sensitive:   isNSFW,
889         }
890
891         s, err := c.PostStatus(ctx, tweet)
892         if err != nil {
893                 return
894         }
895
896         return s.ID, nil
897 }
898
899 func (svc *service) Follow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
900         _, err = c.AccountFollow(ctx, id)
901         return
902 }
903
904 func (svc *service) UnFollow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
905         _, err = c.AccountUnfollow(ctx, id)
906         return
907 }
908
909 func addToReplyMap(m map[string][]mastodon.ReplyInfo, key interface{}, val string, number int) {
910         if key == nil {
911                 return
912         }
913
914         keyStr, ok := key.(string)
915         if !ok {
916                 return
917         }
918         _, ok = m[keyStr]
919         if !ok {
920                 m[keyStr] = []mastodon.ReplyInfo{}
921         }
922
923         m[keyStr] = append(m[keyStr], mastodon.ReplyInfo{val, number})
924 }