Add attachments support
authorr <r@freesoftwareextremist.com>
Sat, 14 Dec 2019 20:19:02 +0000 (20:19 +0000)
committerr <r@freesoftwareextremist.com>
Sat, 14 Dec 2019 20:19:02 +0000 (20:19 +0000)
mastodon/mastodon.go
mastodon/status.go
service/auth.go
service/logging.go
service/service.go
service/transport.go
templates/thread.tmpl
templates/timeline.tmpl

index ff86d2ba782b98fea17ae6c0d83a5be00a8e35d7..22f5a5340bcb6e2019e53a4b8b29b1d68dce1f26 100644 (file)
@@ -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)
index fd69914abc18d30608c452362334f8dc424307f9..b6110d577ff2b3186660eeec1e83a13d488d1ff3 100644 (file)
@@ -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
+}
index 5ee2001fdeb3c43bf7c5388d4c544015ac6a56ce..98012aff608a7c988911cce3467ff0e4311f30bc 100644 (file)
@@ -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)
 }
index 0ed40bdc135ead3bfbe89050586ce4bffeff0e7a..3a95a940f5e4d3eff60933f926fe3a4e9f7ca589 100644 (file)
@@ -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)
 }
index 3bfb163a9f95ff39736d263a07770e1b068137d5..15dab5da34df6d60edf5286926888c535df75923 100644 (file)
@@ -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)
index 00f7430daf2ddb67ca12eb370143502f0a341811..d5a6ee8e4967b44363ec38852a440bca443e36ed 100644 (file)
@@ -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]
+}
index 4bdc2f045efe8bd84fe7deeffd14a5ad163f4b2f..a3f79162adceae4ceda84dcbbbe6119da8b4cbf5 100644 (file)
@@ -8,12 +8,14 @@
 
 {{template "status.tmpl" .Status}}
 {{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}}" />
        <label for="post-content"> Reply to {{.Status.Account.DisplayName}} </label>
        <br/>
        <textarea id="post-content" name="content" class="post-content" cols="50" rows="5">{{.ReplyContent}}</textarea>
        <br/>
+       Attachments <input id="post-file-picker" type="file" name="attachments" multiple>
+       <br/>
        <button type="submit"> Post </button>
 </form>
 {{end}}
index 7f3234c6ef4a3f21bf49c783b5378bf6681fda9d..51bf12e3652afce9552963da37b8cc48c368bef3 100644 (file)
@@ -2,11 +2,13 @@
 <div class="page-title"> Timeline </div>
 {{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>
        <br/>
        <textarea id="post-content" name="content" class="post-content" cols="50" rows="5"></textarea>
        <br/>
+       Attachments <input id="post-file-picker" type="file" name="attachments" multiple>
+       <br/>
        <button type="submit"> Post </button>
 </form>