aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--mastodon/accounts.go43
-rw-r--r--renderer/model.go18
-rw-r--r--renderer/renderer.go5
-rw-r--r--service/auth.go24
-rw-r--r--service/logging.go24
-rw-r--r--service/service.go52
-rw-r--r--service/transport.go60
-rw-r--r--static/main.css39
-rw-r--r--templates/notification.tmpl12
-rw-r--r--templates/status.tmpl12
-rw-r--r--templates/user.tmpl54
11 files changed, 310 insertions, 33 deletions
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 @@
<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>
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 @@
<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
@@ -12,14 +14,18 @@
<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
index 0000000..3347f92
--- /dev/null
+++ b/templates/user.tmpl
@@ -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"}}
+