diff --git a/mastodon/accounts.go b/mastodon/accounts.go index e6f5a6d..8cee6bb 100644 --- a/mastodon/accounts.go +++ b/mastodon/accounts.go @@ -9,27 +9,32 @@ import ( "time" ) +type AccountPleroma struct { + Relationship Relationship `json:"relationship"` +} + // Account hold information for mastodon account. type Account struct { - ID string `json:"id"` - Username string `json:"username"` - Acct string `json:"acct"` - DisplayName string `json:"display_name"` - Locked bool `json:"locked"` - CreatedAt time.Time `json:"created_at"` - FollowersCount int64 `json:"followers_count"` - FollowingCount int64 `json:"following_count"` - StatusesCount int64 `json:"statuses_count"` - Note string `json:"note"` - URL string `json:"url"` - Avatar string `json:"avatar"` - AvatarStatic string `json:"avatar_static"` - Header string `json:"header"` - HeaderStatic string `json:"header_static"` - Emojis []Emoji `json:"emojis"` - Moved *Account `json:"moved"` - Fields []Field `json:"fields"` - Bot bool `json:"bot"` + ID string `json:"id"` + Username string `json:"username"` + Acct string `json:"acct"` + DisplayName string `json:"display_name"` + Locked bool `json:"locked"` + CreatedAt time.Time `json:"created_at"` + FollowersCount int64 `json:"followers_count"` + FollowingCount int64 `json:"following_count"` + StatusesCount int64 `json:"statuses_count"` + Note string `json:"note"` + URL string `json:"url"` + Avatar string `json:"avatar"` + AvatarStatic string `json:"avatar_static"` + Header string `json:"header"` + HeaderStatic string `json:"header_static"` + Emojis []Emoji `json:"emojis"` + Moved *Account `json:"moved"` + Fields []Field `json:"fields"` + Bot bool `json:"bot"` + Pleroma AccountPleroma `json:"pleroma"` } // Field is a Mastodon account profile field. diff --git a/renderer/model.go b/renderer/model.go index ad356e2..7e52850 100644 --- a/renderer/model.go +++ b/renderer/model.go @@ -68,3 +68,21 @@ func NewNotificationPageTemplateData(notifications []*mastodon.Notification, has NavbarData: navbarData, } } + +type UserPageTemplateData struct { + User *mastodon.Account + Statuses []*mastodon.Status + HasNext bool + NextLink string + NavbarData *NavbarTemplateData +} + +func NewUserPageTemplateData(user *mastodon.Account, statuses []*mastodon.Status, hasNext bool, nextLink string, navbarData *NavbarTemplateData) *UserPageTemplateData { + return &UserPageTemplateData{ + User: user, + Statuses: statuses, + HasNext: hasNext, + NextLink: nextLink, + NavbarData: navbarData, + } +} diff --git a/renderer/renderer.go b/renderer/renderer.go index 394d74f..890006b 100644 --- a/renderer/renderer.go +++ b/renderer/renderer.go @@ -18,6 +18,7 @@ type Renderer interface { RenderTimelinePage(ctx context.Context, writer io.Writer, data *TimelinePageTemplateData) (err error) RenderThreadPage(ctx context.Context, writer io.Writer, data *ThreadPageTemplateData) (err error) RenderNotificationPage(ctx context.Context, writer io.Writer, data *NotificationPageTemplateData) (err error) + RenderUserPage(ctx context.Context, writer io.Writer, data *UserPageTemplateData) (err error) } type renderer struct { @@ -65,6 +66,10 @@ func (r *renderer) RenderNotificationPage(ctx context.Context, writer io.Writer, return r.template.ExecuteTemplate(writer, "notification.tmpl", data) } +func (r *renderer) RenderUserPage(ctx context.Context, writer io.Writer, data *UserPageTemplateData) (err error) { + return r.template.ExecuteTemplate(writer, "user.tmpl", data) +} + func WithEmojis(content string, emojis []mastodon.Emoji) string { var emojiNameContentPair []string for _, e := range emojis { diff --git a/service/auth.go b/service/auth.go index 38c0a43..2b6fdd6 100644 --- a/service/auth.go +++ b/service/auth.go @@ -119,6 +119,14 @@ func (s *authService) ServeNotificationPage(ctx context.Context, client io.Write return s.Service.ServeNotificationPage(ctx, client, c, maxID, minID) } +func (s *authService) ServeUserPage(ctx context.Context, client io.Writer, c *mastodon.Client, id string, maxID string, minID string) (err error) { + c, err = s.getClient(ctx) + if err != nil { + return + } + return s.Service.ServeUserPage(ctx, client, c, id, maxID, minID) +} + func (s *authService) Like(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) { c, err = s.getClient(ctx) if err != nil { @@ -158,3 +166,19 @@ func (s *authService) PostTweet(ctx context.Context, client io.Writer, c *mastod } return s.Service.PostTweet(ctx, client, c, content, replyToID, files) } + +func (s *authService) Follow(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) { + c, err = s.getClient(ctx) + if err != nil { + return + } + return s.Service.Follow(ctx, client, c, id) +} + +func (s *authService) UnFollow(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) { + c, err = s.getClient(ctx) + if err != nil { + return + } + return s.Service.UnFollow(ctx, client, c, id) +} diff --git a/service/logging.go b/service/logging.go index aa1da68..9b398af 100644 --- a/service/logging.go +++ b/service/logging.go @@ -85,6 +85,14 @@ func (s *loggingService) ServeNotificationPage(ctx context.Context, client io.Wr return s.Service.ServeNotificationPage(ctx, client, c, maxID, minID) } +func (s *loggingService) ServeUserPage(ctx context.Context, client io.Writer, c *mastodon.Client, id string, maxID string, minID string) (err error) { + defer func(begin time.Time) { + s.logger.Printf("method=%v, id=%v, max_id=%v, min_id=%v, took=%v, err=%v\n", + "ServeUserPage", id, maxID, minID, time.Since(begin), err) + }(time.Now()) + return s.Service.ServeUserPage(ctx, client, c, id, maxID, minID) +} + func (s *loggingService) Like(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) { defer func(begin time.Time) { s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", @@ -124,3 +132,19 @@ func (s *loggingService) PostTweet(ctx context.Context, client io.Writer, c *mas }(time.Now()) return s.Service.PostTweet(ctx, client, c, content, replyToID, files) } + +func (s *loggingService) Follow(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) { + defer func(begin time.Time) { + s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", + "Follow", id, time.Since(begin), err) + }(time.Now()) + return s.Service.Follow(ctx, client, c, id) +} + +func (s *loggingService) UnFollow(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) { + defer func(begin time.Time) { + s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", + "UnFollow", id, time.Since(begin), err) + }(time.Now()) + return s.Service.UnFollow(ctx, client, c, id) +} diff --git a/service/service.go b/service/service.go index 556afa6..63f74d3 100644 --- a/service/service.go +++ b/service/service.go @@ -32,11 +32,14 @@ type Service interface { ServeTimelinePage(ctx context.Context, client io.Writer, c *mastodon.Client, maxID string, sinceID string, minID string) (err error) ServeThreadPage(ctx context.Context, client io.Writer, c *mastodon.Client, id string, reply bool) (err error) ServeNotificationPage(ctx context.Context, client io.Writer, c *mastodon.Client, maxID string, minID string) (err error) + ServeUserPage(ctx context.Context, client io.Writer, c *mastodon.Client, id string, maxID string, minID string) (err error) Like(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) UnLike(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) Retweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) UnRetweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) PostTweet(ctx context.Context, client io.Writer, c *mastodon.Client, content string, replyToID string, files []*multipart.FileHeader) (id string, err error) + Follow(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) + UnFollow(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) } type service struct { @@ -369,6 +372,45 @@ func (svc *service) ServeNotificationPage(ctx context.Context, client io.Writer, return } +func (svc *service) ServeUserPage(ctx context.Context, client io.Writer, c *mastodon.Client, id string, maxID string, minID string) (err error) { + user, err := c.GetAccount(ctx, id) + if err != nil { + return + } + + var hasNext bool + var nextLink string + + var pg = mastodon.Pagination{ + MaxID: maxID, + MinID: minID, + Limit: 20, + } + + statuses, err := c.GetAccountStatuses(ctx, id, &pg) + if err != nil { + return + } + + if len(pg.MaxID) > 0 { + hasNext = true + nextLink = "/user/" + id + "?max_id=" + pg.MaxID + } + + navbarData, err := svc.getNavbarTemplateData(ctx, client, c) + if err != nil { + return + } + + data := renderer.NewUserPageTemplateData(user, statuses, hasNext, nextLink, navbarData) + err = svc.renderer.RenderUserPage(ctx, client, data) + if err != nil { + return + } + + return +} + func (svc *service) getNavbarTemplateData(ctx context.Context, client io.Writer, c *mastodon.Client) (data *renderer.NavbarTemplateData, err error) { notifications, err := c.GetNotifications(ctx, nil) if err != nil { @@ -431,6 +473,16 @@ func (svc *service) PostTweet(ctx context.Context, client io.Writer, c *mastodon return s.ID, nil } +func (svc *service) Follow(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) { + _, err = c.AccountFollow(ctx, id) + return +} + +func (svc *service) UnFollow(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) { + _, err = c.AccountUnfollow(ctx, id) + return +} + func addToReplyMap(m map[string][]mastodon.ReplyInfo, key interface{}, val string, number int) { if key == nil { return diff --git a/service/transport.go b/service/transport.go index 1326c58..6759fcc 100644 --- a/service/transport.go +++ b/service/transport.go @@ -15,14 +15,6 @@ var ( cookieAge = "31536000" ) -func getContextWithSession(ctx context.Context, req *http.Request) context.Context { - sessionID, err := req.Cookie("session_id") - if err != nil { - return ctx - } - return context.WithValue(ctx, "session_id", sessionID.Value) -} - func NewHandler(s Service, staticDir string) http.Handler { r := mux.NewRouter() @@ -192,6 +184,50 @@ func NewHandler(s Service, staticDir string) http.Handler { } }).Methods(http.MethodGet) + r.HandleFunc("/user/{id}", func(w http.ResponseWriter, req *http.Request) { + ctx := getContextWithSession(context.Background(), req) + + id, _ := mux.Vars(req)["id"] + maxID := req.URL.Query().Get("max_id") + minID := req.URL.Query().Get("min_id") + + err := s.ServeUserPage(ctx, w, nil, id, maxID, minID) + if err != nil { + s.ServeErrorPage(ctx, w, err) + return + } + }).Methods(http.MethodGet) + + r.HandleFunc("/follow/{id}", func(w http.ResponseWriter, req *http.Request) { + ctx := getContextWithSession(context.Background(), req) + + id, _ := mux.Vars(req)["id"] + + err := s.Follow(ctx, w, nil, id) + if err != nil { + s.ServeErrorPage(ctx, w, err) + return + } + + w.Header().Add("Location", req.Header.Get("Referer")) + w.WriteHeader(http.StatusFound) + }).Methods(http.MethodPost) + + r.HandleFunc("/unfollow/{id}", func(w http.ResponseWriter, req *http.Request) { + ctx := getContextWithSession(context.Background(), req) + + id, _ := mux.Vars(req)["id"] + + err := s.UnFollow(ctx, w, nil, id) + if err != nil { + s.ServeErrorPage(ctx, w, err) + return + } + + w.Header().Add("Location", req.Header.Get("Referer")) + w.WriteHeader(http.StatusFound) + }).Methods(http.MethodPost) + r.HandleFunc("/signout", func(w http.ResponseWriter, req *http.Request) { // TODO remove session from database w.Header().Add("Set-Cookie", fmt.Sprintf("session_id=;max-age=0")) @@ -202,6 +238,14 @@ func NewHandler(s Service, staticDir string) http.Handler { return r } +func getContextWithSession(ctx context.Context, req *http.Request) context.Context { + sessionID, err := req.Cookie("session_id") + if err != nil { + return ctx + } + return context.WithValue(ctx, "session_id", sessionID.Value) +} + func getMultipartFormValue(mf *multipart.Form, key string) (val string) { vals, ok := mf.Value[key] if !ok { diff --git a/static/main.css b/static/main.css index b3dce7a..8341172 100644 --- a/static/main.css +++ b/static/main.css @@ -207,3 +207,42 @@ .post-attachment-div { margin: 2px 0; } + +.user-profile-img-container { + display: inline-block +} + +.user-profile-details-container { + display: inline-block; + vertical-align: top; + margin: 0 4px; +} + +.user-profile-details-container>div { + margin-bottom: 4px; +} + +.user-profile-img { + max-height: 100px; + max-width: 100px; +} + +.user-profile-decription { + margin: 4px 0; +} + +.d-inline { + display: inline; +} + +.btn-link { + border: none; + outline: none; + background: none; + cursor: pointer; + color: #0000EE; + padding: 0; + text-decoration: underline; + font-family: inherit; + font-size: inherit; +} diff --git a/templates/notification.tmpl b/templates/notification.tmpl index 099f17e..da6164b 100644 --- a/templates/notification.tmpl +++ b/templates/notification.tmpl @@ -6,7 +6,9 @@
{{if eq .Type "follow"}}
- profile-avatar + + profile-avatar +
{{WithEmojis .Account.DisplayName .Account.Emojis}} @@ -24,7 +26,9 @@ {{else if eq .Type "reblog"}}
- profile-avatar + + profile-avatar +
{{WithEmojis .Account.DisplayName .Account.Emojis}} @@ -37,7 +41,9 @@ {{else if eq .Type "favourite"}}
- profile-avatar + + profile-avatar +
{{WithEmojis .Account.DisplayName .Account.Emojis}} diff --git a/templates/status.tmpl b/templates/status.tmpl index 4dbbe3c..618398f 100644 --- a/templates/status.tmpl +++ b/templates/status.tmpl @@ -1,7 +1,9 @@
{{if .Reblog}}
- profile-avatar + + profile-avatar + {{WithEmojis .Account.DisplayName .Account.Emojis}} retweeted @@ -12,14 +14,18 @@
{{if not .HideAccountInfo}} - profile-avatar + + profile-avatar + {{end}}
{{if not .HideAccountInfo}}
{{WithEmojis .Account.DisplayName .Account.Emojis}} - {{.Account.Acct}} + + {{.Account.Acct}} +
{{end}}
diff --git a/templates/user.tmpl b/templates/user.tmpl new file mode 100644 index 0000000..3347f92 --- /dev/null +++ b/templates/user.tmpl @@ -0,0 +1,54 @@ +{{template "header.tmpl"}} +{{template "navigation.tmpl" .NavbarData}} +
User
+ + + +{{range .Statuses}} +{{template "status.tmpl" .}} +{{end}} + + + +{{template "footer.tmpl"}} +