Use account mentions as default text in replies
[bloat] / service / service.go
1 package service
2
3 import (
4         "bytes"
5         "context"
6         "encoding/json"
7         "errors"
8         "fmt"
9         "io"
10         "net/http"
11         "net/url"
12         "path"
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 )
26
27 type Service interface {
28         ServeHomePage(ctx context.Context, client io.Writer) (err error)
29         GetAuthUrl(ctx context.Context, instance string) (url string, sessionID string, err error)
30         GetUserToken(ctx context.Context, sessionID string, c *mastodon.Client, token string) (accessToken string, err error)
31         ServeErrorPage(ctx context.Context, client io.Writer, err error)
32         ServeSigninPage(ctx context.Context, client io.Writer) (err error)
33         ServeTimelinePage(ctx context.Context, client io.Writer, c *mastodon.Client, maxID string, sinceID string, minID string) (err error)
34         ServeThreadPage(ctx context.Context, client io.Writer, c *mastodon.Client, id string, reply bool) (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) (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         if !strings.HasPrefix(instance, "https://") {
67                 instance = "https://" + instance
68         }
69
70         sessionID = util.NewSessionId()
71         err = svc.sessionRepo.Add(model.Session{
72                 ID:          sessionID,
73                 InstanceURL: instance,
74         })
75         if err != nil {
76                 return
77         }
78
79         app, err := svc.appRepo.Get(instance)
80         if err != nil {
81                 if err != model.ErrAppNotFound {
82                         return
83                 }
84
85                 var mastoApp *mastodon.Application
86                 mastoApp, err = mastodon.RegisterApp(ctx, &mastodon.AppConfig{
87                         Server:       instance,
88                         ClientName:   svc.clientName,
89                         Scopes:       svc.clientScope,
90                         Website:      svc.clientWebsite,
91                         RedirectURIs: svc.clientWebsite + "/oauth_callback",
92                 })
93                 if err != nil {
94                         return
95                 }
96
97                 app = model.App{
98                         InstanceURL:  instance,
99                         ClientID:     mastoApp.ClientID,
100                         ClientSecret: mastoApp.ClientSecret,
101                 }
102
103                 err = svc.appRepo.Add(app)
104                 if err != nil {
105                         return
106                 }
107         }
108
109         u, err := url.Parse(path.Join(instance, "/oauth/authorize"))
110         if err != nil {
111                 return
112         }
113
114         q := make(url.Values)
115         q.Set("scope", "read write follow")
116         q.Set("client_id", app.ClientID)
117         q.Set("response_type", "code")
118         q.Set("redirect_uri", svc.clientWebsite+"/oauth_callback")
119         u.RawQuery = q.Encode()
120
121         redirectUrl = u.String()
122
123         return
124 }
125
126 func (svc *service) GetUserToken(ctx context.Context, sessionID string, c *mastodon.Client,
127         code string) (token string, err error) {
128         if len(code) < 1 {
129                 err = ErrInvalidArgument
130                 return
131         }
132
133         session, err := svc.sessionRepo.Get(sessionID)
134         if err != nil {
135                 return
136         }
137
138         app, err := svc.appRepo.Get(session.InstanceURL)
139         if err != nil {
140                 return
141         }
142
143         data := &bytes.Buffer{}
144         err = json.NewEncoder(data).Encode(map[string]string{
145                 "client_id":     app.ClientID,
146                 "client_secret": app.ClientSecret,
147                 "grant_type":    "authorization_code",
148                 "code":          code,
149                 "redirect_uri":  svc.clientWebsite + "/oauth_callback",
150         })
151         if err != nil {
152                 return
153         }
154
155         resp, err := http.Post(app.InstanceURL+"/oauth/token", "application/json", data)
156         if err != nil {
157                 return
158         }
159         defer resp.Body.Close()
160
161         var res struct {
162                 AccessToken string `json:"access_token"`
163         }
164
165         err = json.NewDecoder(resp.Body).Decode(&res)
166         if err != nil {
167                 return
168         }
169         /*
170                 err = c.AuthenticateToken(ctx, code, svc.clientWebsite+"/oauth_callback")
171                 if err != nil {
172                         return
173                 }
174                 err = svc.sessionRepo.Update(sessionID, c.GetAccessToken(ctx))
175         */
176
177         return res.AccessToken, nil
178 }
179
180 func (svc *service) ServeHomePage(ctx context.Context, client io.Writer) (err error) {
181         err = svc.renderer.RenderHomePage(ctx, client)
182         if err != nil {
183                 return
184         }
185
186         return
187 }
188
189 func (svc *service) ServeErrorPage(ctx context.Context, client io.Writer, err error) {
190         svc.renderer.RenderErrorPage(ctx, client, err)
191 }
192
193 func (svc *service) ServeSigninPage(ctx context.Context, client io.Writer) (err error) {
194         err = svc.renderer.RenderSigninPage(ctx, client)
195         if err != nil {
196                 return
197         }
198
199         return
200 }
201
202 func (svc *service) ServeTimelinePage(ctx context.Context, client io.Writer,
203         c *mastodon.Client, maxID string, sinceID string, minID string) (err error) {
204
205         var hasNext, hasPrev bool
206         var nextLink, prevLink string
207
208         var pg = mastodon.Pagination{
209                 MaxID:   maxID,
210                 SinceID: sinceID,
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(pg.MaxID) > 0 {
221                 hasNext = true
222                 nextLink = fmt.Sprintf("/timeline?max_id=%s", pg.MaxID)
223         }
224         if len(pg.SinceID) > 0 {
225                 hasPrev = true
226                 prevLink = fmt.Sprintf("/timeline?since_id=%s", pg.SinceID)
227         }
228
229         data := renderer.NewTimelinePageTemplateData(statuses, hasNext, nextLink, hasPrev, prevLink)
230         err = svc.renderer.RenderTimelinePage(ctx, client, data)
231         if err != nil {
232                 return
233         }
234
235         return
236 }
237
238 func (svc *service) ServeThreadPage(ctx context.Context, client io.Writer, c *mastodon.Client, id string, reply bool) (err error) {
239         status, err := c.GetStatus(ctx, id)
240         if err != nil {
241                 return
242         }
243
244         context, err := c.GetStatusContext(ctx, id)
245         if err != nil {
246                 return
247         }
248
249         var content string
250         if reply {
251                 content += status.Account.Acct + " "
252                 for _, m := range status.Mentions {
253                         content += m.Acct + " "
254                 }
255         }
256
257         fmt.Println("content", content)
258
259         data := renderer.NewThreadPageTemplateData(status, context, reply, id, content)
260         err = svc.renderer.RenderThreadPage(ctx, client, data)
261         if err != nil {
262                 return
263         }
264
265         return
266 }
267
268 func (svc *service) Like(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
269         _, err = c.Favourite(ctx, id)
270         return
271 }
272
273 func (svc *service) UnLike(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
274         _, err = c.Unfavourite(ctx, id)
275         return
276 }
277
278 func (svc *service) Retweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
279         _, err = c.Reblog(ctx, id)
280         return
281 }
282
283 func (svc *service) UnRetweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
284         _, err = c.Unreblog(ctx, id)
285         return
286 }
287
288 func (svc *service) PostTweet(ctx context.Context, client io.Writer, c *mastodon.Client, content string, replyToID string) (err error) {
289         tweet := &mastodon.Toot{
290                 Status:      content,
291                 InReplyToID: replyToID,
292         }
293         _, err = c.PostStatus(ctx, tweet)
294         return
295 }