parent
a68a09a83e
commit
cfec7879e3
|
@ -0,0 +1,38 @@
|
||||||
|
package mastodon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Poll struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
|
Expired bool `json:"expired"`
|
||||||
|
Multiple bool `json:"multiple"`
|
||||||
|
VotesCount int64 `json:"votes_count"`
|
||||||
|
Voted bool `json:"voted"`
|
||||||
|
Emojis []Emoji `json:"emojis"`
|
||||||
|
Options []PollOption `json:"options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll hold information for a mastodon poll option.
|
||||||
|
type PollOption struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
VotesCount int64 `json:"votes_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vote submits a vote with given choices to the poll specified by id.
|
||||||
|
func (c *Client) Vote(ctx context.Context, id string, choices []string) (*Poll, error) {
|
||||||
|
var poll Poll
|
||||||
|
params := make(url.Values)
|
||||||
|
params["choices[]"] = choices
|
||||||
|
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/polls/%s/votes", id), params, &poll, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &poll, nil
|
||||||
|
}
|
|
@ -47,13 +47,14 @@ type Status struct {
|
||||||
Application Application `json:"application"`
|
Application Application `json:"application"`
|
||||||
Language string `json:"language"`
|
Language string `json:"language"`
|
||||||
Pinned interface{} `json:"pinned"`
|
Pinned interface{} `json:"pinned"`
|
||||||
|
Poll *Poll `json:"poll"`
|
||||||
|
|
||||||
// Custom fields
|
// Custom fields
|
||||||
Pleroma StatusPleroma `json:"pleroma"`
|
Pleroma StatusPleroma `json:"pleroma"`
|
||||||
ShowReplies bool `json:"show_replies"`
|
ShowReplies bool `json:"show_replies"`
|
||||||
ReplyMap map[string][]ReplyInfo `json:"reply_map"`
|
ReplyMap map[string][]ReplyInfo `json:"reply_map"`
|
||||||
ReplyNumber int `json:"reply_number"`
|
ReplyNumber int `json:"reply_number"`
|
||||||
RetweetedByID string `json:"retweeted_by_id"`
|
RetweetedByID string `json:"retweeted_by_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context hold information for mastodon context.
|
// Context hold information for mastodon context.
|
||||||
|
|
|
@ -43,6 +43,7 @@ func NewRenderer(templateGlobPattern string) (r *renderer, err error) {
|
||||||
"StatusContentFilter": StatusContentFilter,
|
"StatusContentFilter": StatusContentFilter,
|
||||||
"DisplayInteractionCount": DisplayInteractionCount,
|
"DisplayInteractionCount": DisplayInteractionCount,
|
||||||
"TimeSince": TimeSince,
|
"TimeSince": TimeSince,
|
||||||
|
"TimeUntil": TimeUntil,
|
||||||
"FormatTimeRFC3339": FormatTimeRFC3339,
|
"FormatTimeRFC3339": FormatTimeRFC3339,
|
||||||
"FormatTimeRFC822": FormatTimeRFC822,
|
"FormatTimeRFC822": FormatTimeRFC822,
|
||||||
"WithContext": WithContext,
|
"WithContext": WithContext,
|
||||||
|
@ -86,7 +87,7 @@ func (r *renderer) RenderUserPage(ctx *Context, writer io.Writer,
|
||||||
return r.template.ExecuteTemplate(writer, "user.tmpl", WithContext(data, ctx))
|
return r.template.ExecuteTemplate(writer, "user.tmpl", WithContext(data, ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *renderer) RenderUserSearchPage(ctx *Context, writer io.Writer,
|
func (r *renderer) RenderUserSearchPage(ctx *Context, writer io.Writer,
|
||||||
data *UserSearchData) (err error) {
|
data *UserSearchData) (err error) {
|
||||||
return r.template.ExecuteTemplate(writer, "usersearch.tmpl", WithContext(data, ctx))
|
return r.template.ExecuteTemplate(writer, "usersearch.tmpl", WithContext(data, ctx))
|
||||||
}
|
}
|
||||||
|
@ -158,8 +159,7 @@ func DisplayInteractionCount(c int64) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func TimeSince(t time.Time) string {
|
func DurToStr(dur time.Duration) string {
|
||||||
dur := time.Since(t)
|
|
||||||
s := dur.Seconds()
|
s := dur.Seconds()
|
||||||
if s < 60 {
|
if s < 60 {
|
||||||
return strconv.Itoa(int(s)) + "s"
|
return strconv.Itoa(int(s)) + "s"
|
||||||
|
@ -184,6 +184,14 @@ func TimeSince(t time.Time) string {
|
||||||
return strconv.Itoa(int(y)) + "y"
|
return strconv.Itoa(int(y)) + "y"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TimeSince(t time.Time) string {
|
||||||
|
return DurToStr(time.Since(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TimeUntil(t time.Time) string {
|
||||||
|
return DurToStr(time.Until(t))
|
||||||
|
}
|
||||||
|
|
||||||
func FormatTimeRFC3339(t time.Time) string {
|
func FormatTimeRFC3339(t time.Time) string {
|
||||||
return t.Format(time.RFC3339)
|
return t.Format(time.RFC3339)
|
||||||
}
|
}
|
||||||
|
|
|
@ -250,6 +250,19 @@ func (s *as) UnRetweet(ctx context.Context, c *model.Client, id string) (count i
|
||||||
return s.Service.UnRetweet(ctx, c, id)
|
return s.Service.UnRetweet(ctx, c, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *as) Vote(ctx context.Context, c *model.Client, id string,
|
||||||
|
choices []string) (err error) {
|
||||||
|
err = s.authenticateClient(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = checkCSRF(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return s.Service.Vote(ctx, c, id, choices)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *as) Follow(ctx context.Context, c *model.Client, id string) (err error) {
|
func (s *as) Follow(ctx context.Context, c *model.Client, id string) (err error) {
|
||||||
err = s.authenticateClient(ctx, c)
|
err = s.authenticateClient(ctx, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -77,7 +77,7 @@ func (s *ls) ServeNotificationPage(ctx context.Context, c *model.Client,
|
||||||
return s.Service.ServeNotificationPage(ctx, c, maxID, minID)
|
return s.Service.ServeNotificationPage(ctx, c, maxID, minID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ls) ServeUserPage(ctx context.Context, c *model.Client, id string,
|
func (s *ls) ServeUserPage(ctx context.Context, c *model.Client, id string,
|
||||||
pageType string, maxID string, minID string) (err error) {
|
pageType string, maxID string, minID string) (err error) {
|
||||||
defer func(begin time.Time) {
|
defer func(begin time.Time) {
|
||||||
s.logger.Printf("method=%v, id=%v, type=%v, took=%v, err=%v\n",
|
s.logger.Printf("method=%v, id=%v, type=%v, took=%v, err=%v\n",
|
||||||
|
@ -111,7 +111,7 @@ func (s *ls) ServeSearchPage(ctx context.Context, c *model.Client, q string,
|
||||||
return s.Service.ServeSearchPage(ctx, c, q, qType, offset)
|
return s.Service.ServeSearchPage(ctx, c, q, qType, offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ls) ServeUserSearchPage(ctx context.Context, c *model.Client,
|
func (s *ls) ServeUserSearchPage(ctx context.Context, c *model.Client,
|
||||||
id string, q string, offset int) (err error) {
|
id string, q string, offset int) (err error) {
|
||||||
defer func(begin time.Time) {
|
defer func(begin time.Time) {
|
||||||
s.logger.Printf("method=%v, took=%v, err=%v\n",
|
s.logger.Printf("method=%v, took=%v, err=%v\n",
|
||||||
|
@ -189,6 +189,14 @@ func (s *ls) UnRetweet(ctx context.Context, c *model.Client, id string) (count i
|
||||||
return s.Service.UnRetweet(ctx, c, id)
|
return s.Service.UnRetweet(ctx, c, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ls) Vote(ctx context.Context, c *model.Client, id string, choices []string) (err error) {
|
||||||
|
defer func(begin time.Time) {
|
||||||
|
s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n",
|
||||||
|
"Vote", id, time.Since(begin), err)
|
||||||
|
}(time.Now())
|
||||||
|
return s.Service.Vote(ctx, c, id, choices)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ls) Follow(ctx context.Context, c *model.Client, id string) (err error) {
|
func (s *ls) Follow(ctx context.Context, c *model.Client, id string) (err error) {
|
||||||
defer func(begin time.Time) {
|
defer func(begin time.Time) {
|
||||||
s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n",
|
s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n",
|
||||||
|
|
|
@ -42,6 +42,7 @@ type Service interface {
|
||||||
UnLike(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)
|
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)
|
UnRetweet(ctx context.Context, c *model.Client, id string) (count int64, err error)
|
||||||
|
Vote(ctx context.Context, c *model.Client, id string, choices []string) (err error)
|
||||||
Follow(ctx context.Context, c *model.Client, id string) (err error)
|
Follow(ctx context.Context, c *model.Client, id string) (err error)
|
||||||
UnFollow(ctx context.Context, c *model.Client, id string) (err error)
|
UnFollow(ctx context.Context, c *model.Client, id string) (err error)
|
||||||
Mute(ctx context.Context, c *model.Client, id string) (err error)
|
Mute(ctx context.Context, c *model.Client, id string) (err error)
|
||||||
|
@ -843,6 +844,15 @@ func (svc *service) UnRetweet(ctx context.Context, c *model.Client, id string) (
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (svc *service) Vote(ctx context.Context, c *model.Client, id string,
|
||||||
|
choices []string) (err error) {
|
||||||
|
_, err = c.Vote(ctx, id, choices)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (svc *service) Follow(ctx context.Context, c *model.Client, id string) (err error) {
|
func (svc *service) Follow(ctx context.Context, c *model.Client, id string) (err error) {
|
||||||
_, err = c.AccountFollow(ctx, id)
|
_, err = c.AccountFollow(ctx, id)
|
||||||
return
|
return
|
||||||
|
|
|
@ -419,6 +419,24 @@ func NewHandler(s Service, staticDir string) http.Handler {
|
||||||
w.WriteHeader(http.StatusFound)
|
w.WriteHeader(http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vote := func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
c := newClient(w)
|
||||||
|
ctx := newCtxWithSesionCSRF(req, req.FormValue("csrf_token"))
|
||||||
|
id, _ := mux.Vars(req)["id"]
|
||||||
|
statusID := req.FormValue("status_id")
|
||||||
|
choices, _ := req.PostForm["choices"]
|
||||||
|
|
||||||
|
err := s.Vote(ctx, c, id, choices)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
s.ServeErrorPage(ctx, c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Location", req.Header.Get("Referer")+"#status-"+statusID)
|
||||||
|
w.WriteHeader(http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
follow := func(w http.ResponseWriter, req *http.Request) {
|
follow := func(w http.ResponseWriter, req *http.Request) {
|
||||||
c := newClient(w)
|
c := newClient(w)
|
||||||
ctx := newCtxWithSesionCSRF(req, req.FormValue("csrf_token"))
|
ctx := newCtxWithSesionCSRF(req, req.FormValue("csrf_token"))
|
||||||
|
@ -697,6 +715,7 @@ func NewHandler(s Service, staticDir string) http.Handler {
|
||||||
r.HandleFunc("/unlike/{id}", unlike).Methods(http.MethodPost)
|
r.HandleFunc("/unlike/{id}", unlike).Methods(http.MethodPost)
|
||||||
r.HandleFunc("/retweet/{id}", retweet).Methods(http.MethodPost)
|
r.HandleFunc("/retweet/{id}", retweet).Methods(http.MethodPost)
|
||||||
r.HandleFunc("/unretweet/{id}", unretweet).Methods(http.MethodPost)
|
r.HandleFunc("/unretweet/{id}", unretweet).Methods(http.MethodPost)
|
||||||
|
r.HandleFunc("/vote/{id}", vote).Methods(http.MethodPost)
|
||||||
r.HandleFunc("/follow/{id}", follow).Methods(http.MethodPost)
|
r.HandleFunc("/follow/{id}", follow).Methods(http.MethodPost)
|
||||||
r.HandleFunc("/unfollow/{id}", unfollow).Methods(http.MethodPost)
|
r.HandleFunc("/unfollow/{id}", unfollow).Methods(http.MethodPost)
|
||||||
r.HandleFunc("/mute/{id}", mute).Methods(http.MethodPost)
|
r.HandleFunc("/mute/{id}", mute).Methods(http.MethodPost)
|
||||||
|
|
|
@ -452,6 +452,14 @@ a:hover,
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.poll-form button[type=submit] {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-info {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
background-color: #222222;
|
background-color: #222222;
|
||||||
background-image: none;
|
background-image: none;
|
||||||
|
|
|
@ -25,11 +25,11 @@
|
||||||
<span class="status-uname"> {{.Account.Acct}} </span>
|
<span class="status-uname"> {{.Account.Acct}} </span>
|
||||||
</a>
|
</a>
|
||||||
<div class="more-container" title="more">
|
<div class="more-container" title="more">
|
||||||
<div class="remote-link" title="mute">
|
<div class="remote-link">
|
||||||
{{.Visibility}}
|
{{.Visibility}}
|
||||||
</div>
|
</div>
|
||||||
<div class="more-content">
|
<div class="more-content">
|
||||||
<a class="more-link" href="{{.URL}}" target="_blank" title="mute">
|
<a class="more-link" href="{{.URL}}" target="_blank" title="source">
|
||||||
source
|
source
|
||||||
</a>
|
</a>
|
||||||
{{if .Muted}}
|
{{if .Muted}}
|
||||||
|
@ -74,41 +74,76 @@
|
||||||
<div class="status-content"> {{StatusContentFilter .SpoilerText .Content .Emojis .Mentions}} </div>
|
<div class="status-content"> {{StatusContentFilter .SpoilerText .Content .Emojis .Mentions}} </div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="status-media-container">
|
<div class="status-media-container">
|
||||||
{{range .MediaAttachments}}
|
{{range .MediaAttachments}}
|
||||||
{{if eq .Type "image"}}
|
{{if eq .Type "image"}}
|
||||||
<a class="img-link" href="{{.URL}}" target="_blank">
|
<a class="img-link" href="{{.URL}}" target="_blank">
|
||||||
<img class="status-image" src="{{.URL}}" alt="status-image" />
|
<img class="status-image" src="{{.URL}}" alt="status-image" />
|
||||||
{{if (and $.Ctx.MaskNSFW $s.Sensitive)}}
|
{{if (and $.Ctx.MaskNSFW $s.Sensitive)}}
|
||||||
<div class="status-nsfw-overlay"></div>
|
<div class="status-nsfw-overlay"></div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</a>
|
</a>
|
||||||
{{else if eq .Type "audio"}}
|
{{else if eq .Type "audio"}}
|
||||||
<audio class="status-audio" controls preload="none">
|
<audio class="status-audio" controls preload="none">
|
||||||
<source src="{{.URL}}">
|
|
||||||
<p> Your browser doesn't support HTML5 audio </p>
|
|
||||||
</audio>
|
|
||||||
{{else if eq .Type "video"}}
|
|
||||||
<div class="status-video-container">
|
|
||||||
<video class="status-video" controls preload="none">
|
|
||||||
<source src="{{.URL}}">
|
<source src="{{.URL}}">
|
||||||
<p> Your browser doesn't support HTML5 video </p>
|
<p> Your browser doesn't support HTML5 audio </p>
|
||||||
</video>
|
</audio>
|
||||||
{{if (and $.Ctx.MaskNSFW $s.Sensitive)}}
|
{{else if eq .Type "video"}}
|
||||||
<div class="status-nsfw-overlay"></div>
|
<div class="status-video-container">
|
||||||
|
<video class="status-video" controls preload="none">
|
||||||
|
<source src="{{.URL}}">
|
||||||
|
<p> Your browser doesn't support HTML5 video </p>
|
||||||
|
</video>
|
||||||
|
{{if (and $.Ctx.MaskNSFW $s.Sensitive)}}
|
||||||
|
<div class="status-nsfw-overlay"></div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<a href="{{.URL}}" target="_blank"> attachment </a>
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{if .Poll}}
|
||||||
<a href="{{.URL}}" target="_blank"> attachment </a>
|
<form class="poll-form" action="/vote/{{.Poll.ID}}" method="POST">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
|
||||||
|
<input type="hidden" name="status_id" value="{{$s.ID}}">
|
||||||
|
{{range $i, $o := .Poll.Options}}
|
||||||
|
<div class="poll-option">
|
||||||
|
{{if (or $s.Poll.Expired $s.Poll.Voted)}}
|
||||||
|
<div> {{$o.Title}} - {{$o.VotesCount}} votes </div>
|
||||||
|
{{else}}
|
||||||
|
<input type="{{if $s.Poll.Multiple}}checkbox{{else}}radio{{end}}" name="choices"
|
||||||
|
id="poll-{{$s.ID}}-{{$i}}" value="{{$i}}">
|
||||||
|
<label for="poll-{{$s.ID}}-{{$i}}">
|
||||||
|
{{$o.Title}}
|
||||||
|
</label>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{if not (or .Poll.Expired .Poll.Voted)}}
|
||||||
|
<button type="submit"> Vote </button>
|
||||||
|
{{end}}
|
||||||
|
<div class="poll-info">
|
||||||
|
<span>{{.Poll.VotesCount}} votes</span>
|
||||||
|
{{if .Poll.Expired}}
|
||||||
|
<span> - poll expired </span>
|
||||||
|
{{else}}
|
||||||
|
<span>
|
||||||
|
- poll ends in
|
||||||
|
<time datetime="{{FormatTimeRFC3339 .Poll.ExpiresAt}}" title="{{FormatTimeRFC822 .Poll.ExpiresAt}}">
|
||||||
|
{{TimeUntil .Poll.ExpiresAt}}
|
||||||
|
</time>
|
||||||
|
</span>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
<div class="status-action-container">
|
<div class="status-action-container">
|
||||||
<div class="status-action">
|
<div class="status-action">
|
||||||
<a href="/thread/{{.ID}}?reply=true#status-{{.ID}}" title="reply">
|
<a href="/thread/{{.ID}}?reply=true#status-{{.ID}}" title="reply">
|
||||||
reply
|
reply
|
||||||
</a>
|
</a>
|
||||||
<a class="status-reply-count" href="/thread/{{.ID}}#status-{{.ID}}" {{if $.Ctx.ThreadInNewTab}}target="_blank"{{end}}>
|
<a class="status-reply-count" href="/thread/{{.ID}}#status-{{.ID}}" {{if $.Ctx.ThreadInNewTab}}target="_blank"{{end}}>
|
||||||
{{if .RepliesCount}} ({{DisplayInteractionCount .RepliesCount}}) {{end}}
|
{{if .RepliesCount}} ({{DisplayInteractionCount .RepliesCount}}) {{end}}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-action">
|
<div class="status-action">
|
||||||
|
@ -154,8 +189,11 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-action">
|
<div class="status-action">
|
||||||
<a class="status-time" href="{{if not .ShowReplies}}/thread/{{.ID}}{{end}}#status-{{.ID}}" {{if $.Ctx.ThreadInNewTab}}target="_blank"{{end}}>
|
<a class="status-time" href="{{if not .ShowReplies}}/thread/{{.ID}}{{end}}#status-{{.ID}}"
|
||||||
<time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}"> {{TimeSince .CreatedAt}} </time>
|
{{if $.Ctx.ThreadInNewTab}}target="_blank"{{end}}>
|
||||||
|
<time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">
|
||||||
|
{{TimeSince .CreatedAt}}
|
||||||
|
</time>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue