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, format 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 postFormats []model.PostFormat
57 renderer renderer.Renderer
58 sessionRepo model.SessionRepository
59 appRepo model.AppRepository
62 func NewService(clientName string, clientScope string, clientWebsite string,
63 customCSS string, postFormats []model.PostFormat, renderer renderer.Renderer,
64 sessionRepo model.SessionRepository, appRepo model.AppRepository) Service {
66 clientName: clientName,
67 clientScope: clientScope,
68 clientWebsite: clientWebsite,
70 postFormats: postFormats,
72 sessionRepo: sessionRepo,
77 func (svc *service) GetAuthUrl(ctx context.Context, instance string) (
78 redirectUrl string, sessionID string, err error) {
79 var instanceURL string
80 if strings.HasPrefix(instance, "https://") {
81 instanceURL = instance
82 instance = strings.TrimPrefix(instance, "https://")
84 instanceURL = "https://" + instance
87 sessionID = util.NewSessionId()
88 err = svc.sessionRepo.Add(model.Session{
90 InstanceDomain: instance,
96 app, err := svc.appRepo.Get(instance)
98 if err != model.ErrAppNotFound {
102 var mastoApp *mastodon.Application
103 mastoApp, err = mastodon.RegisterApp(ctx, &mastodon.AppConfig{
105 ClientName: svc.clientName,
106 Scopes: svc.clientScope,
107 Website: svc.clientWebsite,
108 RedirectURIs: svc.clientWebsite + "/oauth_callback",
115 InstanceDomain: instance,
116 InstanceURL: instanceURL,
117 ClientID: mastoApp.ClientID,
118 ClientSecret: mastoApp.ClientSecret,
121 err = svc.appRepo.Add(app)
127 u, err := url.Parse("/oauth/authorize")
132 q := make(url.Values)
133 q.Set("scope", "read write follow")
134 q.Set("client_id", app.ClientID)
135 q.Set("response_type", "code")
136 q.Set("redirect_uri", svc.clientWebsite+"/oauth_callback")
137 u.RawQuery = q.Encode()
139 redirectUrl = instanceURL + u.String()
144 func (svc *service) GetUserToken(ctx context.Context, sessionID string, c *model.Client,
145 code string) (token string, err error) {
147 err = ErrInvalidArgument
151 session, err := svc.sessionRepo.Get(sessionID)
156 app, err := svc.appRepo.Get(session.InstanceDomain)
161 data := &bytes.Buffer{}
162 err = json.NewEncoder(data).Encode(map[string]string{
163 "client_id": app.ClientID,
164 "client_secret": app.ClientSecret,
165 "grant_type": "authorization_code",
167 "redirect_uri": svc.clientWebsite + "/oauth_callback",
173 resp, err := http.Post(app.InstanceURL+"/oauth/token", "application/json", data)
177 defer resp.Body.Close()
180 AccessToken string `json:"access_token"`
183 err = json.NewDecoder(resp.Body).Decode(&res)
188 err = c.AuthenticateToken(ctx, code, svc.clientWebsite+"/oauth_callback")
192 err = svc.sessionRepo.Update(sessionID, c.GetAccessToken(ctx))
195 return res.AccessToken, nil
198 func (svc *service) ServeHomePage(ctx context.Context, client io.Writer) (err error) {
199 commonData, err := svc.getCommonData(ctx, client, nil)
204 data := &renderer.HomePageData{
205 CommonData: commonData,
208 return svc.renderer.RenderHomePage(ctx, client, data)
211 func (svc *service) ServeErrorPage(ctx context.Context, client io.Writer, err error) {
217 commonData, err := svc.getCommonData(ctx, client, nil)
222 data := &renderer.ErrorData{
223 CommonData: commonData,
227 svc.renderer.RenderErrorPage(ctx, client, data)
230 func (svc *service) ServeSigninPage(ctx context.Context, client io.Writer) (err error) {
231 commonData, err := svc.getCommonData(ctx, client, nil)
236 data := &renderer.SigninData{
237 CommonData: commonData,
240 return svc.renderer.RenderSigninPage(ctx, client, data)
243 func (svc *service) ServeTimelinePage(ctx context.Context, client io.Writer,
244 c *model.Client, timelineType string, maxID string, sinceID string, minID string) (err error) {
246 var hasNext, hasPrev bool
247 var nextLink, prevLink string
249 var pg = mastodon.Pagination{
255 var statuses []*mastodon.Status
257 switch timelineType {
259 return ErrInvalidTimeline
261 statuses, err = c.GetTimelineHome(ctx, &pg)
264 statuses, err = c.GetTimelinePublic(ctx, true, &pg)
265 title = "Local Timeline"
267 statuses, err = c.GetTimelinePublic(ctx, false, &pg)
268 title = "The Whole Known Network"
274 if len(maxID) > 0 && len(statuses) > 0 {
276 prevLink = fmt.Sprintf("/timeline/$s?min_id=%s", timelineType, statuses[0].ID)
278 if len(minID) > 0 && len(pg.MinID) > 0 {
279 newStatuses, err := c.GetTimelineHome(ctx, &mastodon.Pagination{MinID: pg.MinID, Limit: 20})
283 newStatusesLen := len(newStatuses)
284 if newStatusesLen == 20 {
286 prevLink = fmt.Sprintf("/timeline/%s?min_id=%s", timelineType, pg.MinID)
288 i := 20 - newStatusesLen - 1
289 if len(statuses) > i {
291 prevLink = fmt.Sprintf("/timeline/%s?min_id=%s", timelineType, statuses[i].ID)
295 if len(pg.MaxID) > 0 {
297 nextLink = fmt.Sprintf("/timeline/%s?max_id=%s", timelineType, pg.MaxID)
300 postContext := model.PostContext{
301 DefaultVisibility: c.Session.Settings.DefaultVisibility,
302 Formats: svc.postFormats,
305 commonData, err := svc.getCommonData(ctx, client, c)
310 data := &renderer.TimelineData{
317 PostContext: postContext,
318 CommonData: commonData,
321 err = svc.renderer.RenderTimelinePage(ctx, client, data)
329 func (svc *service) ServeThreadPage(ctx context.Context, client io.Writer, c *model.Client, id string, reply bool) (err error) {
330 status, err := c.GetStatus(ctx, id)
335 u, err := c.GetAccountCurrentUser(ctx)
340 var postContext model.PostContext
343 if u.ID != status.Account.ID {
344 content += "@" + status.Account.Acct + " "
346 for i := range status.Mentions {
347 if status.Mentions[i].ID != u.ID && status.Mentions[i].ID != status.Account.ID {
348 content += "@" + status.Mentions[i].Acct + " "
352 s, err := c.GetStatus(ctx, id)
357 postContext = model.PostContext{
358 DefaultVisibility: s.Visibility,
359 Formats: svc.postFormats,
360 ReplyContext: &model.ReplyContext{
362 InReplyToName: status.Account.Acct,
363 ReplyContent: content,
368 context, err := c.GetStatusContext(ctx, id)
373 statuses := append(append(context.Ancestors, status), context.Descendants...)
375 replyMap := make(map[string][]mastodon.ReplyInfo)
377 for i := range statuses {
378 statuses[i].ShowReplies = true
379 statuses[i].ReplyMap = replyMap
380 addToReplyMap(replyMap, statuses[i].InReplyToID, statuses[i].ID, i+1)
383 commonData, err := svc.getCommonData(ctx, client, c)
388 data := &renderer.ThreadData{
390 PostContext: postContext,
392 CommonData: commonData,
395 err = svc.renderer.RenderThreadPage(ctx, client, data)
403 func (svc *service) ServeNotificationPage(ctx context.Context, client io.Writer, c *model.Client, maxID string, minID string) (err error) {
407 var pg = mastodon.Pagination{
413 notifications, err := c.GetNotifications(ctx, &pg)
419 for i := range notifications {
420 switch notifications[i].Type {
421 case "reblog", "favourite":
422 if notifications[i].Status != nil {
423 notifications[i].Status.HideAccountInfo = true
426 if notifications[i].Pleroma != nil && notifications[i].Pleroma.IsSeen {
432 err := c.ReadNotifications(ctx, notifications[0].ID)
438 if len(pg.MaxID) > 0 {
440 nextLink = "/notifications?max_id=" + pg.MaxID
443 commonData, err := svc.getCommonData(ctx, client, c)
448 data := &renderer.NotificationData{
449 Notifications: notifications,
452 CommonData: commonData,
454 err = svc.renderer.RenderNotificationPage(ctx, client, data)
462 func (svc *service) ServeUserPage(ctx context.Context, client io.Writer, c *model.Client, id string, maxID string, minID string) (err error) {
463 user, err := c.GetAccount(ctx, id)
471 var pg = mastodon.Pagination{
477 statuses, err := c.GetAccountStatuses(ctx, id, &pg)
482 if len(pg.MaxID) > 0 {
484 nextLink = "/user/" + id + "?max_id=" + pg.MaxID
487 commonData, err := svc.getCommonData(ctx, client, c)
492 data := &renderer.UserData{
497 CommonData: commonData,
500 err = svc.renderer.RenderUserPage(ctx, client, data)
508 func (svc *service) ServeAboutPage(ctx context.Context, client io.Writer, c *model.Client) (err error) {
509 commonData, err := svc.getCommonData(ctx, client, c)
514 data := &renderer.AboutData{
515 CommonData: commonData,
517 err = svc.renderer.RenderAboutPage(ctx, client, data)
525 func (svc *service) ServeEmojiPage(ctx context.Context, client io.Writer, c *model.Client) (err error) {
526 commonData, err := svc.getCommonData(ctx, client, c)
531 emojis, err := c.GetInstanceEmojis(ctx)
536 data := &renderer.EmojiData{
538 CommonData: commonData,
541 err = svc.renderer.RenderEmojiPage(ctx, client, data)
549 func (svc *service) ServeLikedByPage(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
550 likers, err := c.GetFavouritedBy(ctx, id, nil)
555 commonData, err := svc.getCommonData(ctx, client, c)
560 data := &renderer.LikedByData{
561 CommonData: commonData,
565 err = svc.renderer.RenderLikedByPage(ctx, client, data)
573 func (svc *service) ServeRetweetedByPage(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
574 retweeters, err := c.GetRebloggedBy(ctx, id, nil)
579 commonData, err := svc.getCommonData(ctx, client, c)
584 data := &renderer.RetweetedByData{
585 CommonData: commonData,
589 err = svc.renderer.RenderRetweetedByPage(ctx, client, data)
596 func (svc *service) getCommonData(ctx context.Context, client io.Writer, c *model.Client) (data *renderer.CommonData, err error) {
597 data = new(renderer.CommonData)
599 data.HeaderData = &renderer.HeaderData{
601 NotificationCount: 0,
602 CustomCSS: svc.customCSS,
605 if c != nil && c.Session.IsLoggedIn() {
606 notifications, err := c.GetNotifications(ctx, nil)
611 var notificationCount int
612 for i := range notifications {
613 if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen {
618 u, err := c.GetAccountCurrentUser(ctx)
623 data.NavbarData = &renderer.NavbarData{
625 NotificationCount: notificationCount,
628 data.HeaderData.NotificationCount = notificationCount
634 func (svc *service) Like(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
635 _, err = c.Favourite(ctx, id)
639 func (svc *service) UnLike(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
640 _, err = c.Unfavourite(ctx, id)
644 func (svc *service) Retweet(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
645 _, err = c.Reblog(ctx, id)
649 func (svc *service) UnRetweet(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
650 _, err = c.Unreblog(ctx, id)
654 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) {
655 var mediaIds []string
656 for _, f := range files {
657 a, err := c.UploadMediaFromMultipartFileHeader(ctx, f)
661 mediaIds = append(mediaIds, a.ID)
664 // save visibility if it's a non-reply post
665 if len(replyToID) < 1 && visibility != c.Session.Settings.DefaultVisibility {
666 c.Session.Settings.DefaultVisibility = visibility
667 svc.sessionRepo.Add(c.Session)
670 tweet := &mastodon.Toot{
672 InReplyToID: replyToID,
675 Visibility: visibility,
679 s, err := c.PostStatus(ctx, tweet)
687 func (svc *service) Follow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
688 _, err = c.AccountFollow(ctx, id)
692 func (svc *service) UnFollow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) {
693 _, err = c.AccountUnfollow(ctx, id)
697 func addToReplyMap(m map[string][]mastodon.ReplyInfo, key interface{}, val string, number int) {
702 keyStr, ok := key.(string)
708 m[keyStr] = []mastodon.ReplyInfo{}
711 m[keyStr] = append(m[keyStr], mastodon.ReplyInfo{val, number})