mastodon.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. // Package mastodon provides functions and structs for accessing the mastodon API.
  2. package mastodon
  3. import (
  4. "bytes"
  5. "context"
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "io"
  10. "mime/multipart"
  11. "net/http"
  12. "net/url"
  13. "os"
  14. "path"
  15. "path/filepath"
  16. "strings"
  17. "github.com/tomnomnom/linkheader"
  18. )
  19. // Config is a setting for access mastodon APIs.
  20. type Config struct {
  21. Server string
  22. ClientID string
  23. ClientSecret string
  24. AccessToken string
  25. }
  26. // Client is a API client for mastodon.
  27. type Client struct {
  28. *http.Client
  29. config *Config
  30. }
  31. func (c *Client) doAPI(ctx context.Context, method string, uri string, params interface{}, res interface{}, pg *Pagination) error {
  32. u, err := url.Parse(c.config.Server)
  33. if err != nil {
  34. return err
  35. }
  36. u.Path = path.Join(u.Path, uri)
  37. var req *http.Request
  38. ct := "application/x-www-form-urlencoded"
  39. if values, ok := params.(url.Values); ok {
  40. var body io.Reader
  41. if method == http.MethodGet {
  42. if pg != nil {
  43. values = pg.setValues(values)
  44. }
  45. u.RawQuery = values.Encode()
  46. } else {
  47. body = strings.NewReader(values.Encode())
  48. }
  49. req, err = http.NewRequest(method, u.String(), body)
  50. if err != nil {
  51. return err
  52. }
  53. } else if file, ok := params.(string); ok {
  54. f, err := os.Open(file)
  55. if err != nil {
  56. return err
  57. }
  58. defer f.Close()
  59. var buf bytes.Buffer
  60. mw := multipart.NewWriter(&buf)
  61. part, err := mw.CreateFormFile("file", filepath.Base(file))
  62. if err != nil {
  63. return err
  64. }
  65. _, err = io.Copy(part, f)
  66. if err != nil {
  67. return err
  68. }
  69. err = mw.Close()
  70. if err != nil {
  71. return err
  72. }
  73. req, err = http.NewRequest(method, u.String(), &buf)
  74. if err != nil {
  75. return err
  76. }
  77. ct = mw.FormDataContentType()
  78. } else if file, ok := params.(*multipart.FileHeader); ok {
  79. f, err := file.Open()
  80. if err != nil {
  81. return err
  82. }
  83. defer f.Close()
  84. var buf bytes.Buffer
  85. mw := multipart.NewWriter(&buf)
  86. part, err := mw.CreateFormFile("file", filepath.Base(file.Filename))
  87. if err != nil {
  88. return err
  89. }
  90. _, err = io.Copy(part, f)
  91. if err != nil {
  92. return err
  93. }
  94. err = mw.Close()
  95. if err != nil {
  96. return err
  97. }
  98. req, err = http.NewRequest(method, u.String(), &buf)
  99. if err != nil {
  100. return err
  101. }
  102. ct = mw.FormDataContentType()
  103. } else if reader, ok := params.(io.Reader); ok {
  104. var buf bytes.Buffer
  105. mw := multipart.NewWriter(&buf)
  106. part, err := mw.CreateFormFile("file", "upload")
  107. if err != nil {
  108. return err
  109. }
  110. _, err = io.Copy(part, reader)
  111. if err != nil {
  112. return err
  113. }
  114. err = mw.Close()
  115. if err != nil {
  116. return err
  117. }
  118. req, err = http.NewRequest(method, u.String(), &buf)
  119. if err != nil {
  120. return err
  121. }
  122. ct = mw.FormDataContentType()
  123. } else {
  124. if method == http.MethodGet && pg != nil {
  125. u.RawQuery = pg.toValues().Encode()
  126. }
  127. req, err = http.NewRequest(method, u.String(), nil)
  128. if err != nil {
  129. return err
  130. }
  131. }
  132. req = req.WithContext(ctx)
  133. req.Header.Set("Authorization", "Bearer "+c.config.AccessToken)
  134. if params != nil {
  135. req.Header.Set("Content-Type", ct)
  136. }
  137. resp, err := c.Do(req)
  138. if err != nil {
  139. return err
  140. }
  141. defer resp.Body.Close()
  142. if resp.StatusCode != http.StatusOK {
  143. return parseAPIError("bad request", resp)
  144. } else if res == nil {
  145. return nil
  146. } else if pg != nil {
  147. if lh := resp.Header.Get("Link"); lh != "" {
  148. pg2, err := newPagination(lh)
  149. if err != nil {
  150. return err
  151. }
  152. *pg = *pg2
  153. }
  154. }
  155. return json.NewDecoder(resp.Body).Decode(&res)
  156. }
  157. // NewClient return new mastodon API client.
  158. func NewClient(config *Config) *Client {
  159. return &Client{
  160. Client: http.DefaultClient,
  161. config: config,
  162. }
  163. }
  164. // Authenticate get access-token to the API.
  165. func (c *Client) Authenticate(ctx context.Context, username, password string) error {
  166. params := url.Values{
  167. "client_id": {c.config.ClientID},
  168. "client_secret": {c.config.ClientSecret},
  169. "grant_type": {"password"},
  170. "username": {username},
  171. "password": {password},
  172. "scope": {"read write follow"},
  173. }
  174. return c.authenticate(ctx, params)
  175. }
  176. // AuthenticateToken logs in using a grant token returned by Application.AuthURI.
  177. //
  178. // redirectURI should be the same as Application.RedirectURI.
  179. func (c *Client) AuthenticateToken(ctx context.Context, authCode, redirectURI string) error {
  180. params := url.Values{
  181. "client_id": {c.config.ClientID},
  182. "client_secret": {c.config.ClientSecret},
  183. "grant_type": {"authorization_code"},
  184. "code": {authCode},
  185. "redirect_uri": {redirectURI},
  186. }
  187. return c.authenticate(ctx, params)
  188. }
  189. func (c *Client) authenticate(ctx context.Context, params url.Values) error {
  190. u, err := url.Parse(c.config.Server)
  191. if err != nil {
  192. return err
  193. }
  194. u.Path = path.Join(u.Path, "/oauth/token")
  195. req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(params.Encode()))
  196. if err != nil {
  197. return err
  198. }
  199. req = req.WithContext(ctx)
  200. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  201. resp, err := c.Do(req)
  202. if err != nil {
  203. return err
  204. }
  205. defer resp.Body.Close()
  206. if resp.StatusCode != http.StatusOK {
  207. return parseAPIError("bad authorization", resp)
  208. }
  209. var res struct {
  210. AccessToken string `json:"access_token"`
  211. }
  212. err = json.NewDecoder(resp.Body).Decode(&res)
  213. if err != nil {
  214. return err
  215. }
  216. c.config.AccessToken = res.AccessToken
  217. return nil
  218. }
  219. func (c *Client) GetAccessToken(ctx context.Context) string {
  220. if c == nil || c.config == nil {
  221. return ""
  222. }
  223. return c.config.AccessToken
  224. }
  225. // Toot is struct to post status.
  226. type Toot struct {
  227. Status string `json:"status"`
  228. InReplyToID string `json:"in_reply_to_id"`
  229. MediaIDs []string `json:"media_ids"`
  230. Sensitive bool `json:"sensitive"`
  231. SpoilerText string `json:"spoiler_text"`
  232. Visibility string `json:"visibility"`
  233. ContentType string `json:"content_type"`
  234. }
  235. // Mention hold information for mention.
  236. type Mention struct {
  237. URL string `json:"url"`
  238. Username string `json:"username"`
  239. Acct string `json:"acct"`
  240. ID string `json:"id"`
  241. }
  242. // Tag hold information for tag.
  243. type Tag struct {
  244. Name string `json:"name"`
  245. URL string `json:"url"`
  246. History []History `json:"history"`
  247. }
  248. // History hold information for history.
  249. type History struct {
  250. Day string `json:"day"`
  251. Uses int64 `json:"uses"`
  252. Accounts int64 `json:"accounts"`
  253. }
  254. // Attachment hold information for attachment.
  255. type Attachment struct {
  256. ID string `json:"id"`
  257. Type string `json:"type"`
  258. URL string `json:"url"`
  259. RemoteURL string `json:"remote_url"`
  260. PreviewURL string `json:"preview_url"`
  261. TextURL string `json:"text_url"`
  262. Description string `json:"description"`
  263. Meta AttachmentMeta `json:"meta"`
  264. }
  265. // AttachmentMeta holds information for attachment metadata.
  266. type AttachmentMeta struct {
  267. Original AttachmentSize `json:"original"`
  268. Small AttachmentSize `json:"small"`
  269. }
  270. // AttachmentSize holds information for attatchment size.
  271. type AttachmentSize struct {
  272. Width int64 `json:"width"`
  273. Height int64 `json:"height"`
  274. Size string `json:"size"`
  275. Aspect float64 `json:"aspect"`
  276. }
  277. // Emoji hold information for CustomEmoji.
  278. type Emoji struct {
  279. ShortCode string `json:"shortcode"`
  280. StaticURL string `json:"static_url"`
  281. URL string `json:"url"`
  282. VisibleInPicker bool `json:"visible_in_picker"`
  283. }
  284. // Results hold information for search result.
  285. type Results struct {
  286. Accounts []*Account `json:"accounts"`
  287. Statuses []*Status `json:"statuses"`
  288. // Hashtags []string `json:"hashtags"`
  289. }
  290. // Pagination is a struct for specifying the get range.
  291. type Pagination struct {
  292. MaxID string
  293. SinceID string
  294. MinID string
  295. Limit int64
  296. }
  297. func newPagination(rawlink string) (*Pagination, error) {
  298. if rawlink == "" {
  299. return nil, errors.New("empty link header")
  300. }
  301. p := &Pagination{}
  302. for _, link := range linkheader.Parse(rawlink) {
  303. switch link.Rel {
  304. case "next":
  305. maxID, err := getPaginationID(link.URL, "max_id")
  306. if err != nil {
  307. return nil, err
  308. }
  309. p.MaxID = maxID
  310. case "prev":
  311. sinceID, err := getPaginationID(link.URL, "since_id")
  312. if err != nil {
  313. return nil, err
  314. }
  315. p.SinceID = sinceID
  316. minID, err := getPaginationID(link.URL, "min_id")
  317. if err != nil {
  318. return nil, err
  319. }
  320. p.MinID = minID
  321. }
  322. }
  323. return p, nil
  324. }
  325. func getPaginationID(rawurl, key string) (string, error) {
  326. u, err := url.Parse(rawurl)
  327. if err != nil {
  328. return "", err
  329. }
  330. val := u.Query().Get(key)
  331. if val == "" {
  332. return "", nil
  333. }
  334. return string(val), nil
  335. }
  336. func (p *Pagination) toValues() url.Values {
  337. return p.setValues(url.Values{})
  338. }
  339. func (p *Pagination) setValues(params url.Values) url.Values {
  340. if p.MaxID != "" {
  341. params.Set("max_id", string(p.MaxID))
  342. }
  343. if p.SinceID != "" {
  344. params.Set("since_id", string(p.SinceID))
  345. }
  346. if p.MinID != "" {
  347. params.Set("min_id", string(p.MinID))
  348. }
  349. if p.Limit > 0 {
  350. params.Set("limit", fmt.Sprint(p.Limit))
  351. }
  352. return params
  353. }