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