21 ErrInvalidArgument = errors.New("invalid argument")
22 ErrInvalidToken = errors.New("invalid token")
23 ErrInvalidClient = errors.New("invalid client")
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 *model.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 *model.Client, maxID string, sinceID string, minID string) (err error)
33 ServeThreadPage(ctx context.Context, client io.Writer, c *model.Client, id string, reply bool) (err error)
34 ServeNotificationPage(ctx context.Context, client io.Writer, c *model.Client, maxID string, minID string) (err error)
35 ServeUserPage(ctx context.Context, client io.Writer, c *model.Client, id string, maxID string, minID string) (err error)
36 ServeAboutPage(ctx context.Context, client io.Writer, c *model.Client) (err error)
37 Like(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
38 UnLike(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
39 Retweet(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
40 UnRetweet(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
41 PostTweet(ctx context.Context, client io.Writer, c *model.Client, content string, replyToID string, files []*multipart.FileHeader) (id string, err error)
42 Follow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
43 UnFollow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
50 renderer renderer.Renderer
51 sessionRepo model.SessionRepository
52 appRepo model.AppRepository
55 func NewService(clientName string, clientScope string, clientWebsite string,
56 renderer renderer.Renderer, sessionRepo model.SessionRepository,
57 appRepo model.AppRepository) Service {
59 clientName: clientName,
60 clientScope: clientScope,
61 clientWebsite: clientWebsite,
63 sessionRepo: sessionRepo,
68 func (svc *service) GetAuthUrl(ctx context.Context, instance string) (
69 redirectUrl string, sessionID string, err error) {
70 var instanceURL string
71 if strings.HasPrefix(instance, "https://") {
72 instanceURL = instance
73 instance = strings.TrimPrefix(instance, "https://")
75 instanceURL = "https://" + instance
78 sessionID = util.NewSessionId()
79 err = svc.sessionRepo.Add(model.Session{
81 InstanceDomain: instance,
87 app, err := svc.appRepo.Get(instance)
89 if err != model.ErrAppNotFound {
93 var mastoApp *mastodon.Application
94 mastoApp, err = mastodon.RegisterApp(ctx, &mastodon.AppConfig{
96 ClientName: svc.clientName,
97 Scopes: svc.clientScope,
98 Website: svc.clientWebsite,
99 RedirectURIs: svc.clientWebsite + "/oauth_callback",
106 InstanceDomain: instance,
107 InstanceURL: instanceURL,
108 ClientID: mastoApp.ClientID,
109 ClientSecret: mastoApp.ClientSecret,
112 err = svc.appRepo.Add(app)
118 u, err := url.Parse("/oauth/authorize")
123 q := make(url.Values)
124 q.Set("scope", "read write follow")
125 q.Set("client_id", app.ClientID)
126 q.Set("response_type", "code")
127 q.Set("redirect_uri", svc.clientWebsite+"/oauth_callback")
128 u.RawQuery = q.Encode()
130 redirectUrl = instanceURL + u.String()
135 func (svc *service) GetUserToken(ctx context.Context, sessionID string, c *model.Client,
136 code string) (token string, err error) {
138 err = ErrInvalidArgument
142 session, err := svc.sessionRepo.Get(sessionID)
147 app, err := svc.appRepo.Get(session.InstanceDomain)
152 data := &bytes.Buffer{}
153 err = json.NewEncoder(data).Encode(map[string]string{
154 "client_id": app.ClientID,
155 "client_secret": app.ClientSecret,
156 "grant_type": "authorization_code",
158 "redirect_uri": svc.clientWebsite + "/oauth_callback",
164 resp, err := http.Post(app.InstanceURL+"/oauth/token", "application/json", data)
168 defer resp.Body.Close()
171 AccessToken string `json:"access_token"`
174 err = json.NewDecoder(resp.Body).Decode(&res)
179 err = c.AuthenticateToken(ctx, code, svc.clientWebsite+"/oauth_callback")
183 err = svc.sessionRepo.Update(sessionID, c.GetAccessToken(ctx))
186 return res.AccessToken, nil
189 func (svc *service) ServeHomePage(ctx context.Context, client io.Writer) (err error) {
190 err = svc.renderer.RenderHomePage(ctx, client)
198 func (svc *service) ServeErrorPage(ctx context.Context, client io.Writer, err error) {
199 svc.renderer.RenderErrorPage(ctx, client, err)
202 func (svc *service) ServeSigninPage(ctx context.Context, client io.Writer) (err error) {
203 err = svc.renderer.RenderSigninPage(ctx, client)
211 func (svc *service) ServeTimelinePage(ctx context.Context, client io.Writer,
212 c *model.Client, maxID string, sinceID string, minID string) (err error) {
214 var hasNext, hasPrev bool
215 var nextLink, prevLink string
217 var pg = mastodon.Pagination{
223 statuses, err := c.GetTimelineHome(ctx, &pg)
228 if len(maxID) > 0 && len(statuses) > 0 {
230 prevLink = "/timeline?min_id=" + statuses[0].ID
232 if len(minID) > 0 && len(pg.MinID) > 0 {
233 newStatuses, err := c.GetTimelineHome(ctx, &mastodon.Pagination{MinID: pg.MinID, Limit: 20})
237 newStatusesLen := len(newStatuses)
238 if newStatusesLen == 20 {
240 prevLink = "/timeline?min_id=" + pg.MinID
242 i := 20 - newStatusesLen - 1
243 if len(statuses) > i {
245 prevLink = "/timeline?min_id=" + statuses[i].ID
249 if len(pg.MaxID) > 0 {
251 nextLink = "/timeline?max_id=" + pg.MaxID
254 navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
259 data := renderer.NewTimelinePageTemplateData(statuses, hasNext, nextLink, hasPrev, prevLink, navbarData)
260 err = svc.renderer.RenderTimelinePage(ctx, client, data)
268 func (svc *service) ServeThreadPage(ctx context.Context, client io.Writer, c *model.Client, id string, reply bool) (err error) {
269 status, err := c.GetStatus(ctx, id)
274 u, err := c.GetAccountCurrentUser(ctx)
279 var replyContext *model.ReplyContext
282 if u.ID != status.Account.ID {
283 content += "@" + status.Account.Acct + " "
285 for i := range status.Mentions {
286 if status.Mentions[i].ID != u.ID && status.Mentions[i].ID != status.Account.ID {
287 content += "@" + status.Mentions[i].Acct + " "
290 replyContext = &model.ReplyContext{
292 InReplyToName: status.Account.Acct,
293 ReplyContent: content,
297 context, err := c.GetStatusContext(ctx, id)
302 statuses := append(append(context.Ancestors, status), context.Descendants...)
304 replyMap := make(map[string][]mastodon.ReplyInfo)
306 for i := range statuses {
307 statuses[i].ShowReplies = true
308 statuses[i].ReplyMap = replyMap
309 addToReplyMap(replyMap, statuses[i].InReplyToID, statuses[i].ID, i+1)
312 navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
317 data := renderer.NewThreadPageTemplateData(statuses, replyContext, replyMap, navbarData)
318 err = svc.renderer.RenderThreadPage(ctx, client, data)
326 func (svc *service) ServeNotificationPage(ctx context.Context, client io.Writer, c *model.Client, maxID string, minID string) (err error) {
330 var pg = mastodon.Pagination{
336 notifications, err := c.GetNotifications(ctx, &pg)
342 for i := range notifications {
343 switch notifications[i].Type {
344 case "reblog", "favourite":
345 if notifications[i].Status != nil {
346 notifications[i].Status.HideAccountInfo = true
349 if notifications[i].Pleroma != nil && notifications[i].Pleroma.IsSeen {
355 err := c.ReadNotifications(ctx, notifications[0].ID)
361 if len(pg.MaxID) > 0 {
363 nextLink = "/notifications?max_id=" + pg.MaxID
366 navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
371 data := renderer.NewNotificationPageTemplateData(notifications, hasNext, nextLink, navbarData)
372 err = svc.renderer.RenderNotificationPage(ctx, client, data)
380 func (svc *service) ServeUserPage(ctx context.Context, client io.Writer, c *model.Client, id string, maxID string, minID string) (err error) {
381 user, err := c.GetAccount(ctx, id)
389 var pg = mastodon.Pagination{
395 statuses, err := c.GetAccountStatuses(ctx, id, &pg)
400 if len(pg.MaxID) > 0 {
402 nextLink = "/user/" + id + "?max_id=" + pg.MaxID
405 navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
410 data := renderer.NewUserPageTemplateData(user, statuses, hasNext, nextLink, navbarData)
411 err = svc.renderer.RenderUserPage(ctx, client, data)
419 func (svc *service) ServeAboutPage(ctx context.Context, client io.Writer, c *model.Client) (err error) {
420 navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
425 data := renderer.NewAboutPageTemplateData(navbarData)
426 err = svc.renderer.RenderAboutPage(ctx, client, data)
434 func (svc *service) getNavbarTemplateData(ctx context.Context, client io.Writer, c *model.Client) (data *renderer.NavbarTemplateData, err error) {
435 notifications, err := c.GetNotifications(ctx, nil)
440 var notificationCount int
441 for i := range notifications {
442 if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen {
447 data = renderer.NewNavbarTemplateData(notificationCount)
452 func (svc *service) Like(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
453 _, err = c.Favourite(ctx, id)
457 func (svc *service) UnLike(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
458 _, err = c.Unfavourite(ctx, id)
462 func (svc *service) Retweet(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
463 _, err = c.Reblog(ctx, id)
467 func (svc *service) UnRetweet(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
468 _, err = c.Unreblog(ctx, id)
472 func (svc *service) PostTweet(ctx context.Context, client io.Writer, c *model.Client, content string, replyToID string, files []*multipart.FileHeader) (id string, err error) {
473 var mediaIds []string
474 for _, f := range files {
475 a, err := c.UploadMediaFromMultipartFileHeader(ctx, f)
479 mediaIds = append(mediaIds, a.ID)
482 tweet := &mastodon.Toot{
484 InReplyToID: replyToID,
488 s, err := c.PostStatus(ctx, tweet)
496 func (svc *service) Follow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
497 _, err = c.AccountFollow(ctx, id)
501 func (svc *service) UnFollow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
502 _, err = c.AccountUnfollow(ctx, id)
506 func addToReplyMap(m map[string][]mastodon.ReplyInfo, key interface{}, val string, number int) {
511 keyStr, ok := key.(string)
517 m[keyStr] = []mastodon.ReplyInfo{}
520 m[keyStr] = append(m[keyStr], mastodon.ReplyInfo{val, number})