22 ErrInvalidArgument = errors.New("invalid argument")
23 ErrInvalidToken = errors.New("invalid token")
24 ErrInvalidClient = errors.New("invalid client")
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 ServeNotificationPage(ctx context.Context, client io.Writer, c *mastodon.Client, maxID string, minID string) (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)
47 renderer renderer.Renderer
48 sessionRepo model.SessionRepository
49 appRepo model.AppRepository
52 func NewService(clientName string, clientScope string, clientWebsite string,
53 renderer renderer.Renderer, sessionRepo model.SessionRepository,
54 appRepo model.AppRepository) Service {
56 clientName: clientName,
57 clientScope: clientScope,
58 clientWebsite: clientWebsite,
60 sessionRepo: sessionRepo,
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
71 sessionID = util.NewSessionId()
72 err = svc.sessionRepo.Add(model.Session{
74 InstanceURL: instance,
80 app, err := svc.appRepo.Get(instance)
82 if err != model.ErrAppNotFound {
86 var mastoApp *mastodon.Application
87 mastoApp, err = mastodon.RegisterApp(ctx, &mastodon.AppConfig{
89 ClientName: svc.clientName,
90 Scopes: svc.clientScope,
91 Website: svc.clientWebsite,
92 RedirectURIs: svc.clientWebsite + "/oauth_callback",
99 InstanceURL: instance,
100 ClientID: mastoApp.ClientID,
101 ClientSecret: mastoApp.ClientSecret,
104 err = svc.appRepo.Add(app)
110 u, err := url.Parse("/oauth/authorize")
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()
122 redirectUrl = instanceURL + u.String()
127 func (svc *service) GetUserToken(ctx context.Context, sessionID string, c *mastodon.Client,
128 code string) (token string, err error) {
130 err = ErrInvalidArgument
134 session, err := svc.sessionRepo.Get(sessionID)
139 app, err := svc.appRepo.Get(session.InstanceURL)
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",
150 "redirect_uri": svc.clientWebsite + "/oauth_callback",
156 resp, err := http.Post(app.InstanceURL+"/oauth/token", "application/json", data)
160 defer resp.Body.Close()
163 AccessToken string `json:"access_token"`
166 err = json.NewDecoder(resp.Body).Decode(&res)
171 err = c.AuthenticateToken(ctx, code, svc.clientWebsite+"/oauth_callback")
175 err = svc.sessionRepo.Update(sessionID, c.GetAccessToken(ctx))
178 return res.AccessToken, nil
181 func (svc *service) ServeHomePage(ctx context.Context, client io.Writer) (err error) {
182 err = svc.renderer.RenderHomePage(ctx, client)
190 func (svc *service) ServeErrorPage(ctx context.Context, client io.Writer, err error) {
191 svc.renderer.RenderErrorPage(ctx, client, err)
194 func (svc *service) ServeSigninPage(ctx context.Context, client io.Writer) (err error) {
195 err = svc.renderer.RenderSigninPage(ctx, client)
203 func (svc *service) ServeTimelinePage(ctx context.Context, client io.Writer,
204 c *mastodon.Client, maxID string, sinceID string, minID string) (err error) {
206 var hasNext, hasPrev bool
207 var nextLink, prevLink string
209 var pg = mastodon.Pagination{
215 statuses, err := c.GetTimelineHome(ctx, &pg)
220 if len(maxID) > 0 && len(statuses) > 0 {
222 prevLink = "/timeline?min_id=" + statuses[0].ID
224 if len(minID) > 0 && len(pg.MinID) > 0 {
225 newStatuses, err := c.GetTimelineHome(ctx, &mastodon.Pagination{MinID: pg.MinID, Limit: 20})
229 newStatusesLen := len(newStatuses)
230 if newStatusesLen == 20 {
232 prevLink = "/timeline?min_id=" + pg.MinID
234 i := 20 - newStatusesLen - 1
235 if len(statuses) > i {
237 prevLink = "/timeline?min_id=" + statuses[i].ID
241 if len(pg.MaxID) > 0 {
243 nextLink = "/timeline?max_id=" + pg.MaxID
246 navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
251 data := renderer.NewTimelinePageTemplateData(statuses, hasNext, nextLink, hasPrev, prevLink, navbarData)
252 err = svc.renderer.RenderTimelinePage(ctx, client, data)
260 func (svc *service) ServeThreadPage(ctx context.Context, client io.Writer, c *mastodon.Client, id string, reply bool) (err error) {
261 status, err := c.GetStatus(ctx, id)
266 context, err := c.GetStatusContext(ctx, id)
271 u, err := c.GetAccountCurrentUser(ctx)
278 if u.ID != status.Account.ID {
279 content += "@" + status.Account.Acct + " "
281 for _, m := range status.Mentions {
283 content += "@" + m.Acct + " "
288 navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
293 data := renderer.NewThreadPageTemplateData(status, context, reply, id, content, navbarData)
294 err = svc.renderer.RenderThreadPage(ctx, client, data)
302 func (svc *service) ServeNotificationPage(ctx context.Context, client io.Writer, c *mastodon.Client, maxID string, minID string) (err error) {
306 var pg = mastodon.Pagination{
312 notifications, err := c.GetNotifications(ctx, &pg)
318 for i := range notifications {
319 switch notifications[i].Type {
320 case "reblog", "favourite":
321 if notifications[i].Status != nil {
322 notifications[i].Status.Account.ID = ""
325 if notifications[i].Pleroma != nil && notifications[i].Pleroma.IsSeen {
331 err := c.ReadNotifications(ctx, notifications[0].ID)
337 if len(pg.MaxID) > 0 {
339 nextLink = "/notifications?max_id=" + pg.MaxID
342 navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
347 data := renderer.NewNotificationPageTemplateData(notifications, hasNext, nextLink, navbarData)
348 err = svc.renderer.RenderNotificationPage(ctx, client, data)
356 func (svc *service) getNavbarTemplateData(ctx context.Context, client io.Writer, c *mastodon.Client) (data *renderer.NavbarTemplateData, err error) {
357 notifications, err := c.GetNotifications(ctx, nil)
362 var notificationCount int
363 for i := range notifications {
364 if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen {
369 data = renderer.NewNavbarTemplateData(notificationCount)
374 func (svc *service) Like(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
375 _, err = c.Favourite(ctx, id)
379 func (svc *service) UnLike(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
380 _, err = c.Unfavourite(ctx, id)
384 func (svc *service) Retweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
385 _, err = c.Reblog(ctx, id)
389 func (svc *service) UnRetweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
390 _, err = c.Unreblog(ctx, id)
394 func (svc *service) PostTweet(ctx context.Context, client io.Writer, c *mastodon.Client, content string, replyToID string, files []*multipart.FileHeader) (id string, err error) {
395 var mediaIds []string
396 for _, f := range files {
397 a, err := c.UploadMediaFromMultipartFileHeader(ctx, f)
401 mediaIds = append(mediaIds, a.ID)
404 tweet := &mastodon.Toot{
406 InReplyToID: replyToID,
410 s, err := c.PostStatus(ctx, tweet)