diff options
Diffstat (limited to 'service/service.go')
-rw-r--r-- | service/service.go | 811 |
1 files changed, 362 insertions, 449 deletions
diff --git a/service/service.go b/service/service.go index c9fccb4..7ad860f 100644 --- a/service/service.go +++ b/service/service.go @@ -1,14 +1,10 @@ package service import ( - "bytes" "context" - "encoding/json" "errors" "fmt" - "io" "mime/multipart" - "net/http" "net/url" "strings" @@ -19,37 +15,35 @@ import ( ) var ( - ErrInvalidArgument = errors.New("invalid argument") - ErrInvalidToken = errors.New("invalid token") - ErrInvalidClient = errors.New("invalid client") - ErrInvalidTimeline = errors.New("invalid timeline") + errInvalidArgument = errors.New("invalid argument") ) type Service interface { - GetAuthUrl(ctx context.Context, instance string) (url string, sessionID string, err error) - GetUserToken(ctx context.Context, sessionID string, c *model.Client, token string) (accessToken string, err error) - ServeErrorPage(ctx context.Context, client io.Writer, c *model.Client, err error) - ServeSigninPage(ctx context.Context, client io.Writer) (err error) - ServeTimelinePage(ctx context.Context, client io.Writer, c *model.Client, timelineType string, maxID string, sinceID string, minID string) (err error) - ServeThreadPage(ctx context.Context, client io.Writer, c *model.Client, id string, reply bool) (err error) - ServeNotificationPage(ctx context.Context, client io.Writer, c *model.Client, maxID string, minID string) (err error) - ServeUserPage(ctx context.Context, client io.Writer, c *model.Client, id string, maxID string, minID string) (err error) - ServeAboutPage(ctx context.Context, client io.Writer, c *model.Client) (err error) - ServeEmojiPage(ctx context.Context, client io.Writer, c *model.Client) (err error) - ServeLikedByPage(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) - ServeRetweetedByPage(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) - ServeFollowingPage(ctx context.Context, client io.Writer, c *model.Client, id string, maxID string, minID string) (err error) - ServeFollowersPage(ctx context.Context, client io.Writer, c *model.Client, id string, maxID string, minID string) (err error) - ServeSearchPage(ctx context.Context, client io.Writer, c *model.Client, q string, qType string, offset int) (err error) - ServeSettingsPage(ctx context.Context, client io.Writer, c *model.Client) (err error) - SaveSettings(ctx context.Context, client io.Writer, c *model.Client, settings *model.Settings) (err error) - Like(ctx context.Context, client io.Writer, c *model.Client, id string) (count int64, err error) - UnLike(ctx context.Context, client io.Writer, c *model.Client, id string) (count int64, err error) - Retweet(ctx context.Context, client io.Writer, c *model.Client, id string) (count int64, err error) - UnRetweet(ctx context.Context, client io.Writer, c *model.Client, id string) (count int64, err error) - 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) - Follow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) - UnFollow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) + ServeErrorPage(ctx context.Context, c *model.Client, err error) + ServeSigninPage(ctx context.Context, c *model.Client) (err error) + ServeTimelinePage(ctx context.Context, c *model.Client, tType string, maxID string, minID string) (err error) + ServeThreadPage(ctx context.Context, c *model.Client, id string, reply bool) (err error) + ServeLikedByPage(ctx context.Context, c *model.Client, id string) (err error) + ServeRetweetedByPage(ctx context.Context, c *model.Client, id string) (err error) + ServeFollowingPage(ctx context.Context, c *model.Client, id string, maxID string, minID string) (err error) + ServeFollowersPage(ctx context.Context, c *model.Client, id string, maxID string, minID string) (err error) + ServeNotificationPage(ctx context.Context, c *model.Client, maxID string, minID string) (err error) + ServeUserPage(ctx context.Context, c *model.Client, id string, maxID string, minID string) (err error) + ServeAboutPage(ctx context.Context, c *model.Client) (err error) + ServeEmojiPage(ctx context.Context, c *model.Client) (err error) + ServeSearchPage(ctx context.Context, c *model.Client, q string, qType string, offset int) (err error) + ServeSettingsPage(ctx context.Context, c *model.Client) (err error) + NewSession(ctx context.Context, instance string) (redirectUrl string, sessionID string, err error) + Signin(ctx context.Context, c *model.Client, sessionID string, code string) (token string, err error) + Post(ctx context.Context, c *model.Client, content string, replyToID string, format string, + visibility string, isNSFW bool, files []*multipart.FileHeader) (id string, err error) + Like(ctx context.Context, c *model.Client, id string) (count int64, err error) + UnLike(ctx context.Context, c *model.Client, id string) (count int64, err error) + Retweet(ctx context.Context, c *model.Client, id string) (count int64, err error) + UnRetweet(ctx context.Context, c *model.Client, id string) (count int64, err error) + Follow(ctx context.Context, c *model.Client, id string) (err error) + UnFollow(ctx context.Context, c *model.Client, id string) (err error) + SaveSettings(ctx context.Context, c *model.Client, settings *model.Settings) (err error) } type service struct { @@ -59,13 +53,19 @@ type service struct { customCSS string postFormats []model.PostFormat renderer renderer.Renderer - sessionRepo model.SessionRepository - appRepo model.AppRepository + sessionRepo model.SessionRepo + appRepo model.AppRepo } -func NewService(clientName string, clientScope string, clientWebsite string, - customCSS string, postFormats []model.PostFormat, renderer renderer.Renderer, - sessionRepo model.SessionRepository, appRepo model.AppRepository) Service { +func NewService(clientName string, + clientScope string, + clientWebsite string, + customCSS string, + postFormats []model.PostFormat, + renderer renderer.Renderer, + sessionRepo model.SessionRepo, + appRepo model.AppRepo, +) Service { return &service{ clientName: clientName, clientScope: clientScope, @@ -96,137 +96,75 @@ func getRendererContext(c *model.Client) *renderer.Context { } } -func (svc *service) GetAuthUrl(ctx context.Context, instance string) ( - redirectUrl string, sessionID string, err error) { - var instanceURL string - if strings.HasPrefix(instance, "https://") { - instanceURL = instance - instance = strings.TrimPrefix(instance, "https://") - } else { - instanceURL = "https://" + instance - } - - sessionID, err = util.NewSessionId() - if err != nil { - return - } - csrfToken, err := util.NewCSRFToken() - if err != nil { - return - } - session := model.Session{ - ID: sessionID, - InstanceDomain: instance, - CSRFToken: csrfToken, - Settings: *model.NewSettings(), - } - err = svc.sessionRepo.Add(session) - if err != nil { +func addToReplyMap(m map[string][]mastodon.ReplyInfo, key interface{}, + val string, number int) { + if key == nil { return } - app, err := svc.appRepo.Get(instance) - if err != nil { - if err != model.ErrAppNotFound { - return - } - - var mastoApp *mastodon.Application - mastoApp, err = mastodon.RegisterApp(ctx, &mastodon.AppConfig{ - Server: instanceURL, - ClientName: svc.clientName, - Scopes: svc.clientScope, - Website: svc.clientWebsite, - RedirectURIs: svc.clientWebsite + "/oauth_callback", - }) - if err != nil { - return - } - - app = model.App{ - InstanceDomain: instance, - InstanceURL: instanceURL, - ClientID: mastoApp.ClientID, - ClientSecret: mastoApp.ClientSecret, - } - - err = svc.appRepo.Add(app) - if err != nil { - return - } - } - - u, err := url.Parse("/oauth/authorize") - if err != nil { + keyStr, ok := key.(string) + if !ok { return } - q := make(url.Values) - q.Set("scope", "read write follow") - q.Set("client_id", app.ClientID) - q.Set("response_type", "code") - q.Set("redirect_uri", svc.clientWebsite+"/oauth_callback") - u.RawQuery = q.Encode() - - redirectUrl = instanceURL + u.String() + _, ok = m[keyStr] + if !ok { + m[keyStr] = []mastodon.ReplyInfo{} + } - return + m[keyStr] = append(m[keyStr], mastodon.ReplyInfo{val, number}) } -func (svc *service) GetUserToken(ctx context.Context, sessionID string, c *model.Client, - code string) (token string, err error) { - if len(code) < 1 { - err = ErrInvalidArgument - return +func (svc *service) getCommonData(ctx context.Context, c *model.Client, + title string) (data *renderer.CommonData, err error) { + + data = new(renderer.CommonData) + data.HeaderData = &renderer.HeaderData{ + Title: title + " - " + svc.clientName, + NotificationCount: 0, + CustomCSS: svc.customCSS, } - session, err := svc.sessionRepo.Get(sessionID) - if err != nil { + if c == nil || !c.Session.IsLoggedIn() { return } - app, err := svc.appRepo.Get(session.InstanceDomain) + notifications, err := c.GetNotifications(ctx, nil) if err != nil { - return + return nil, err } - data := &bytes.Buffer{} - err = json.NewEncoder(data).Encode(map[string]string{ - "client_id": app.ClientID, - "client_secret": app.ClientSecret, - "grant_type": "authorization_code", - "code": code, - "redirect_uri": svc.clientWebsite + "/oauth_callback", - }) - if err != nil { - return + var notificationCount int + for i := range notifications { + if notifications[i].Pleroma != nil && + !notifications[i].Pleroma.IsSeen { + notificationCount++ + } } - resp, err := http.Post(app.InstanceURL+"/oauth/token", "application/json", data) + u, err := c.GetAccountCurrentUser(ctx) if err != nil { - return + return nil, err } - defer resp.Body.Close() - var res struct { - AccessToken string `json:"access_token"` + data.NavbarData = &renderer.NavbarData{ + User: u, + NotificationCount: notificationCount, } - err = json.NewDecoder(resp.Body).Decode(&res) - if err != nil { - return - } + data.HeaderData.NotificationCount = notificationCount + data.HeaderData.CSRFToken = c.Session.CSRFToken - return res.AccessToken, nil + return } -func (svc *service) ServeErrorPage(ctx context.Context, client io.Writer, c *model.Client, err error) { +func (svc *service) ServeErrorPage(ctx context.Context, c *model.Client, err error) { var errStr string if err != nil { errStr = err.Error() } - commonData, err := svc.getCommonData(ctx, client, nil, "error") + commonData, err := svc.getCommonData(ctx, nil, "error") if err != nil { return } @@ -237,12 +175,13 @@ func (svc *service) ServeErrorPage(ctx context.Context, client io.Writer, c *mod } rCtx := getRendererContext(c) - - svc.renderer.RenderErrorPage(rCtx, client, data) + svc.renderer.RenderErrorPage(rCtx, c.Writer, data) } -func (svc *service) ServeSigninPage(ctx context.Context, client io.Writer) (err error) { - commonData, err := svc.getCommonData(ctx, client, nil, "signin") +func (svc *service) ServeSigninPage(ctx context.Context, c *model.Client) ( + err error) { + + commonData, err := svc.getCommonData(ctx, nil, "signin") if err != nil { return } @@ -252,26 +191,23 @@ func (svc *service) ServeSigninPage(ctx context.Context, client io.Writer) (err } rCtx := getRendererContext(nil) - return svc.renderer.RenderSigninPage(rCtx, client, data) + return svc.renderer.RenderSigninPage(rCtx, c.Writer, data) } -func (svc *service) ServeTimelinePage(ctx context.Context, client io.Writer, - c *model.Client, timelineType string, maxID string, sinceID string, minID string) (err error) { - - var hasNext, hasPrev bool - var nextLink, prevLink string +func (svc *service) ServeTimelinePage(ctx context.Context, c *model.Client, + tType string, maxID string, minID string) (err error) { + var nextLink, prevLink, title string + var statuses []*mastodon.Status var pg = mastodon.Pagination{ MaxID: maxID, MinID: minID, Limit: 20, } - var statuses []*mastodon.Status - var title string - switch timelineType { + switch tType { default: - return ErrInvalidTimeline + return errInvalidArgument case "home": statuses, err = c.GetTimelineHome(ctx, &pg) title = "Timeline" @@ -293,29 +229,31 @@ func (svc *service) ServeTimelinePage(ctx context.Context, client io.Writer, } if len(maxID) > 0 && len(statuses) > 0 { - hasPrev = true - prevLink = fmt.Sprintf("/timeline/$s?min_id=%s", timelineType, statuses[0].ID) + prevLink = fmt.Sprintf("/timeline/%s?min_id=%s", tType, + statuses[0].ID) } + if len(minID) > 0 && len(pg.MinID) > 0 { - newStatuses, err := c.GetTimelineHome(ctx, &mastodon.Pagination{MinID: pg.MinID, Limit: 20}) + newPg := &mastodon.Pagination{MinID: pg.MinID, Limit: 20} + newStatuses, err := c.GetTimelineHome(ctx, newPg) if err != nil { return err } - newStatusesLen := len(newStatuses) - if newStatusesLen == 20 { - hasPrev = true - prevLink = fmt.Sprintf("/timeline/%s?min_id=%s", timelineType, pg.MinID) + newLen := len(newStatuses) + if newLen == 20 { + prevLink = fmt.Sprintf("/timeline/%s?min_id=%s", + tType, pg.MinID) } else { - i := 20 - newStatusesLen - 1 + i := 20 - newLen - 1 if len(statuses) > i { - hasPrev = true - prevLink = fmt.Sprintf("/timeline/%s?min_id=%s", timelineType, statuses[i].ID) + prevLink = fmt.Sprintf("/timeline/%s?min_id=%s", + tType, statuses[i].ID) } } } + if len(pg.MaxID) > 0 { - hasNext = true - nextLink = fmt.Sprintf("/timeline/%s?max_id=%s", timelineType, pg.MaxID) + nextLink = fmt.Sprintf("/timeline/%s?max_id=%s", tType, pg.MaxID) } postContext := model.PostContext{ @@ -323,7 +261,7 @@ func (svc *service) ServeTimelinePage(ctx context.Context, client io.Writer, Formats: svc.postFormats, } - commonData, err := svc.getCommonData(ctx, client, c, timelineType+" timeline ") + commonData, err := svc.getCommonData(ctx, c, tType+" timeline ") if err != nil { return } @@ -331,24 +269,21 @@ func (svc *service) ServeTimelinePage(ctx context.Context, client io.Writer, data := &renderer.TimelineData{ Title: title, Statuses: statuses, - HasNext: hasNext, NextLink: nextLink, - HasPrev: hasPrev, PrevLink: prevLink, PostContext: postContext, CommonData: commonData, } + rCtx := getRendererContext(c) + return svc.renderer.RenderTimelinePage(rCtx, c.Writer, data) +} - err = svc.renderer.RenderTimelinePage(rCtx, client, data) - if err != nil { - return - } +func (svc *service) ServeThreadPage(ctx context.Context, c *model.Client, + id string, reply bool) (err error) { - return -} + var postContext model.PostContext -func (svc *service) ServeThreadPage(ctx context.Context, client io.Writer, c *model.Client, id string, reply bool) (err error) { status, err := c.GetStatus(ctx, id) if err != nil { return @@ -359,19 +294,19 @@ func (svc *service) ServeThreadPage(ctx context.Context, client io.Writer, c *mo return } - var postContext model.PostContext if reply { var content string + var visibility string if u.ID != status.Account.ID { content += "@" + status.Account.Acct + " " } for i := range status.Mentions { - if status.Mentions[i].ID != u.ID && status.Mentions[i].ID != status.Account.ID { + if status.Mentions[i].ID != u.ID && + status.Mentions[i].ID != status.Account.ID { content += "@" + status.Mentions[i].Acct + " " } } - var visibility string if c.Session.Settings.CopyScope { s, err := c.GetStatus(ctx, id) if err != nil { @@ -400,16 +335,15 @@ func (svc *service) ServeThreadPage(ctx context.Context, client io.Writer, c *mo } statuses := append(append(context.Ancestors, status), context.Descendants...) - - replyMap := make(map[string][]mastodon.ReplyInfo) + replies := make(map[string][]mastodon.ReplyInfo) for i := range statuses { statuses[i].ShowReplies = true - statuses[i].ReplyMap = replyMap - addToReplyMap(replyMap, statuses[i].InReplyToID, statuses[i].ID, i+1) + statuses[i].ReplyMap = replies + addToReplyMap(replies, statuses[i].InReplyToID, statuses[i].ID, i+1) } - commonData, err := svc.getCommonData(ctx, client, c, "post by "+status.Account.DisplayName) + commonData, err := svc.getCommonData(ctx, c, "post by "+status.Account.DisplayName) if err != nil { return } @@ -417,224 +351,182 @@ func (svc *service) ServeThreadPage(ctx context.Context, client io.Writer, c *mo data := &renderer.ThreadData{ Statuses: statuses, PostContext: postContext, - ReplyMap: replyMap, + ReplyMap: replies, CommonData: commonData, } - rCtx := getRendererContext(c) - err = svc.renderer.RenderThreadPage(rCtx, client, data) - if err != nil { - return - } - - return + rCtx := getRendererContext(c) + return svc.renderer.RenderThreadPage(rCtx, c.Writer, data) } -func (svc *service) ServeNotificationPage(ctx context.Context, client io.Writer, c *model.Client, maxID string, minID string) (err error) { - var hasNext bool - var nextLink string +func (svc *service) ServeLikedByPage(ctx context.Context, c *model.Client, + id string) (err error) { - var pg = mastodon.Pagination{ - MaxID: maxID, - MinID: minID, - Limit: 20, + likers, err := c.GetFavouritedBy(ctx, id, nil) + if err != nil { + return } - notifications, err := c.GetNotifications(ctx, &pg) + commonData, err := svc.getCommonData(ctx, c, "likes") if err != nil { return } - var unreadCount int - for i := range notifications { - if notifications[i].Status != nil { - notifications[i].Status.CreatedAt = notifications[i].CreatedAt - switch notifications[i].Type { - case "reblog", "favourite": - notifications[i].Status.HideAccountInfo = true - } - } - if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen { - unreadCount++ - } + data := &renderer.LikedByData{ + CommonData: commonData, + Users: likers, } - if unreadCount > 0 { - err := c.ReadNotifications(ctx, notifications[0].ID) - if err != nil { - return err - } - } + rCtx := getRendererContext(c) + return svc.renderer.RenderLikedByPage(rCtx, c.Writer, data) +} - if len(pg.MaxID) > 0 { - hasNext = true - nextLink = "/notifications?max_id=" + pg.MaxID - } +func (svc *service) ServeRetweetedByPage(ctx context.Context, c *model.Client, + id string) (err error) { - commonData, err := svc.getCommonData(ctx, client, c, "notifications") + retweeters, err := c.GetRebloggedBy(ctx, id, nil) if err != nil { return } - data := &renderer.NotificationData{ - Notifications: notifications, - HasNext: hasNext, - NextLink: nextLink, - CommonData: commonData, - } - rCtx := getRendererContext(c) - - err = svc.renderer.RenderNotificationPage(rCtx, client, data) + commonData, err := svc.getCommonData(ctx, c, "retweets") if err != nil { return } - return + data := &renderer.RetweetedByData{ + CommonData: commonData, + Users: retweeters, + } + + rCtx := getRendererContext(c) + return svc.renderer.RenderRetweetedByPage(rCtx, c.Writer, data) } -func (svc *service) ServeUserPage(ctx context.Context, client io.Writer, c *model.Client, id string, maxID string, minID string) (err error) { - user, err := c.GetAccount(ctx, id) - if err != nil { - return - } +func (svc *service) ServeFollowingPage(ctx context.Context, c *model.Client, + id string, maxID string, minID string) (err error) { - var hasNext bool var nextLink string - var pg = mastodon.Pagination{ MaxID: maxID, MinID: minID, Limit: 20, } - statuses, err := c.GetAccountStatuses(ctx, id, &pg) + followings, err := c.GetAccountFollowing(ctx, id, &pg) if err != nil { return } - if len(pg.MaxID) > 0 { - hasNext = true - nextLink = "/user/" + id + "?max_id=" + pg.MaxID + if len(followings) == 20 && len(pg.MaxID) > 0 { + nextLink = "/following/" + id + "?max_id=" + pg.MaxID } - commonData, err := svc.getCommonData(ctx, client, c, user.DisplayName) + commonData, err := svc.getCommonData(ctx, c, "following") if err != nil { return } - data := &renderer.UserData{ - User: user, - Statuses: statuses, - HasNext: hasNext, - NextLink: nextLink, + data := &renderer.FollowingData{ CommonData: commonData, - } - rCtx := getRendererContext(c) - - err = svc.renderer.RenderUserPage(rCtx, client, data) - if err != nil { - return + Users: followings, + NextLink: nextLink, } - return + rCtx := getRendererContext(c) + return svc.renderer.RenderFollowingPage(rCtx, c.Writer, data) } -func (svc *service) ServeAboutPage(ctx context.Context, client io.Writer, c *model.Client) (err error) { - commonData, err := svc.getCommonData(ctx, client, c, "about") - if err != nil { - return - } +func (svc *service) ServeFollowersPage(ctx context.Context, c *model.Client, + id string, maxID string, minID string) (err error) { - data := &renderer.AboutData{ - CommonData: commonData, + var nextLink string + var pg = mastodon.Pagination{ + MaxID: maxID, + MinID: minID, + Limit: 20, } - rCtx := getRendererContext(c) - err = svc.renderer.RenderAboutPage(rCtx, client, data) + followers, err := c.GetAccountFollowers(ctx, id, &pg) if err != nil { return } - return -} - -func (svc *service) ServeEmojiPage(ctx context.Context, client io.Writer, c *model.Client) (err error) { - commonData, err := svc.getCommonData(ctx, client, c, "emojis") - if err != nil { - return + if len(followers) == 20 && len(pg.MaxID) > 0 { + nextLink = "/followers/" + id + "?max_id=" + pg.MaxID } - emojis, err := c.GetInstanceEmojis(ctx) + commonData, err := svc.getCommonData(ctx, c, "followers") if err != nil { return } - data := &renderer.EmojiData{ - Emojis: emojis, + data := &renderer.FollowersData{ CommonData: commonData, + Users: followers, + NextLink: nextLink, } rCtx := getRendererContext(c) - - err = svc.renderer.RenderEmojiPage(rCtx, client, data) - if err != nil { - return - } - - return + return svc.renderer.RenderFollowersPage(rCtx, c.Writer, data) } -func (svc *service) ServeLikedByPage(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) { - likers, err := c.GetFavouritedBy(ctx, id, nil) - if err != nil { - return +func (svc *service) ServeNotificationPage(ctx context.Context, c *model.Client, + maxID string, minID string) (err error) { + + var nextLink string + var unreadCount int + var pg = mastodon.Pagination{ + MaxID: maxID, + MinID: minID, + Limit: 20, } - commonData, err := svc.getCommonData(ctx, client, c, "likes") + notifications, err := c.GetNotifications(ctx, &pg) if err != nil { return } - data := &renderer.LikedByData{ - CommonData: commonData, - Users: likers, + for i := range notifications { + if notifications[i].Status != nil { + notifications[i].Status.CreatedAt = notifications[i].CreatedAt + switch notifications[i].Type { + case "reblog", "favourite": + notifications[i].Status.HideAccountInfo = true + } + } + if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen { + unreadCount++ + } } - rCtx := getRendererContext(c) - err = svc.renderer.RenderLikedByPage(rCtx, client, data) - if err != nil { - return + if unreadCount > 0 { + err := c.ReadNotifications(ctx, notifications[0].ID) + if err != nil { + return err + } } - return -} - -func (svc *service) ServeRetweetedByPage(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) { - retweeters, err := c.GetRebloggedBy(ctx, id, nil) - if err != nil { - return + if len(pg.MaxID) > 0 { + nextLink = "/notifications?max_id=" + pg.MaxID } - commonData, err := svc.getCommonData(ctx, client, c, "retweets") + commonData, err := svc.getCommonData(ctx, c, "notifications") if err != nil { return } - data := &renderer.RetweetedByData{ - CommonData: commonData, - Users: retweeters, + data := &renderer.NotificationData{ + Notifications: notifications, + NextLink: nextLink, + CommonData: commonData, } rCtx := getRendererContext(c) - - err = svc.renderer.RenderRetweetedByPage(rCtx, client, data) - if err != nil { - return - } - - return + return svc.renderer.RenderNotificationPage(rCtx, c.Writer, data) } -func (svc *service) ServeFollowingPage(ctx context.Context, client io.Writer, c *model.Client, id string, maxID string, minID string) (err error) { - var hasNext bool +func (svc *service) ServeUserPage(ctx context.Context, c *model.Client, + id string, maxID string, minID string) (err error) { + var nextLink string var pg = mastodon.Pagination{ @@ -643,104 +535,91 @@ func (svc *service) ServeFollowingPage(ctx context.Context, client io.Writer, c Limit: 20, } - followings, err := c.GetAccountFollowing(ctx, id, &pg) + user, err := c.GetAccount(ctx, id) if err != nil { return } - if len(followings) == 20 && len(pg.MaxID) > 0 { - hasNext = true - nextLink = "/following/" + id + "?max_id=" + pg.MaxID + statuses, err := c.GetAccountStatuses(ctx, id, &pg) + if err != nil { + return + } + + if len(pg.MaxID) > 0 { + nextLink = "/user/" + id + "?max_id=" + pg.MaxID } - commonData, err := svc.getCommonData(ctx, client, c, "following") + commonData, err := svc.getCommonData(ctx, c, user.DisplayName) if err != nil { return } - data := &renderer.FollowingData{ - CommonData: commonData, - Users: followings, - HasNext: hasNext, + data := &renderer.UserData{ + User: user, + Statuses: statuses, NextLink: nextLink, + CommonData: commonData, } rCtx := getRendererContext(c) + return svc.renderer.RenderUserPage(rCtx, c.Writer, data) +} - err = svc.renderer.RenderFollowingPage(rCtx, client, data) +func (svc *service) ServeAboutPage(ctx context.Context, c *model.Client) (err error) { + commonData, err := svc.getCommonData(ctx, c, "about") if err != nil { return } - return -} - -func (svc *service) ServeFollowersPage(ctx context.Context, client io.Writer, c *model.Client, id string, maxID string, minID string) (err error) { - var hasNext bool - var nextLink string - - var pg = mastodon.Pagination{ - MaxID: maxID, - MinID: minID, - Limit: 20, + data := &renderer.AboutData{ + CommonData: commonData, } - followers, err := c.GetAccountFollowers(ctx, id, &pg) + rCtx := getRendererContext(c) + return svc.renderer.RenderAboutPage(rCtx, c.Writer, data) +} + +func (svc *service) ServeEmojiPage(ctx context.Context, c *model.Client) (err error) { + commonData, err := svc.getCommonData(ctx, c, "emojis") if err != nil { return } - if len(followers) == 20 && len(pg.MaxID) > 0 { - hasNext = true - nextLink = "/followers/" + id + "?max_id=" + pg.MaxID - } - - commonData, err := svc.getCommonData(ctx, client, c, "followers") + emojis, err := c.GetInstanceEmojis(ctx) if err != nil { return } - data := &renderer.FollowersData{ + data := &renderer.EmojiData{ + Emojis: emojis, CommonData: commonData, - Users: followers, - HasNext: hasNext, - NextLink: nextLink, } - rCtx := getRendererContext(c) - err = svc.renderer.RenderFollowersPage(rCtx, client, data) - if err != nil { - return - } - - return + rCtx := getRendererContext(c) + return svc.renderer.RenderEmojiPage(rCtx, c.Writer, data) } -func (svc *service) ServeSearchPage(ctx context.Context, client io.Writer, c *model.Client, q string, qType string, offset int) (err error) { - var hasNext bool +func (svc *service) ServeSearchPage(ctx context.Context, c *model.Client, + q string, qType string, offset int) (err error) { + var nextLink string + var title = "search" results, err := c.Search(ctx, q, qType, 20, true, offset) if err != nil { return } - switch qType { - case "accounts": - hasNext = len(results.Accounts) == 20 - case "statuses": - hasNext = len(results.Statuses) == 20 - } - - if hasNext { + if (qType == "accounts" && len(results.Accounts) == 20) || + (qType == "statuses" && len(results.Statuses) == 20) { offset += 20 nextLink = fmt.Sprintf("/search?q=%s&type=%s&offset=%d", q, qType, offset) } - var title = "search" if len(q) > 0 { title += " \"" + q + "\"" } - commonData, err := svc.getCommonData(ctx, client, c, title) + + commonData, err := svc.getCommonData(ctx, c, title) if err != nil { return } @@ -751,21 +630,15 @@ func (svc *service) ServeSearchPage(ctx context.Context, client io.Writer, c *mo Type: qType, Users: results.Accounts, Statuses: results.Statuses, - HasNext: hasNext, NextLink: nextLink, } - rCtx := getRendererContext(c) - err = svc.renderer.RenderSearchPage(rCtx, client, data) - if err != nil { - return - } - - return + rCtx := getRendererContext(c) + return svc.renderer.RenderSearchPage(rCtx, c.Writer, data) } -func (svc *service) ServeSettingsPage(ctx context.Context, client io.Writer, c *model.Client) (err error) { - commonData, err := svc.getCommonData(ctx, client, c, "settings") +func (svc *service) ServeSettingsPage(ctx context.Context, c *model.Client) (err error) { + commonData, err := svc.getCommonData(ctx, c, "settings") if err != nil { return } @@ -774,122 +647,125 @@ func (svc *service) ServeSettingsPage(ctx context.Context, client io.Writer, c * CommonData: commonData, Settings: &c.Session.Settings, } + rCtx := getRendererContext(c) + return svc.renderer.RenderSettingsPage(rCtx, c.Writer, data) +} - err = svc.renderer.RenderSettingsPage(rCtx, client, data) +func (svc *service) NewSession(ctx context.Context, instance string) ( + redirectUrl string, sessionID string, err error) { + + var instanceURL string + if strings.HasPrefix(instance, "https://") { + instanceURL = instance + instance = strings.TrimPrefix(instance, "https://") + } else { + instanceURL = "https://" + instance + } + + sessionID, err = util.NewSessionID() if err != nil { return } - return -} - -func (svc *service) SaveSettings(ctx context.Context, client io.Writer, c *model.Client, settings *model.Settings) (err error) { - session, err := svc.sessionRepo.Get(c.Session.ID) + csrfToken, err := util.NewCSRFToken() if err != nil { return } - session.Settings = *settings + session := model.Session{ + ID: sessionID, + InstanceDomain: instance, + CSRFToken: csrfToken, + Settings: *model.NewSettings(), + } + err = svc.sessionRepo.Add(session) if err != nil { return } - return -} - -func (svc *service) getCommonData(ctx context.Context, client io.Writer, c *model.Client, title string) (data *renderer.CommonData, err error) { - data = new(renderer.CommonData) - - data.HeaderData = &renderer.HeaderData{ - Title: title + " - " + svc.clientName, - NotificationCount: 0, - CustomCSS: svc.customCSS, - } + app, err := svc.appRepo.Get(instance) + if err != nil { + if err != model.ErrAppNotFound { + return + } - if c != nil && c.Session.IsLoggedIn() { - notifications, err := c.GetNotifications(ctx, nil) + mastoApp, err := mastodon.RegisterApp(ctx, &mastodon.AppConfig{ + Server: instanceURL, + ClientName: svc.clientName, + Scopes: svc.clientScope, + Website: svc.clientWebsite, + RedirectURIs: svc.clientWebsite + "/oauth_callback", + }) if err != nil { - return nil, err + return "", "", err } - var notificationCount int - for i := range notifications { - if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen { - notificationCount++ - } + app = model.App{ + InstanceDomain: instance, + InstanceURL: instanceURL, + ClientID: mastoApp.ClientID, + ClientSecret: mastoApp.ClientSecret, } - u, err := c.GetAccountCurrentUser(ctx) + err = svc.appRepo.Add(app) if err != nil { - return nil, err - } - - data.NavbarData = &renderer.NavbarData{ - User: u, - NotificationCount: notificationCount, + return "", "", err } - - data.HeaderData.NotificationCount = notificationCount - data.HeaderData.CSRFToken = c.Session.CSRFToken } - return -} - -func (svc *service) Like(ctx context.Context, client io.Writer, c *model.Client, id string) (count int64, err error) { - s, err := c.Favourite(ctx, id) + u, err := url.Parse("/oauth/authorize") if err != nil { return } - count = s.FavouritesCount - return -} -func (svc *service) UnLike(ctx context.Context, client io.Writer, c *model.Client, id string) (count int64, err error) { - s, err := c.Unfavourite(ctx, id) - if err != nil { - return - } - count = s.FavouritesCount + q := make(url.Values) + q.Set("scope", "read write follow") + q.Set("client_id", app.ClientID) + q.Set("response_type", "code") + q.Set("redirect_uri", svc.clientWebsite+"/oauth_callback") + u.RawQuery = q.Encode() + + redirectUrl = instanceURL + u.String() + return } -func (svc *service) Retweet(ctx context.Context, client io.Writer, c *model.Client, id string) (count int64, err error) { - s, err := c.Reblog(ctx, id) - if err != nil { +func (svc *service) Signin(ctx context.Context, c *model.Client, + sessionID string, code string) (token string, err error) { + + if len(code) < 1 { + err = errInvalidArgument return } - if s.Reblog != nil { - count = s.Reblog.ReblogsCount - } - return -} -func (svc *service) UnRetweet(ctx context.Context, client io.Writer, c *model.Client, id string) (count int64, err error) { - s, err := c.Unreblog(ctx, id) + err = c.AuthenticateToken(ctx, code, svc.clientWebsite+"/oauth_callback") if err != nil { return } - count = s.ReblogsCount + token = c.GetAccessToken(ctx) + return } -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) { - var mediaIds []string +func (svc *service) Post(ctx context.Context, c *model.Client, content string, + replyToID string, format string, visibility string, isNSFW bool, + files []*multipart.FileHeader) (id string, err error) { + + var mediaIDs []string for _, f := range files { a, err := c.UploadMediaFromMultipartFileHeader(ctx, f) if err != nil { return "", err } - mediaIds = append(mediaIds, a.ID) + mediaIDs = append(mediaIDs, a.ID) } tweet := &mastodon.Toot{ Status: content, InReplyToID: replyToID, - MediaIDs: mediaIds, + MediaIDs: mediaIDs, ContentType: format, Visibility: visibility, Sensitive: isNSFW, @@ -903,29 +779,66 @@ func (svc *service) PostTweet(ctx context.Context, client io.Writer, c *model.Cl return s.ID, nil } -func (svc *service) Follow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) { - _, err = c.AccountFollow(ctx, id) +func (svc *service) Like(ctx context.Context, c *model.Client, id string) ( + count int64, err error) { + s, err := c.Favourite(ctx, id) + if err != nil { + return + } + count = s.FavouritesCount return } -func (svc *service) UnFollow(ctx context.Context, client io.Writer, c *model.Client, id string) (err error) { - _, err = c.AccountUnfollow(ctx, id) +func (svc *service) UnLike(ctx context.Context, c *model.Client, id string) ( + count int64, err error) { + s, err := c.Unfavourite(ctx, id) + if err != nil { + return + } + count = s.FavouritesCount return } -func addToReplyMap(m map[string][]mastodon.ReplyInfo, key interface{}, val string, number int) { - if key == nil { +func (svc *service) Retweet(ctx context.Context, c *model.Client, id string) ( + count int64, err error) { + s, err := c.Reblog(ctx, id) + if err != nil { return } + if s.Reblog != nil { + count = s.Reblog.ReblogsCount + } + return +} - keyStr, ok := key.(string) - if !ok { +func (svc *service) UnRetweet(ctx context.Context, c *model.Client, id string) ( + count int64, err error) { + s, err := c.Unreblog(ctx, id) + if err != nil { return } - _, ok = m[keyStr] - if !ok { - m[keyStr] = []mastodon.ReplyInfo{} + count = s.ReblogsCount + return +} + +func (svc *service) Follow(ctx context.Context, c *model.Client, id string) (err error) { + _, err = c.AccountFollow(ctx, id) + return +} + +func (svc *service) UnFollow(ctx context.Context, c *model.Client, id string) (err error) { + _, err = c.AccountUnfollow(ctx, id) + return +} + +func (svc *service) SaveSettings(ctx context.Context, c *model.Client, + settings *model.Settings) (err error) { + + session, err := svc.sessionRepo.Get(c.Session.ID) + if err != nil { + return } - m[keyStr] = append(m[keyStr], mastodon.ReplyInfo{val, number}) + session.Settings = *settings + return svc.sessionRepo.Add(session) } |