Add user page and follow/unfollow calls
authorr <r@freesoftwareextremist.com>
Fri, 20 Dec 2019 18:30:20 +0000 (18:30 +0000)
committerr <r@freesoftwareextremist.com>
Fri, 20 Dec 2019 18:30:20 +0000 (18:30 +0000)
mastodon/accounts.go
renderer/model.go
renderer/renderer.go
service/auth.go
service/logging.go
service/service.go
service/transport.go
static/main.css
templates/notification.tmpl
templates/status.tmpl
templates/user.tmpl [new file with mode: 0644]

index e6f5a6dfc2027d9a0c0fc707b242c6d919f18caa..8cee6bbef32a4b0b701806c132168325ab4bb3c4 100644 (file)
@@ -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.
index ad356e2691dd87c7f9789c3568ae85ac0dd0cf25..7e52850fe729fcbd0be1ad46b6a4776c106e283c 100644 (file)
@@ -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,
+       }
+}
index 394d74fb2210f311968fcfd770f334e540142692..890006b98e0157ba5d6bd1052f3b559a59d0b007 100644 (file)
@@ -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 {
index 38c0a43c3eca598c298510a6f5d7453675c6adbc..2b6fdd6ec955d478f5d71d098acca1b3c320f3c0 100644 (file)
@@ -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)
+}
index aa1da687b05d616555e1c4839956fbbc2dd86f7f..9b398af309317e5a6d6bdd361684895756b34335 100644 (file)
@@ -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)
+}
index 556afa675098d9643cca8c3daa48c7e172840f40..63f74d360efd814d3aa2b952be0abb3bf752cac9 100644 (file)
@@ -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
index 1326c580f99575685094a33272a2083ac23304ae..6759fcc0fa2bfda266e8bce954e658ffc8f77112 100644 (file)
@@ -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 {
index b3dce7a066705de4836d460888e354046b054979..834117213c52191790aea6eb8fe788227c53c3a3 100644 (file)
 .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;
+}
index 099f17eff633e7ef0c2b700895d5ffa56315bfc6..da6164b4dc8ea56931c2d6ee589b10379ee7dd3a 100644 (file)
@@ -6,7 +6,9 @@
 <div class="notification-container {{if .Pleroma}}{{if not .Pleroma.IsSeen}}unread{{end}}{{end}}">
        {{if eq .Type "follow"}}
        <div class="notification-follow-container">
-               <img class="status-profile-img" src="{{.Account.AvatarStatic}}" alt="profile-avatar" />
+               <a href="/user/{{.Account.ID}}" >
+                       <img class="status-profile-img" src="{{.Account.AvatarStatic}}" alt="profile-avatar" />
+               </a>
                <div>
                        <div>
                                <span class="status-dname"> {{WithEmojis .Account.DisplayName .Account.Emojis}} </span>  
@@ -24,7 +26,9 @@
 
        {{else if eq .Type "reblog"}}
        <div class="notification-retweet-container">
-               <img class="status-profile-img" src="{{.Account.AvatarStatic}}" alt="profile-avatar" />
+               <a href="/user/{{.Account.ID}}" >
+                       <img class="status-profile-img" src="{{.Account.AvatarStatic}}" alt="profile-avatar" />
+               </a>
                <div>
                        <div>
                                <span class="status-dname"> {{WithEmojis .Account.DisplayName .Account.Emojis}} </span>  
@@ -37,7 +41,9 @@
 
        {{else if eq .Type "favourite"}}
        <div class="notification-like-container">
-               <img class="status-profile-img" src="{{.Account.AvatarStatic}}" alt="profile-avatar" />
+               <a href="/user/{{.Account.ID}}" >
+                       <img class="status-profile-img" src="{{.Account.AvatarStatic}}" alt="profile-avatar" />
+               </a>
                <div>
                        <div>
                                <span class="status-dname"> {{WithEmojis .Account.DisplayName .Account.Emojis}} </span>  
index 4dbbe3c255c5ae420a9707fa640e61ceb8ac9502..618398f36a42562d91b3d71ab51a93adb3a272be 100644 (file)
@@ -1,7 +1,9 @@
 <div id="status-{{if .Reblog}}{{.Reblog.ID}}{{else}}{{.ID}}{{end}}" class="status-container-container">
        {{if .Reblog}}
        <div class="retweet-info">
-               <img class="status-profile-img" src="{{.Account.AvatarStatic}}" alt="profile-avatar" />
+               <a href="/user/{{.Account.ID}}" >
+                       <img class="status-profile-img" src="{{.Account.AvatarStatic}}" alt="profile-avatar" />
+               </a>
                <span class="status-dname"> {{WithEmojis .Account.DisplayName .Account.Emojis}} </span>  
                <span class="icon dripicons-retweet retweeted"></span> 
                retweeted
        <div class="status-container">
                <div>
                        {{if not .HideAccountInfo}}
-                       <img class="status-profile-img" src="{{.Account.AvatarStatic}}" alt="profile-avatar" />
+                       <a href="/user/{{.Account.ID}}" >
+                               <img class="status-profile-img" src="{{.Account.AvatarStatic}}" alt="profile-avatar" />
+                       </a>
                        {{end}}
                </div>
                <div class="status"> 
                        {{if not .HideAccountInfo}}
                        <div class="status-name">
                                <span class="status-dname"> {{WithEmojis .Account.DisplayName .Account.Emojis}} </span> 
-                               <span class="status-uname"> {{.Account.Acct}} </span>
+                               <a href="/user/{{.Account.ID}}" >
+                                       <span class="status-uname"> {{.Account.Acct}} </span>
+                               </a>
                        </div>
                        {{end}}
                        <div class="status-reply-container">
diff --git a/templates/user.tmpl b/templates/user.tmpl
new file mode 100644 (file)
index 0000000..3347f92
--- /dev/null
@@ -0,0 +1,54 @@
+{{template "header.tmpl"}}
+{{template "navigation.tmpl" .NavbarData}}
+<div class="page-title"> User </div>
+
+<div class="user-info-container">
+<div>
+       <div class="user-profile-img-container">
+               <img class="user-profile-img" src="{{.User.AvatarStatic}}" alt="profile-avatar" />
+       </div>
+       <div class="user-profile-details-container">
+               <div>
+                       <span class="status-dname"> {{WithEmojis .User.DisplayName .User.Emojis}} </span>  
+                       <span class="status-uname"> {{.User.Acct}} </span>
+               </div>
+               <div>
+                       <span> {{if .User.Pleroma.Relationship.FollowedBy}} follows you - {{end}} </span>  
+                       {{if .User.Pleroma.Relationship.Following}} 
+                       <form class="d-inline" action="/unfollow/{{.User.ID}}" method="post">
+                           <input type="submit" value="unfollow" class="btn-link">
+                       </form>
+                       {{end}} 
+                       {{if .User.Pleroma.Relationship.Requested}} 
+                       <form class="d-inline" action="/unfollow/{{.User.ID}}" method="post">
+                           <input type="submit" value="cancel request" class="btn-link">
+                       </form>
+                       {{end}} 
+                       {{if not .User.Pleroma.Relationship.Following}} 
+                       <form class="d-inline" action="/follow/{{.User.ID}}" method="post">
+                           <input type="submit" value="{{if .User.Pleroma.Relationship.Requested}}resend request{{else}}follow{{end}}" class="btn-link">
+                       </form>
+                       {{end}} 
+               </div>
+               <div>
+                       {{.User.StatusesCount}} statuses - {{.User.FollowingCount}} following - {{.User.FollowersCount}} followers
+               </div>
+       </div>
+       <div class="user-profile-decription">
+       {{.User.Note}}
+       </div>
+</div>
+</div>
+
+{{range .Statuses}}
+{{template "status.tmpl" .}}
+{{end}}
+
+<div class="pagination">
+       {{if .HasNext}}
+               <a href="{{.NextLink}}">next</a>
+       {{end}}
+</div>
+
+{{template "footer.tmpl"}}
+