21 ErrInvalidArgument = errors.New("invalid argument")
22 ErrInvalidToken = errors.New("invalid token")
23 ErrInvalidClient = errors.New("invalid client")
24 ErrInvalidTimeline = errors.New("invalid timeline")
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 *model.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 *model.Client, timelineType string, maxID string, sinceID string, minID string) (err error)
34 ServeThreadPage(ctx context.Context, client io.Writer, c *model.Client, id string, reply bool) (err error)
35 ServeNotificationPage(ctx context.Context, client io.Writer, c *model.Client, maxID string, minID string) (err error)
36 ServeUserPage(ctx context.Context, client io.Writer, c *model.Client, id string, maxID string, minID string) (err error)
37 ServeAboutPage(ctx context.Context, client io.Writer, c *model.Client) (err error)
38 ServeEmojiPage(ctx context.Context, client io.Writer, c *model.Client) (err error)
39 Like(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
40 UnLike(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
41 Retweet(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
42 UnRetweet(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
43 PostTweet(ctx context.Context, client io.Writer, c *model.Client, content string, replyToID string, visibility string, isNSFW bool, files []*multipart.FileHeader) (id string, err error)
44 Follow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
45 UnFollow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
52 renderer renderer.Renderer
53 sessionRepo model.SessionRepository
54 appRepo model.AppRepository
57 func NewService(clientName string, clientScope string, clientWebsite string,
58 renderer renderer.Renderer, sessionRepo model.SessionRepository,
59 appRepo model.AppRepository) Service {
61 clientName: clientName,
62 clientScope: clientScope,
63 clientWebsite: clientWebsite,
65 sessionRepo: sessionRepo,
70 func (svc *service) GetAuthUrl(ctx context.Context, instance string) (
71 redirectUrl string, sessionID string, err error) {
72 var instanceURL string
73 if strings.HasPrefix(instance, "https://") {
74 instanceURL = instance
75 instance = strings.TrimPrefix(instance, "https://")
77 instanceURL = "https://" + instance
80 sessionID = util.NewSessionId()
81 err = svc.sessionRepo.Add(model.Session{
83 InstanceDomain: instance,
89 app, err := svc.appRepo.Get(instance)
91 if err != model.ErrAppNotFound {
95 var mastoApp *mastodon.Application
96 mastoApp, err = mastodon.RegisterApp(ctx, &mastodon.AppConfig{
98 ClientName: svc.clientName,
99 Scopes: svc.clientScope,
100 Website: svc.clientWebsite,
101 RedirectURIs: svc.clientWebsite + "/oauth_callback",
108 InstanceDomain: instance,
109 InstanceURL: instanceURL,
110 ClientID: mastoApp.ClientID,
111 ClientSecret: mastoApp.ClientSecret,
114 err = svc.appRepo.Add(app)
120 u, err := url.Parse("/oauth/authorize")
125 q := make(url.Values)
126 q.Set("scope", "read write follow")
127 q.Set("client_id", app.ClientID)
128 q.Set("response_type", "code")
129 q.Set("redirect_uri", svc.clientWebsite+"/oauth_callback")
130 u.RawQuery = q.Encode()
132 redirectUrl = instanceURL + u.String()
137 func (svc *service) GetUserToken(ctx context.Context, sessionID string, c *model.Client,
138 code string) (token string, err error) {
140 err = ErrInvalidArgument
144 session, err := svc.sessionRepo.Get(sessionID)
149 app, err := svc.appRepo.Get(session.InstanceDomain)
154 data := &bytes.Buffer{}
155 err = json.NewEncoder(data).Encode(map[string]string{
156 "client_id": app.ClientID,
157 "client_secret": app.ClientSecret,
158 "grant_type": "authorization_code",
160 "redirect_uri": svc.clientWebsite + "/oauth_callback",
166 resp, err := http.Post(app.InstanceURL+"/oauth/token", "application/json", data)
170 defer resp.Body.Close()
173 AccessToken string `json:"access_token"`
176 err = json.NewDecoder(resp.Body).Decode(&res)
181 err = c.AuthenticateToken(ctx, code, svc.clientWebsite+"/oauth_callback")
185 err = svc.sessionRepo.Update(sessionID, c.GetAccessToken(ctx))
188 return res.AccessToken, nil
191 func (svc *service) ServeHomePage(ctx context.Context, client io.Writer) (err error) {
192 err = svc.renderer.RenderHomePage(ctx, client)
200 func (svc *service) ServeErrorPage(ctx context.Context, client io.Writer, err error) {
201 svc.renderer.RenderErrorPage(ctx, client, err)
204 func (svc *service) ServeSigninPage(ctx context.Context, client io.Writer) (err error) {
205 err = svc.renderer.RenderSigninPage(ctx, client)
213 func (svc *service) ServeTimelinePage(ctx context.Context, client io.Writer,
214 c *model.Client, timelineType string, maxID string, sinceID string, minID string) (err error) {
216 var hasNext, hasPrev bool
217 var nextLink, prevLink string
219 var pg = mastodon.Pagination{
225 var statuses []*mastodon.Status
227 switch timelineType {
229 return ErrInvalidTimeline
231 statuses, err = c.GetTimelineHome(ctx, &pg)
234 statuses, err = c.GetTimelinePublic(ctx, true, &pg)
235 title = "Local Timeline"
237 statuses, err = c.GetTimelinePublic(ctx, false, &pg)
238 title = "The Whole Known Network"
244 if len(maxID) > 0 && len(statuses) > 0 {
246 prevLink = "/timeline?min_id=" + statuses[0].ID
248 if len(minID) > 0 && len(pg.MinID) > 0 {
249 newStatuses, err := c.GetTimelineHome(ctx, &mastodon.Pagination{MinID: pg.MinID, Limit: 20})
253 newStatusesLen := len(newStatuses)
254 if newStatusesLen == 20 {
256 prevLink = "/timeline?min_id=" + pg.MinID
258 i := 20 - newStatusesLen - 1
259 if len(statuses) > i {
261 prevLink = "/timeline?min_id=" + statuses[i].ID
265 if len(pg.MaxID) > 0 {
267 nextLink = "/timeline?max_id=" + pg.MaxID
270 postContext := model.PostContext{
271 DefaultVisibility: c.Session.Settings.DefaultVisibility,
274 navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
279 data := renderer.NewTimelinePageTemplateData(title, statuses, hasNext, nextLink, hasPrev, prevLink, postContext, navbarData)
280 err = svc.renderer.RenderTimelinePage(ctx, client, data)
288 func (svc *service) ServeThreadPage(ctx context.Context, client io.Writer, c *model.Client, id string, reply bool) (err error) {
289 status, err := c.GetStatus(ctx, id)
294 u, err := c.GetAccountCurrentUser(ctx)
299 var postContext model.PostContext
302 if u.ID != status.Account.ID {
303 content += "@" + status.Account.Acct + " "
305 for i := range status.Mentions {
306 if status.Mentions[i].ID != u.ID && status.Mentions[i].ID != status.Account.ID {
307 content += "@" + status.Mentions[i].Acct + " "
311 s, err := c.GetStatus(ctx, id)
316 postContext = model.PostContext{
317 DefaultVisibility: s.Visibility,
318 ReplyContext: &model.ReplyContext{
320 InReplyToName: status.Account.Acct,
321 ReplyContent: content,
326 context, err := c.GetStatusContext(ctx, id)
331 statuses := append(append(context.Ancestors, status), context.Descendants...)
333 replyMap := make(map[string][]mastodon.ReplyInfo)
335 for i := range statuses {
336 statuses[i].ShowReplies = true
337 statuses[i].ReplyMap = replyMap
338 addToReplyMap(replyMap, statuses[i].InReplyToID, statuses[i].ID, i+1)
341 navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
346 data := renderer.NewThreadPageTemplateData(statuses, postContext, replyMap, navbarData)
347 err = svc.renderer.RenderThreadPage(ctx, client, data)
355 func (svc *service) ServeNotificationPage(ctx context.Context, client io.Writer, c *model.Client, maxID string, minID string) (err error) {
359 var pg = mastodon.Pagination{
365 notifications, err := c.GetNotifications(ctx, &pg)
371 for i := range notifications {
372 switch notifications[i].Type {
373 case "reblog", "favourite":
374 if notifications[i].Status != nil {
375 notifications[i].Status.HideAccountInfo = true
378 if notifications[i].Pleroma != nil && notifications[i].Pleroma.IsSeen {
384 err := c.ReadNotifications(ctx, notifications[0].ID)
390 if len(pg.MaxID) > 0 {
392 nextLink = "/notifications?max_id=" + pg.MaxID
395 navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
400 data := renderer.NewNotificationPageTemplateData(notifications, hasNext, nextLink, navbarData)
401 err = svc.renderer.RenderNotificationPage(ctx, client, data)
409 func (svc *service) ServeUserPage(ctx context.Context, client io.Writer, c *model.Client, id string, maxID string, minID string) (err error) {
410 user, err := c.GetAccount(ctx, id)
418 var pg = mastodon.Pagination{
424 statuses, err := c.GetAccountStatuses(ctx, id, &pg)
429 if len(pg.MaxID) > 0 {
431 nextLink = "/user/" + id + "?max_id=" + pg.MaxID
434 navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
439 data := renderer.NewUserPageTemplateData(user, statuses, hasNext, nextLink, navbarData)
440 err = svc.renderer.RenderUserPage(ctx, client, data)
448 func (svc *service) ServeAboutPage(ctx context.Context, client io.Writer, c *model.Client) (err error) {
449 navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
454 data := renderer.NewAboutPageTemplateData(navbarData)
455 err = svc.renderer.RenderAboutPage(ctx, client, data)
463 func (svc *service) ServeEmojiPage(ctx context.Context, client io.Writer, c *model.Client) (err error) {
464 navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
469 emojis, err := c.GetInstanceEmojis(ctx)
474 data := renderer.NewEmojiPageTemplateData(navbarData, emojis)
475 err = svc.renderer.RenderEmojiPage(ctx, client, data)
483 func (svc *service) getNavbarTemplateData(ctx context.Context, client io.Writer, c *model.Client) (data *renderer.NavbarTemplateData, err error) {
484 notifications, err := c.GetNotifications(ctx, nil)
489 var notificationCount int
490 for i := range notifications {
491 if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen {
496 u, err := c.GetAccountCurrentUser(ctx)
501 data = renderer.NewNavbarTemplateData(notificationCount, u)
506 func (svc *service) Like(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
507 _, err = c.Favourite(ctx, id)
511 func (svc *service) UnLike(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
512 _, err = c.Unfavourite(ctx, id)
516 func (svc *service) Retweet(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
517 _, err = c.Reblog(ctx, id)
521 func (svc *service) UnRetweet(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
522 _, err = c.Unreblog(ctx, id)
526 func (svc *service) PostTweet(ctx context.Context, client io.Writer, c *model.Client, content string, replyToID string, visibility string, isNSFW bool, files []*multipart.FileHeader) (id string, err error) {
527 var mediaIds []string
528 for _, f := range files {
529 a, err := c.UploadMediaFromMultipartFileHeader(ctx, f)
533 mediaIds = append(mediaIds, a.ID)
536 // save visibility if it's a non-reply post
537 if len(replyToID) < 1 && visibility != c.Session.Settings.DefaultVisibility {
538 c.Session.Settings.DefaultVisibility = visibility
539 svc.sessionRepo.Add(c.Session)
542 tweet := &mastodon.Toot{
544 InReplyToID: replyToID,
546 Visibility: visibility,
550 s, err := c.PostStatus(ctx, tweet)
558 func (svc *service) Follow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
559 _, err = c.AccountFollow(ctx, id)
563 func (svc *service) UnFollow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
564 _, err = c.AccountUnfollow(ctx, id)
568 func addToReplyMap(m map[string][]mastodon.ReplyInfo, key interface{}, val string, number int) {
573 keyStr, ok := key.(string)
579 m[keyStr] = []mastodon.ReplyInfo{}
582 m[keyStr] = append(m[keyStr], mastodon.ReplyInfo{val, number})