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 ServeSearchPage(ctx context.Context, client io.Writer, c *model.Client, q string, qType string, offset int) (err error)
43 Like(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
44 UnLike(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
45 Retweet(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
46 UnRetweet(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
47 PostTweet(ctx context.Context, client io.Writer, c *model.Client, content string, replyToID string, format string, visibility string, isNSFW bool, files []*multipart.FileHeader) (id string, err error)
48 Follow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
49 UnFollow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error)
57 postFormats []model.PostFormat
58 renderer renderer.Renderer
59 sessionRepo model.SessionRepository
60 appRepo model.AppRepository
63 func NewService(clientName string, clientScope string, clientWebsite string,
64 customCSS string, postFormats []model.PostFormat, renderer renderer.Renderer,
65 sessionRepo model.SessionRepository, appRepo model.AppRepository) Service {
67 clientName: clientName,
68 clientScope: clientScope,
69 clientWebsite: clientWebsite,
71 postFormats: postFormats,
73 sessionRepo: sessionRepo,
78 func (svc *service) GetAuthUrl(ctx context.Context, instance string) (
79 redirectUrl string, sessionID string, err error) {
80 var instanceURL string
81 if strings.HasPrefix(instance, "https://") {
82 instanceURL = instance
83 instance = strings.TrimPrefix(instance, "https://")
85 instanceURL = "https://" + instance
88 sessionID = util.NewSessionId()
89 err = svc.sessionRepo.Add(model.Session{
91 InstanceDomain: instance,
97 app, err := svc.appRepo.Get(instance)
99 if err != model.ErrAppNotFound {
103 var mastoApp *mastodon.Application
104 mastoApp, err = mastodon.RegisterApp(ctx, &mastodon.AppConfig{
106 ClientName: svc.clientName,
107 Scopes: svc.clientScope,
108 Website: svc.clientWebsite,
109 RedirectURIs: svc.clientWebsite + "/oauth_callback",
116 InstanceDomain: instance,
117 InstanceURL: instanceURL,
118 ClientID: mastoApp.ClientID,
119 ClientSecret: mastoApp.ClientSecret,
122 err = svc.appRepo.Add(app)
128 u, err := url.Parse("/oauth/authorize")
133 q := make(url.Values)
134 q.Set("scope", "read write follow")
135 q.Set("client_id", app.ClientID)
136 q.Set("response_type", "code")
137 q.Set("redirect_uri", svc.clientWebsite+"/oauth_callback")
138 u.RawQuery = q.Encode()
140 redirectUrl = instanceURL + u.String()
145 func (svc *service) GetUserToken(ctx context.Context, sessionID string, c *model.Client,
146 code string) (token string, err error) {
148 err = ErrInvalidArgument
152 session, err := svc.sessionRepo.Get(sessionID)
157 app, err := svc.appRepo.Get(session.InstanceDomain)
162 data := &bytes.Buffer{}
163 err = json.NewEncoder(data).Encode(map[string]string{
164 "client_id": app.ClientID,
165 "client_secret": app.ClientSecret,
166 "grant_type": "authorization_code",
168 "redirect_uri": svc.clientWebsite + "/oauth_callback",
174 resp, err := http.Post(app.InstanceURL+"/oauth/token", "application/json", data)
178 defer resp.Body.Close()
181 AccessToken string `json:"access_token"`
184 err = json.NewDecoder(resp.Body).Decode(&res)
189 err = c.AuthenticateToken(ctx, code, svc.clientWebsite+"/oauth_callback")
193 err = svc.sessionRepo.Update(sessionID, c.GetAccessToken(ctx))
196 return res.AccessToken, nil
199 func (svc *service) ServeHomePage(ctx context.Context, client io.Writer) (err error) {
200 commonData, err := svc.getCommonData(ctx, client, nil)
205 data := &renderer.HomePageData{
206 CommonData: commonData,
209 return svc.renderer.RenderHomePage(ctx, client, data)
212 func (svc *service) ServeErrorPage(ctx context.Context, client io.Writer, err error) {
218 commonData, err := svc.getCommonData(ctx, client, nil)
223 data := &renderer.ErrorData{
224 CommonData: commonData,
228 svc.renderer.RenderErrorPage(ctx, client, data)
231 func (svc *service) ServeSigninPage(ctx context.Context, client io.Writer) (err error) {
232 commonData, err := svc.getCommonData(ctx, client, nil)
237 data := &renderer.SigninData{
238 CommonData: commonData,
241 return svc.renderer.RenderSigninPage(ctx, client, data)
244 func (svc *service) ServeTimelinePage(ctx context.Context, client io.Writer,
245 c *model.Client, timelineType string, maxID string, sinceID string, minID string) (err error) {
247 var hasNext, hasPrev bool
248 var nextLink, prevLink string
250 var pg = mastodon.Pagination{
256 var statuses []*mastodon.Status
258 switch timelineType {
260 return ErrInvalidTimeline
262 statuses, err = c.GetTimelineHome(ctx, &pg)
265 statuses, err = c.GetTimelinePublic(ctx, true, &pg)
266 title = "Local Timeline"
268 statuses, err = c.GetTimelinePublic(ctx, false, &pg)
269 title = "The Whole Known Network"
275 if len(maxID) > 0 && len(statuses) > 0 {
277 prevLink = fmt.Sprintf("/timeline/$s?min_id=%s", timelineType, statuses[0].ID)
279 if len(minID) > 0 && len(pg.MinID) > 0 {
280 newStatuses, err := c.GetTimelineHome(ctx, &mastodon.Pagination{MinID: pg.MinID, Limit: 20})
284 newStatusesLen := len(newStatuses)
285 if newStatusesLen == 20 {
287 prevLink = fmt.Sprintf("/timeline/%s?min_id=%s", timelineType, pg.MinID)
289 i := 20 - newStatusesLen - 1
290 if len(statuses) > i {
292 prevLink = fmt.Sprintf("/timeline/%s?min_id=%s", timelineType, statuses[i].ID)
296 if len(pg.MaxID) > 0 {
298 nextLink = fmt.Sprintf("/timeline/%s?max_id=%s", timelineType, pg.MaxID)
301 postContext := model.PostContext{
302 DefaultVisibility: c.Session.Settings.DefaultVisibility,
303 Formats: svc.postFormats,
306 commonData, err := svc.getCommonData(ctx, client, c)
311 data := &renderer.TimelineData{
318 PostContext: postContext,
319 CommonData: commonData,
322 err = svc.renderer.RenderTimelinePage(ctx, client, data)
330 func (svc *service) ServeThreadPage(ctx context.Context, client io.Writer, c *model.Client, id string, reply bool) (err error) {
331 status, err := c.GetStatus(ctx, id)
336 u, err := c.GetAccountCurrentUser(ctx)
341 var postContext model.PostContext
344 if u.ID != status.Account.ID {
345 content += "@" + status.Account.Acct + " "
347 for i := range status.Mentions {
348 if status.Mentions[i].ID != u.ID && status.Mentions[i].ID != status.Account.ID {
349 content += "@" + status.Mentions[i].Acct + " "
353 s, err := c.GetStatus(ctx, id)
358 postContext = model.PostContext{
359 DefaultVisibility: s.Visibility,
360 Formats: svc.postFormats,
361 ReplyContext: &model.ReplyContext{
363 InReplyToName: status.Account.Acct,
364 ReplyContent: content,
369 context, err := c.GetStatusContext(ctx, id)
374 statuses := append(append(context.Ancestors, status), context.Descendants...)
376 replyMap := make(map[string][]mastodon.ReplyInfo)
378 for i := range statuses {
379 statuses[i].ShowReplies = true
380 statuses[i].ReplyMap = replyMap
381 addToReplyMap(replyMap, statuses[i].InReplyToID, statuses[i].ID, i+1)
384 commonData, err := svc.getCommonData(ctx, client, c)
389 data := &renderer.ThreadData{
391 PostContext: postContext,
393 CommonData: commonData,
396 err = svc.renderer.RenderThreadPage(ctx, client, data)
404 func (svc *service) ServeNotificationPage(ctx context.Context, client io.Writer, c *model.Client, maxID string, minID string) (err error) {
408 var pg = mastodon.Pagination{
414 notifications, err := c.GetNotifications(ctx, &pg)
420 for i := range notifications {
421 switch notifications[i].Type {
422 case "reblog", "favourite":
423 if notifications[i].Status != nil {
424 notifications[i].Status.HideAccountInfo = true
427 if notifications[i].Pleroma != nil && notifications[i].Pleroma.IsSeen {
433 err := c.ReadNotifications(ctx, notifications[0].ID)
439 if len(pg.MaxID) > 0 {
441 nextLink = "/notifications?max_id=" + pg.MaxID
444 commonData, err := svc.getCommonData(ctx, client, c)
449 data := &renderer.NotificationData{
450 Notifications: notifications,
453 CommonData: commonData,
455 err = svc.renderer.RenderNotificationPage(ctx, client, data)
463 func (svc *service) ServeUserPage(ctx context.Context, client io.Writer, c *model.Client, id string, maxID string, minID string) (err error) {
464 user, err := c.GetAccount(ctx, id)
472 var pg = mastodon.Pagination{
478 statuses, err := c.GetAccountStatuses(ctx, id, &pg)
483 if len(pg.MaxID) > 0 {
485 nextLink = "/user/" + id + "?max_id=" + pg.MaxID
488 commonData, err := svc.getCommonData(ctx, client, c)
493 data := &renderer.UserData{
498 CommonData: commonData,
501 err = svc.renderer.RenderUserPage(ctx, client, data)
509 func (svc *service) ServeAboutPage(ctx context.Context, client io.Writer, c *model.Client) (err error) {
510 commonData, err := svc.getCommonData(ctx, client, c)
515 data := &renderer.AboutData{
516 CommonData: commonData,
518 err = svc.renderer.RenderAboutPage(ctx, client, data)
526 func (svc *service) ServeEmojiPage(ctx context.Context, client io.Writer, c *model.Client) (err error) {
527 commonData, err := svc.getCommonData(ctx, client, c)
532 emojis, err := c.GetInstanceEmojis(ctx)
537 data := &renderer.EmojiData{
539 CommonData: commonData,
542 err = svc.renderer.RenderEmojiPage(ctx, client, data)
550 func (svc *service) ServeLikedByPage(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
551 likers, err := c.GetFavouritedBy(ctx, id, nil)
556 commonData, err := svc.getCommonData(ctx, client, c)
561 data := &renderer.LikedByData{
562 CommonData: commonData,
566 err = svc.renderer.RenderLikedByPage(ctx, client, data)
574 func (svc *service) ServeRetweetedByPage(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
575 retweeters, err := c.GetRebloggedBy(ctx, id, nil)
580 commonData, err := svc.getCommonData(ctx, client, c)
585 data := &renderer.RetweetedByData{
586 CommonData: commonData,
590 err = svc.renderer.RenderRetweetedByPage(ctx, client, data)
598 func (svc *service) ServeSearchPage(ctx context.Context, client io.Writer, c *model.Client, q string, qType string, offset int) (err error) {
602 results, err := c.Search(ctx, q, qType, 20, true, offset)
609 hasNext = len(results.Accounts) == 20
611 hasNext = len(results.Statuses) == 20
616 nextLink = fmt.Sprintf("/search?q=%s&type=%s&offset=%d", q, qType, offset)
619 commonData, err := svc.getCommonData(ctx, client, c)
624 data := &renderer.SearchData{
625 CommonData: commonData,
628 Users: results.Accounts,
629 Statuses: results.Statuses,
634 err = svc.renderer.RenderSearchPage(ctx, client, data)
642 func (svc *service) getCommonData(ctx context.Context, client io.Writer, c *model.Client) (data *renderer.CommonData, err error) {
643 data = new(renderer.CommonData)
645 data.HeaderData = &renderer.HeaderData{
647 NotificationCount: 0,
648 CustomCSS: svc.customCSS,
651 if c != nil && c.Session.IsLoggedIn() {
652 notifications, err := c.GetNotifications(ctx, nil)
657 var notificationCount int
658 for i := range notifications {
659 if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen {
664 u, err := c.GetAccountCurrentUser(ctx)
669 data.NavbarData = &renderer.NavbarData{
671 NotificationCount: notificationCount,
674 data.HeaderData.NotificationCount = notificationCount
680 func (svc *service) Like(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
681 _, err = c.Favourite(ctx, id)
685 func (svc *service) UnLike(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
686 _, err = c.Unfavourite(ctx, id)
690 func (svc *service) Retweet(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
691 _, err = c.Reblog(ctx, id)
695 func (svc *service) UnRetweet(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
696 _, err = c.Unreblog(ctx, id)
700 func (svc *service) PostTweet(ctx context.Context, client io.Writer, c *model.Client, content string, replyToID string, format string, visibility string, isNSFW bool, files []*multipart.FileHeader) (id string, err error) {
701 var mediaIds []string
702 for _, f := range files {
703 a, err := c.UploadMediaFromMultipartFileHeader(ctx, f)
707 mediaIds = append(mediaIds, a.ID)
710 // save visibility if it's a non-reply post
711 if len(replyToID) < 1 && visibility != c.Session.Settings.DefaultVisibility {
712 c.Session.Settings.DefaultVisibility = visibility
713 svc.sessionRepo.Add(c.Session)
716 tweet := &mastodon.Toot{
718 InReplyToID: replyToID,
721 Visibility: visibility,
725 s, err := c.PostStatus(ctx, tweet)
733 func (svc *service) Follow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
734 _, err = c.AccountFollow(ctx, id)
738 func (svc *service) UnFollow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
739 _, err = c.AccountUnfollow(ctx, id)
743 func addToReplyMap(m map[string][]mastodon.ReplyInfo, key interface{}, val string, number int) {
748 keyStr, ok := key.(string)
754 m[keyStr] = []mastodon.ReplyInfo{}
757 m[keyStr] = append(m[keyStr], mastodon.ReplyInfo{val, number})