aboutsummaryrefslogtreecommitdiff
path: root/mastodon
diff options
context:
space:
mode:
Diffstat (limited to 'mastodon')
-rw-r--r--mastodon/accounts.go176
-rw-r--r--mastodon/apps.go3
-rw-r--r--mastodon/http.go45
-rw-r--r--mastodon/mastodon.go100
-rw-r--r--mastodon/status.go74
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
}