Add attachments support

This commit is contained in:
r 2019-12-14 20:19:02 +00:00
parent ea66bd539d
commit e129ea922e
8 changed files with 86 additions and 11 deletions

View file

@ -83,6 +83,32 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in
return err return err
} }
ct = mw.FormDataContentType() ct = mw.FormDataContentType()
} else if file, ok := params.(*multipart.FileHeader); ok {
f, err := file.Open()
if err != nil {
return err
}
defer f.Close()
var buf bytes.Buffer
mw := multipart.NewWriter(&buf)
part, err := mw.CreateFormFile("file", filepath.Base(file.Filename))
if err != nil {
return err
}
_, err = io.Copy(part, f)
if err != nil {
return err
}
err = mw.Close()
if err != nil {
return err
}
req, err = http.NewRequest(method, u.String(), &buf)
if err != nil {
return err
}
ct = mw.FormDataContentType()
} else if reader, ok := params.(io.Reader); ok { } else if reader, ok := params.(io.Reader); ok {
var buf bytes.Buffer var buf bytes.Buffer
mw := multipart.NewWriter(&buf) mw := multipart.NewWriter(&buf)

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"mime/multipart"
"net/http" "net/http"
"net/url" "net/url"
"time" "time"
@ -295,3 +296,13 @@ func (c *Client) UploadMediaFromReader(ctx context.Context, reader io.Reader) (*
} }
return &attachment, nil return &attachment, nil
} }
// UploadMediaFromReader uploads a media attachment from a io.Reader.
func (c *Client) UploadMediaFromMultipartFileHeader(ctx context.Context, fh *multipart.FileHeader) (*Attachment, error) {
var attachment Attachment
err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", fh, &attachment, nil)
if err != nil {
return nil, err
}
return &attachment, nil
}

View file

@ -5,6 +5,7 @@ import (
"errors" "errors"
"io" "io"
"mastodon" "mastodon"
"mime/multipart"
"web/model" "web/model"
) )
@ -142,10 +143,10 @@ func (s *authService) UnRetweet(ctx context.Context, client io.Writer, c *mastod
return s.Service.UnRetweet(ctx, client, c, id) return s.Service.UnRetweet(ctx, client, c, id)
} }
func (s *authService) PostTweet(ctx context.Context, client io.Writer, c *mastodon.Client, content string, replyToID string) (id string, err error) { func (s *authService) PostTweet(ctx context.Context, client io.Writer, c *mastodon.Client, content string, replyToID string, files []*multipart.FileHeader) (id string, err error) {
c, err = s.getClient(ctx) c, err = s.getClient(ctx)
if err != nil { if err != nil {
return return
} }
return s.Service.PostTweet(ctx, client, c, content, replyToID) return s.Service.PostTweet(ctx, client, c, content, replyToID, files)
} }

View file

@ -5,6 +5,7 @@ import (
"io" "io"
"log" "log"
"mastodon" "mastodon"
"mime/multipart"
"time" "time"
) )
@ -108,10 +109,10 @@ func (s *loggingService) UnRetweet(ctx context.Context, client io.Writer, c *mas
return s.Service.UnRetweet(ctx, client, c, id) return s.Service.UnRetweet(ctx, client, c, id)
} }
func (s *loggingService) PostTweet(ctx context.Context, client io.Writer, c *mastodon.Client, content string, replyToID string) (id string, err error) { func (s *loggingService) PostTweet(ctx context.Context, client io.Writer, c *mastodon.Client, content string, replyToID string, files []*multipart.FileHeader) (id string, err error) {
defer func(begin time.Time) { defer func(begin time.Time) {
s.logger.Printf("method=%v, content=%v, reply_to_id=%v, took=%v, err=%v\n", s.logger.Printf("method=%v, content=%v, reply_to_id=%v, took=%v, err=%v\n",
"PostTweet", content, replyToID, time.Since(begin), err) "PostTweet", content, replyToID, time.Since(begin), err)
}(time.Now()) }(time.Now())
return s.Service.PostTweet(ctx, client, c, content, replyToID) return s.Service.PostTweet(ctx, client, c, content, replyToID, files)
} }

View file

@ -7,6 +7,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"mime/multipart"
"net/http" "net/http"
"net/url" "net/url"
"path" "path"
@ -36,7 +37,7 @@ type Service interface {
UnLike(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) 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) 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) (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)
} }
type service struct { type service struct {
@ -292,10 +293,20 @@ func (svc *service) UnRetweet(ctx context.Context, client io.Writer, c *mastodon
return return
} }
func (svc *service) PostTweet(ctx context.Context, client io.Writer, c *mastodon.Client, content string, replyToID string) (id string, err error) { func (svc *service) PostTweet(ctx context.Context, client io.Writer, c *mastodon.Client, content string, replyToID string, files []*multipart.FileHeader) (id string, err error) {
var mediaIds []string
for _, f := range files {
a, err := c.UploadMediaFromMultipartFileHeader(ctx, f)
if err != nil {
return "", err
}
mediaIds = append(mediaIds, a.ID)
}
tweet := &mastodon.Toot{ tweet := &mastodon.Toot{
Status: content, Status: content,
InReplyToID: replyToID, InReplyToID: replyToID,
MediaIDs: mediaIds,
} }
s, err := c.PostStatus(ctx, tweet) s, err := c.PostStatus(ctx, tweet)

View file

@ -3,6 +3,7 @@ package service
import ( import (
"context" "context"
"fmt" "fmt"
"mime/multipart"
"net/http" "net/http"
"path" "path"
@ -153,9 +154,18 @@ func NewHandler(s Service, staticDir string) http.Handler {
r.HandleFunc("/post", func(w http.ResponseWriter, req *http.Request) { r.HandleFunc("/post", func(w http.ResponseWriter, req *http.Request) {
ctx := getContextWithSession(context.Background(), req) ctx := getContextWithSession(context.Background(), req)
content := req.FormValue("content")
replyToID := req.FormValue("reply_to_id") err := req.ParseMultipartForm(4 << 20)
id, err := s.PostTweet(ctx, w, nil, content, replyToID) if err != nil {
s.ServeErrorPage(ctx, w, err)
return
}
content := getMultipartFormValue(req.MultipartForm, "content")
replyToID := getMultipartFormValue(req.MultipartForm, "reply_to_id")
files := req.MultipartForm.File["attachments"]
id, err := s.PostTweet(ctx, w, nil, content, replyToID, files)
if err != nil { if err != nil {
s.ServeErrorPage(ctx, w, err) s.ServeErrorPage(ctx, w, err)
return return
@ -178,3 +188,14 @@ func NewHandler(s Service, staticDir string) http.Handler {
return r return r
} }
func getMultipartFormValue(mf *multipart.Form, key string) (val string) {
vals, ok := mf.Value[key]
if !ok {
return ""
}
if len(vals) < 1 {
return ""
}
return vals[0]
}

View file

@ -8,12 +8,14 @@
{{template "status.tmpl" .Status}} {{template "status.tmpl" .Status}}
{{if .PostReply}} {{if .PostReply}}
<form class="timeline-post-form" action="/post" method="POST"> <form class="timeline-post-form" action="/post" method="POST" enctype="multipart/form-data">
<input type="hidden" name="reply_to_id" value="{{.ReplyToID}}" /> <input type="hidden" name="reply_to_id" value="{{.ReplyToID}}" />
<label for="post-content"> Reply to {{.Status.Account.DisplayName}} </label> <label for="post-content"> Reply to {{.Status.Account.DisplayName}} </label>
<br/> <br/>
<textarea id="post-content" name="content" class="post-content" cols="50" rows="5">{{.ReplyContent}}</textarea> <textarea id="post-content" name="content" class="post-content" cols="50" rows="5">{{.ReplyContent}}</textarea>
<br/> <br/>
Attachments <input id="post-file-picker" type="file" name="attachments" multiple>
<br/>
<button type="submit"> Post </button> <button type="submit"> Post </button>
</form> </form>
{{end}} {{end}}

View file

@ -2,11 +2,13 @@
<div class="page-title"> Timeline </div> <div class="page-title"> Timeline </div>
{{template "navigation.tmpl"}} {{template "navigation.tmpl"}}
<form class="timeline-post-form" action="/post" method="POST"> <form class="timeline-post-form" action="/post" method="POST" enctype="multipart/form-data">
<label for="post-content"> New Post </label> <label for="post-content"> New Post </label>
<br/> <br/>
<textarea id="post-content" name="content" class="post-content" cols="50" rows="5"></textarea> <textarea id="post-content" name="content" class="post-content" cols="50" rows="5"></textarea>
<br/> <br/>
Attachments <input id="post-file-picker" type="file" name="attachments" multiple>
<br/>
<button type="submit"> Post </button> <button type="submit"> Post </button>
</form> </form>