mastodon.go 8.6 KB

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