22 ErrInvalidArgument = errors.New("invalid argument")
23 ErrInvalidToken = errors.New("invalid token")
24 ErrInvalidClient = errors.New("invalid client")
25 ErrInvalidTimeline = errors.New("invalid timeline")
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 *model.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 *model.Client, timelineType string, maxID string, sinceID string, minID string) (err error)
35 ServeThreadPage(ctx context.Context, client io.Writer, c *model.Client, id string, reply bool) (err error)
36 ServeNotificationPage(ctx context.Context, client io.Writer, c *model.Client, maxID string, minID string) (err error)
37 ServeUserPage(ctx context.Context, client io.Writer, c *model.Client, id string, maxID string, minID string) (err error)
38 ServeAboutPage(ctx context.Context, client io.Writer, c *model.Client) (err error)
39 ServeEmojiPage(ctx context.Context, client io.Writer, c *model.Client) (err error)
40 ServeLikedByPage(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
41 ServeRetweetedByPage(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
42 Like(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
43 UnLike(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
44 Retweet(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
45 UnRetweet(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
46 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)
47 Follow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
48 UnFollow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
56 renderer renderer.Renderer
57 sessionRepo model.SessionRepository
58 appRepo model.AppRepository
61 func NewService(clientName string, clientScope string, clientWebsite string,
62 customCSS string, renderer renderer.Renderer, sessionRepo model.SessionRepository,
63 appRepo model.AppRepository) Service {
65 clientName: clientName,
66 clientScope: clientScope,
67 clientWebsite: clientWebsite,
70 sessionRepo: sessionRepo,
75 func (svc *service) GetAuthUrl(ctx context.Context, instance string) (
76 redirectUrl string, sessionID string, err error) {
77 var instanceURL string
78 if strings.HasPrefix(instance, "https://") {
79 instanceURL = instance
80 instance = strings.TrimPrefix(instance, "https://")
82 instanceURL = "https://" + instance
85 sessionID = util.NewSessionId()
86 err = svc.sessionRepo.Add(model.Session{
88 InstanceDomain: instance,
94 app, err := svc.appRepo.Get(instance)
96 if err != model.ErrAppNotFound {
100 var mastoApp *mastodon.Application
101 mastoApp, err = mastodon.RegisterApp(ctx, &mastodon.AppConfig{
103 ClientName: svc.clientName,
104 Scopes: svc.clientScope,
105 Website: svc.clientWebsite,
106 RedirectURIs: svc.clientWebsite + "/oauth_callback",
113 InstanceDomain: instance,
114 InstanceURL: instanceURL,
115 ClientID: mastoApp.ClientID,
116 ClientSecret: mastoApp.ClientSecret,
119 err = svc.appRepo.Add(app)
125 u, err := url.Parse("/oauth/authorize")
130 q := make(url.Values)
131 q.Set("scope", "read write follow")
132 q.Set("client_id", app.ClientID)
133 q.Set("response_type", "code")
134 q.Set("redirect_uri", svc.clientWebsite+"/oauth_callback")
135 u.RawQuery = q.Encode()
137 redirectUrl = instanceURL + u.String()
142 func (svc *service) GetUserToken(ctx context.Context, sessionID string, c *model.Client,
143 code string) (token string, err error) {
145 err = ErrInvalidArgument
149 session, err := svc.sessionRepo.Get(sessionID)
154 app, err := svc.appRepo.Get(session.InstanceDomain)
159 data := &bytes.Buffer{}
160 err = json.NewEncoder(data).Encode(map[string]string{
161 "client_id": app.ClientID,
162 "client_secret": app.ClientSecret,
163 "grant_type": "authorization_code",
165 "redirect_uri": svc.clientWebsite + "/oauth_callback",
171 resp, err := http.Post(app.InstanceURL+"/oauth/token", "application/json", data)
175 defer resp.Body.Close()
178 AccessToken string `json:"access_token"`
181 err = json.NewDecoder(resp.Body).Decode(&res)
186 err = c.AuthenticateToken(ctx, code, svc.clientWebsite+"/oauth_callback")
190 err = svc.sessionRepo.Update(sessionID, c.GetAccessToken(ctx))
193 return res.AccessToken, nil
196 func (svc *service) ServeHomePage(ctx context.Context, client io.Writer) (err error) {
197 commonData, err := svc.getCommonData(ctx, client, nil)
202 data := &renderer.HomePageData{
203 CommonData: commonData,
206 return svc.renderer.RenderHomePage(ctx, client, data)
209 func (svc *service) ServeErrorPage(ctx context.Context, client io.Writer, err error) {
215 commonData, err := svc.getCommonData(ctx, client, nil)
220 data := &renderer.ErrorData{
221 CommonData: commonData,
225 svc.renderer.RenderErrorPage(ctx, client, data)
228 func (svc *service) ServeSigninPage(ctx context.Context, client io.Writer) (err error) {
229 commonData, err := svc.getCommonData(ctx, client, nil)
234 data := &renderer.SigninData{
235 CommonData: commonData,
238 return svc.renderer.RenderSigninPage(ctx, client, data)
241 func (svc *service) ServeTimelinePage(ctx context.Context, client io.Writer,
242 c *model.Client, timelineType string, maxID string, sinceID string, minID string) (err error) {
244 var hasNext, hasPrev bool
245 var nextLink, prevLink string
247 var pg = mastodon.Pagination{
253 var statuses []*mastodon.Status
255 switch timelineType {
257 return ErrInvalidTimeline
259 statuses, err = c.GetTimelineHome(ctx, &pg)
262 statuses, err = c.GetTimelinePublic(ctx, true, &pg)
263 title = "Local Timeline"
265 statuses, err = c.GetTimelinePublic(ctx, false, &pg)
266 title = "The Whole Known Network"
272 if len(maxID) > 0 && len(statuses) > 0 {
274 prevLink = fmt.Sprintf("/timeline/$s?min_id=%s", timelineType, statuses[0].ID)
276 if len(minID) > 0 && len(pg.MinID) > 0 {
277 newStatuses, err := c.GetTimelineHome(ctx, &mastodon.Pagination{MinID: pg.MinID, Limit: 20})
281 newStatusesLen := len(newStatuses)
282 if newStatusesLen == 20 {
284 prevLink = fmt.Sprintf("/timeline/%s?min_id=%s", timelineType, pg.MinID)
286 i := 20 - newStatusesLen - 1
287 if len(statuses) > i {
289 prevLink = fmt.Sprintf("/timeline/%s?min_id=%s", timelineType, statuses[i].ID)
293 if len(pg.MaxID) > 0 {
295 nextLink = fmt.Sprintf("/timeline/%s?max_id=%s", timelineType, pg.MaxID)
298 postContext := model.PostContext{
299 DefaultVisibility: c.Session.Settings.DefaultVisibility,
302 commonData, err := svc.getCommonData(ctx, client, c)
307 data := &renderer.TimelineData{
314 PostContext: postContext,
315 CommonData: commonData,
318 err = svc.renderer.RenderTimelinePage(ctx, client, data)
326 func (svc *service) ServeThreadPage(ctx context.Context, client io.Writer, c *model.Client, id string, reply bool) (err error) {
327 status, err := c.GetStatus(ctx, id)
332 u, err := c.GetAccountCurrentUser(ctx)
337 var postContext model.PostContext
340 if u.ID != status.Account.ID {
341 content += "@" + status.Account.Acct + " "
343 for i := range status.Mentions {
344 if status.Mentions[i].ID != u.ID && status.Mentions[i].ID != status.Account.ID {
345 content += "@" + status.Mentions[i].Acct + " "
349 s, err := c.GetStatus(ctx, id)
354 postContext = model.PostContext{
355 DefaultVisibility: s.Visibility,
356 ReplyContext: &model.ReplyContext{
358 InReplyToName: status.Account.Acct,
359 ReplyContent: content,
364 context, err := c.GetStatusContext(ctx, id)
369 statuses := append(append(context.Ancestors, status), context.Descendants...)
371 replyMap := make(map[string][]mastodon.ReplyInfo)
373 for i := range statuses {
374 statuses[i].ShowReplies = true
375 statuses[i].ReplyMap = replyMap
376 addToReplyMap(replyMap, statuses[i].InReplyToID, statuses[i].ID, i+1)
379 commonData, err := svc.getCommonData(ctx, client, c)
384 data := &renderer.ThreadData{
386 PostContext: postContext,
388 CommonData: commonData,
391 err = svc.renderer.RenderThreadPage(ctx, client, data)
399 func (svc *service) ServeNotificationPage(ctx context.Context, client io.Writer, c *model.Client, maxID string, minID string) (err error) {
403 var pg = mastodon.Pagination{
409 notifications, err := c.GetNotifications(ctx, &pg)
415 for i := range notifications {
416 switch notifications[i].Type {
417 case "reblog", "favourite":
418 if notifications[i].Status != nil {
419 notifications[i].Status.HideAccountInfo = true
422 if notifications[i].Pleroma != nil && notifications[i].Pleroma.IsSeen {
428 err := c.ReadNotifications(ctx, notifications[0].ID)
434 if len(pg.MaxID) > 0 {
436 nextLink = "/notifications?max_id=" + pg.MaxID
439 commonData, err := svc.getCommonData(ctx, client, c)
444 data := &renderer.NotificationData{
445 Notifications: notifications,
448 CommonData: commonData,
450 err = svc.renderer.RenderNotificationPage(ctx, client, data)
458 func (svc *service) ServeUserPage(ctx context.Context, client io.Writer, c *model.Client, id string, maxID string, minID string) (err error) {
459 user, err := c.GetAccount(ctx, id)
467 var pg = mastodon.Pagination{
473 statuses, err := c.GetAccountStatuses(ctx, id, &pg)
478 if len(pg.MaxID) > 0 {
480 nextLink = "/user/" + id + "?max_id=" + pg.MaxID
483 commonData, err := svc.getCommonData(ctx, client, c)
488 data := &renderer.UserData{
493 CommonData: commonData,
496 err = svc.renderer.RenderUserPage(ctx, client, data)
504 func (svc *service) ServeAboutPage(ctx context.Context, client io.Writer, c *model.Client) (err error) {
505 commonData, err := svc.getCommonData(ctx, client, c)
510 data := &renderer.AboutData{
511 CommonData: commonData,
513 err = svc.renderer.RenderAboutPage(ctx, client, data)
521 func (svc *service) ServeEmojiPage(ctx context.Context, client io.Writer, c *model.Client) (err error) {
522 commonData, err := svc.getCommonData(ctx, client, c)
527 emojis, err := c.GetInstanceEmojis(ctx)
532 data := &renderer.EmojiData{
534 CommonData: commonData,
537 err = svc.renderer.RenderEmojiPage(ctx, client, data)
545 func (svc *service) ServeLikedByPage(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
546 likers, err := c.GetFavouritedBy(ctx, id, nil)
551 commonData, err := svc.getCommonData(ctx, client, c)
556 data := &renderer.LikedByData{
557 CommonData: commonData,
561 err = svc.renderer.RenderLikedByPage(ctx, client, data)
569 func (svc *service) ServeRetweetedByPage(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
570 retweeters, err := c.GetRebloggedBy(ctx, id, nil)
575 commonData, err := svc.getCommonData(ctx, client, c)
580 data := &renderer.RetweetedByData{
581 CommonData: commonData,
585 err = svc.renderer.RenderRetweetedByPage(ctx, client, data)
592 func (svc *service) getCommonData(ctx context.Context, client io.Writer, c *model.Client) (data *renderer.CommonData, err error) {
593 data = new(renderer.CommonData)
595 data.HeaderData = &renderer.HeaderData{
597 NotificationCount: 0,
598 CustomCSS: svc.customCSS,
601 if c != nil && c.Session.IsLoggedIn() {
602 notifications, err := c.GetNotifications(ctx, nil)
607 var notificationCount int
608 for i := range notifications {
609 if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen {
614 u, err := c.GetAccountCurrentUser(ctx)
619 data.NavbarData = &renderer.NavbarData{
621 NotificationCount: notificationCount,
624 data.HeaderData.NotificationCount = notificationCount
630 func (svc *service) Like(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
631 _, err = c.Favourite(ctx, id)
635 func (svc *service) UnLike(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
636 _, err = c.Unfavourite(ctx, id)
640 func (svc *service) Retweet(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
641 _, err = c.Reblog(ctx, id)
645 func (svc *service) UnRetweet(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
646 _, err = c.Unreblog(ctx, id)
650 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) {
651 var mediaIds []string
652 for _, f := range files {
653 a, err := c.UploadMediaFromMultipartFileHeader(ctx, f)
657 mediaIds = append(mediaIds, a.ID)
660 // save visibility if it's a non-reply post
661 if len(replyToID) < 1 && visibility != c.Session.Settings.DefaultVisibility {
662 c.Session.Settings.DefaultVisibility = visibility
663 svc.sessionRepo.Add(c.Session)
666 tweet := &mastodon.Toot{
668 InReplyToID: replyToID,
670 Visibility: visibility,
674 s, err := c.PostStatus(ctx, tweet)
682 func (svc *service) Follow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
683 _, err = c.AccountFollow(ctx, id)
687 func (svc *service) UnFollow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
688 _, err = c.AccountUnfollow(ctx, id)
692 func addToReplyMap(m map[string][]mastodon.ReplyInfo, key interface{}, val string, number int) {
697 keyStr, ok := key.(string)
703 m[keyStr] = []mastodon.ReplyInfo{}
706 m[keyStr] = append(m[keyStr], mastodon.ReplyInfo{val, number})