bb03c263401347ed6e37b78c49229d62017e15d0
[bloat] / service / service.go
1 package service
2
3 import (
4         "bytes"
5         "context"
6         "encoding/json"
7         "errors"
8         "io"
9         "mime/multipart"
10         "net/http"
11         "net/url"
12         "strings"
13
14         "mastodon"
15         "web/model"
16         "web/renderer"
17         "web/util"
18 )
19
20 var (
21         ErrInvalidArgument = errors.New("invalid argument")
22         ErrInvalidToken    = errors.New("invalid token")
23         ErrInvalidClient   = errors.New("invalid client")
24 )
25
26 type Service interface {
27         ServeHomePage(ctx context.Context, client io.Writer) (err error)
28         GetAuthUrl(ctx context.Context, instance string) (url string, sessionID string, err error)
29         GetUserToken(ctx context.Context, sessionID string, c *mastodon.Client, token string) (accessToken string, err error)
30         ServeErrorPage(ctx context.Context, client io.Writer, err error)
31         ServeSigninPage(ctx context.Context, client io.Writer) (err error)
32         ServeTimelinePage(ctx context.Context, client io.Writer, c *mastodon.Client, maxID string, sinceID string, minID string) (err error)
33         ServeThreadPage(ctx context.Context, client io.Writer, c *mastodon.Client, id string, reply bool) (err error)
34         ServeNotificationPage(ctx context.Context, client io.Writer, c *mastodon.Client, maxID string, minID string) (err error)
35         Like(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error)
36         UnLike(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error)
37         Retweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error)
38         UnRetweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error)
39         PostTweet(ctx context.Context, client io.Writer, c *mastodon.Client, content string, replyToID string, files []*multipart.FileHeader) (id string, err error)
40 }
41
42 type service struct {
43         clientName    string
44         clientScope   string
45         clientWebsite string
46         renderer      renderer.Renderer
47         sessionRepo   model.SessionRepository
48         appRepo       model.AppRepository
49 }
50
51 func NewService(clientName string, clientScope string, clientWebsite string,
52         renderer renderer.Renderer, sessionRepo model.SessionRepository,
53         appRepo model.AppRepository) Service {
54         return &service{
55                 clientName:    clientName,
56                 clientScope:   clientScope,
57                 clientWebsite: clientWebsite,
58                 renderer:      renderer,
59                 sessionRepo:   sessionRepo,
60                 appRepo:       appRepo,
61         }
62 }
63
64 func (svc *service) GetAuthUrl(ctx context.Context, instance string) (
65         redirectUrl string, sessionID string, err error) {
66         var instanceURL string
67         if strings.HasPrefix(instance, "https://") {
68                 instanceURL = instance
69                 instance = strings.TrimPrefix(instance, "https://")
70         } else {
71                 instanceURL = "https://" + instance
72         }
73
74         sessionID = util.NewSessionId()
75         err = svc.sessionRepo.Add(model.Session{
76                 ID:             sessionID,
77                 InstanceDomain: instance,
78         })
79         if err != nil {
80                 return
81         }
82
83         app, err := svc.appRepo.Get(instance)
84         if err != nil {
85                 if err != model.ErrAppNotFound {
86                         return
87                 }
88
89                 var mastoApp *mastodon.Application
90                 mastoApp, err = mastodon.RegisterApp(ctx, &mastodon.AppConfig{
91                         Server:       instanceURL,
92                         ClientName:   svc.clientName,
93                         Scopes:       svc.clientScope,
94                         Website:      svc.clientWebsite,
95                         RedirectURIs: svc.clientWebsite + "/oauth_callback",
96                 })
97                 if err != nil {
98                         return
99                 }
100
101                 app = model.App{
102                         InstanceDomain: instance,
103                         InstanceURL:    instanceURL,
104                         ClientID:       mastoApp.ClientID,
105                         ClientSecret:   mastoApp.ClientSecret,
106                 }
107
108                 err = svc.appRepo.Add(app)
109                 if err != nil {
110                         return
111                 }
112         }
113
114         u, err := url.Parse("/oauth/authorize")
115         if err != nil {
116                 return
117         }
118
119         q := make(url.Values)
120         q.Set("scope", "read write follow")
121         q.Set("client_id", app.ClientID)
122         q.Set("response_type", "code")
123         q.Set("redirect_uri", svc.clientWebsite+"/oauth_callback")
124         u.RawQuery = q.Encode()
125
126         redirectUrl = instanceURL + u.String()
127
128         return
129 }
130
131 func (svc *service) GetUserToken(ctx context.Context, sessionID string, c *mastodon.Client,
132         code string) (token string, err error) {
133         if len(code) < 1 {
134                 err = ErrInvalidArgument
135                 return
136         }
137
138         session, err := svc.sessionRepo.Get(sessionID)
139         if err != nil {
140                 return
141         }
142
143         app, err := svc.appRepo.Get(session.InstanceDomain)
144         if err != nil {
145                 return
146         }
147
148         data := &bytes.Buffer{}
149         err = json.NewEncoder(data).Encode(map[string]string{
150                 "client_id":     app.ClientID,
151                 "client_secret": app.ClientSecret,
152                 "grant_type":    "authorization_code",
153                 "code":          code,
154                 "redirect_uri":  svc.clientWebsite + "/oauth_callback",
155         })
156         if err != nil {
157                 return
158         }
159
160         resp, err := http.Post(app.InstanceURL+"/oauth/token", "application/json", data)
161         if err != nil {
162                 return
163         }
164         defer resp.Body.Close()
165
166         var res struct {
167                 AccessToken string `json:"access_token"`
168         }
169
170         err = json.NewDecoder(resp.Body).Decode(&res)
171         if err != nil {
172                 return
173         }
174         /*
175                 err = c.AuthenticateToken(ctx, code, svc.clientWebsite+"/oauth_callback")
176                 if err != nil {
177                         return
178                 }
179                 err = svc.sessionRepo.Update(sessionID, c.GetAccessToken(ctx))
180         */
181
182         return res.AccessToken, nil
183 }
184
185 func (svc *service) ServeHomePage(ctx context.Context, client io.Writer) (err error) {
186         err = svc.renderer.RenderHomePage(ctx, client)
187         if err != nil {
188                 return
189         }
190
191         return
192 }
193
194 func (svc *service) ServeErrorPage(ctx context.Context, client io.Writer, err error) {
195         svc.renderer.RenderErrorPage(ctx, client, err)
196 }
197
198 func (svc *service) ServeSigninPage(ctx context.Context, client io.Writer) (err error) {
199         err = svc.renderer.RenderSigninPage(ctx, client)
200         if err != nil {
201                 return
202         }
203
204         return
205 }
206
207 func (svc *service) ServeTimelinePage(ctx context.Context, client io.Writer,
208         c *mastodon.Client, maxID string, sinceID string, minID string) (err error) {
209
210         var hasNext, hasPrev bool
211         var nextLink, prevLink string
212
213         var pg = mastodon.Pagination{
214                 MaxID: maxID,
215                 MinID: minID,
216                 Limit: 20,
217         }
218
219         statuses, err := c.GetTimelineHome(ctx, &pg)
220         if err != nil {
221                 return err
222         }
223
224         if len(maxID) > 0 && len(statuses) > 0 {
225                 hasPrev = true
226                 prevLink = "/timeline?min_id=" + statuses[0].ID
227         }
228         if len(minID) > 0 && len(pg.MinID) > 0 {
229                 newStatuses, err := c.GetTimelineHome(ctx, &mastodon.Pagination{MinID: pg.MinID, Limit: 20})
230                 if err != nil {
231                         return err
232                 }
233                 newStatusesLen := len(newStatuses)
234                 if newStatusesLen == 20 {
235                         hasPrev = true
236                         prevLink = "/timeline?min_id=" + pg.MinID
237                 } else {
238                         i := 20 - newStatusesLen - 1
239                         if len(statuses) > i {
240                                 hasPrev = true
241                                 prevLink = "/timeline?min_id=" + statuses[i].ID
242                         }
243                 }
244         }
245         if len(pg.MaxID) > 0 {
246                 hasNext = true
247                 nextLink = "/timeline?max_id=" + pg.MaxID
248         }
249
250         navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
251         if err != nil {
252                 return
253         }
254
255         data := renderer.NewTimelinePageTemplateData(statuses, hasNext, nextLink, hasPrev, prevLink, navbarData)
256         err = svc.renderer.RenderTimelinePage(ctx, client, data)
257         if err != nil {
258                 return
259         }
260
261         return
262 }
263
264 func (svc *service) ServeThreadPage(ctx context.Context, client io.Writer, c *mastodon.Client, id string, reply bool) (err error) {
265         status, err := c.GetStatus(ctx, id)
266         if err != nil {
267                 return
268         }
269
270         context, err := c.GetStatusContext(ctx, id)
271         if err != nil {
272                 return
273         }
274
275         u, err := c.GetAccountCurrentUser(ctx)
276         if err != nil {
277                 return
278         }
279
280         var content string
281         if reply {
282                 if u.ID != status.Account.ID {
283                         content += "@" + status.Account.Acct + " "
284                 }
285                 for _, m := range status.Mentions {
286                         if u.ID != m.ID {
287                                 content += "@" + m.Acct + " "
288                         }
289                 }
290         }
291
292         navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
293         if err != nil {
294                 return
295         }
296
297         data := renderer.NewThreadPageTemplateData(status, context, reply, id, content, navbarData)
298         err = svc.renderer.RenderThreadPage(ctx, client, data)
299         if err != nil {
300                 return
301         }
302
303         return
304 }
305
306 func (svc *service) ServeNotificationPage(ctx context.Context, client io.Writer, c *mastodon.Client, maxID string, minID string) (err error) {
307         var hasNext bool
308         var nextLink string
309
310         var pg = mastodon.Pagination{
311                 MaxID: maxID,
312                 MinID: minID,
313                 Limit: 20,
314         }
315
316         notifications, err := c.GetNotifications(ctx, &pg)
317         if err != nil {
318                 return
319         }
320
321         var unreadCount int
322         for i := range notifications {
323                 switch notifications[i].Type {
324                 case "reblog", "favourite":
325                         if notifications[i].Status != nil {
326                                 notifications[i].Status.Account.ID = ""
327                         }
328                 }
329                 if notifications[i].Pleroma != nil && notifications[i].Pleroma.IsSeen {
330                         unreadCount++
331                 }
332         }
333
334         if unreadCount > 0 {
335                 err := c.ReadNotifications(ctx, notifications[0].ID)
336                 if err != nil {
337                         return err
338                 }
339         }
340
341         if len(pg.MaxID) > 0 {
342                 hasNext = true
343                 nextLink = "/notifications?max_id=" + pg.MaxID
344         }
345
346         navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
347         if err != nil {
348                 return
349         }
350
351         data := renderer.NewNotificationPageTemplateData(notifications, hasNext, nextLink, navbarData)
352         err = svc.renderer.RenderNotificationPage(ctx, client, data)
353         if err != nil {
354                 return
355         }
356
357         return
358 }
359
360 func (svc *service) getNavbarTemplateData(ctx context.Context, client io.Writer, c *mastodon.Client) (data *renderer.NavbarTemplateData, err error) {
361         notifications, err := c.GetNotifications(ctx, nil)
362         if err != nil {
363                 return
364         }
365
366         var notificationCount int
367         for i := range notifications {
368                 if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen {
369                         notificationCount++
370                 }
371         }
372
373         data = renderer.NewNavbarTemplateData(notificationCount)
374
375         return
376 }
377
378 func (svc *service) Like(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
379         _, err = c.Favourite(ctx, id)
380         return
381 }
382
383 func (svc *service) UnLike(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
384         _, err = c.Unfavourite(ctx, id)
385         return
386 }
387
388 func (svc *service) Retweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
389         _, err = c.Reblog(ctx, id)
390         return
391 }
392
393 func (svc *service) UnRetweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
394         _, err = c.Unreblog(ctx, id)
395         return
396 }
397
398 func (svc *service) PostTweet(ctx context.Context, client io.Writer, c *mastodon.Client, content string, replyToID string, files []*multipart.FileHeader) (id string, err error) {
399         var mediaIds []string
400         for _, f := range files {
401                 a, err := c.UploadMediaFromMultipartFileHeader(ctx, f)
402                 if err != nil {
403                         return "", err
404                 }
405                 mediaIds = append(mediaIds, a.ID)
406         }
407
408         tweet := &mastodon.Toot{
409                 Status:      content,
410                 InReplyToID: replyToID,
411                 MediaIDs:    mediaIds,
412         }
413
414         s, err := c.PostStatus(ctx, tweet)
415         if err != nil {
416                 return
417         }
418
419         return s.ID, nil
420 }