21 ErrInvalidArgument = errors.New("invalid argument")
22 ErrInvalidToken = errors.New("invalid token")
23 ErrInvalidClient = errors.New("invalid client")
26 type Service interface {
27 ServeHomePage(ctx context.Context, client io.Writer) (err error)
28 GetAuthUrl(ctx context.Context, instance string) (url string, sessionID string, err error)
29 GetUserToken(ctx context.Context, sessionID string, c *mastodon.Client, token string) (accessToken string, err error)
30 ServeErrorPage(ctx context.Context, client io.Writer, err error)
31 ServeSigninPage(ctx context.Context, client io.Writer) (err error)
32 ServeTimelinePage(ctx context.Context, client io.Writer, c *mastodon.Client, maxID string, sinceID string, minID string) (err error)
33 ServeThreadPage(ctx context.Context, client io.Writer, c *mastodon.Client, id string, reply bool) (err error)
34 ServeNotificationPage(ctx context.Context, client io.Writer, c *mastodon.Client, maxID string, minID string) (err error)
35 Like(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error)
36 UnLike(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error)
37 Retweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error)
38 UnRetweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error)
39 PostTweet(ctx context.Context, client io.Writer, c *mastodon.Client, content string, replyToID string, files []*multipart.FileHeader) (id string, err error)
46 renderer renderer.Renderer
47 sessionRepo model.SessionRepository
48 appRepo model.AppRepository
51 func NewService(clientName string, clientScope string, clientWebsite string,
52 renderer renderer.Renderer, sessionRepo model.SessionRepository,
53 appRepo model.AppRepository) Service {
55 clientName: clientName,
56 clientScope: clientScope,
57 clientWebsite: clientWebsite,
59 sessionRepo: sessionRepo,
64 func (svc *service) GetAuthUrl(ctx context.Context, instance string) (
65 redirectUrl string, sessionID string, err error) {
66 var instanceURL string
67 if strings.HasPrefix(instance, "https://") {
68 instanceURL = instance
69 instance = strings.TrimPrefix(instance, "https://")
71 instanceURL = "https://" + instance
74 sessionID = util.NewSessionId()
75 err = svc.sessionRepo.Add(model.Session{
77 InstanceDomain: instance,
83 app, err := svc.appRepo.Get(instance)
85 if err != model.ErrAppNotFound {
89 var mastoApp *mastodon.Application
90 mastoApp, err = mastodon.RegisterApp(ctx, &mastodon.AppConfig{
92 ClientName: svc.clientName,
93 Scopes: svc.clientScope,
94 Website: svc.clientWebsite,
95 RedirectURIs: svc.clientWebsite + "/oauth_callback",
102 InstanceDomain: instance,
103 InstanceURL: instanceURL,
104 ClientID: mastoApp.ClientID,
105 ClientSecret: mastoApp.ClientSecret,
108 err = svc.appRepo.Add(app)
114 u, err := url.Parse("/oauth/authorize")
119 q := make(url.Values)
120 q.Set("scope", "read write follow")
121 q.Set("client_id", app.ClientID)
122 q.Set("response_type", "code")
123 q.Set("redirect_uri", svc.clientWebsite+"/oauth_callback")
124 u.RawQuery = q.Encode()
126 redirectUrl = instanceURL + u.String()
131 func (svc *service) GetUserToken(ctx context.Context, sessionID string, c *mastodon.Client,
132 code string) (token string, err error) {
134 err = ErrInvalidArgument
138 session, err := svc.sessionRepo.Get(sessionID)
143 app, err := svc.appRepo.Get(session.InstanceDomain)
148 data := &bytes.Buffer{}
149 err = json.NewEncoder(data).Encode(map[string]string{
150 "client_id": app.ClientID,
151 "client_secret": app.ClientSecret,
152 "grant_type": "authorization_code",
154 "redirect_uri": svc.clientWebsite + "/oauth_callback",
160 resp, err := http.Post(app.InstanceURL+"/oauth/token", "application/json", data)
164 defer resp.Body.Close()
167 AccessToken string `json:"access_token"`
170 err = json.NewDecoder(resp.Body).Decode(&res)
175 err = c.AuthenticateToken(ctx, code, svc.clientWebsite+"/oauth_callback")
179 err = svc.sessionRepo.Update(sessionID, c.GetAccessToken(ctx))
182 return res.AccessToken, nil
185 func (svc *service) ServeHomePage(ctx context.Context, client io.Writer) (err error) {
186 err = svc.renderer.RenderHomePage(ctx, client)
194 func (svc *service) ServeErrorPage(ctx context.Context, client io.Writer, err error) {
195 svc.renderer.RenderErrorPage(ctx, client, err)
198 func (svc *service) ServeSigninPage(ctx context.Context, client io.Writer) (err error) {
199 err = svc.renderer.RenderSigninPage(ctx, client)
207 func (svc *service) ServeTimelinePage(ctx context.Context, client io.Writer,
208 c *mastodon.Client, maxID string, sinceID string, minID string) (err error) {
210 var hasNext, hasPrev bool
211 var nextLink, prevLink string
213 var pg = mastodon.Pagination{
219 statuses, err := c.GetTimelineHome(ctx, &pg)
224 if len(maxID) > 0 && len(statuses) > 0 {
226 prevLink = "/timeline?min_id=" + statuses[0].ID
228 if len(minID) > 0 && len(pg.MinID) > 0 {
229 newStatuses, err := c.GetTimelineHome(ctx, &mastodon.Pagination{MinID: pg.MinID, Limit: 20})
233 newStatusesLen := len(newStatuses)
234 if newStatusesLen == 20 {
236 prevLink = "/timeline?min_id=" + pg.MinID
238 i := 20 - newStatusesLen - 1
239 if len(statuses) > i {
241 prevLink = "/timeline?min_id=" + statuses[i].ID
245 if len(pg.MaxID) > 0 {
247 nextLink = "/timeline?max_id=" + pg.MaxID
250 navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
255 data := renderer.NewTimelinePageTemplateData(statuses, hasNext, nextLink, hasPrev, prevLink, navbarData)
256 err = svc.renderer.RenderTimelinePage(ctx, client, data)
264 func (svc *service) ServeThreadPage(ctx context.Context, client io.Writer, c *mastodon.Client, id string, reply bool) (err error) {
265 status, err := c.GetStatus(ctx, id)
270 u, err := c.GetAccountCurrentUser(ctx)
279 if u.ID != status.Account.ID {
280 content += "@" + status.Account.Acct + " "
282 for i := range status.Mentions {
283 if status.Mentions[i].ID != u.ID && status.Mentions[i].ID != status.Account.ID {
284 content += "@" + status.Mentions[i].Acct + " "
289 context, err := c.GetStatusContext(ctx, id)
294 statuses := append(append(context.Ancestors, status), context.Descendants...)
296 replyMap := make(map[string][]mastodon.ReplyInfo)
298 for i := range statuses {
299 statuses[i].ShowReplies = true
300 statuses[i].ReplyMap = replyMap
301 addToReplyMap(replyMap, statuses[i].InReplyToID, statuses[i].ID, i+1)
304 navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
309 data := renderer.NewThreadPageTemplateData(statuses, reply, replyToID, content, replyMap, navbarData)
310 err = svc.renderer.RenderThreadPage(ctx, client, data)
318 func (svc *service) ServeNotificationPage(ctx context.Context, client io.Writer, c *mastodon.Client, maxID string, minID string) (err error) {
322 var pg = mastodon.Pagination{
328 notifications, err := c.GetNotifications(ctx, &pg)
334 for i := range notifications {
335 switch notifications[i].Type {
336 case "reblog", "favourite":
337 if notifications[i].Status != nil {
338 notifications[i].Status.HideAccountInfo = true
341 if notifications[i].Pleroma != nil && notifications[i].Pleroma.IsSeen {
347 err := c.ReadNotifications(ctx, notifications[0].ID)
353 if len(pg.MaxID) > 0 {
355 nextLink = "/notifications?max_id=" + pg.MaxID
358 navbarData, err := svc.getNavbarTemplateData(ctx, client, c)
363 data := renderer.NewNotificationPageTemplateData(notifications, hasNext, nextLink, navbarData)
364 err = svc.renderer.RenderNotificationPage(ctx, client, data)
372 func (svc *service) getNavbarTemplateData(ctx context.Context, client io.Writer, c *mastodon.Client) (data *renderer.NavbarTemplateData, err error) {
373 notifications, err := c.GetNotifications(ctx, nil)
378 var notificationCount int
379 for i := range notifications {
380 if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen {
385 data = renderer.NewNavbarTemplateData(notificationCount)
390 func (svc *service) Like(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
391 _, err = c.Favourite(ctx, id)
395 func (svc *service) UnLike(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
396 _, err = c.Unfavourite(ctx, id)
400 func (svc *service) Retweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
401 _, err = c.Reblog(ctx, id)
405 func (svc *service) UnRetweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) {
406 _, err = c.Unreblog(ctx, id)
410 func (svc *service) PostTweet(ctx context.Context, client io.Writer, c *mastodon.Client, content string, replyToID string, files []*multipart.FileHeader) (id string, err error) {
411 var mediaIds []string
412 for _, f := range files {
413 a, err := c.UploadMediaFromMultipartFileHeader(ctx, f)
417 mediaIds = append(mediaIds, a.ID)
420 tweet := &mastodon.Toot{
422 InReplyToID: replyToID,
426 s, err := c.PostStatus(ctx, tweet)
434 func addToReplyMap(m map[string][]mastodon.ReplyInfo, key interface{}, val string, number int) {
438 keyStr, ok := key.(string)
444 m[keyStr] = []mastodon.ReplyInfo{}
447 m[keyStr] = append(m[keyStr], mastodon.ReplyInfo{val, number})