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 ServeRootPage(ctx context.Context, c *model.Client) (err error)
25 ServeNavPage(ctx context.Context, c *model.Client) (err error)
26 ServeTimelinePage(ctx context.Context, c *model.Client, tType string, maxID string, minID string) (err error)
27 ServeThreadPage(ctx context.Context, c *model.Client, id string, reply bool) (err error)
28 ServeLikedByPage(ctx context.Context, c *model.Client, id string) (err error)
29 ServeRetweetedByPage(ctx context.Context, c *model.Client, id 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, pageType string,
32 maxID string, minID string) (err error)
33 ServeAboutPage(ctx context.Context, c *model.Client) (err error)
34 ServeEmojiPage(ctx context.Context, c *model.Client) (err error)
35 ServeSearchPage(ctx context.Context, c *model.Client, q string, qType string, offset int) (err error)
36 ServeUserSearchPage(ctx context.Context, c *model.Client, id string, q string, offset int) (err error)
37 ServeSettingsPage(ctx context.Context, c *model.Client) (err error)
38 NewSession(ctx context.Context, instance string) (redirectUrl string, sessionID string, err error)
39 Signin(ctx context.Context, c *model.Client, sessionID string,
40 code string) (token string, userID string, err error)
41 Post(ctx context.Context, c *model.Client, content string, replyToID string, format string,
42 visibility string, isNSFW bool, files []*multipart.FileHeader) (id string, err error)
43 Like(ctx context.Context, c *model.Client, id string) (count int64, err error)
44 UnLike(ctx context.Context, c *model.Client, id string) (count int64, err error)
45 Retweet(ctx context.Context, c *model.Client, id string) (count int64, err error)
46 UnRetweet(ctx context.Context, c *model.Client, id string) (count int64, err error)
47 Vote(ctx context.Context, c *model.Client, id string, choices []string) (err error)
48 Follow(ctx context.Context, c *model.Client, id string) (err error)
49 UnFollow(ctx context.Context, c *model.Client, id string) (err error)
50 Mute(ctx context.Context, c *model.Client, id string) (err error)
51 UnMute(ctx context.Context, c *model.Client, id string) (err error)
52 Block(ctx context.Context, c *model.Client, id string) (err error)
53 UnBlock(ctx context.Context, c *model.Client, id string) (err error)
54 SaveSettings(ctx context.Context, c *model.Client, settings *model.Settings) (err error)
55 MuteConversation(ctx context.Context, c *model.Client, id string) (err error)
56 UnMuteConversation(ctx context.Context, c *model.Client, id string) (err error)
57 Delete(ctx context.Context, c *model.Client, id string) (err error)
58 ReadNotifications(ctx context.Context, c *model.Client, maxID string) (err error)
66 postFormats []model.PostFormat
67 renderer renderer.Renderer
68 sessionRepo model.SessionRepo
72 func NewService(clientName string,
76 postFormats []model.PostFormat,
77 renderer renderer.Renderer,
78 sessionRepo model.SessionRepo,
79 appRepo model.AppRepo,
82 clientName: clientName,
83 clientScope: clientScope,
84 clientWebsite: clientWebsite,
86 postFormats: postFormats,
88 sessionRepo: sessionRepo,
93 func getRendererContext(c *model.Client) *renderer.Context {
94 var settings model.Settings
95 var session model.Session
97 settings = c.Session.Settings
100 settings = *model.NewSettings()
102 return &renderer.Context{
103 MaskNSFW: settings.MaskNSFW,
104 ThreadInNewTab: settings.ThreadInNewTab,
105 FluorideMode: settings.FluorideMode,
106 DarkMode: settings.DarkMode,
107 CSRFToken: session.CSRFToken,
108 UserID: session.UserID,
112 func addToReplyMap(m map[string][]mastodon.ReplyInfo, key interface{},
113 val string, number int) {
118 keyStr, ok := key.(string)
125 m[keyStr] = []mastodon.ReplyInfo{}
128 m[keyStr] = append(m[keyStr], mastodon.ReplyInfo{val, number})
131 func (svc *service) getCommonData(ctx context.Context, c *model.Client,
132 title string) (data *renderer.CommonData) {
133 data = &renderer.CommonData{
134 Title: title + " - " + svc.clientName,
135 CustomCSS: svc.customCSS,
137 if c != nil && c.Session.IsLoggedIn() {
138 data.CSRFToken = c.Session.CSRFToken
143 func (svc *service) ServeErrorPage(ctx context.Context, c *model.Client, err error) {
149 commonData := svc.getCommonData(ctx, nil, "error")
150 data := &renderer.ErrorData{
151 CommonData: commonData,
155 rCtx := getRendererContext(c)
156 svc.renderer.RenderErrorPage(rCtx, c.Writer, data)
159 func (svc *service) ServeSigninPage(ctx context.Context, c *model.Client) (
162 commonData := svc.getCommonData(ctx, nil, "signin")
163 data := &renderer.SigninData{
164 CommonData: commonData,
167 rCtx := getRendererContext(nil)
168 return svc.renderer.RenderSigninPage(rCtx, c.Writer, data)
171 func (svc *service) ServeRootPage(ctx context.Context, c *model.Client) (err error) {
172 data := &renderer.RootData{
173 Title: svc.clientName,
176 rCtx := getRendererContext(c)
177 return svc.renderer.RenderRootPage(rCtx, c.Writer, data)
180 func (svc *service) ServeNavPage(ctx context.Context, c *model.Client) (err error) {
181 u, err := c.GetAccountCurrentUser(ctx)
186 postContext := model.PostContext{
187 DefaultVisibility: c.Session.Settings.DefaultVisibility,
188 Formats: svc.postFormats,
191 commonData := svc.getCommonData(ctx, c, "Nav")
192 data := &renderer.NavData{
194 CommonData: commonData,
195 PostContext: postContext,
198 rCtx := getRendererContext(c)
199 return svc.renderer.RenderNavPage(rCtx, c.Writer, data)
202 func (svc *service) ServeTimelinePage(ctx context.Context, c *model.Client,
203 tType string, maxID string, minID string) (err error) {
205 var nextLink, prevLink, title string
206 var statuses []*mastodon.Status
207 var pg = mastodon.Pagination{
215 return errInvalidArgument
217 statuses, err = c.GetTimelineHome(ctx, &pg)
220 statuses, err = c.GetTimelineDirect(ctx, &pg)
221 title = "Local Timeline"
223 statuses, err = c.GetTimelinePublic(ctx, true, &pg)
224 title = "Local Timeline"
226 statuses, err = c.GetTimelinePublic(ctx, false, &pg)
227 title = "The Whole Known Network"
233 for i := range statuses {
234 if statuses[i].Reblog != nil {
235 statuses[i].Reblog.RetweetedByID = statuses[i].ID
239 if len(maxID) > 0 && len(statuses) > 0 {
240 prevLink = fmt.Sprintf("/timeline/%s?min_id=%s", tType,
244 if len(minID) > 0 && len(pg.MinID) > 0 {
245 newPg := &mastodon.Pagination{MinID: pg.MinID, Limit: 20}
246 newStatuses, err := c.GetTimelineHome(ctx, newPg)
250 newLen := len(newStatuses)
252 prevLink = fmt.Sprintf("/timeline/%s?min_id=%s",
256 if len(statuses) > i {
257 prevLink = fmt.Sprintf("/timeline/%s?min_id=%s",
258 tType, statuses[i].ID)
263 if len(pg.MaxID) > 0 {
264 nextLink = fmt.Sprintf("/timeline/%s?max_id=%s", tType, pg.MaxID)
267 commonData := svc.getCommonData(ctx, c, tType+" timeline ")
268 data := &renderer.TimelineData{
273 CommonData: commonData,
276 rCtx := getRendererContext(c)
277 return svc.renderer.RenderTimelinePage(rCtx, c.Writer, data)
280 func (svc *service) ServeThreadPage(ctx context.Context, c *model.Client,
281 id string, reply bool) (err error) {
283 var postContext model.PostContext
285 status, err := c.GetStatus(ctx, id)
290 u, err := c.GetAccountCurrentUser(ctx)
297 var visibility string
298 if u.ID != status.Account.ID {
299 content += "@" + status.Account.Acct + " "
301 for i := range status.Mentions {
302 if status.Mentions[i].ID != u.ID &&
303 status.Mentions[i].ID != status.Account.ID {
304 content += "@" + status.Mentions[i].Acct + " "
308 if c.Session.Settings.CopyScope {
309 s, err := c.GetStatus(ctx, id)
313 visibility = s.Visibility
315 visibility = c.Session.Settings.DefaultVisibility
318 postContext = model.PostContext{
319 DefaultVisibility: visibility,
320 Formats: svc.postFormats,
321 ReplyContext: &model.ReplyContext{
323 InReplyToName: status.Account.Acct,
324 ReplyContent: content,
326 DarkMode: c.Session.Settings.DarkMode,
330 context, err := c.GetStatusContext(ctx, id)
335 statuses := append(append(context.Ancestors, status), context.Descendants...)
336 replies := make(map[string][]mastodon.ReplyInfo)
338 for i := range statuses {
339 statuses[i].ShowReplies = true
340 statuses[i].ReplyMap = replies
341 addToReplyMap(replies, statuses[i].InReplyToID, statuses[i].ID, i+1)
344 commonData := svc.getCommonData(ctx, c, "post by "+status.Account.DisplayName)
345 data := &renderer.ThreadData{
347 PostContext: postContext,
349 CommonData: commonData,
352 rCtx := getRendererContext(c)
353 return svc.renderer.RenderThreadPage(rCtx, c.Writer, data)
356 func (svc *service) ServeLikedByPage(ctx context.Context, c *model.Client,
357 id string) (err error) {
359 likers, err := c.GetFavouritedBy(ctx, id, nil)
364 commonData := svc.getCommonData(ctx, c, "likes")
365 data := &renderer.LikedByData{
366 CommonData: commonData,
370 rCtx := getRendererContext(c)
371 return svc.renderer.RenderLikedByPage(rCtx, c.Writer, data)
374 func (svc *service) ServeRetweetedByPage(ctx context.Context, c *model.Client,
375 id string) (err error) {
377 retweeters, err := c.GetRebloggedBy(ctx, id, nil)
382 commonData := svc.getCommonData(ctx, c, "retweets")
383 data := &renderer.RetweetedByData{
384 CommonData: commonData,
388 rCtx := getRendererContext(c)
389 return svc.renderer.RenderRetweetedByPage(rCtx, c.Writer, data)
392 func (svc *service) ServeNotificationPage(ctx context.Context, c *model.Client,
393 maxID string, minID string) (err error) {
398 var pg = mastodon.Pagination{
404 notifications, err := c.GetNotifications(ctx, &pg)
409 for i := range notifications {
410 if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen {
416 readID = notifications[0].ID
419 if len(notifications) == 20 && len(pg.MaxID) > 0 {
420 nextLink = "/notifications?max_id=" + pg.MaxID
423 commonData := svc.getCommonData(ctx, c, "notifications")
424 commonData.AutoRefresh = c.Session.Settings.AutoRefreshNotifications
425 data := &renderer.NotificationData{
426 Notifications: notifications,
427 UnreadCount: unreadCount,
430 CommonData: commonData,
432 rCtx := getRendererContext(c)
433 return svc.renderer.RenderNotificationPage(rCtx, c.Writer, data)
436 func (svc *service) ServeUserPage(ctx context.Context, c *model.Client,
437 id string, pageType string, maxID string, minID string) (err error) {
440 var statuses []*mastodon.Status
441 var users []*mastodon.Account
442 var pg = mastodon.Pagination{
448 user, err := c.GetAccount(ctx, id)
455 statuses, err = c.GetAccountStatuses(ctx, id, false, &pg)
459 if len(statuses) == 20 && len(pg.MaxID) > 0 {
460 nextLink = fmt.Sprintf("/user/%s?max_id=%s", id,
464 users, err = c.GetAccountFollowing(ctx, id, &pg)
468 if len(users) == 20 && len(pg.MaxID) > 0 {
469 nextLink = fmt.Sprintf("/user/%s/following?max_id=%s",
473 users, err = c.GetAccountFollowers(ctx, id, &pg)
477 if len(users) == 20 && len(pg.MaxID) > 0 {
478 nextLink = fmt.Sprintf("/user/%s/followers?max_id=%s",
482 statuses, err = c.GetAccountStatuses(ctx, id, true, &pg)
486 if len(statuses) == 20 && len(pg.MaxID) > 0 {
487 nextLink = fmt.Sprintf("/user/%s/media?max_id=%s",
491 return errInvalidArgument
494 commonData := svc.getCommonData(ctx, c, user.DisplayName)
495 data := &renderer.UserData{
497 IsCurrent: c.Session.UserID == user.ID,
502 CommonData: commonData,
504 rCtx := getRendererContext(c)
505 return svc.renderer.RenderUserPage(rCtx, c.Writer, data)
508 func (svc *service) ServeUserSearchPage(ctx context.Context, c *model.Client,
509 id string, q string, offset int) (err error) {
514 user, err := c.GetAccount(ctx, id)
519 results, err := c.Search(ctx, q, "statuses", 20, true, offset, id)
524 if len(results.Statuses) == 20 {
526 nextLink = fmt.Sprintf("/usersearch/%s?q=%s&offset=%d", id, q, offset)
530 title += " \"" + q + "\""
533 commonData := svc.getCommonData(ctx, c, title)
534 data := &renderer.UserSearchData{
535 CommonData: commonData,
538 Statuses: results.Statuses,
542 rCtx := getRendererContext(c)
543 return svc.renderer.RenderUserSearchPage(rCtx, c.Writer, data)
546 func (svc *service) ServeAboutPage(ctx context.Context, c *model.Client) (err error) {
547 commonData := svc.getCommonData(ctx, c, "about")
548 data := &renderer.AboutData{
549 CommonData: commonData,
552 rCtx := getRendererContext(c)
553 return svc.renderer.RenderAboutPage(rCtx, c.Writer, data)
556 func (svc *service) ServeEmojiPage(ctx context.Context, c *model.Client) (err error) {
557 emojis, err := c.GetInstanceEmojis(ctx)
562 commonData := svc.getCommonData(ctx, c, "emojis")
563 data := &renderer.EmojiData{
565 CommonData: commonData,
568 rCtx := getRendererContext(c)
569 return svc.renderer.RenderEmojiPage(rCtx, c.Writer, data)
572 func (svc *service) ServeSearchPage(ctx context.Context, c *model.Client,
573 q string, qType string, offset int) (err error) {
578 results, err := c.Search(ctx, q, qType, 20, true, offset, "")
583 if (qType == "accounts" && len(results.Accounts) == 20) ||
584 (qType == "statuses" && len(results.Statuses) == 20) {
586 nextLink = fmt.Sprintf("/search?q=%s&type=%s&offset=%d", q, qType, offset)
590 title += " \"" + q + "\""
593 commonData := svc.getCommonData(ctx, c, title)
594 data := &renderer.SearchData{
595 CommonData: commonData,
598 Users: results.Accounts,
599 Statuses: results.Statuses,
603 rCtx := getRendererContext(c)
604 return svc.renderer.RenderSearchPage(rCtx, c.Writer, data)
607 func (svc *service) ServeSettingsPage(ctx context.Context, c *model.Client) (err error) {
608 commonData := svc.getCommonData(ctx, c, "settings")
609 data := &renderer.SettingsData{
610 CommonData: commonData,
611 Settings: &c.Session.Settings,
614 rCtx := getRendererContext(c)
615 return svc.renderer.RenderSettingsPage(rCtx, c.Writer, data)
618 func (svc *service) NewSession(ctx context.Context, instance string) (
619 redirectUrl string, sessionID string, err error) {
621 var instanceURL string
622 if strings.HasPrefix(instance, "https://") {
623 instanceURL = instance
624 instance = strings.TrimPrefix(instance, "https://")
626 instanceURL = "https://" + instance
629 sessionID, err = util.NewSessionID()
634 csrfToken, err := util.NewCSRFToken()
639 session := model.Session{
641 InstanceDomain: instance,
642 CSRFToken: csrfToken,
643 Settings: *model.NewSettings(),
646 err = svc.sessionRepo.Add(session)
651 app, err := svc.appRepo.Get(instance)
653 if err != model.ErrAppNotFound {
657 mastoApp, err := mastodon.RegisterApp(ctx, &mastodon.AppConfig{
659 ClientName: svc.clientName,
660 Scopes: svc.clientScope,
661 Website: svc.clientWebsite,
662 RedirectURIs: svc.clientWebsite + "/oauth_callback",
669 InstanceDomain: instance,
670 InstanceURL: instanceURL,
671 ClientID: mastoApp.ClientID,
672 ClientSecret: mastoApp.ClientSecret,
675 err = svc.appRepo.Add(app)
681 u, err := url.Parse("/oauth/authorize")
686 q := make(url.Values)
687 q.Set("scope", "read write follow")
688 q.Set("client_id", app.ClientID)
689 q.Set("response_type", "code")
690 q.Set("redirect_uri", svc.clientWebsite+"/oauth_callback")
691 u.RawQuery = q.Encode()
693 redirectUrl = instanceURL + u.String()
698 func (svc *service) Signin(ctx context.Context, c *model.Client,
699 sessionID string, code string) (token string, userID string, err error) {
702 err = errInvalidArgument
706 err = c.AuthenticateToken(ctx, code, svc.clientWebsite+"/oauth_callback")
710 token = c.GetAccessToken(ctx)
712 u, err := c.GetAccountCurrentUser(ctx)
721 func (svc *service) Post(ctx context.Context, c *model.Client, content string,
722 replyToID string, format string, visibility string, isNSFW bool,
723 files []*multipart.FileHeader) (id string, err error) {
725 var mediaIDs []string
726 for _, f := range files {
727 a, err := c.UploadMediaFromMultipartFileHeader(ctx, f)
731 mediaIDs = append(mediaIDs, a.ID)
734 tweet := &mastodon.Toot{
736 InReplyToID: replyToID,
739 Visibility: visibility,
743 s, err := c.PostStatus(ctx, tweet)
751 func (svc *service) Like(ctx context.Context, c *model.Client, id string) (
752 count int64, err error) {
753 s, err := c.Favourite(ctx, id)
757 count = s.FavouritesCount
761 func (svc *service) UnLike(ctx context.Context, c *model.Client, id string) (
762 count int64, err error) {
763 s, err := c.Unfavourite(ctx, id)
767 count = s.FavouritesCount
771 func (svc *service) Retweet(ctx context.Context, c *model.Client, id string) (
772 count int64, err error) {
773 s, err := c.Reblog(ctx, id)
778 count = s.Reblog.ReblogsCount
783 func (svc *service) UnRetweet(ctx context.Context, c *model.Client, id string) (
784 count int64, err error) {
785 s, err := c.Unreblog(ctx, id)
789 count = s.ReblogsCount
793 func (svc *service) Vote(ctx context.Context, c *model.Client, id string,
794 choices []string) (err error) {
795 _, err = c.Vote(ctx, id, choices)
802 func (svc *service) Follow(ctx context.Context, c *model.Client, id string) (err error) {
803 _, err = c.AccountFollow(ctx, id)
807 func (svc *service) UnFollow(ctx context.Context, c *model.Client, id string) (err error) {
808 _, err = c.AccountUnfollow(ctx, id)
812 func (svc *service) Mute(ctx context.Context, c *model.Client, id string) (err error) {
813 _, err = c.AccountMute(ctx, id)
817 func (svc *service) UnMute(ctx context.Context, c *model.Client, id string) (err error) {
818 _, err = c.AccountUnmute(ctx, id)
822 func (svc *service) Block(ctx context.Context, c *model.Client, id string) (err error) {
823 _, err = c.AccountBlock(ctx, id)
827 func (svc *service) UnBlock(ctx context.Context, c *model.Client, id string) (err error) {
828 _, err = c.AccountUnblock(ctx, id)
832 func (svc *service) SaveSettings(ctx context.Context, c *model.Client,
833 settings *model.Settings) (err error) {
835 session, err := svc.sessionRepo.Get(c.Session.ID)
840 session.Settings = *settings
841 return svc.sessionRepo.Add(session)
844 func (svc *service) MuteConversation(ctx context.Context, c *model.Client,
845 id string) (err error) {
846 _, err = c.MuteConversation(ctx, id)
850 func (svc *service) UnMuteConversation(ctx context.Context, c *model.Client,
851 id string) (err error) {
852 _, err = c.UnmuteConversation(ctx, id)
856 func (svc *service) Delete(ctx context.Context, c *model.Client,
857 id string) (err error) {
858 return c.DeleteStatus(ctx, id)
861 func (svc *service) ReadNotifications(ctx context.Context, c *model.Client,
862 maxID string) (err error) {
863 return c.ReadNotifications(ctx, maxID)