status.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. package mastodon
  2. import (
  3. "bytes"
  4. "context"
  5. "fmt"
  6. "io"
  7. "mime/multipart"
  8. "net/http"
  9. "net/url"
  10. "path/filepath"
  11. "time"
  12. "encoding/json"
  13. "strconv"
  14. "strings"
  15. )
  16. type StatusPleroma struct {
  17. InReplyToAccountAcct string `json:"in_reply_to_account_acct"`
  18. Reactions []*ReactionsPleroma `json:"emoji_reactions"`
  19. Quote *Status `json:"quote"` // Quoted statuses
  20. }
  21. type ReactionsPleroma struct {
  22. Accounts []Account `json:"accounts"`
  23. Count int `json:"count"`
  24. Me bool `json:"me"`
  25. Name string `json:"name"`
  26. // For support akkoma reactions :)
  27. Url *string `json:"url"`
  28. }
  29. type ReplyInfo struct {
  30. ID string `json:"id"`
  31. Number int `json:"number"`
  32. }
  33. type CreatedAt struct {
  34. time.Time
  35. }
  36. type EditedAt struct{
  37. time.Time
  38. }
  39. func (t *CreatedAt) UnmarshalJSON(d []byte) error {
  40. // Special case to handle retweets from GNU Social
  41. // which returns empty string ("") in created_at
  42. if len(d) == 2 && string(d) == `""` {
  43. return nil
  44. }
  45. return t.Time.UnmarshalJSON(d)
  46. }
  47. // Status is struct to hold status.
  48. type Status struct {
  49. ID string `json:"id"`
  50. URI string `json:"uri"`
  51. URL string `json:"url"`
  52. Account Account `json:"account"`
  53. InReplyToID interface{} `json:"in_reply_to_id"`
  54. InReplyToAccountID interface{} `json:"in_reply_to_account_id"`
  55. Reblog *Status `json:"reblog"`
  56. Content string `json:"content"`
  57. CreatedAt CreatedAt `json:"created_at"`
  58. EditedAt *EditedAt `json:"edited_at"`
  59. Emojis []Emoji `json:"emojis"`
  60. RepliesCount int64 `json:"replies_count"`
  61. ReblogsCount int64 `json:"reblogs_count"`
  62. FavouritesCount int64 `json:"favourites_count"`
  63. Reblogged interface{} `json:"reblogged"`
  64. Favourited interface{} `json:"favourited"`
  65. Muted interface{} `json:"muted"`
  66. Sensitive bool `json:"sensitive"`
  67. SpoilerText string `json:"spoiler_text"`
  68. Visibility string `json:"visibility"`
  69. MediaAttachments []Attachment `json:"media_attachments"`
  70. Mentions []Mention `json:"mentions"`
  71. Tags []Tag `json:"tags"`
  72. Application Application `json:"application"`
  73. Language string `json:"language"`
  74. Pinned interface{} `json:"pinned"`
  75. Bookmarked bool `json:"bookmarked"`
  76. Poll *Poll `json:"poll"`
  77. // Custom fields
  78. Pleroma StatusPleroma `json:"pleroma"`
  79. ShowReplies bool `json:"show_replies"`
  80. IDReplies map[string][]ReplyInfo `json:"id_replies"`
  81. IDNumbers map[string]int `json:"id_numbers"`
  82. RetweetedByID string `json:"retweeted_by_id"`
  83. }
  84. // Context hold information for mastodon context.
  85. type Context struct {
  86. Ancestors []*Status `json:"ancestors"`
  87. Descendants []*Status `json:"descendants"`
  88. }
  89. // GetFavourites return the favorite list of the current user.
  90. func (c *Client) GetFavourites(ctx context.Context, pg *Pagination) ([]*Status, error) {
  91. var statuses []*Status
  92. err := c.doAPI(ctx, http.MethodGet, "/api/v1/favourites", nil, &statuses, pg)
  93. if err != nil {
  94. return nil, err
  95. }
  96. return statuses, nil
  97. }
  98. // GetStatus return status specified by id.
  99. func (c *Client) GetStatus(ctx context.Context, id string) (*Status, error) {
  100. var status Status
  101. err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s", id), nil, &status, nil)
  102. if err != nil {
  103. return nil, err
  104. }
  105. return &status, nil
  106. }
  107. // GetStatusContext return status specified by id.
  108. func (c *Client) GetStatusContext(ctx context.Context, id string) (*Context, error) {
  109. var context Context
  110. err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/context", id), nil, &context, nil)
  111. if err != nil {
  112. return nil, err
  113. }
  114. return &context, nil
  115. }
  116. // GetRebloggedBy returns the account list of the user who reblogged the toot of id.
  117. func (c *Client) GetRebloggedBy(ctx context.Context, id string, pg *Pagination) ([]*Account, error) {
  118. var accounts []*Account
  119. err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/reblogged_by", id), nil, &accounts, pg)
  120. if err != nil {
  121. return nil, err
  122. }
  123. return accounts, nil
  124. }
  125. // GetFavouritedBy returns the account list of the user who liked the toot of id.
  126. func (c *Client) GetFavouritedBy(ctx context.Context, id string, pg *Pagination) ([]*Account, error) {
  127. var accounts []*Account
  128. err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/favourited_by", id), nil, &accounts, pg)
  129. if err != nil {
  130. return nil, err
  131. }
  132. return accounts, nil
  133. }
  134. // GetReactionBy returns the reactions list of the user who reacted the toot of id. (Pleroma)
  135. func (c *Client) GetReactedBy(ctx context.Context, id string) ([]*ReactionsPleroma, error) {
  136. var reactions []*ReactionsPleroma
  137. err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/pleroma/statuses/%s/reactions", id), nil, &reactions, nil)
  138. if err != nil {
  139. return nil, err
  140. }
  141. return reactions, nil
  142. }
  143. // PutReaction is reaction on status with unicode emoji (Pleroma)
  144. func (c *Client) PutReaction(ctx context.Context, id string, emoji string) (*Status, error) {
  145. var status Status
  146. err := c.doAPI(ctx, http.MethodPut, fmt.Sprintf("/api/v1/pleroma/statuses/%s/reactions/%s", id, emoji), nil, &status, nil)
  147. if err != nil {
  148. return nil, err
  149. }
  150. return &status, nil
  151. }
  152. // UnReaction is unreaction on status with unicode emoji (Pleroma)
  153. func (c *Client) UnReaction(ctx context.Context, id string, emoji string) (*Status, error) {
  154. var status Status
  155. err := c.doAPI(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/pleroma/statuses/%s/reactions/%s", id, emoji), nil, &status, nil)
  156. if err != nil {
  157. return nil, err
  158. }
  159. return &status, nil
  160. }
  161. // Reblog is reblog the toot of id and return status of reblog.
  162. func (c *Client) Reblog(ctx context.Context, id string, visibility string) (*Status, error) {
  163. var status Status
  164. params := url.Values{}
  165. params.Set("visibility", visibility)
  166. err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/reblog", id), params, &status, nil)
  167. if err != nil {
  168. return nil, err
  169. }
  170. return &status, nil
  171. }
  172. // Unreblog is unreblog the toot of id and return status of the original toot.
  173. func (c *Client) Unreblog(ctx context.Context, id string) (*Status, error) {
  174. var status Status
  175. err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/unreblog", id), nil, &status, nil)
  176. if err != nil {
  177. return nil, err
  178. }
  179. return &status, nil
  180. }
  181. // Favourite is favourite the toot of id and return status of the favourite toot.
  182. func (c *Client) Favourite(ctx context.Context, id string) (*Status, error) {
  183. var status Status
  184. err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/favourite", id), nil, &status, nil)
  185. if err != nil {
  186. return nil, err
  187. }
  188. return &status, nil
  189. }
  190. // Unfavourite is unfavourite the toot of id and return status of the unfavourite toot.
  191. func (c *Client) Unfavourite(ctx context.Context, id string) (*Status, error) {
  192. var status Status
  193. err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/unfavourite", id), nil, &status, nil)
  194. if err != nil {
  195. return nil, err
  196. }
  197. return &status, nil
  198. }
  199. // GetTimelineHome return statuses from home timeline.
  200. func (c *Client) GetTimelineHome(ctx context.Context, pg *Pagination) ([]*Status, error) {
  201. var statuses []*Status
  202. err := c.doAPI(ctx, http.MethodGet, "/api/v1/timelines/home", nil, &statuses, pg)
  203. if err != nil {
  204. return nil, err
  205. }
  206. return statuses, nil
  207. }
  208. // TrueRemoteTimeline get public timeline from remote Mastodon API compatible instance directly
  209. func (c *Client) TrueRemoteTimeline(ctx context.Context, instance string, instance_type string, pg *Pagination) ([]*Status, error) {
  210. var publicstatuses []*Status
  211. var instanceParams []string
  212. instanceParams = strings.Split(instance, ":")[1:]
  213. instance = strings.TrimSpace(strings.Split(instance, ":")[0])
  214. params := url.Values{}
  215. params.Set("local", "true")
  216. if pg != nil {
  217. params = pg.setValues(params)
  218. }
  219. perform := url.URL{
  220. Scheme: "https",
  221. Host: instance,
  222. }
  223. var paramval []string
  224. withFiles := "false"
  225. withReplies := "false"
  226. globalTimeline := false
  227. for _, instanceParam := range instanceParams {
  228. switch instanceParam {
  229. case "withFiles":
  230. withFiles = "true"
  231. params.Set("only_media", "true")
  232. case "withReplies":
  233. withReplies = "true"
  234. case "remote":
  235. globalTimeline = true
  236. params.Set(instanceParam, "true")
  237. default:
  238. paramval = strings.Split(instanceParam, "=")
  239. if len(paramval) == 2 {
  240. params.Set(paramval[0], paramval[1])
  241. } else {
  242. params.Set(instanceParam, "true")
  243. }
  244. }
  245. }
  246. var method string
  247. var ContentType string
  248. var bytesAttach []byte
  249. switch instance_type {
  250. case "misskey":
  251. if globalTimeline {
  252. perform.Path = "api/notes/global-timeline"
  253. } else {
  254. perform.Path = "api/notes/local-timeline"
  255. }
  256. perform.RawQuery = ""
  257. method = http.MethodPost
  258. ContentType = "application/json"
  259. bytesAttach = []byte(fmt.Sprintf(
  260. `{"limit":20,"withRenotes":false, "withReplies": %s, "withFiles": %s}`,
  261. withReplies, withFiles))
  262. if pg != nil {
  263. if pg.MaxID != "" {
  264. bytesAttach = []byte(fmt.Sprintf(
  265. `{"limit": %s,"withRenotes": false,"untilId":"%s", "withReplies": %s, "withFiles": %s}`,
  266. strconv.Itoa(int(pg.Limit)), pg.MaxID, withReplies, withFiles))
  267. }
  268. }
  269. default:
  270. if globalTimeline {
  271. params.Set("local", "false")
  272. }
  273. perform.RawQuery = params.Encode()
  274. perform.Path = "api/v1/timelines/public"
  275. method = http.MethodGet
  276. ContentType = "application/x-www-form-urlencoded"
  277. bytesAttach = []byte("")
  278. }
  279. req, err := http.NewRequest(method, perform.String(), bytes.NewBuffer(bytesAttach))
  280. req = req.WithContext(ctx)
  281. req.Header.Set("Content-Type", ContentType)
  282. req.Header.Set("User-Agent", "Bloat")
  283. client := http.Client{}
  284. resp, err := client.Do(req)
  285. if err != nil {
  286. return nil, err
  287. }
  288. defer resp.Body.Close()
  289. if resp.StatusCode != http.StatusOK {
  290. return nil, parseAPIError("Can't get remote timeline for " + instance + ", try select another type instance. Error" , resp)
  291. }
  292. switch instance_type {
  293. case "misskey":
  294. var misskeyData []MisskeyStatus
  295. err = json.NewDecoder(resp.Body).Decode(&misskeyData)
  296. if err != nil {
  297. return nil, err
  298. }
  299. for _, statusMisskey := range misskeyData {
  300. var status Status
  301. status.ID = statusMisskey.ID
  302. if statusMisskey.Renote != nil {
  303. // small handle for strange reblogs in misskey
  304. // handle as quoted post because akkoma/pleroma makes same
  305. var quote Status
  306. quote.ID = statusMisskey.Renote.ID
  307. quote.Content = strings.Replace(statusMisskey.Renote.Text, "\n", "<br>", -1)
  308. status.Pleroma.Quote = &quote
  309. }
  310. status.Account.ID = statusMisskey.User.ID
  311. status.Account.DisplayName = statusMisskey.User.Name
  312. if statusMisskey.User.Host != "" {
  313. status.Account.Acct = statusMisskey.User.Username + "@" + statusMisskey.User.Host
  314. } else {
  315. status.Account.Acct = statusMisskey.User.Username
  316. }
  317. status.Account.Username = statusMisskey.User.Username
  318. status.Account.Avatar = statusMisskey.User.AvatarURL
  319. status.CreatedAt = statusMisskey.CreatedAt
  320. status.Visibility = statusMisskey.Visibility
  321. status.Content = strings.Replace(statusMisskey.Text, "\n", "<br>", -1) + "<br><br>"
  322. for reaction, count := range statusMisskey.Reactions { // woozyface
  323. if reaction == "❤" {
  324. status.FavouritesCount = int64(count)
  325. continue
  326. }
  327. status.Content = status.Content + "[" + reaction + strconv.Itoa(count) + "]"
  328. }
  329. status.MediaAttachments = statusMisskey.Files
  330. for idx, attach := range statusMisskey.Files {
  331. status.MediaAttachments[idx].Type = strings.Split(attach.Type, "/")[0]
  332. status.MediaAttachments[idx].Description = strings.Replace(attach.Comment, "\n", "<br>", -1)
  333. status.MediaAttachments[idx].PreviewURL = attach.ThumbnailUrl
  334. status.MediaAttachments[idx].RemoteURL = attach.URL
  335. status.MediaAttachments[idx].TextURL = attach.URL
  336. if status.Sensitive == false {
  337. if attach.Sensitive { // mark status as NSFW if any attachment marked as NSFW
  338. status.Sensitive = true
  339. }
  340. }
  341. }
  342. if statusMisskey.CW != "" {
  343. status.Sensitive = true
  344. status.SpoilerText = statusMisskey.CW
  345. }
  346. status.RepliesCount = statusMisskey.RepliesCount
  347. status.ReblogsCount = statusMisskey.RenoteCount
  348. status.Account.Bot = statusMisskey.User.IsBot
  349. status.URL = "https://" + instance + "/notes/" + statusMisskey.ID
  350. publicstatuses = append(publicstatuses, &status)
  351. }
  352. case "friendica":
  353. err = json.NewDecoder(resp.Body).Decode(&publicstatuses)
  354. if err != nil {
  355. return nil, err
  356. }
  357. for _, status := range publicstatuses {
  358. status.URL = status.URI // Fix federate URL
  359. }
  360. default:
  361. err = json.NewDecoder(resp.Body).Decode(&publicstatuses)
  362. if err != nil {
  363. return nil, err
  364. }
  365. }
  366. return publicstatuses, nil
  367. }
  368. // GetTimelinePublic return statuses from public timeline.
  369. func (c *Client) GetTimelinePublic(ctx context.Context, isLocal bool, instance string, pg *Pagination) ([]*Status, error) {
  370. params := url.Values{}
  371. if len(instance) > 0 {
  372. params.Set("instance", instance)
  373. } else if isLocal {
  374. params.Set("local", "true")
  375. }
  376. var statuses []*Status
  377. err := c.doAPI(ctx, http.MethodGet, "/api/v1/timelines/public", params, &statuses, pg)
  378. if err != nil {
  379. return nil, err
  380. }
  381. return statuses, nil
  382. }
  383. // GetTimelineHashtag return statuses from tagged timeline.
  384. func (c *Client) GetTimelineHashtag(ctx context.Context, tag string, isLocal bool, pg *Pagination) ([]*Status, error) {
  385. params := url.Values{}
  386. if isLocal {
  387. params.Set("local", "t")
  388. }
  389. var statuses []*Status
  390. err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/timelines/tag/%s", url.PathEscape(tag)), params, &statuses, pg)
  391. if err != nil {
  392. return nil, err
  393. }
  394. return statuses, nil
  395. }
  396. // GetTimelineList return statuses from a list timeline.
  397. func (c *Client) GetTimelineList(ctx context.Context, id string, pg *Pagination) ([]*Status, error) {
  398. var statuses []*Status
  399. err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/timelines/list/%s", url.PathEscape(string(id))), nil, &statuses, pg)
  400. if err != nil {
  401. return nil, err
  402. }
  403. return statuses, nil
  404. }
  405. // GetTimelineMedia return statuses from media timeline.
  406. // NOTE: This is an experimental feature of pawoo.net.
  407. func (c *Client) GetTimelineMedia(ctx context.Context, isLocal bool, pg *Pagination) ([]*Status, error) {
  408. params := url.Values{}
  409. params.Set("media", "t")
  410. if isLocal {
  411. params.Set("local", "t")
  412. }
  413. var statuses []*Status
  414. err := c.doAPI(ctx, http.MethodGet, "/api/v1/timelines/public", params, &statuses, pg)
  415. if err != nil {
  416. return nil, err
  417. }
  418. return statuses, nil
  419. }
  420. // PostStatus post the toot.
  421. func (c *Client) PostStatus(ctx context.Context, toot *Toot) (*Status, error) {
  422. params := url.Values{}
  423. params.Set("status", toot.Status)
  424. if toot.InReplyToID != "" {
  425. params.Set("in_reply_to_id", string(toot.InReplyToID))
  426. }
  427. if toot.MediaIDs != nil {
  428. for _, media := range toot.MediaIDs {
  429. params.Add("media_ids[]", string(media))
  430. }
  431. }
  432. if toot.Visibility != "" {
  433. params.Set("visibility", fmt.Sprint(toot.Visibility))
  434. }
  435. if toot.Sensitive {
  436. params.Set("sensitive", "true")
  437. }
  438. if toot.SpoilerText != "" {
  439. params.Set("spoiler_text", toot.SpoilerText)
  440. }
  441. if toot.ContentType != "" {
  442. params.Set("content_type", toot.ContentType)
  443. }
  444. if toot.Language != "" {
  445. params.Set("language", toot.Language)
  446. }
  447. if toot.ExpiresIn >= 3600 {
  448. params.Set("expires_in", fmt.Sprint(toot.ExpiresIn))
  449. }
  450. if toot.ScheduledAt != "" {
  451. params.Set("scheduled_at", toot.ScheduledAt)
  452. }
  453. if len(toot.Poll.Options) > 2 {
  454. for _, option := range toot.Poll.Options {
  455. params.Add("poll[options][]", string(option))
  456. }
  457. params.Set("poll[expires_in]", strconv.Itoa(toot.Poll.ExpiresIn))
  458. if toot.Poll.Multiple {
  459. params.Set("poll[multiple]", "true")
  460. }
  461. if toot.Poll.HideTotals {
  462. params.Set("poll[hide_totals]", "true")
  463. }
  464. }
  465. var status Status
  466. if toot.Edit != "" {
  467. err := c.doAPI(ctx, http.MethodPut, fmt.Sprintf("/api/v1/statuses/%s", toot.Edit), params, &status, nil)
  468. if err != nil {
  469. return nil, err
  470. }
  471. } else {
  472. err := c.doAPI(ctx, http.MethodPost, "/api/v1/statuses", params, &status, nil)
  473. if err != nil {
  474. return nil, err
  475. }
  476. }
  477. return &status, nil
  478. }
  479. // Pin pin your status.
  480. func (c *Client) Pin(ctx context.Context, id string) error {
  481. return c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/pin", id), nil, nil, nil)
  482. }
  483. // UnPin unpin your status.
  484. func (c *Client) UnPin(ctx context.Context, id string) error {
  485. return c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/unpin", id), nil, nil, nil)
  486. }
  487. // DeleteStatus delete the toot.
  488. func (c *Client) DeleteStatus(ctx context.Context, id string) error {
  489. return c.doAPI(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/statuses/%s", id), nil, nil, nil)
  490. }
  491. // Search search content with query.
  492. func (c *Client) Search(ctx context.Context, q string, qType string, limit int, resolve bool, offset int, accountID string, following bool) (*Results, error) {
  493. var results Results
  494. params := url.Values{}
  495. params.Set("q", q)
  496. params.Set("type", qType)
  497. params.Set("limit", fmt.Sprint(limit))
  498. params.Set("resolve", fmt.Sprint(resolve))
  499. params.Set("offset", fmt.Sprint(offset))
  500. params.Set("following", fmt.Sprint(following))
  501. if len(accountID) > 0 {
  502. params.Set("account_id", accountID)
  503. }
  504. err := c.doAPI(ctx, http.MethodGet, "/api/v2/search", params, &results, nil)
  505. if err != nil {
  506. return nil, err
  507. }
  508. return &results, nil
  509. }
  510. func (c *Client) UploadMediaFromMultipartFileHeader(ctx context.Context, fh *multipart.FileHeader, descr string) (*Attachment, error) {
  511. f, err := fh.Open()
  512. if err != nil {
  513. return nil, err
  514. }
  515. defer f.Close()
  516. var buf bytes.Buffer
  517. mw := multipart.NewWriter(&buf)
  518. fname := filepath.Base(fh.Filename)
  519. err = mw.WriteField("description", descr)
  520. if err != nil {
  521. return nil, err
  522. }
  523. part, err := mw.CreateFormFile("file", fname)
  524. if err != nil {
  525. return nil, err
  526. }
  527. _, err = io.Copy(part, f)
  528. if err != nil {
  529. return nil, err
  530. }
  531. err = mw.Close()
  532. if err != nil {
  533. return nil, err
  534. }
  535. params := &multipartRequest{Data: &buf, ContentType: mw.FormDataContentType()}
  536. var attachment Attachment
  537. err = c.doAPI(ctx, http.MethodPost, "/api/v1/media", params, &attachment, nil)
  538. if err != nil {
  539. return nil, err
  540. }
  541. return &attachment, nil
  542. }
  543. // GetTimelineDirect return statuses from direct timeline.
  544. func (c *Client) GetTimelineDirect(ctx context.Context, pg *Pagination) ([]*Status, error) {
  545. params := url.Values{}
  546. var statuses []*Status
  547. err := c.doAPI(ctx, http.MethodGet, "/api/v1/timelines/direct", params, &statuses, pg)
  548. if err != nil {
  549. return nil, err
  550. }
  551. return statuses, nil
  552. }
  553. // MuteConversation mutes status specified by id.
  554. func (c *Client) MuteConversation(ctx context.Context, id string) (*Status, error) {
  555. var status Status
  556. err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/mute", id), nil, &status, nil)
  557. if err != nil {
  558. return nil, err
  559. }
  560. return &status, nil
  561. }
  562. // UnmuteConversation unmutes status specified by id.
  563. func (c *Client) UnmuteConversation(ctx context.Context, id string) (*Status, error) {
  564. var status Status
  565. err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/unmute", id), nil, &status, nil)
  566. if err != nil {
  567. return nil, err
  568. }
  569. return &status, nil
  570. }
  571. // Bookmark bookmarks status specified by id.
  572. func (c *Client) Bookmark(ctx context.Context, id string) (*Status, error) {
  573. var status Status
  574. err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/bookmark", id), nil, &status, nil)
  575. if err != nil {
  576. return nil, err
  577. }
  578. return &status, nil
  579. }
  580. // Unbookmark bookmarks status specified by id.
  581. func (c *Client) Unbookmark(ctx context.Context, id string) (*Status, error) {
  582. var status Status
  583. err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/unbookmark", id), nil, &status, nil)
  584. if err != nil {
  585. return nil, err
  586. }
  587. return &status, nil
  588. }