e502b65f3c86da1253b12fc6022f2056cd6d2051
[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         "path"
14         "strings"
15
16         "mastodon"
17         "web/model"
18         "web/renderer"
19         "web/util"
20 )
21
22 var (
23         ErrInvalidArgument = errors.New("invalid argument")
24         ErrInvalidToken    = errors.New("invalid token")
25         ErrInvalidClient   = errors.New("invalid client")
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 *mastodon.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 *mastodon.Client, maxID string, sinceID string, minID string) (err error)
35         ServeThreadPage(ctx context.Context, client io.Writer, c *mastodon.Client, id string, reply bool) (err error)
36         Like(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error)
37         UnLike(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error)
38         Retweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error)
39         UnRetweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error)
40         PostTweet(ctx context.Context, client io.Writer, c *mastodon.Client, content string, replyToID string, files []*multipart.FileHeader) (id string, err error)
41 }
42
43 type service struct {
44         clientName    string
45         clientScope   string
46         clientWebsite string
47         renderer      renderer.Renderer
48         sessionRepo   model.SessionRepository
49         appRepo       model.AppRepository
50 }
51
52 func NewService(clientName string, clientScope string, clientWebsite string,
53         renderer renderer.Renderer, sessionRepo model.SessionRepository,
54         appRepo model.AppRepository) Service {
55         return &service{
56                 clientName:    clientName,
57                 clientScope:   clientScope,
58                 clientWebsite: clientWebsite,
59                 renderer:      renderer,
60                 sessionRepo:   sessionRepo,
61                 appRepo:       appRepo,
62         }
63 }
64
65 func (svc *service) GetAuthUrl(ctx context.Context, instance string) (
66         redirectUrl string, sessionID string, err error) {
67         if !strings.HasPrefix(instance, "https://") {
68                 instance = "https://" + instance
69         }
70
71         sessionID = util.NewSessionId()
72         err = svc.sessionRepo.Add(model.Session{
73                 ID:          sessionID,
74                 InstanceURL: instance,
75         })
76         if err != nil {
77                 return
78         }
79
80         app, err := svc.appRepo.Get(instance)
81         if err != nil {
82                 if err != model.ErrAppNotFound {
83                         return
84                 }
85
86                 var mastoApp *mastodon.Application
87                 mastoApp, err = mastodon.RegisterApp(ctx, &mastodon.AppConfig{
88                         Server:       instance,
89                         ClientName:   svc.clientName,
90                         Scopes:       svc.clientScope,
91                         Website:      svc.clientWebsite,
92                         RedirectURIs: svc.clientWebsite + "/oauth_callback",
93                 })
94                 if err != nil {
95                         return
96                 }
97
98                 app = model.App{
99                         InstanceURL:  instance,
100                         ClientID:     mastoApp.ClientID,
101                         ClientSecret: mastoApp.ClientSecret,
102                 }
103
104                 err = svc.appRepo.Add(app)
105                 if err != nil {
106                         return
107                 }
108         }
109
110         u, err := url.Parse(path.Join(instance, "/oauth/authorize"))
111         if err != nil {
112                 return
113         }
114
115         q := make(url.Values)
116         q.Set("scope", "read write follow")
117         q.Set("client_id", app.ClientID)
118         q.Set("response_type", "code")
119         q.Set("redirect_uri", svc.clientWebsite+"/oauth_callback")
120         u.RawQuery = q.Encode()
121
122         redirectUrl = u.String()
123
124         return
125 }
126
127 func (svc *service) GetUserToken(ctx context.Context, sessionID string, c *mastodon.Client,
128         code string) (token string, err error) {
129         if len(code) < 1 {
130                 err = ErrInvalidArgument
131                 return
132         }
133
134         session, err := svc.sessionRepo.Get(sessionID)
135         if err != nil {
136                 return
137         }
138
139         app, err := svc.appRepo.Get(session.InstanceURL)
140         if err != nil {
141                 return
142         }
143
144         data := &bytes.Buffer{}
145         err = json.NewEncoder(data).Encode(map[string]string{
146                 "client_id":     app.ClientID,
147                 "client_secret": app.ClientSecret,
148                 "grant_type":    "authorization_code",
149                 "code":          code,
150                 "redirect_uri":  svc.clientWebsite + "/oauth_callback",
151         })
152         if err != nil {
153                 return
154         }
155
156         resp, err := http.Post(app.InstanceURL+"/oauth/token", "application/json", data)
157         if err != nil {
158                 return
159         }
160         defer resp.Body.Close()
161
162         var res struct {
163                 AccessToken string `json:"access_token"`
164         }
165
166         err = json.NewDecoder(resp.Body).Decode(&res)
167         if err != nil {
168                 return
169         }
170         /*
171                 err = c.AuthenticateToken(ctx, code, svc.clientWebsite+"/oauth_callback")
172                 if err != nil {
173                         return
174                 }
175                 err = svc.sessionRepo.Update(sessionID, c.GetAccessToken(ctx))
176         */
177
178         return res.AccessToken, nil
179 }
180
181 func (svc *service) ServeHomePage(ctx context.Context, client io.Writer) (err error) {
182         err = svc.renderer.RenderHomePage(ctx, client)
183         if err != nil {
184                 return
185         }
186
187         return
188 }
189
190 func (svc *service) ServeErrorPage(ctx context.Context, client io.Writer, err error) {
191         svc.renderer.RenderErrorPage(ctx, client, err)
192 }
193
194 func (svc *service) ServeSigninPage(ctx context.Context, client io.Writer) (err error) {
195         err = svc.renderer.RenderSigninPage(ctx, client)
196         if err != nil {
197                 return
198         }
199
200         return
201 }
202
203 func (svc *service) ServeTimelinePage(ctx context.Context, client io.Writer,
204         c *mastodon.Client, maxID string, sinceID string, minID string) (err error) {
205
206         var hasNext, hasPrev bool
207         var nextLink, prevLink string
208
209         var pg = mastodon.Pagination{
210                 MaxID: maxID,
211                 MinID: minID,
212                 Limit: 20,
213         }
214
215         statuses, err := c.GetTimelineHome(ctx, &pg)
216         if err != nil {
217                 return err
218         }
219
220         if len(maxID) > 0 && len(statuses) > 0 {
221                 hasPrev = true
222                 prevLink = fmt.Sprintf("/timeline?min_id=%s", statuses[0].ID)
223         }
224         if len(minID) > 0 && len(pg.MinID) > 0 {
225                 newStatuses, err := c.GetTimelineHome(ctx, &mastodon.Pagination{MinID: pg.MinID, Limit: 20})
226                 if err != nil {
227                         return err
228                 }
229                 newStatusesLen := len(newStatuses)
230                 if newStatusesLen == 20 {
231                         hasPrev = true
232                         prevLink = fmt.Sprintf("/timeline?min_id=%s", pg.MinID)
233                 } else {
234                         i := 20 - newStatusesLen - 1
235                         if len(statuses) > i {
236                                 hasPrev = true
237                                 prevLink = fmt.Sprintf("/timeline?min_id=%s", statuses[i].ID)
238                         }
239                 }
240         }
241         if len(pg.MaxID) > 0 {
242                 hasNext = true
243                 nextLink = fmt.Sprintf("/timeline?max_id=%s", pg.MaxID)
244         }
245
246         data := renderer.NewTimelinePageTemplateData(statuses, hasNext, nextLink, hasPrev, prevLink)
247         err = svc.renderer.RenderTimelinePage(ctx, client, data)
248         if err != nil {
249                 return
250         }
251
252         return
253 }
254
255 func (svc *service) ServeThreadPage(ctx context.Context, client io.Writer, c *mastodon.Client, id string, reply bool) (err error) {
256         status, err := c.GetStatus(ctx, id)
257         if err != nil {
258                 return
259         }
260
261         context, err := c.GetStatusContext(ctx, id)
262         if err != nil {
263                 return
264         }
265
266         u, err := c.GetAccountCurrentUser(ctx)
267         if err != nil {
268                 return
269         }
270
271         var content string
272         if reply {
273                 if u.ID != status.Account.ID {
274                         content += "@" + status.Account.Acct + " "
275                 }
276                 for _, m := range status.Mentions {
277                         if u.ID != m.ID {
278                                 content += "@" + m.Acct + " "
279                         }
280                 }
281         }
282
283         data := renderer.NewThreadPageTemplateData(status, context, reply, id, content)
284         err = svc.renderer.RenderThreadPage(ctx, client, data)
285         if err != nil {
286                 return
287         }
288
289         return
290 }
291
292 func (svc *service) Like(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
293         _, err = c.Favourite(ctx, id)
294         return
295 }
296
297 func (svc *service) UnLike(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
298         _, err = c.Unfavourite(ctx, id)
299         return
300 }
301
302 func (svc *service) Retweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
303         _, err = c.Reblog(ctx, id)
304         return
305 }
306
307 func (svc *service) UnRetweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
308         _, err = c.Unreblog(ctx, id)
309         return
310 }
311
312 func (svc *service) PostTweet(ctx context.Context, client io.Writer, c *mastodon.Client, content string, replyToID string, files []*multipart.FileHeader) (id string, err error) {
313         var mediaIds []string
314         for _, f := range files {
315                 a, err := c.UploadMediaFromMultipartFileHeader(ctx, f)
316                 if err != nil {
317                         return "", err
318                 }
319                 mediaIds = append(mediaIds, a.ID)
320         }
321
322         tweet := &mastodon.Toot{
323                 Status:      content,
324                 InReplyToID: replyToID,
325                 MediaIDs:    mediaIds,
326         }
327
328         s, err := c.PostStatus(ctx, tweet)
329         if err != nil {
330                 return
331         }
332
333         return s.ID, nil
334 }