diff options
Diffstat (limited to 'mastodon')
-rw-r--r-- | mastodon/accounts.go | 176 | ||||
-rw-r--r-- | mastodon/apps.go | 3 | ||||
-rw-r--r-- | mastodon/http.go | 45 | ||||
-rw-r--r-- | mastodon/mastodon.go | 100 | ||||
-rw-r--r-- | mastodon/status.go | 74 |
5 files changed, 239 insertions, 159 deletions
diff --git a/mastodon/accounts.go b/mastodon/accounts.go index df0a3b7..31021fd 100644 --- a/mastodon/accounts.go +++ b/mastodon/accounts.go @@ -1,16 +1,20 @@ package mastodon import ( + "bytes" "context" "fmt" + "io" + "mime/multipart" "net/http" "net/url" + "path/filepath" "strconv" "time" ) type AccountPleroma struct { - Relationship Relationship `json:"relationship"` + Relationship *Relationship `json:"relationship"` } // Account hold information for mastodon account. @@ -34,7 +38,11 @@ type Account struct { Moved *Account `json:"moved"` Fields []Field `json:"fields"` Bot bool `json:"bot"` + Source *AccountSource `json:"source"` Pleroma *AccountPleroma `json:"pleroma"` + + // Duplicate field for compatibilty with Pleroma + FollowRequestsCount int64 `json:"follow_requests_count"` } // Field is a Mastodon account profile field. @@ -46,17 +54,20 @@ type Field struct { // AccountSource is a Mastodon account profile field. type AccountSource struct { - Privacy *string `json:"privacy"` - Sensitive *bool `json:"sensitive"` - Language *string `json:"language"` - Note *string `json:"note"` - Fields *[]Field `json:"fields"` + Privacy *string `json:"privacy"` + Sensitive *bool `json:"sensitive"` + Language *string `json:"language"` + Note *string `json:"note"` + Fields *[]Field `json:"fields"` + FollowRequestsCount int64 `json:"follow_requests_count"` } // GetAccount return Account. func (c *Client) GetAccount(ctx context.Context, id string) (*Account, error) { var account Account - err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s", url.PathEscape(string(id))), nil, &account, nil) + params := url.Values{} + params.Set("with_relationships", strconv.FormatBool(true)) + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s", url.PathEscape(string(id))), params, &account, nil) if err != nil { return nil, err } @@ -66,7 +77,7 @@ func (c *Client) GetAccount(ctx context.Context, id string) (*Account, error) { return nil, err } if len(rs) > 0 { - account.Pleroma = &AccountPleroma{*rs[0]} + account.Pleroma = &AccountPleroma{rs[0]} } } return &account, nil @@ -93,54 +104,115 @@ type Profile struct { Source *AccountSource // Set the base64 encoded character string of the image. - Avatar string - Header string + Avatar *multipart.FileHeader + Header *multipart.FileHeader } // AccountUpdate updates the information of the current user. func (c *Client) AccountUpdate(ctx context.Context, profile *Profile) (*Account, error) { - params := url.Values{} + var buf bytes.Buffer + mw := multipart.NewWriter(&buf) if profile.DisplayName != nil { - params.Set("display_name", *profile.DisplayName) + err := mw.WriteField("display_name", *profile.DisplayName) + if err != nil { + return nil, err + } } if profile.Note != nil { - params.Set("note", *profile.Note) + err := mw.WriteField("note", *profile.Note) + if err != nil { + return nil, err + } } if profile.Locked != nil { - params.Set("locked", strconv.FormatBool(*profile.Locked)) + err := mw.WriteField("locked", strconv.FormatBool(*profile.Locked)) + if err != nil { + return nil, err + } } if profile.Fields != nil { for idx, field := range *profile.Fields { - params.Set(fmt.Sprintf("fields_attributes[%d][name]", idx), field.Name) - params.Set(fmt.Sprintf("fields_attributes[%d][value]", idx), field.Value) + err := mw.WriteField(fmt.Sprintf("fields_attributes[%d][name]", idx), field.Name) + if err != nil { + return nil, err + } + err = mw.WriteField(fmt.Sprintf("fields_attributes[%d][value]", idx), field.Value) + if err != nil { + return nil, err + } } } - if profile.Source != nil { - if profile.Source.Privacy != nil { - params.Set("source[privacy]", *profile.Source.Privacy) + if profile.Avatar != nil { + f, err := profile.Avatar.Open() + if err != nil { + return nil, err } - if profile.Source.Sensitive != nil { - params.Set("source[sensitive]", strconv.FormatBool(*profile.Source.Sensitive)) + fname := filepath.Base(profile.Avatar.Filename) + part, err := mw.CreateFormFile("avatar", fname) + if err != nil { + return nil, err } - if profile.Source.Language != nil { - params.Set("source[language]", *profile.Source.Language) + _, err = io.Copy(part, f) + if err != nil { + return nil, err + } + } + if profile.Header != nil { + f, err := profile.Header.Open() + if err != nil { + return nil, err + } + fname := filepath.Base(profile.Header.Filename) + part, err := mw.CreateFormFile("header", fname) + if err != nil { + return nil, err + } + _, err = io.Copy(part, f) + if err != nil { + return nil, err } } - if profile.Avatar != "" { - params.Set("avatar", profile.Avatar) + err := mw.Close() + if err != nil { + return nil, err } - if profile.Header != "" { - params.Set("header", profile.Header) + params := &multipartRequest{Data: &buf, ContentType: mw.FormDataContentType()} + var account Account + err = c.doAPI(ctx, http.MethodPatch, "/api/v1/accounts/update_credentials", params, &account, nil) + if err != nil { + return nil, err } + return &account, nil +} +func (c *Client) accountDeleteField(ctx context.Context, field string) (*Account, error) { + var buf bytes.Buffer + mw := multipart.NewWriter(&buf) + _, err := mw.CreateFormField(field) + if err != nil { + return nil, err + } + err = mw.Close() + if err != nil { + return nil, err + } + params := &multipartRequest{Data: &buf, ContentType: mw.FormDataContentType()} var account Account - err := c.doAPI(ctx, http.MethodPatch, "/api/v1/accounts/update_credentials", params, &account, nil) + err = c.doAPI(ctx, http.MethodPatch, "/api/v1/accounts/update_credentials", params, &account, nil) if err != nil { return nil, err } return &account, nil } +func (c *Client) AccountDeleteAvatar(ctx context.Context) (*Account, error) { + return c.accountDeleteField(ctx, "avatar") +} + +func (c *Client) AccountDeleteHeader(ctx context.Context) (*Account, error) { + return c.accountDeleteField(ctx, "header") +} + // GetAccountStatuses return statuses by specified accuont. func (c *Client) GetAccountStatuses(ctx context.Context, id string, onlyMedia bool, pg *Pagination) ([]*Status, error) { var statuses []*Status @@ -153,10 +225,40 @@ func (c *Client) GetAccountStatuses(ctx context.Context, id string, onlyMedia bo return statuses, nil } +func (c *Client) getMissingRelationships(ctx context.Context, accounts []*Account) ([]*Account, error) { + var ids []string + for _, a := range accounts { + if a.Pleroma == nil || len(a.Pleroma.Relationship.ID) < 1 { + ids = append(ids, a.ID) + } + } + if len(ids) < 1 { + return accounts, nil + } + rs, err := c.GetAccountRelationships(ctx, ids) + if err != nil { + return nil, err + } + rsm := make(map[string]*Relationship, len(rs)) + for _, r := range rs { + rsm[r.ID] = r + } + for _, a := range accounts { + a.Pleroma = &AccountPleroma{rsm[a.ID]} + } + return accounts, nil +} + // GetAccountFollowers return followers list. func (c *Client) GetAccountFollowers(ctx context.Context, id string, pg *Pagination) ([]*Account, error) { var accounts []*Account - err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s/followers", url.PathEscape(string(id))), nil, &accounts, pg) + params := url.Values{} + params.Set("with_relationships", strconv.FormatBool(true)) + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s/followers", url.PathEscape(string(id))), params, &accounts, pg) + if err != nil { + return nil, err + } + accounts, err = c.getMissingRelationships(ctx, accounts) if err != nil { return nil, err } @@ -166,7 +268,13 @@ func (c *Client) GetAccountFollowers(ctx context.Context, id string, pg *Paginat // GetAccountFollowing return following list. func (c *Client) GetAccountFollowing(ctx context.Context, id string, pg *Pagination) ([]*Account, error) { var accounts []*Account - err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s/following", url.PathEscape(string(id))), nil, &accounts, pg) + params := url.Values{} + params.Set("with_relationships", strconv.FormatBool(true)) + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s/following", url.PathEscape(string(id))), params, &accounts, pg) + if err != nil { + return nil, err + } + accounts, err = c.getMissingRelationships(ctx, accounts) if err != nil { return nil, err } @@ -189,6 +297,7 @@ type Relationship struct { Following bool `json:"following"` FollowedBy bool `json:"followed_by"` Blocking bool `json:"blocking"` + BlockedBy bool `json:"blocked_by"` Muting bool `json:"muting"` MutingNotifications bool `json:"muting_notifications"` Subscribing bool `json:"subscribing"` @@ -243,11 +352,10 @@ func (c *Client) AccountUnblock(ctx context.Context, id string) (*Relationship, } // AccountMute mute the account. -func (c *Client) AccountMute(ctx context.Context, id string, notifications *bool) (*Relationship, error) { +func (c *Client) AccountMute(ctx context.Context, id string, notifications bool, duration int) (*Relationship, error) { params := url.Values{} - if notifications != nil { - params.Set("notifications", strconv.FormatBool(*notifications)) - } + params.Set("notifications", strconv.FormatBool(notifications)) + params.Set("duration", strconv.Itoa(duration)) var relationship Relationship err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/mute", url.PathEscape(string(id))), params, &relationship, nil) if err != nil { diff --git a/mastodon/apps.go b/mastodon/apps.go index 5d925c3..12d2e86 100644 --- a/mastodon/apps.go +++ b/mastodon/apps.go @@ -11,7 +11,6 @@ import ( // AppConfig is a setting for registering applications. type AppConfig struct { - http.Client Server string ClientName string @@ -62,7 +61,7 @@ func RegisterApp(ctx context.Context, appConfig *AppConfig) (*Application, error } req = req.WithContext(ctx) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - resp, err := appConfig.Do(req) + resp, err := httpClient.Do(req) if err != nil { return nil, err } diff --git a/mastodon/http.go b/mastodon/http.go new file mode 100644 index 0000000..7d1c1c4 --- /dev/null +++ b/mastodon/http.go @@ -0,0 +1,45 @@ +package mastodon + +import ( + "fmt" + "io" + "net/http" + "time" +) + +type lr struct { + io.ReadCloser + n int64 + r *http.Request +} + +func (r *lr) Read(p []byte) (n int, err error) { + if r.n <= 0 { + return 0, fmt.Errorf("%s \"%s\": response body too large", r.r.Method, r.r.URL) + } + if int64(len(p)) > r.n { + p = p[0:r.n] + } + n, err = r.ReadCloser.Read(p) + r.n -= int64(n) + return +} + +type transport struct { + t http.RoundTripper +} + +func (t *transport) RoundTrip(r *http.Request) (*http.Response, error) { + resp, err := t.t.RoundTrip(r) + if resp != nil && resp.Body != nil { + resp.Body = &lr{resp.Body, 8 << 20, r} + } + return resp, err +} + +var httpClient = &http.Client{ + Transport: &transport{ + t: http.DefaultTransport, + }, + Timeout: 30 * time.Second, +} diff --git a/mastodon/mastodon.go b/mastodon/mastodon.go index 8678314..ca2089c 100644 --- a/mastodon/mastodon.go +++ b/mastodon/mastodon.go @@ -2,18 +2,14 @@ package mastodon import ( - "bytes" "context" "encoding/json" "errors" "fmt" "io" - "mime/multipart" "net/http" "net/url" - "os" "path" - "path/filepath" "strings" "github.com/tomnomnom/linkheader" @@ -33,6 +29,11 @@ type Client struct { config *Config } +type multipartRequest struct { + Data io.Reader + ContentType string +} + func (c *Client) doAPI(ctx context.Context, method string, uri string, params interface{}, res interface{}, pg *Pagination) error { u, err := url.Parse(c.config.Server) if err != nil { @@ -56,83 +57,12 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in if err != nil { return err } - } else if file, ok := params.(string); ok { - f, err := os.Open(file) - if err != nil { - return err - } - defer f.Close() - - var buf bytes.Buffer - mw := multipart.NewWriter(&buf) - part, err := mw.CreateFormFile("file", filepath.Base(file)) - 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 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) - fname := filepath.Base(file.Filename) - err = mw.WriteField("description", fname) - if err != nil { - return err - } - part, err := mw.CreateFormFile("file", fname) - 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) + } else if mr, ok := params.(*multipartRequest); ok { + req, err = http.NewRequest(method, u.String(), mr.Data) if err != nil { return err } - ct = mw.FormDataContentType() - } else if reader, ok := params.(io.Reader); ok { - var buf bytes.Buffer - mw := multipart.NewWriter(&buf) - part, err := mw.CreateFormFile("file", "upload") - if err != nil { - return err - } - _, err = io.Copy(part, reader) - 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() + ct = mr.ContentType } else { if method == http.MethodGet && pg != nil { u.RawQuery = pg.toValues().Encode() @@ -168,12 +98,13 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in } } return json.NewDecoder(resp.Body).Decode(&res) + } // NewClient return new mastodon API client. func NewClient(config *Config) *Client { return &Client{ - Client: http.DefaultClient, + Client: httpClient, config: config, } } @@ -207,6 +138,16 @@ func (c *Client) AuthenticateToken(ctx context.Context, authCode, redirectURI st return c.authenticate(ctx, params) } +func (c *Client) RevokeToken(ctx context.Context) error { + params := url.Values{ + "client_id": {c.config.ClientID}, + "client_secret": {c.config.ClientSecret}, + "token": {c.GetAccessToken(ctx)}, + } + + return c.doAPI(ctx, http.MethodPost, "/oauth/revoke", params, nil, nil) +} + func (c *Client) authenticate(ctx context.Context, params url.Values) error { u, err := url.Parse(c.config.Server) if err != nil { @@ -257,6 +198,7 @@ type Toot struct { SpoilerText string `json:"spoiler_text"` Visibility string `json:"visibility"` ContentType string `json:"content_type"` + QuoteID string `json:"quote_id"` } // Mention hold information for mention. diff --git a/mastodon/status.go b/mastodon/status.go index 2fae6ee..34f8727 100644 --- a/mastodon/status.go +++ b/mastodon/status.go @@ -1,17 +1,22 @@ package mastodon import ( + "bytes" "context" "fmt" "io" "mime/multipart" "net/http" "net/url" + "path/filepath" "time" ) type StatusPleroma struct { - InReplyToAccountAcct string `json:"in_reply_to_account_acct"` + InReplyToAccountAcct string `json:"in_reply_to_account_acct"` + Quote *Status `json:"quote"` + QuoteID string `json:"quote_id"` + QuoteVisible bool `json:"quote_visible"` } type ReplyInfo struct { @@ -56,7 +61,6 @@ type Status struct { MediaAttachments []Attachment `json:"media_attachments"` Mentions []Mention `json:"mentions"` Tags []Tag `json:"tags"` - Card *Card `json:"card"` Application Application `json:"application"` Language string `json:"language"` Pinned interface{} `json:"pinned"` @@ -77,22 +81,6 @@ type Context struct { Descendants []*Status `json:"descendants"` } -// Card hold information for mastodon card. -type Card struct { - URL string `json:"url"` - Title string `json:"title"` - Description string `json:"description"` - Image string `json:"image"` - Type string `json:"type"` - AuthorName string `json:"author_name"` - AuthorURL string `json:"author_url"` - ProviderName string `json:"provider_name"` - ProviderURL string `json:"provider_url"` - HTML string `json:"html"` - Width int64 `json:"width"` - Height int64 `json:"height"` -} - // GetFavourites return the favorite list of the current user. func (c *Client) GetFavourites(ctx context.Context, pg *Pagination) ([]*Status, error) { var statuses []*Status @@ -123,16 +111,6 @@ func (c *Client) GetStatusContext(ctx context.Context, id string) (*Context, err return &context, nil } -// GetStatusCard return status specified by id. -func (c *Client) GetStatusCard(ctx context.Context, id string) (*Card, error) { - var card Card - err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/card", id), nil, &card, nil) - if err != nil { - return nil, err - } - return &card, nil -} - // GetRebloggedBy returns the account list of the user who reblogged the toot of id. func (c *Client) GetRebloggedBy(ctx context.Context, id string, pg *Pagination) ([]*Account, error) { var accounts []*Account @@ -286,6 +264,9 @@ func (c *Client) PostStatus(ctx context.Context, toot *Toot) (*Status, error) { if toot.ContentType != "" { params.Set("content_type", toot.ContentType) } + if toot.QuoteID != "" { + params.Set("quote_id", toot.QuoteID) + } var status Status err := c.doAPI(ctx, http.MethodPost, "/api/v1/statuses", params, &status, nil) @@ -320,30 +301,35 @@ func (c *Client) Search(ctx context.Context, q string, qType string, limit int, return &results, nil } -// UploadMedia upload a media attachment from a file. -func (c *Client) UploadMedia(ctx context.Context, file string) (*Attachment, error) { - var attachment Attachment - err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", file, &attachment, nil) +func (c *Client) UploadMediaFromMultipartFileHeader(ctx context.Context, fh *multipart.FileHeader) (*Attachment, error) { + f, err := fh.Open() if err != nil { return nil, err } - return &attachment, nil -} + defer f.Close() -// UploadMediaFromReader uploads a media attachment from a io.Reader. -func (c *Client) UploadMediaFromReader(ctx context.Context, reader io.Reader) (*Attachment, error) { - var attachment Attachment - err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", reader, &attachment, nil) + var buf bytes.Buffer + mw := multipart.NewWriter(&buf) + fname := filepath.Base(fh.Filename) + err = mw.WriteField("description", fname) if err != nil { return nil, err } - return &attachment, nil -} - -// UploadMediaFromReader uploads a media attachment from a io.Reader. -func (c *Client) UploadMediaFromMultipartFileHeader(ctx context.Context, fh *multipart.FileHeader) (*Attachment, error) { + part, err := mw.CreateFormFile("file", fname) + if err != nil { + return nil, err + } + _, err = io.Copy(part, f) + if err != nil { + return nil, err + } + err = mw.Close() + if err != nil { + return nil, err + } + params := &multipartRequest{Data: &buf, ContentType: mw.FormDataContentType()} var attachment Attachment - err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", fh, &attachment, nil) + err = c.doAPI(ctx, http.MethodPost, "/api/v1/media", params, &attachment, nil) if err != nil { return nil, err } |