Add poll support
Currenlty only voting is possible.
This commit is contained in:
parent
a68a09a83e
commit
cfec7879e3
38
mastodon/poll.go
Normal file
38
mastodon/poll.go
Normal file
@ -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"`
|
||||
Language string `json:"language"`
|
||||
Pinned interface{} `json:"pinned"`
|
||||
Poll *Poll `json:"poll"`
|
||||
|
||||
// Custom fields
|
||||
Pleroma StatusPleroma `json:"pleroma"`
|
||||
ShowReplies bool `json:"show_replies"`
|
||||
ReplyMap map[string][]ReplyInfo `json:"reply_map"`
|
||||
ReplyNumber int `json:"reply_number"`
|
||||
RetweetedByID string `json:"retweeted_by_id"`
|
||||
Pleroma StatusPleroma `json:"pleroma"`
|
||||
ShowReplies bool `json:"show_replies"`
|
||||
ReplyMap map[string][]ReplyInfo `json:"reply_map"`
|
||||
ReplyNumber int `json:"reply_number"`
|
||||
RetweetedByID string `json:"retweeted_by_id"`
|
||||
}
|
||||
|
||||
// Context hold information for mastodon context.
|
||||
|
@ -43,6 +43,7 @@ func NewRenderer(templateGlobPattern string) (r *renderer, err error) {
|
||||
"StatusContentFilter": StatusContentFilter,
|
||||
"DisplayInteractionCount": DisplayInteractionCount,
|
||||
"TimeSince": TimeSince,
|
||||
"TimeUntil": TimeUntil,
|
||||
"FormatTimeRFC3339": FormatTimeRFC3339,
|
||||
"FormatTimeRFC822": FormatTimeRFC822,
|
||||
"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))
|
||||
}
|
||||
|
||||
func (r *renderer) RenderUserSearchPage(ctx *Context, writer io.Writer,
|
||||
func (r *renderer) RenderUserSearchPage(ctx *Context, writer io.Writer,
|
||||
data *UserSearchData) (err error) {
|
||||
return r.template.ExecuteTemplate(writer, "usersearch.tmpl", WithContext(data, ctx))
|
||||
}
|
||||
@ -158,8 +159,7 @@ func DisplayInteractionCount(c int64) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func TimeSince(t time.Time) string {
|
||||
dur := time.Since(t)
|
||||
func DurToStr(dur time.Duration) string {
|
||||
s := dur.Seconds()
|
||||
if s < 60 {
|
||||
return strconv.Itoa(int(s)) + "s"
|
||||
@ -184,6 +184,14 @@ func TimeSince(t time.Time) string {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
err = s.authenticateClient(ctx, c)
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
defer func(begin time.Time) {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
defer func(begin time.Time) {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
defer func(begin time.Time) {
|
||||
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)
|
||||
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)
|
||||
Vote(ctx context.Context, c *model.Client, id string, choices []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)
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
_, err = c.AccountFollow(ctx, id)
|
||||
return
|
||||
|
@ -419,6 +419,24 @@ func NewHandler(s Service, staticDir string) http.Handler {
|
||||
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) {
|
||||
c := newClient(w)
|
||||
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("/retweet/{id}", retweet).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("/unfollow/{id}", unfollow).Methods(http.MethodPost)
|
||||
r.HandleFunc("/mute/{id}", mute).Methods(http.MethodPost)
|
||||
|
@ -452,6 +452,14 @@ a:hover,
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.poll-form button[type=submit] {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.poll-info {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.dark {
|
||||
background-color: #222222;
|
||||
background-image: none;
|
||||
|
@ -25,11 +25,11 @@
|
||||
<span class="status-uname"> {{.Account.Acct}} </span>
|
||||
</a>
|
||||
<div class="more-container" title="more">
|
||||
<div class="remote-link" title="mute">
|
||||
<div class="remote-link">
|
||||
{{.Visibility}}
|
||||
</div>
|
||||
<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
|
||||
</a>
|
||||
{{if .Muted}}
|
||||
@ -74,41 +74,76 @@
|
||||
<div class="status-content"> {{StatusContentFilter .SpoilerText .Content .Emojis .Mentions}} </div>
|
||||
{{end}}
|
||||
<div class="status-media-container">
|
||||
{{range .MediaAttachments}}
|
||||
{{if eq .Type "image"}}
|
||||
<a class="img-link" href="{{.URL}}" target="_blank">
|
||||
<img class="status-image" src="{{.URL}}" alt="status-image" />
|
||||
{{if (and $.Ctx.MaskNSFW $s.Sensitive)}}
|
||||
<div class="status-nsfw-overlay"></div>
|
||||
{{end}}
|
||||
</a>
|
||||
{{else if eq .Type "audio"}}
|
||||
<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">
|
||||
{{range .MediaAttachments}}
|
||||
{{if eq .Type "image"}}
|
||||
<a class="img-link" href="{{.URL}}" target="_blank">
|
||||
<img class="status-image" src="{{.URL}}" alt="status-image" />
|
||||
{{if (and $.Ctx.MaskNSFW $s.Sensitive)}}
|
||||
<div class="status-nsfw-overlay"></div>
|
||||
{{end}}
|
||||
</a>
|
||||
{{else if eq .Type "audio"}}
|
||||
<audio class="status-audio" 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>
|
||||
<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}}">
|
||||
<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}}
|
||||
</div>
|
||||
{{else}}
|
||||
<a href="{{.URL}}" target="_blank"> attachment </a>
|
||||
{{if .Poll}}
|
||||
<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}}
|
||||
</div>
|
||||
<div class="status-action-container">
|
||||
<div class="status-action">
|
||||
<a href="/thread/{{.ID}}?reply=true#status-{{.ID}}" title="reply">
|
||||
reply
|
||||
</a>
|
||||
<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>
|
||||
</div>
|
||||
<div class="status-action">
|
||||
@ -154,8 +189,11 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="status-action">
|
||||
<a class="status-time" href="{{if not .ShowReplies}}/thread/{{.ID}}{{end}}#status-{{.ID}}" {{if $.Ctx.ThreadInNewTab}}target="_blank"{{end}}>
|
||||
<time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}"> {{TimeSince .CreatedAt}} </time>
|
||||
<a class="status-time" href="{{if not .ShowReplies}}/thread/{{.ID}}{{end}}#status-{{.ID}}"
|
||||
{{if $.Ctx.ThreadInNewTab}}target="_blank"{{end}}>
|
||||
<time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">
|
||||
{{TimeSince .CreatedAt}}
|
||||
</time>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user