Refector render structs
[bloat] / service / service.go
1 package service
2
3 import (
4         "bytes"
5         "context"
6         "encoding/json"
7         "errors"
8         "fmt"
9         "io"
10         "mime/multipart"
11         "net/http"
12         "net/url"
13         "strings"
14
15         "mastodon"
16         "web/model"
17         "web/renderer"
18         "web/util"
19 )
20
21 var (
22         ErrInvalidArgument = errors.New("invalid argument")
23         ErrInvalidToken    = errors.New("invalid token")
24         ErrInvalidClient   = errors.New("invalid client")
25         ErrInvalidTimeline = errors.New("invalid timeline")
26 )
27
28 type Service interface {
29         ServeHomePage(ctx context.Context, client io.Writer) (err error)
30         GetAuthUrl(ctx context.Context, instance string) (url string, sessionID string, err error)
31         GetUserToken(ctx context.Context, sessionID string, c *model.Client, token string) (accessToken string, err error)
32         ServeErrorPage(ctx context.Context, client io.Writer, err error)
33         ServeSigninPage(ctx context.Context, client io.Writer) (err error)
34         ServeTimelinePage(ctx context.Context, client io.Writer, c *model.Client, timelineType string, maxID string, sinceID string, minID string) (err error)
35         ServeThreadPage(ctx context.Context, client io.Writer, c *model.Client, id string, reply bool) (err error)
36         ServeNotificationPage(ctx context.Context, client io.Writer, c *model.Client, maxID string, minID string) (err error)
37         ServeUserPage(ctx context.Context, client io.Writer, c *model.Client, id string, maxID string, minID string) (err error)
38         ServeAboutPage(ctx context.Context, client io.Writer, c *model.Client) (err error)
39         ServeEmojiPage(ctx context.Context, client io.Writer, c *model.Client) (err error)
40         Like(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
41         UnLike(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
42         Retweet(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
43         UnRetweet(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
44         PostTweet(ctx context.Context, client io.Writer, c *model.Client, content string, replyToID string, visibility string, isNSFW bool, files []*multipart.FileHeader) (id string, err error)
45         Follow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
46         UnFollow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
47 }
48
49 type service struct {
50         clientName    string
51         clientScope   string
52         clientWebsite string
53         renderer      renderer.Renderer
54         sessionRepo   model.SessionRepository
55         appRepo       model.AppRepository
56 }
57
58 func NewService(clientName string, clientScope string, clientWebsite string,
59         renderer renderer.Renderer, sessionRepo model.SessionRepository,
60         appRepo model.AppRepository) Service {
61         return &service{
62                 clientName:    clientName,
63                 clientScope:   clientScope,
64                 clientWebsite: clientWebsite,
65                 renderer:      renderer,
66                 sessionRepo:   sessionRepo,
67                 appRepo:       appRepo,
68         }
69 }
70
71 func (svc *service) GetAuthUrl(ctx context.Context, instance string) (
72         redirectUrl string, sessionID string, err error) {
73         var instanceURL string
74         if strings.HasPrefix(instance, "https://") {
75                 instanceURL = instance
76                 instance = strings.TrimPrefix(instance, "https://")
77         } else {
78                 instanceURL = "https://" + instance
79         }
80
81         sessionID = util.NewSessionId()
82         err = svc.sessionRepo.Add(model.Session{
83                 ID:             sessionID,
84                 InstanceDomain: instance,
85         })
86         if err != nil {
87                 return
88         }
89
90         app, err := svc.appRepo.Get(instance)
91         if err != nil {
92                 if err != model.ErrAppNotFound {
93                         return
94                 }
95
96                 var mastoApp *mastodon.Application
97                 mastoApp, err = mastodon.RegisterApp(ctx, &mastodon.AppConfig{
98                         Server:       instanceURL,
99                         ClientName:   svc.clientName,
100                         Scopes:       svc.clientScope,
101                         Website:      svc.clientWebsite,
102                         RedirectURIs: svc.clientWebsite + "/oauth_callback",
103                 })
104                 if err != nil {
105                         return
106                 }
107
108                 app = model.App{
109                         InstanceDomain: instance,
110                         InstanceURL:    instanceURL,
111                         ClientID:       mastoApp.ClientID,
112                         ClientSecret:   mastoApp.ClientSecret,
113                 }
114
115                 err = svc.appRepo.Add(app)
116                 if err != nil {
117                         return
118                 }
119         }
120
121         u, err := url.Parse("/oauth/authorize")
122         if err != nil {
123                 return
124         }
125
126         q := make(url.Values)
127         q.Set("scope", "read write follow")
128         q.Set("client_id", app.ClientID)
129         q.Set("response_type", "code")
130         q.Set("redirect_uri", svc.clientWebsite+"/oauth_callback")
131         u.RawQuery = q.Encode()
132
133         redirectUrl = instanceURL + u.String()
134
135         return
136 }
137
138 func (svc *service) GetUserToken(ctx context.Context, sessionID string, c *model.Client,
139         code string) (token string, err error) {
140         if len(code) < 1 {
141                 err = ErrInvalidArgument
142                 return
143         }
144
145         session, err := svc.sessionRepo.Get(sessionID)
146         if err != nil {
147                 return
148         }
149
150         app, err := svc.appRepo.Get(session.InstanceDomain)
151         if err != nil {
152                 return
153         }
154
155         data := &bytes.Buffer{}
156         err = json.NewEncoder(data).Encode(map[string]string{
157                 "client_id":     app.ClientID,
158                 "client_secret": app.ClientSecret,
159                 "grant_type":    "authorization_code",
160                 "code":          code,
161                 "redirect_uri":  svc.clientWebsite + "/oauth_callback",
162         })
163         if err != nil {
164                 return
165         }
166
167         resp, err := http.Post(app.InstanceURL+"/oauth/token", "application/json", data)
168         if err != nil {
169                 return
170         }
171         defer resp.Body.Close()
172
173         var res struct {
174                 AccessToken string `json:"access_token"`
175         }
176
177         err = json.NewDecoder(resp.Body).Decode(&res)
178         if err != nil {
179                 return
180         }
181         /*
182                 err = c.AuthenticateToken(ctx, code, svc.clientWebsite+"/oauth_callback")
183                 if err != nil {
184                         return
185                 }
186                 err = svc.sessionRepo.Update(sessionID, c.GetAccessToken(ctx))
187         */
188
189         return res.AccessToken, nil
190 }
191
192 func (svc *service) ServeHomePage(ctx context.Context, client io.Writer) (err error) {
193         err = svc.renderer.RenderHomePage(ctx, client)
194         if err != nil {
195                 return
196         }
197
198         return
199 }
200
201 func (svc *service) ServeErrorPage(ctx context.Context, client io.Writer, err error) {
202         svc.renderer.RenderErrorPage(ctx, client, err)
203 }
204
205 func (svc *service) ServeSigninPage(ctx context.Context, client io.Writer) (err error) {
206         err = svc.renderer.RenderSigninPage(ctx, client)
207         if err != nil {
208                 return
209         }
210
211         return
212 }
213
214 func (svc *service) ServeTimelinePage(ctx context.Context, client io.Writer,
215         c *model.Client, timelineType string, maxID string, sinceID string, minID string) (err error) {
216
217         var hasNext, hasPrev bool
218         var nextLink, prevLink string
219
220         var pg = mastodon.Pagination{
221                 MaxID: maxID,
222                 MinID: minID,
223                 Limit: 20,
224         }
225
226         var statuses []*mastodon.Status
227         var title string
228         switch timelineType {
229         default:
230                 return ErrInvalidTimeline
231         case "home":
232                 statuses, err = c.GetTimelineHome(ctx, &pg)
233                 title = "Timeline"
234         case "local":
235                 statuses, err = c.GetTimelinePublic(ctx, true, &pg)
236                 title = "Local Timeline"
237         case "twkn":
238                 statuses, err = c.GetTimelinePublic(ctx, false, &pg)
239                 title = "The Whole Known Network"
240         }
241         if err != nil {
242                 return err
243         }
244
245         if len(maxID) > 0 && len(statuses) > 0 {
246                 hasPrev = true
247                 prevLink = fmt.Sprintf("/timeline/$s?min_id=%s", timelineType, statuses[0].ID)
248         }
249         if len(minID) > 0 && len(pg.MinID) > 0 {
250                 newStatuses, err := c.GetTimelineHome(ctx, &mastodon.Pagination{MinID: pg.MinID, Limit: 20})
251                 if err != nil {
252                         return err
253                 }
254                 newStatusesLen := len(newStatuses)
255                 if newStatusesLen == 20 {
256                         hasPrev = true
257                         prevLink = fmt.Sprintf("/timeline/%s?min_id=%s", timelineType, pg.MinID)
258                 } else {
259                         i := 20 - newStatusesLen - 1
260                         if len(statuses) > i {
261                                 hasPrev = true
262                                 prevLink = fmt.Sprintf("/timeline/%s?min_id=%s", timelineType, statuses[i].ID)
263                         }
264                 }
265         }
266         if len(pg.MaxID) > 0 {
267                 hasNext = true
268                 nextLink = fmt.Sprintf("/timeline/%s?max_id=%s", timelineType, pg.MaxID)
269         }
270
271         postContext := model.PostContext{
272                 DefaultVisibility: c.Session.Settings.DefaultVisibility,
273         }
274
275         navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
276         if err != nil {
277                 return
278         }
279
280         data := &renderer.TimelineData{
281                 Title:       title,
282                 Statuses:    statuses,
283                 HasNext:     hasNext,
284                 NextLink:    nextLink,
285                 HasPrev:     hasPrev,
286                 PrevLink:    prevLink,
287                 PostContext: postContext,
288                 NavbarData:   navbarData,
289         }
290
291         err = svc.renderer.RenderTimelinePage(ctx, client, data)
292         if err != nil {
293                 return
294         }
295
296         return
297 }
298
299 func (svc *service) ServeThreadPage(ctx context.Context, client io.Writer, c *model.Client, id string, reply bool) (err error) {
300         status, err := c.GetStatus(ctx, id)
301         if err != nil {
302                 return
303         }
304
305         u, err := c.GetAccountCurrentUser(ctx)
306         if err != nil {
307                 return
308         }
309
310         var postContext model.PostContext
311         if reply {
312                 var content string
313                 if u.ID != status.Account.ID {
314                         content += "@" + status.Account.Acct + " "
315                 }
316                 for i := range status.Mentions {
317                         if status.Mentions[i].ID != u.ID && status.Mentions[i].ID != status.Account.ID {
318                                 content += "@" + status.Mentions[i].Acct + " "
319                         }
320                 }
321
322                 s, err := c.GetStatus(ctx, id)
323                 if err != nil {
324                         return err
325                 }
326
327                 postContext = model.PostContext{
328                         DefaultVisibility: s.Visibility,
329                         ReplyContext: &model.ReplyContext{
330                                 InReplyToID:   id,
331                                 InReplyToName: status.Account.Acct,
332                                 ReplyContent:  content,
333                         },
334                 }
335         }
336
337         context, err := c.GetStatusContext(ctx, id)
338         if err != nil {
339                 return
340         }
341
342         statuses := append(append(context.Ancestors, status), context.Descendants...)
343
344         replyMap := make(map[string][]mastodon.ReplyInfo)
345
346         for i := range statuses {
347                 statuses[i].ShowReplies = true
348                 statuses[i].ReplyMap = replyMap
349                 addToReplyMap(replyMap, statuses[i].InReplyToID, statuses[i].ID, i+1)
350         }
351
352         navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
353         if err != nil {
354                 return
355         }
356
357         data := &renderer.ThreadData{
358                 Statuses:    statuses,
359                 PostContext: postContext,
360                 ReplyMap:    replyMap,
361                 NavbarData:  navbarData,
362         }
363
364         err = svc.renderer.RenderThreadPage(ctx, client, data)
365         if err != nil {
366                 return
367         }
368
369         return
370 }
371
372 func (svc *service) ServeNotificationPage(ctx context.Context, client io.Writer, c *model.Client, maxID string, minID string) (err error) {
373         var hasNext bool
374         var nextLink string
375
376         var pg = mastodon.Pagination{
377                 MaxID: maxID,
378                 MinID: minID,
379                 Limit: 20,
380         }
381
382         notifications, err := c.GetNotifications(ctx, &pg)
383         if err != nil {
384                 return
385         }
386
387         var unreadCount int
388         for i := range notifications {
389                 switch notifications[i].Type {
390                 case "reblog", "favourite":
391                         if notifications[i].Status != nil {
392                                 notifications[i].Status.HideAccountInfo = true
393                         }
394                 }
395                 if notifications[i].Pleroma != nil && notifications[i].Pleroma.IsSeen {
396                         unreadCount++
397                 }
398         }
399
400         if unreadCount > 0 {
401                 err := c.ReadNotifications(ctx, notifications[0].ID)
402                 if err != nil {
403                         return err
404                 }
405         }
406
407         if len(pg.MaxID) > 0 {
408                 hasNext = true
409                 nextLink = "/notifications?max_id=" + pg.MaxID
410         }
411
412         navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
413         if err != nil {
414                 return
415         }
416
417         data := &renderer.NotificationData{
418                 Notifications: notifications,
419                 HasNext:       hasNext,
420                 NextLink:      nextLink,
421                 NavbarData:    navbarData,
422         }
423         err = svc.renderer.RenderNotificationPage(ctx, client, data)
424         if err != nil {
425                 return
426         }
427
428         return
429 }
430
431 func (svc *service) ServeUserPage(ctx context.Context, client io.Writer, c *model.Client, id string, maxID string, minID string) (err error) {
432         user, err := c.GetAccount(ctx, id)
433         if err != nil {
434                 return
435         }
436
437         var hasNext bool
438         var nextLink string
439
440         var pg = mastodon.Pagination{
441                 MaxID: maxID,
442                 MinID: minID,
443                 Limit: 20,
444         }
445
446         statuses, err := c.GetAccountStatuses(ctx, id, &pg)
447         if err != nil {
448                 return
449         }
450
451         if len(pg.MaxID) > 0 {
452                 hasNext = true
453                 nextLink = "/user/" + id + "?max_id=" + pg.MaxID
454         }
455
456         navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
457         if err != nil {
458                 return
459         }
460
461         data := &renderer.UserData{
462                 User:       user,
463                 Statuses:   statuses,
464                 HasNext:    hasNext,
465                 NextLink:   nextLink,
466                 NavbarData: navbarData,
467         }
468
469         err = svc.renderer.RenderUserPage(ctx, client, data)
470         if err != nil {
471                 return
472         }
473
474         return
475 }
476
477 func (svc *service) ServeAboutPage(ctx context.Context, client io.Writer, c *model.Client) (err error) {
478         navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
479         if err != nil {
480                 return
481         }
482
483         data := &renderer.AboutData{
484                 NavbarData: navbarData,
485         }
486         err = svc.renderer.RenderAboutPage(ctx, client, data)
487         if err != nil {
488                 return
489         }
490
491         return
492 }
493
494 func (svc *service) ServeEmojiPage(ctx context.Context, client io.Writer, c *model.Client) (err error) {
495         navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
496         if err != nil {
497                 return
498         }
499
500         emojis, err := c.GetInstanceEmojis(ctx)
501         if err != nil {
502                 return
503         }
504
505         data := &renderer.EmojiData{
506                 Emojis:     emojis,
507                 NavbarData: navbarData,
508         }
509
510         err = svc.renderer.RenderEmojiPage(ctx, client, data)
511         if err != nil {
512                 return
513         }
514
515         return
516 }
517
518 func (svc *service) getNavbarTemplateData(ctx context.Context, client io.Writer, c *model.Client) (data *renderer.NavbarData, err error) {
519         notifications, err := c.GetNotifications(ctx, nil)
520         if err != nil {
521                 return
522         }
523
524         var notificationCount int
525         for i := range notifications {
526                 if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen {
527                         notificationCount++
528                 }
529         }
530
531         u, err := c.GetAccountCurrentUser(ctx)
532         if err != nil {
533                 return
534         }
535
536         data = &renderer.NavbarData{
537                 User:              u,
538                 NotificationCount: notificationCount,
539         }
540
541         return
542 }
543
544 func (svc *service) Like(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
545         _, err = c.Favourite(ctx, id)
546         return
547 }
548
549 func (svc *service) UnLike(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
550         _, err = c.Unfavourite(ctx, id)
551         return
552 }
553
554 func (svc *service) Retweet(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
555         _, err = c.Reblog(ctx, id)
556         return
557 }
558
559 func (svc *service) UnRetweet(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
560         _, err = c.Unreblog(ctx, id)
561         return
562 }
563
564 func (svc *service) PostTweet(ctx context.Context, client io.Writer, c *model.Client, content string, replyToID string, visibility string, isNSFW bool, files []*multipart.FileHeader) (id string, err error) {
565         var mediaIds []string
566         for _, f := range files {
567                 a, err := c.UploadMediaFromMultipartFileHeader(ctx, f)
568                 if err != nil {
569                         return "", err
570                 }
571                 mediaIds = append(mediaIds, a.ID)
572         }
573
574         // save visibility if it's a non-reply post
575         if len(replyToID) < 1 && visibility != c.Session.Settings.DefaultVisibility {
576                 c.Session.Settings.DefaultVisibility = visibility
577                 svc.sessionRepo.Add(c.Session)
578         }
579
580         tweet := &mastodon.Toot{
581                 Status:      content,
582                 InReplyToID: replyToID,
583                 MediaIDs:    mediaIds,
584                 Visibility:  visibility,
585                 Sensitive:   isNSFW,
586         }
587
588         s, err := c.PostStatus(ctx, tweet)
589         if err != nil {
590                 return
591         }
592
593         return s.ID, nil
594 }
595
596 func (svc *service) Follow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
597         _, err = c.AccountFollow(ctx, id)
598         return
599 }
600
601 func (svc *service) UnFollow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
602         _, err = c.AccountUnfollow(ctx, id)
603         return
604 }
605
606 func addToReplyMap(m map[string][]mastodon.ReplyInfo, key interface{}, val string, number int) {
607         if key == nil {
608                 return
609         }
610
611         keyStr, ok := key.(string)
612         if !ok {
613                 return
614         }
615         _, ok = m[keyStr]
616         if !ok {
617                 m[keyStr] = []mastodon.ReplyInfo{}
618         }
619
620         m[keyStr] = append(m[keyStr], mastodon.ReplyInfo{val, number})
621 }