18 errInvalidArgument = errors.New("invalid argument")
21 type Service interface {
22 ServeErrorPage(ctx context.Context, c *model.Client, err error)
23 ServeSigninPage(ctx context.Context, c *model.Client) (err error)
24 ServeTimelinePage(ctx context.Context, c *model.Client, tType string, maxID string, minID string) (err error)
25 ServeThreadPage(ctx context.Context, c *model.Client, id string, reply bool) (err error)
26 ServeLikedByPage(ctx context.Context, c *model.Client, id string) (err error)
27 ServeRetweetedByPage(ctx context.Context, c *model.Client, id string) (err error)
28 ServeFollowingPage(ctx context.Context, c *model.Client, id string, maxID string, minID string) (err error)
29 ServeFollowersPage(ctx context.Context, c *model.Client, id string, maxID string, minID string) (err error)
30 ServeNotificationPage(ctx context.Context, c *model.Client, maxID string, minID string) (err error)
31 ServeUserPage(ctx context.Context, c *model.Client, id string, maxID string, minID string) (err error)
32 ServeAboutPage(ctx context.Context, c *model.Client) (err error)
33 ServeEmojiPage(ctx context.Context, c *model.Client) (err error)
34 ServeSearchPage(ctx context.Context, c *model.Client, q string, qType string, offset int) (err error)
35 ServeSettingsPage(ctx context.Context, c *model.Client) (err error)
36 NewSession(ctx context.Context, instance string) (redirectUrl string, sessionID string, err error)
37 Signin(ctx context.Context, c *model.Client, sessionID string, code string) (token string, err error)
38 Post(ctx context.Context, c *model.Client, content string, replyToID string, format string,
39 visibility string, isNSFW bool, files []*multipart.FileHeader) (id string, err error)
40 Like(ctx context.Context, c *model.Client, id string) (count int64, err error)
41 UnLike(ctx context.Context, c *model.Client, id string) (count int64, err error)
42 Retweet(ctx context.Context, c *model.Client, id string) (count int64, err error)
43 UnRetweet(ctx context.Context, c *model.Client, id string) (count int64, err error)
44 Follow(ctx context.Context, c *model.Client, id string) (err error)
45 UnFollow(ctx context.Context, c *model.Client, id string) (err error)
46 SaveSettings(ctx context.Context, c *model.Client, settings *model.Settings) (err error)
54 postFormats []model.PostFormat
55 renderer renderer.Renderer
56 sessionRepo model.SessionRepo
60 func NewService(clientName string,
64 postFormats []model.PostFormat,
65 renderer renderer.Renderer,
66 sessionRepo model.SessionRepo,
67 appRepo model.AppRepo,
70 clientName: clientName,
71 clientScope: clientScope,
72 clientWebsite: clientWebsite,
74 postFormats: postFormats,
76 sessionRepo: sessionRepo,
81 func getRendererContext(c *model.Client) *renderer.Context {
82 var settings model.Settings
83 var session model.Session
85 settings = c.Session.Settings
88 settings = *model.NewSettings()
90 return &renderer.Context{
91 MaskNSFW: settings.MaskNSFW,
92 ThreadInNewTab: settings.ThreadInNewTab,
93 FluorideMode: settings.FluorideMode,
94 DarkMode: settings.DarkMode,
95 CSRFToken: session.CSRFToken,
99 func addToReplyMap(m map[string][]mastodon.ReplyInfo, key interface{},
100 val string, number int) {
105 keyStr, ok := key.(string)
112 m[keyStr] = []mastodon.ReplyInfo{}
115 m[keyStr] = append(m[keyStr], mastodon.ReplyInfo{val, number})
118 func (svc *service) getCommonData(ctx context.Context, c *model.Client,
119 title string) (data *renderer.CommonData, err error) {
121 data = new(renderer.CommonData)
122 data.HeaderData = &renderer.HeaderData{
123 Title: title + " - " + svc.clientName,
124 NotificationCount: 0,
125 CustomCSS: svc.customCSS,
128 if c == nil || !c.Session.IsLoggedIn() {
132 notifications, err := c.GetNotifications(ctx, nil)
137 var notificationCount int
138 for i := range notifications {
139 if notifications[i].Pleroma != nil &&
140 !notifications[i].Pleroma.IsSeen {
145 u, err := c.GetAccountCurrentUser(ctx)
150 data.NavbarData = &renderer.NavbarData{
152 NotificationCount: notificationCount,
155 data.HeaderData.NotificationCount = notificationCount
156 data.HeaderData.CSRFToken = c.Session.CSRFToken
161 func (svc *service) ServeErrorPage(ctx context.Context, c *model.Client, err error) {
167 commonData, err := svc.getCommonData(ctx, nil, "error")
172 data := &renderer.ErrorData{
173 CommonData: commonData,
177 rCtx := getRendererContext(c)
178 svc.renderer.RenderErrorPage(rCtx, c.Writer, data)
181 func (svc *service) ServeSigninPage(ctx context.Context, c *model.Client) (
184 commonData, err := svc.getCommonData(ctx, nil, "signin")
189 data := &renderer.SigninData{
190 CommonData: commonData,
193 rCtx := getRendererContext(nil)
194 return svc.renderer.RenderSigninPage(rCtx, c.Writer, data)
197 func (svc *service) ServeTimelinePage(ctx context.Context, c *model.Client,
198 tType string, maxID string, minID string) (err error) {
200 var nextLink, prevLink, title string
201 var statuses []*mastodon.Status
202 var pg = mastodon.Pagination{
210 return errInvalidArgument
212 statuses, err = c.GetTimelineHome(ctx, &pg)
215 statuses, err = c.GetTimelineDirect(ctx, &pg)
216 title = "Local Timeline"
218 statuses, err = c.GetTimelinePublic(ctx, true, &pg)
219 title = "Local Timeline"
221 statuses, err = c.GetTimelinePublic(ctx, false, &pg)
222 title = "The Whole Known Network"
228 for i := range statuses {
229 if statuses[i].Reblog != nil {
230 statuses[i].Reblog.RetweetedByID = statuses[i].ID
234 if len(maxID) > 0 && len(statuses) > 0 {
235 prevLink = fmt.Sprintf("/timeline/%s?min_id=%s", tType,
239 if len(minID) > 0 && len(pg.MinID) > 0 {
240 newPg := &mastodon.Pagination{MinID: pg.MinID, Limit: 20}
241 newStatuses, err := c.GetTimelineHome(ctx, newPg)
245 newLen := len(newStatuses)
247 prevLink = fmt.Sprintf("/timeline/%s?min_id=%s",
251 if len(statuses) > i {
252 prevLink = fmt.Sprintf("/timeline/%s?min_id=%s",
253 tType, statuses[i].ID)
258 if len(pg.MaxID) > 0 {
259 nextLink = fmt.Sprintf("/timeline/%s?max_id=%s", tType, pg.MaxID)
262 postContext := model.PostContext{
263 DefaultVisibility: c.Session.Settings.DefaultVisibility,
264 Formats: svc.postFormats,
267 commonData, err := svc.getCommonData(ctx, c, tType+" timeline ")
272 data := &renderer.TimelineData{
277 PostContext: postContext,
278 CommonData: commonData,
281 rCtx := getRendererContext(c)
282 return svc.renderer.RenderTimelinePage(rCtx, c.Writer, data)
285 func (svc *service) ServeThreadPage(ctx context.Context, c *model.Client,
286 id string, reply bool) (err error) {
288 var postContext model.PostContext
290 status, err := c.GetStatus(ctx, id)
295 u, err := c.GetAccountCurrentUser(ctx)
302 var visibility string
303 if u.ID != status.Account.ID {
304 content += "@" + status.Account.Acct + " "
306 for i := range status.Mentions {
307 if status.Mentions[i].ID != u.ID &&
308 status.Mentions[i].ID != status.Account.ID {
309 content += "@" + status.Mentions[i].Acct + " "
313 if c.Session.Settings.CopyScope {
314 s, err := c.GetStatus(ctx, id)
318 visibility = s.Visibility
320 visibility = c.Session.Settings.DefaultVisibility
323 postContext = model.PostContext{
324 DefaultVisibility: visibility,
325 Formats: svc.postFormats,
326 ReplyContext: &model.ReplyContext{
328 InReplyToName: status.Account.Acct,
329 ReplyContent: content,
331 DarkMode: c.Session.Settings.DarkMode,
335 context, err := c.GetStatusContext(ctx, id)
340 statuses := append(append(context.Ancestors, status), context.Descendants...)
341 replies := make(map[string][]mastodon.ReplyInfo)
343 for i := range statuses {
344 statuses[i].ShowReplies = true
345 statuses[i].ReplyMap = replies
346 addToReplyMap(replies, statuses[i].InReplyToID, statuses[i].ID, i+1)
349 commonData, err := svc.getCommonData(ctx, c, "post by "+status.Account.DisplayName)
354 data := &renderer.ThreadData{
356 PostContext: postContext,
358 CommonData: commonData,
361 rCtx := getRendererContext(c)
362 return svc.renderer.RenderThreadPage(rCtx, c.Writer, data)
365 func (svc *service) ServeLikedByPage(ctx context.Context, c *model.Client,
366 id string) (err error) {
368 likers, err := c.GetFavouritedBy(ctx, id, nil)
373 commonData, err := svc.getCommonData(ctx, c, "likes")
378 data := &renderer.LikedByData{
379 CommonData: commonData,
383 rCtx := getRendererContext(c)
384 return svc.renderer.RenderLikedByPage(rCtx, c.Writer, data)
387 func (svc *service) ServeRetweetedByPage(ctx context.Context, c *model.Client,
388 id string) (err error) {
390 retweeters, err := c.GetRebloggedBy(ctx, id, nil)
395 commonData, err := svc.getCommonData(ctx, c, "retweets")
400 data := &renderer.RetweetedByData{
401 CommonData: commonData,
405 rCtx := getRendererContext(c)
406 return svc.renderer.RenderRetweetedByPage(rCtx, c.Writer, data)
409 func (svc *service) ServeFollowingPage(ctx context.Context, c *model.Client,
410 id string, maxID string, minID string) (err error) {
413 var pg = mastodon.Pagination{
419 followings, err := c.GetAccountFollowing(ctx, id, &pg)
424 if len(followings) == 20 && len(pg.MaxID) > 0 {
425 nextLink = "/following/" + id + "?max_id=" + pg.MaxID
428 commonData, err := svc.getCommonData(ctx, c, "following")
433 data := &renderer.FollowingData{
434 CommonData: commonData,
439 rCtx := getRendererContext(c)
440 return svc.renderer.RenderFollowingPage(rCtx, c.Writer, data)
443 func (svc *service) ServeFollowersPage(ctx context.Context, c *model.Client,
444 id string, maxID string, minID string) (err error) {
447 var pg = mastodon.Pagination{
453 followers, err := c.GetAccountFollowers(ctx, id, &pg)
458 if len(followers) == 20 && len(pg.MaxID) > 0 {
459 nextLink = "/followers/" + id + "?max_id=" + pg.MaxID
462 commonData, err := svc.getCommonData(ctx, c, "followers")
467 data := &renderer.FollowersData{
468 CommonData: commonData,
472 rCtx := getRendererContext(c)
473 return svc.renderer.RenderFollowersPage(rCtx, c.Writer, data)
476 func (svc *service) ServeNotificationPage(ctx context.Context, c *model.Client,
477 maxID string, minID string) (err error) {
481 var pg = mastodon.Pagination{
487 notifications, err := c.GetNotifications(ctx, &pg)
492 for i := range notifications {
493 if notifications[i].Status != nil {
494 notifications[i].Status.CreatedAt = notifications[i].CreatedAt
495 switch notifications[i].Type {
496 case "reblog", "favourite":
497 notifications[i].Status.HideAccountInfo = true
500 if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen {
506 err := c.ReadNotifications(ctx, notifications[0].ID)
512 if len(pg.MaxID) > 0 {
513 nextLink = "/notifications?max_id=" + pg.MaxID
516 commonData, err := svc.getCommonData(ctx, c, "notifications")
521 data := &renderer.NotificationData{
522 Notifications: notifications,
524 CommonData: commonData,
526 rCtx := getRendererContext(c)
527 return svc.renderer.RenderNotificationPage(rCtx, c.Writer, data)
530 func (svc *service) ServeUserPage(ctx context.Context, c *model.Client,
531 id string, maxID string, minID string) (err error) {
535 var pg = mastodon.Pagination{
541 user, err := c.GetAccount(ctx, id)
546 statuses, err := c.GetAccountStatuses(ctx, id, &pg)
551 if len(pg.MaxID) > 0 {
552 nextLink = "/user/" + id + "?max_id=" + pg.MaxID
555 commonData, err := svc.getCommonData(ctx, c, user.DisplayName)
560 data := &renderer.UserData{
564 CommonData: commonData,
566 rCtx := getRendererContext(c)
567 return svc.renderer.RenderUserPage(rCtx, c.Writer, data)
570 func (svc *service) ServeAboutPage(ctx context.Context, c *model.Client) (err error) {
571 commonData, err := svc.getCommonData(ctx, c, "about")
576 data := &renderer.AboutData{
577 CommonData: commonData,
580 rCtx := getRendererContext(c)
581 return svc.renderer.RenderAboutPage(rCtx, c.Writer, data)
584 func (svc *service) ServeEmojiPage(ctx context.Context, c *model.Client) (err error) {
585 commonData, err := svc.getCommonData(ctx, c, "emojis")
590 emojis, err := c.GetInstanceEmojis(ctx)
595 data := &renderer.EmojiData{
597 CommonData: commonData,
600 rCtx := getRendererContext(c)
601 return svc.renderer.RenderEmojiPage(rCtx, c.Writer, data)
604 func (svc *service) ServeSearchPage(ctx context.Context, c *model.Client,
605 q string, qType string, offset int) (err error) {
610 results, err := c.Search(ctx, q, qType, 20, true, offset)
615 if (qType == "accounts" && len(results.Accounts) == 20) ||
616 (qType == "statuses" && len(results.Statuses) == 20) {
618 nextLink = fmt.Sprintf("/search?q=%s&type=%s&offset=%d", q, qType, offset)
622 title += " \"" + q + "\""
625 commonData, err := svc.getCommonData(ctx, c, title)
630 data := &renderer.SearchData{
631 CommonData: commonData,
634 Users: results.Accounts,
635 Statuses: results.Statuses,
639 rCtx := getRendererContext(c)
640 return svc.renderer.RenderSearchPage(rCtx, c.Writer, data)
643 func (svc *service) ServeSettingsPage(ctx context.Context, c *model.Client) (err error) {
644 commonData, err := svc.getCommonData(ctx, c, "settings")
649 data := &renderer.SettingsData{
650 CommonData: commonData,
651 Settings: &c.Session.Settings,
654 rCtx := getRendererContext(c)
655 return svc.renderer.RenderSettingsPage(rCtx, c.Writer, data)
658 func (svc *service) NewSession(ctx context.Context, instance string) (
659 redirectUrl string, sessionID string, err error) {
661 var instanceURL string
662 if strings.HasPrefix(instance, "https://") {
663 instanceURL = instance
664 instance = strings.TrimPrefix(instance, "https://")
666 instanceURL = "https://" + instance
669 sessionID, err = util.NewSessionID()
674 csrfToken, err := util.NewCSRFToken()
679 session := model.Session{
681 InstanceDomain: instance,
682 CSRFToken: csrfToken,
683 Settings: *model.NewSettings(),
686 err = svc.sessionRepo.Add(session)
691 app, err := svc.appRepo.Get(instance)
693 if err != model.ErrAppNotFound {
697 mastoApp, err := mastodon.RegisterApp(ctx, &mastodon.AppConfig{
699 ClientName: svc.clientName,
700 Scopes: svc.clientScope,
701 Website: svc.clientWebsite,
702 RedirectURIs: svc.clientWebsite + "/oauth_callback",
709 InstanceDomain: instance,
710 InstanceURL: instanceURL,
711 ClientID: mastoApp.ClientID,
712 ClientSecret: mastoApp.ClientSecret,
715 err = svc.appRepo.Add(app)
721 u, err := url.Parse("/oauth/authorize")
726 q := make(url.Values)
727 q.Set("scope", "read write follow")
728 q.Set("client_id", app.ClientID)
729 q.Set("response_type", "code")
730 q.Set("redirect_uri", svc.clientWebsite+"/oauth_callback")
731 u.RawQuery = q.Encode()
733 redirectUrl = instanceURL + u.String()
738 func (svc *service) Signin(ctx context.Context, c *model.Client,
739 sessionID string, code string) (token string, err error) {
742 err = errInvalidArgument
746 err = c.AuthenticateToken(ctx, code, svc.clientWebsite+"/oauth_callback")
750 token = c.GetAccessToken(ctx)
755 func (svc *service) Post(ctx context.Context, c *model.Client, content string,
756 replyToID string, format string, visibility string, isNSFW bool,
757 files []*multipart.FileHeader) (id string, err error) {
759 var mediaIDs []string
760 for _, f := range files {
761 a, err := c.UploadMediaFromMultipartFileHeader(ctx, f)
765 mediaIDs = append(mediaIDs, a.ID)
768 tweet := &mastodon.Toot{
770 InReplyToID: replyToID,
773 Visibility: visibility,
777 s, err := c.PostStatus(ctx, tweet)
785 func (svc *service) Like(ctx context.Context, c *model.Client, id string) (
786 count int64, err error) {
787 s, err := c.Favourite(ctx, id)
791 count = s.FavouritesCount
795 func (svc *service) UnLike(ctx context.Context, c *model.Client, id string) (
796 count int64, err error) {
797 s, err := c.Unfavourite(ctx, id)
801 count = s.FavouritesCount
805 func (svc *service) Retweet(ctx context.Context, c *model.Client, id string) (
806 count int64, err error) {
807 s, err := c.Reblog(ctx, id)
812 count = s.Reblog.ReblogsCount
817 func (svc *service) UnRetweet(ctx context.Context, c *model.Client, id string) (
818 count int64, err error) {
819 s, err := c.Unreblog(ctx, id)
823 count = s.ReblogsCount
827 func (svc *service) Follow(ctx context.Context, c *model.Client, id string) (err error) {
828 _, err = c.AccountFollow(ctx, id)
832 func (svc *service) UnFollow(ctx context.Context, c *model.Client, id string) (err error) {
833 _, err = c.AccountUnfollow(ctx, id)
837 func (svc *service) SaveSettings(ctx context.Context, c *model.Client,
838 settings *model.Settings) (err error) {
840 session, err := svc.sessionRepo.Get(c.Session.ID)
845 session.Settings = *settings
846 return svc.sessionRepo.Add(session)