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