From e129ea922e6cbe8966f9a9f0b1c6c94401516c61 Mon Sep 17 00:00:00 2001 From: r Date: Sat, 14 Dec 2019 20:19:02 +0000 Subject: [PATCH] Add attachments support --- mastodon/mastodon.go | 26 ++++++++++++++++++++++++++ mastodon/status.go | 11 +++++++++++ service/auth.go | 5 +++-- service/logging.go | 5 +++-- service/service.go | 15 +++++++++++++-- service/transport.go | 27 ++++++++++++++++++++++++--- templates/thread.tmpl | 4 +++- templates/timeline.tmpl | 4 +++- 8 files changed, 86 insertions(+), 11 deletions(-) diff --git a/mastodon/mastodon.go b/mastodon/mastodon.go index ff86d2b..22f5a53 100644 --- a/mastodon/mastodon.go +++ b/mastodon/mastodon.go @@ -83,6 +83,32 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in return err } 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 { var buf bytes.Buffer mw := multipart.NewWriter(&buf) diff --git a/mastodon/status.go b/mastodon/status.go index fd69914..b6110d5 100644 --- a/mastodon/status.go +++ b/mastodon/status.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "mime/multipart" "net/http" "net/url" "time" @@ -295,3 +296,13 @@ func (c *Client) UploadMediaFromReader(ctx context.Context, reader io.Reader) (* } 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 +} diff --git a/service/auth.go b/service/auth.go index 5ee2001..98012af 100644 --- a/service/auth.go +++ b/service/auth.go @@ -5,6 +5,7 @@ import ( "errors" "io" "mastodon" + "mime/multipart" "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) } -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) if err != nil { return } - return s.Service.PostTweet(ctx, client, c, content, replyToID) + return s.Service.PostTweet(ctx, client, c, content, replyToID, files) } diff --git a/service/logging.go b/service/logging.go index 0ed40bd..3a95a94 100644 --- a/service/logging.go +++ b/service/logging.go @@ -5,6 +5,7 @@ import ( "io" "log" "mastodon" + "mime/multipart" "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) } -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) { s.logger.Printf("method=%v, content=%v, reply_to_id=%v, took=%v, err=%v\n", "PostTweet", content, replyToID, time.Since(begin), err) }(time.Now()) - return s.Service.PostTweet(ctx, client, c, content, replyToID) + return s.Service.PostTweet(ctx, client, c, content, replyToID, files) } diff --git a/service/service.go b/service/service.go index 3bfb163..15dab5d 100644 --- a/service/service.go +++ b/service/service.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "mime/multipart" "net/http" "net/url" "path" @@ -36,7 +37,7 @@ type Service interface { 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) (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 { @@ -292,10 +293,20 @@ func (svc *service) UnRetweet(ctx context.Context, client io.Writer, c *mastodon 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{ Status: content, InReplyToID: replyToID, + MediaIDs: mediaIds, } s, err := c.PostStatus(ctx, tweet) diff --git a/service/transport.go b/service/transport.go index 00f7430..d5a6ee8 100644 --- a/service/transport.go +++ b/service/transport.go @@ -3,6 +3,7 @@ package service import ( "context" "fmt" + "mime/multipart" "net/http" "path" @@ -153,9 +154,18 @@ func NewHandler(s Service, staticDir string) http.Handler { r.HandleFunc("/post", func(w http.ResponseWriter, req *http.Request) { ctx := getContextWithSession(context.Background(), req) - content := req.FormValue("content") - replyToID := req.FormValue("reply_to_id") - id, err := s.PostTweet(ctx, w, nil, content, replyToID) + + err := req.ParseMultipartForm(4 << 20) + 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 { s.ServeErrorPage(ctx, w, err) return @@ -178,3 +188,14 @@ func NewHandler(s Service, staticDir string) http.Handler { 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] +} diff --git a/templates/thread.tmpl b/templates/thread.tmpl index 4bdc2f0..a3f7916 100644 --- a/templates/thread.tmpl +++ b/templates/thread.tmpl @@ -8,12 +8,14 @@ {{template "status.tmpl" .Status}} {{if .PostReply}} -
+

+ Attachments +
{{end}} diff --git a/templates/timeline.tmpl b/templates/timeline.tmpl index 7f3234c..51bf12e 100644 --- a/templates/timeline.tmpl +++ b/templates/timeline.tmpl @@ -2,11 +2,13 @@
Timeline
{{template "navigation.tmpl"}} -
+

+ Attachments +