123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643 |
- package mastodon
- import (
- "bytes"
- "context"
- "fmt"
- "io"
- "mime/multipart"
- "net/http"
- "net/url"
- "path/filepath"
- "time"
- "encoding/json"
- "strconv"
- "strings"
- )
- type StatusPleroma struct {
- InReplyToAccountAcct string `json:"in_reply_to_account_acct"`
- Reactions []*ReactionsPleroma `json:"emoji_reactions"`
- Quote *Status `json:"quote"` // Quoted statuses
- }
- type ReactionsPleroma struct {
- Accounts []Account `json:"accounts"`
- Count int `json:"count"`
- Me bool `json:"me"`
- Name string `json:"name"`
-
- // For support akkoma reactions :)
- Url *string `json:"url"`
- }
- type ReplyInfo struct {
- ID string `json:"id"`
- Number int `json:"number"`
- }
- type CreatedAt struct {
- time.Time
- }
- type EditedAt struct{
- time.Time
- }
- func (t *CreatedAt) UnmarshalJSON(d []byte) error {
- // Special case to handle retweets from GNU Social
- // which returns empty string ("") in created_at
- if len(d) == 2 && string(d) == `""` {
- return nil
- }
- return t.Time.UnmarshalJSON(d)
- }
- // Status is struct to hold status.
- type Status struct {
- ID string `json:"id"`
- URI string `json:"uri"`
- URL string `json:"url"`
- Account Account `json:"account"`
- InReplyToID interface{} `json:"in_reply_to_id"`
- InReplyToAccountID interface{} `json:"in_reply_to_account_id"`
- Reblog *Status `json:"reblog"`
- Content string `json:"content"`
- CreatedAt CreatedAt `json:"created_at"`
- EditedAt *EditedAt `json:"edited_at"`
- Emojis []Emoji `json:"emojis"`
- RepliesCount int64 `json:"replies_count"`
- ReblogsCount int64 `json:"reblogs_count"`
- FavouritesCount int64 `json:"favourites_count"`
- Reblogged interface{} `json:"reblogged"`
- Favourited interface{} `json:"favourited"`
- Muted interface{} `json:"muted"`
- Sensitive bool `json:"sensitive"`
- SpoilerText string `json:"spoiler_text"`
- Visibility string `json:"visibility"`
- MediaAttachments []Attachment `json:"media_attachments"`
- Mentions []Mention `json:"mentions"`
- Tags []Tag `json:"tags"`
- Application Application `json:"application"`
- Language string `json:"language"`
- Pinned interface{} `json:"pinned"`
- Bookmarked bool `json:"bookmarked"`
- Poll *Poll `json:"poll"`
- // Custom fields
- Pleroma StatusPleroma `json:"pleroma"`
- ShowReplies bool `json:"show_replies"`
- IDReplies map[string][]ReplyInfo `json:"id_replies"`
- IDNumbers map[string]int `json:"id_numbers"`
- RetweetedByID string `json:"retweeted_by_id"`
- }
- // Context hold information for mastodon context.
- type Context struct {
- Ancestors []*Status `json:"ancestors"`
- Descendants []*Status `json:"descendants"`
- }
- // GetFavourites return the favorite list of the current user.
- func (c *Client) GetFavourites(ctx context.Context, pg *Pagination) ([]*Status, error) {
- var statuses []*Status
- err := c.doAPI(ctx, http.MethodGet, "/api/v1/favourites", nil, &statuses, pg)
- if err != nil {
- return nil, err
- }
- return statuses, nil
- }
- // GetStatus return status specified by id.
- func (c *Client) GetStatus(ctx context.Context, id string) (*Status, error) {
- var status Status
- err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s", id), nil, &status, nil)
- if err != nil {
- return nil, err
- }
- return &status, nil
- }
- // GetStatusContext return status specified by id.
- func (c *Client) GetStatusContext(ctx context.Context, id string) (*Context, error) {
- var context Context
- err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/context", id), nil, &context, nil)
- if err != nil {
- return nil, err
- }
- return &context, 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
- err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/reblogged_by", id), nil, &accounts, pg)
- if err != nil {
- return nil, err
- }
- return accounts, nil
- }
- // GetFavouritedBy returns the account list of the user who liked the toot of id.
- func (c *Client) GetFavouritedBy(ctx context.Context, id string, pg *Pagination) ([]*Account, error) {
- var accounts []*Account
- err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/favourited_by", id), nil, &accounts, pg)
- if err != nil {
- return nil, err
- }
- return accounts, nil
- }
- // GetReactionBy returns the reactions list of the user who reacted the toot of id. (Pleroma)
- func (c *Client) GetReactedBy(ctx context.Context, id string) ([]*ReactionsPleroma, error) {
- var reactions []*ReactionsPleroma
- err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/pleroma/statuses/%s/reactions", id), nil, &reactions, nil)
- if err != nil {
- return nil, err
- }
- return reactions, nil
- }
- // PutReaction is reaction on status with unicode emoji (Pleroma)
- func (c *Client) PutReaction(ctx context.Context, id string, emoji string) (*Status, error) {
- var status Status
- err := c.doAPI(ctx, http.MethodPut, fmt.Sprintf("/api/v1/pleroma/statuses/%s/reactions/%s", id, emoji), nil, &status, nil)
- if err != nil {
- return nil, err
- }
- return &status, nil
- }
- // UnReaction is unreaction on status with unicode emoji (Pleroma)
- func (c *Client) UnReaction(ctx context.Context, id string, emoji string) (*Status, error) {
- var status Status
- err := c.doAPI(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/pleroma/statuses/%s/reactions/%s", id, emoji), nil, &status, nil)
- if err != nil {
- return nil, err
- }
- return &status, nil
- }
- // Reblog is reblog the toot of id and return status of reblog.
- func (c *Client) Reblog(ctx context.Context, id string, visibility string) (*Status, error) {
- var status Status
- params := url.Values{}
- params.Set("visibility", visibility)
- err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/reblog", id), params, &status, nil)
- if err != nil {
- return nil, err
- }
- return &status, nil
- }
- // Unreblog is unreblog the toot of id and return status of the original toot.
- func (c *Client) Unreblog(ctx context.Context, id string) (*Status, error) {
- var status Status
- err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/unreblog", id), nil, &status, nil)
- if err != nil {
- return nil, err
- }
- return &status, nil
- }
- // Favourite is favourite the toot of id and return status of the favourite toot.
- func (c *Client) Favourite(ctx context.Context, id string) (*Status, error) {
- var status Status
- err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/favourite", id), nil, &status, nil)
- if err != nil {
- return nil, err
- }
- return &status, nil
- }
- // Unfavourite is unfavourite the toot of id and return status of the unfavourite toot.
- func (c *Client) Unfavourite(ctx context.Context, id string) (*Status, error) {
- var status Status
- err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/unfavourite", id), nil, &status, nil)
- if err != nil {
- return nil, err
- }
- return &status, nil
- }
- // GetTimelineHome return statuses from home timeline.
- func (c *Client) GetTimelineHome(ctx context.Context, pg *Pagination) ([]*Status, error) {
- var statuses []*Status
- err := c.doAPI(ctx, http.MethodGet, "/api/v1/timelines/home", nil, &statuses, pg)
- if err != nil {
- return nil, err
- }
- return statuses, nil
- }
- // TrueRemoteTimeline get public timeline from remote Mastodon API compatible instance directly
- func (c *Client) TrueRemoteTimeline(ctx context.Context, instance string, instance_type string, pg *Pagination) ([]*Status, error) {
- var publicstatuses []*Status
- var instanceParams []string
- instanceParams = strings.Split(instance, ":")[1:]
- instance = strings.TrimSpace(strings.Split(instance, ":")[0])
- params := url.Values{}
- params.Set("local", "true")
- if pg != nil {
- params = pg.setValues(params)
- }
- perform := url.URL{
- Scheme: "https",
- Host: instance,
- }
- var paramval []string
- withFiles := "false"
- withReplies := "false"
- globalTimeline := false
- for _, instanceParam := range instanceParams {
- switch instanceParam {
- case "withFiles":
- withFiles = "true"
- params.Set("only_media", "true")
- case "withReplies":
- withReplies = "true"
- case "remote":
- globalTimeline = true
- params.Set(instanceParam, "true")
- default:
- paramval = strings.Split(instanceParam, "=")
- if len(paramval) == 2 {
- params.Set(paramval[0], paramval[1])
- } else {
- params.Set(instanceParam, "true")
- }
- }
- }
- var method string
- var ContentType string
- var bytesAttach []byte
- switch instance_type {
- case "misskey":
- if globalTimeline {
- perform.Path = "api/notes/global-timeline"
- } else {
- perform.Path = "api/notes/local-timeline"
- }
- perform.RawQuery = ""
- method = http.MethodPost
- ContentType = "application/json"
- bytesAttach = []byte(fmt.Sprintf(
- `{"limit":20,"withRenotes":false, "withReplies": %s, "withFiles": %s}`,
- withReplies, withFiles))
- if pg != nil {
- if pg.MaxID != "" {
- bytesAttach = []byte(fmt.Sprintf(
- `{"limit": %s,"withRenotes": false,"untilId":"%s", "withReplies": %s, "withFiles": %s}`,
- strconv.Itoa(int(pg.Limit)), pg.MaxID, withReplies, withFiles))
- }
- }
- default:
- if globalTimeline {
- params.Set("local", "false")
- }
- perform.RawQuery = params.Encode()
- perform.Path = "api/v1/timelines/public"
- method = http.MethodGet
- ContentType = "application/x-www-form-urlencoded"
- bytesAttach = []byte("")
- }
- req, err := http.NewRequest(method, perform.String(), bytes.NewBuffer(bytesAttach))
- req = req.WithContext(ctx)
- req.Header.Set("Content-Type", ContentType)
- req.Header.Set("User-Agent", "Bloat")
- client := http.Client{}
- resp, err := client.Do(req)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- return nil, parseAPIError("Can't get remote timeline for " + instance + ", try select another type instance. Error" , resp)
- }
- switch instance_type {
- case "misskey":
- var misskeyData []MisskeyStatus
- err = json.NewDecoder(resp.Body).Decode(&misskeyData)
- if err != nil {
- return nil, err
- }
- for _, statusMisskey := range misskeyData {
- var status Status
- status.ID = statusMisskey.ID
- if statusMisskey.Renote != nil {
- // small handle for strange reblogs in misskey
- // handle as quoted post because akkoma/pleroma makes same
- var quote Status
- quote.ID = statusMisskey.Renote.ID
- quote.Content = strings.Replace(statusMisskey.Renote.Text, "\n", "<br>", -1)
- status.Pleroma.Quote = "e
- }
- status.Account.ID = statusMisskey.User.ID
- status.Account.DisplayName = statusMisskey.User.Name
- if statusMisskey.User.Host != "" {
- status.Account.Acct = statusMisskey.User.Username + "@" + statusMisskey.User.Host
- } else {
- status.Account.Acct = statusMisskey.User.Username
- }
- status.Account.Username = statusMisskey.User.Username
- status.Account.Avatar = statusMisskey.User.AvatarURL
- status.CreatedAt = statusMisskey.CreatedAt
- status.Visibility = statusMisskey.Visibility
- status.Content = strings.Replace(statusMisskey.Text, "\n", "<br>", -1) + "<br><br>"
- for reaction, count := range statusMisskey.Reactions { // woozyface
- if reaction == "❤" {
- status.FavouritesCount = int64(count)
- continue
- }
- status.Content = status.Content + "[" + reaction + strconv.Itoa(count) + "]"
- }
- status.MediaAttachments = statusMisskey.Files
- for idx, attach := range statusMisskey.Files {
- status.MediaAttachments[idx].Type = strings.Split(attach.Type, "/")[0]
- status.MediaAttachments[idx].Description = strings.Replace(attach.Comment, "\n", "<br>", -1)
- status.MediaAttachments[idx].PreviewURL = attach.ThumbnailUrl
- status.MediaAttachments[idx].RemoteURL = attach.URL
- status.MediaAttachments[idx].TextURL = attach.URL
- if status.Sensitive == false {
- if attach.Sensitive { // mark status as NSFW if any attachment marked as NSFW
- status.Sensitive = true
- }
- }
- }
- if statusMisskey.CW != "" {
- status.Sensitive = true
- status.SpoilerText = statusMisskey.CW
- }
- status.RepliesCount = statusMisskey.RepliesCount
- status.ReblogsCount = statusMisskey.RenoteCount
- status.Account.Bot = statusMisskey.User.IsBot
- status.URL = "https://" + instance + "/notes/" + statusMisskey.ID
- publicstatuses = append(publicstatuses, &status)
- }
- case "friendica":
- err = json.NewDecoder(resp.Body).Decode(&publicstatuses)
- if err != nil {
- return nil, err
- }
- for _, status := range publicstatuses {
- status.URL = status.URI // Fix federate URL
- }
- default:
- err = json.NewDecoder(resp.Body).Decode(&publicstatuses)
- if err != nil {
- return nil, err
- }
- }
- return publicstatuses, nil
- }
- // GetTimelinePublic return statuses from public timeline.
- func (c *Client) GetTimelinePublic(ctx context.Context, isLocal bool, instance string, pg *Pagination) ([]*Status, error) {
- params := url.Values{}
- if len(instance) > 0 {
- params.Set("instance", instance)
- } else if isLocal {
- params.Set("local", "true")
- }
- var statuses []*Status
- err := c.doAPI(ctx, http.MethodGet, "/api/v1/timelines/public", params, &statuses, pg)
- if err != nil {
- return nil, err
- }
- return statuses, nil
- }
- // GetTimelineHashtag return statuses from tagged timeline.
- func (c *Client) GetTimelineHashtag(ctx context.Context, tag string, isLocal bool, pg *Pagination) ([]*Status, error) {
- params := url.Values{}
- if isLocal {
- params.Set("local", "t")
- }
- var statuses []*Status
- err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/timelines/tag/%s", url.PathEscape(tag)), params, &statuses, pg)
- if err != nil {
- return nil, err
- }
- return statuses, nil
- }
- // GetTimelineList return statuses from a list timeline.
- func (c *Client) GetTimelineList(ctx context.Context, id string, pg *Pagination) ([]*Status, error) {
- var statuses []*Status
- err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/timelines/list/%s", url.PathEscape(string(id))), nil, &statuses, pg)
- if err != nil {
- return nil, err
- }
- return statuses, nil
- }
- // GetTimelineMedia return statuses from media timeline.
- // NOTE: This is an experimental feature of pawoo.net.
- func (c *Client) GetTimelineMedia(ctx context.Context, isLocal bool, pg *Pagination) ([]*Status, error) {
- params := url.Values{}
- params.Set("media", "t")
- if isLocal {
- params.Set("local", "t")
- }
- var statuses []*Status
- err := c.doAPI(ctx, http.MethodGet, "/api/v1/timelines/public", params, &statuses, pg)
- if err != nil {
- return nil, err
- }
- return statuses, nil
- }
- // PostStatus post the toot.
- func (c *Client) PostStatus(ctx context.Context, toot *Toot) (*Status, error) {
- params := url.Values{}
- params.Set("status", toot.Status)
- if toot.InReplyToID != "" {
- params.Set("in_reply_to_id", string(toot.InReplyToID))
- }
- if toot.MediaIDs != nil {
- for _, media := range toot.MediaIDs {
- params.Add("media_ids[]", string(media))
- }
- }
- if toot.Visibility != "" {
- params.Set("visibility", fmt.Sprint(toot.Visibility))
- }
- if toot.Sensitive {
- params.Set("sensitive", "true")
- }
- if toot.SpoilerText != "" {
- params.Set("spoiler_text", toot.SpoilerText)
- }
- if toot.ContentType != "" {
- params.Set("content_type", toot.ContentType)
- }
- if toot.Language != "" {
- params.Set("language", toot.Language)
- }
- if toot.ExpiresIn >= 3600 {
- params.Set("expires_in", fmt.Sprint(toot.ExpiresIn))
- }
- if toot.ScheduledAt != "" {
- params.Set("scheduled_at", toot.ScheduledAt)
- }
- if len(toot.Poll.Options) > 2 {
- for _, option := range toot.Poll.Options {
- params.Add("poll[options][]", string(option))
- }
- params.Set("poll[expires_in]", strconv.Itoa(toot.Poll.ExpiresIn))
- if toot.Poll.Multiple {
- params.Set("poll[multiple]", "true")
- }
- if toot.Poll.HideTotals {
- params.Set("poll[hide_totals]", "true")
- }
- }
- var status Status
- if toot.Edit != "" {
- err := c.doAPI(ctx, http.MethodPut, fmt.Sprintf("/api/v1/statuses/%s", toot.Edit), params, &status, nil)
- if err != nil {
- return nil, err
- }
- } else {
- err := c.doAPI(ctx, http.MethodPost, "/api/v1/statuses", params, &status, nil)
- if err != nil {
- return nil, err
- }
- }
- return &status, nil
- }
- // Pin pin your status.
- func (c *Client) Pin(ctx context.Context, id string) error {
- return c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/pin", id), nil, nil, nil)
- }
- // UnPin unpin your status.
- func (c *Client) UnPin(ctx context.Context, id string) error {
- return c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/unpin", id), nil, nil, nil)
- }
- // DeleteStatus delete the toot.
- func (c *Client) DeleteStatus(ctx context.Context, id string) error {
- return c.doAPI(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/statuses/%s", id), nil, nil, nil)
- }
- // Search search content with query.
- func (c *Client) Search(ctx context.Context, q string, qType string, limit int, resolve bool, offset int, accountID string, following bool) (*Results, error) {
- var results Results
- params := url.Values{}
- params.Set("q", q)
- params.Set("type", qType)
- params.Set("limit", fmt.Sprint(limit))
- params.Set("resolve", fmt.Sprint(resolve))
- params.Set("offset", fmt.Sprint(offset))
- params.Set("following", fmt.Sprint(following))
- if len(accountID) > 0 {
- params.Set("account_id", accountID)
- }
- err := c.doAPI(ctx, http.MethodGet, "/api/v2/search", params, &results, nil)
- if err != nil {
- return nil, err
- }
- return &results, nil
- }
- func (c *Client) UploadMediaFromMultipartFileHeader(ctx context.Context, fh *multipart.FileHeader, descr string) (*Attachment, error) {
- f, err := fh.Open()
- if err != nil {
- return nil, err
- }
- defer f.Close()
- var buf bytes.Buffer
- mw := multipart.NewWriter(&buf)
- fname := filepath.Base(fh.Filename)
- err = mw.WriteField("description", descr)
- if err != nil {
- return nil, err
- }
- 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", params, &attachment, nil)
- if err != nil {
- return nil, err
- }
- return &attachment, nil
- }
- // GetTimelineDirect return statuses from direct timeline.
- func (c *Client) GetTimelineDirect(ctx context.Context, pg *Pagination) ([]*Status, error) {
- params := url.Values{}
- var statuses []*Status
- err := c.doAPI(ctx, http.MethodGet, "/api/v1/timelines/direct", params, &statuses, pg)
- if err != nil {
- return nil, err
- }
- return statuses, nil
- }
- // MuteConversation mutes status specified by id.
- func (c *Client) MuteConversation(ctx context.Context, id string) (*Status, error) {
- var status Status
- err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/mute", id), nil, &status, nil)
- if err != nil {
- return nil, err
- }
- return &status, nil
- }
- // UnmuteConversation unmutes status specified by id.
- func (c *Client) UnmuteConversation(ctx context.Context, id string) (*Status, error) {
- var status Status
- err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/unmute", id), nil, &status, nil)
- if err != nil {
- return nil, err
- }
- return &status, nil
- }
- // Bookmark bookmarks status specified by id.
- func (c *Client) Bookmark(ctx context.Context, id string) (*Status, error) {
- var status Status
- err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/bookmark", id), nil, &status, nil)
- if err != nil {
- return nil, err
- }
- return &status, nil
- }
- // Unbookmark bookmarks status specified by id.
- func (c *Client) Unbookmark(ctx context.Context, id string) (*Status, error) {
- var status Status
- err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/unbookmark", id), nil, &status, nil)
- if err != nil {
- return nil, err
- }
- return &status, nil
- }
|