diff options
| author | Christine Dodrill <me@christine.website> | 2018-10-10 05:16:15 +0000 |
|---|---|---|
| committer | Christine Dodrill <me@christine.website> | 2018-10-10 05:16:15 +0000 |
| commit | 78c1bf58fe30f0660de32c05edc2d60d74214636 (patch) | |
| tree | 9f2545568bcc0d99e8e0c86a8a527be198bb3c77 | |
| parent | 6f56286a19da821f8b5ebbe60086e935a171b7bb (diff) | |
| download | x-78c1bf58fe30f0660de32c05edc2d60d74214636.tar.xz x-78c1bf58fe30f0660de32c05edc2d60d74214636.zip | |
sona-pi-toki-pona: only trigger if the user has a magic field
31 files changed, 2788 insertions, 62 deletions
@@ -3,6 +3,7 @@ module github.com/Xe/x require ( github.com/GeertJohan/yubigo v0.0.0-20140521141543-b1764f04aa9b // indirect github.com/McKael/madon v2.3.0+incompatible + github.com/McKael/madon/v2 v2.0.0-20180929094633-c679abc985d6 github.com/Xe/ln v0.1.2 github.com/aclements/go-moremath v0.0.0-20180329182055-b1aff36309c7 // indirect github.com/bearbin/mcgorcon v0.0.0-20141104170123-f611ad04551a @@ -24,7 +25,6 @@ require ( github.com/google/go-github v17.0.0+incompatible github.com/google/go-querystring v1.0.0 // indirect github.com/google/gops v0.3.5 - github.com/gorilla/websocket v1.4.0 // indirect github.com/hullerob/go.farbfeld v0.0.0-20160317142651-b572f0728b69 github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 github.com/joeshaw/envdecode v0.0.0-20180312135643-c9e015854467 @@ -39,7 +39,6 @@ require ( github.com/pborman/uuid v1.2.0 github.com/peterh/liner v1.1.0 github.com/pkg/errors v0.8.0 - github.com/sendgrid/rest v2.4.1+incompatible // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/streamrail/concurrent-map v0.0.0-20160823150647-8bf1e9bacbf6 github.com/technoweenie/multipartstreamer v1.0.1 // indirect @@ -52,7 +51,6 @@ require ( golang.org/x/image v0.0.0-20180926015637-991ec62608f3 // indirect golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced golang.org/x/sys v0.0.0-20181004145325-8469e314837c // indirect - google.golang.org/appengine v1.2.0 // indirect gopkg.in/irc.v1 v1.3.0 gopkg.in/yaml.v2 v2.2.1 // indirect ) @@ -6,6 +6,8 @@ github.com/GeertJohan/yubigo v0.0.0-20140521141543-b1764f04aa9b h1:SDBD2Avdba3sX github.com/GeertJohan/yubigo v0.0.0-20140521141543-b1764f04aa9b/go.mod h1:njRCDrl+1RQ/A/+KVU8Ho2EWAxUSkohOWczdW3dzDG0= github.com/McKael/madon v2.3.0+incompatible h1:xMUA+Fy4saDV+8tN3MMnwJUoYWC//5Fy8LeOqJsRNIM= github.com/McKael/madon v2.3.0+incompatible/go.mod h1:+issnvJjN1rpjAHZwXRB/x30uHh/NoQR7QaojJK/lSI= +github.com/McKael/madon/v2 v2.0.0-20180929094633-c679abc985d6 h1:9cJcTOeILzInNo+DCYmXKME1QfAP07FYdo3M9/9jyc4= +github.com/McKael/madon/v2 v2.0.0-20180929094633-c679abc985d6/go.mod h1:mvlJhxZCchfiasx3XvN3hBu5RekGwTDm09dKlSM/dQQ= github.com/Xe/ln v0.1.2 h1:VTF6Z95Kdd6S1RSjpnn0DFJV5Do9cfmHTHsE9OGI1dw= github.com/Xe/ln v0.1.2/go.mod h1:GWZDvjL0ZC/9SSjY8DJ2IOrID6Lejs7D8pgQyjqMYSk= github.com/aclements/go-moremath v0.0.0-20180329182055-b1aff36309c7 h1:/NWziSfuX4cu00adiPldD8sdCpKxu6BZtLoALSyElZQ= @@ -25,6 +27,7 @@ github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE github.com/coreos/go-systemd v0.0.0-20180705093442-88bfeed483d3/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-failure v0.0.0-20151001134759-4963dbd58fd0 h1:SJzP6kZNFsdxFN4fRtVXu2yrf8QrKnofjQouzaCn36w= github.com/dgryski/go-failure v0.0.0-20151001134759-4963dbd58fd0/go.mod h1:vVcpVd0tzL5XdRrkHax1asNJsHVpgA0cd9fHPHU5a/w= github.com/dgryski/go-onlinestats v0.0.0-20170612111826-1c7d19468768 h1:Xzl7CSuSnGsyU+9xmSU2h8w3d7Tnis66xeoNN207tLo= @@ -171,7 +174,10 @@ golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20180808004115-f9ce57c11b24/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180811021610-c39426892332 h1:efGso+ep0DjyCBJPjvoz0HI6UldX4Md2F1rZFe1ir0E= golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3 h1:dgd4x4kJt7G4k4m93AYLzM8Ni6h2qLTfh9n9vXJT3/0= +golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/oauth2 v0.0.0-20180724155351-3d292e4d0cdc/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced h1:4oqSq7eft7MdPKBGQK11X9WYUxmj6ZLgGTqYIbY1kyw= golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= diff --git a/mastodon/sona-pi-toki-pona/main.go b/mastodon/sona-pi-toki-pona/main.go index ddd2d4f..99d3f56 100644 --- a/mastodon/sona-pi-toki-pona/main.go +++ b/mastodon/sona-pi-toki-pona/main.go @@ -6,7 +6,7 @@ import ( "io/ioutil" "net/http" - madon "github.com/McKael/madon" + madon "github.com/McKael/madon/v2" "github.com/Xe/ln" "github.com/Xe/ln/opname" "github.com/Xe/x/web/tokipana" @@ -35,6 +35,8 @@ func main() { ln.FatalErr(opname.With(ctx, "restore-app"), err) } + ln.Log(ctx, ln.Info("waiting for messages on #topipona...")) + for { evChan := make(chan madon.StreamEvent, 10) stop := make(chan bool) @@ -62,6 +64,17 @@ func main() { "originating_status_url": s.URL, }) + found := false + for _, f := range *s.Account.Fields { + if f.Name == "enable-bot" && f.Value == "sona-pi-toki-pona" { + found = true + } + } + if !found { + ln.Log(ctx, ln.Info("ignoring message")) + continue + } + text, err := html2text.FromString(s.Content, html2text.Options{PrettyTables: true}) if err != nil { ln.Error(ctx, err) @@ -87,12 +100,11 @@ func main() { } st, err := c.PostStatus( - fmt.Sprintf(translationTemplate, string(data)), - s.ID, - nil, - false, - "", - "public", + madon.PostStatusParams{ + Text: fmt.Sprintf(translationTemplate, string(data)), + InReplyTo: s.ID, + Visibility: "public", + }, ) if err != nil { ln.Error(ctx, err) diff --git a/vendor/github.com/McKael/madon/v2/.gitignore b/vendor/github.com/McKael/madon/v2/.gitignore new file mode 100644 index 0000000..836e7a0 --- /dev/null +++ b/vendor/github.com/McKael/madon/v2/.gitignore @@ -0,0 +1,20 @@ +# Created by .ignore support plugin (hsz.mobi) +### Go template +# Binaries for programs and plugins +*.exe +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 +.glide/ +*.swp + +.idea/ +.local/ diff --git a/vendor/github.com/McKael/madon/v2/.travis.yml b/vendor/github.com/McKael/madon/v2/.travis.yml new file mode 100644 index 0000000..5e14f30 --- /dev/null +++ b/vendor/github.com/McKael/madon/v2/.travis.yml @@ -0,0 +1,19 @@ +language: go +go: +- "1.9" +- "1.10" +- "1.11" +- master +matrix: + allow_failures: + - go: master + fast_finish: true +branches: + only: + - master +install: +- go get golang.org/x/oauth2 +- go get github.com/pkg/errors +- go get github.com/stretchr/testify/assert +- go get github.com/sendgrid/rest +- go get github.com/gorilla/websocket diff --git a/vendor/github.com/McKael/madon/v2/LICENSE b/vendor/github.com/McKael/madon/v2/LICENSE new file mode 100644 index 0000000..f0f42d2 --- /dev/null +++ b/vendor/github.com/McKael/madon/v2/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2017 Ollivier Robert +Copyright (c) 2017 Mikael Berthe <mikael@lilotux.net> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/McKael/madon/v2/README.md b/vendor/github.com/McKael/madon/v2/README.md new file mode 100644 index 0000000..5449329 --- /dev/null +++ b/vendor/github.com/McKael/madon/v2/README.md @@ -0,0 +1,49 @@ +# madon + +Golang library for the Mastodon API + +[](https://godoc.org/github.com/McKael/madon) +[](https://raw.githubusercontent.com/McKael/madon/master/LICENSE) +[](https://travis-ci.org/McKael/madon) +[](https://goreportcard.com/report/github.com/McKael/madon) + +`madon` is a [Go](https://golang.org/) library to access the Mastondon REST API. + +This implementation covers 100% of the current API, including the streaming API. + +The [madonctl](https://github.com/McKael/madonctl) console client uses this library exhaustively. + +## Installation + +To install the library (Go >= v1.5 required): + + go get github.com/McKael/madon + +For minimal compatibility with Go modules support (in Go v1.11), it is +recommended to use Go version 1.9+. + +You can test it with my CLI tool: + + go get github.com/McKael/madonctl + +## Usage + +This section has not been written yet (PR welcome). + +For now please check [godoc](https://godoc.org/github.com/McKael/madon) and +check the [madonctl](https://github.com/McKael/madonctl) project +implementation. + +## History + +This API implementation was initially submitted as a PR for gondole. + +The repository is actually a fork of my gondole branch so that +history and credits are preserved. + +## References + +- [madonctl](https://github.com/McKael/madonctl) (console client based on madon) +- [Mastodon API documentation](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md) +- [Mastodon Streaming API documentation](https://github.com/tootsuite/documentation/blob/master/Using-the-API/Streaming-API.md) +- [Mastodon repository](https://github.com/tootsuite/mastodon) diff --git a/vendor/github.com/McKael/madon/v2/account.go b/vendor/github.com/McKael/madon/v2/account.go new file mode 100644 index 0000000..0947393 --- /dev/null +++ b/vendor/github.com/McKael/madon/v2/account.go @@ -0,0 +1,562 @@ +/* +Copyright 2017-2018 Mikael Berthe + +Licensed under the MIT license. Please see the LICENSE file is this directory. +*/ + +package madon + +import ( + "bytes" + "encoding/json" + "fmt" + "mime/multipart" + "os" + "path/filepath" + "strconv" + + "github.com/pkg/errors" + "github.com/sendgrid/rest" +) + +// getAccountsOptions contains option fields for POST and DELETE API calls +type getAccountsOptions struct { + // The ID is used for most commands + ID int64 + + // Following can be set to true to limit a search to "following" accounts + Following bool + + // The Q field (query) is used when searching for accounts + Q string + + Limit *LimitParams +} + +// UpdateAccountParams contains option fields for the UpdateAccount command +type UpdateAccountParams struct { + DisplayName *string + Note *string + AvatarImagePath *string + HeaderImagePath *string + Locked *bool + Bot *bool + FieldsAttributes *[]Field + Source *SourceParams +} + +// updateRelationship returns a Relationship entity +// The operation 'op' can be "follow", "unfollow", "block", "unblock", +// "mute", "unmute". +// The id is optional and depends on the operation. +func (mc *Client) updateRelationship(op string, id int64, params apiCallParams) (*Relationship, error) { + var endPoint string + method := rest.Post + strID := strconv.FormatInt(id, 10) + + switch op { + case "follow", "unfollow", "block", "unblock", "mute", "unmute", "pin", "unpin": + endPoint = "accounts/" + strID + "/" + op + default: + return nil, ErrInvalidParameter + } + + var rel Relationship + if err := mc.apiCall("v1/"+endPoint, method, params, nil, nil, &rel); err != nil { + return nil, err + } + return &rel, nil +} + +// getSingleAccount returns an account entity +// The operation 'op' can be "account", "verify_credentials", +// "follow_requests/authorize" or // "follow_requests/reject". +// The id is optional and depends on the operation. +func (mc *Client) getSingleAccount(op string, id int64) (*Account, error) { + var endPoint string + method := rest.Get + strID := strconv.FormatInt(id, 10) + + switch op { + case "account": + endPoint = "accounts/" + strID + case "verify_credentials": + endPoint = "accounts/verify_credentials" + case "follow_requests/authorize", "follow_requests/reject": + // The documentation is incorrect, the endpoint actually + // is "follow_requests/:id/{authorize|reject}" + endPoint = op[:16] + strID + "/" + op[16:] + method = rest.Post + default: + return nil, ErrInvalidParameter + } + + var account Account + if err := mc.apiCall("v1/"+endPoint, method, nil, nil, nil, &account); err != nil { + return nil, err + } + return &account, nil +} + +// getMultipleAccounts returns a list of account entities +// If lopt.All is true, several requests will be made until the API server +// has nothing to return. +func (mc *Client) getMultipleAccounts(endPoint string, params apiCallParams, lopt *LimitParams) ([]Account, error) { + var accounts []Account + var links apiLinks + if err := mc.apiCall("v1/"+endPoint, rest.Get, params, lopt, &links, &accounts); err != nil { + return nil, err + } + if lopt != nil { // Fetch more pages to reach our limit + var accountSlice []Account + for (lopt.All || lopt.Limit > len(accounts)) && links.next != nil { + newlopt := links.next + links = apiLinks{} + if err := mc.apiCall("v1/"+endPoint, rest.Get, params, newlopt, &links, &accountSlice); err != nil { + return nil, err + } + accounts = append(accounts, accountSlice...) + accountSlice = accountSlice[:0] // Clear struct + } + } + return accounts, nil +} + +// getMultipleAccountsHelper returns a list of account entities +// The operation 'op' can be "followers", "following", "search", "blocks", +// "mutes", "follow_requests". +// The id is optional and depends on the operation. +// If opts.All is true, several requests will be made until the API server +// has nothing to return. +func (mc *Client) getMultipleAccountsHelper(op string, opts *getAccountsOptions) ([]Account, error) { + var endPoint string + var lopt *LimitParams + + if opts != nil { + lopt = opts.Limit + } + + switch op { + case "followers", "following": + if opts == nil || opts.ID < 1 { + return []Account{}, ErrInvalidID + } + endPoint = "accounts/" + strconv.FormatInt(opts.ID, 10) + "/" + op + case "follow_requests", "blocks", "mutes": + endPoint = op + case "search": + if opts == nil || opts.Q == "" { + return []Account{}, ErrInvalidParameter + } + endPoint = "accounts/" + op + case "reblogged_by", "favourited_by": + if opts == nil || opts.ID < 1 { + return []Account{}, ErrInvalidID + } + endPoint = "statuses/" + strconv.FormatInt(opts.ID, 10) + "/" + op + default: + return nil, ErrInvalidParameter + } + + // Handle target-specific query parameters + params := make(apiCallParams) + if op == "search" { + params["q"] = opts.Q + if opts.Following { + params["following"] = "true" + } + } + + return mc.getMultipleAccounts(endPoint, params, lopt) +} + +// GetAccount returns an account entity +// The returned value can be nil if there is an error or if the +// requested ID does not exist. +func (mc *Client) GetAccount(accountID int64) (*Account, error) { + account, err := mc.getSingleAccount("account", accountID) + if err != nil { + return nil, err + } + if account != nil && account.ID == 0 { + return nil, ErrEntityNotFound + } + return account, nil +} + +// GetCurrentAccount returns the current user account +func (mc *Client) GetCurrentAccount() (*Account, error) { + account, err := mc.getSingleAccount("verify_credentials", 0) + if err != nil { + return nil, err + } + if account != nil && account.ID == 0 { + return nil, ErrEntityNotFound + } + return account, nil +} + +// GetAccountFollowers returns the list of accounts following a given account +func (mc *Client) GetAccountFollowers(accountID int64, lopt *LimitParams) ([]Account, error) { + o := &getAccountsOptions{ID: accountID, Limit: lopt} + return mc.getMultipleAccountsHelper("followers", o) +} + +// GetAccountFollowing returns the list of accounts a given account is following +func (mc *Client) GetAccountFollowing(accountID int64, lopt *LimitParams) ([]Account, error) { + o := &getAccountsOptions{ID: accountID, Limit: lopt} + return mc.getMultipleAccountsHelper("following", o) +} + +// FollowAccount follows an account +// 'reblogs' can be used to specify if boots should be displayed or hidden. +func (mc *Client) FollowAccount(accountID int64, reblogs *bool) (*Relationship, error) { + var params apiCallParams + if reblogs != nil { + params = make(apiCallParams) + if *reblogs { + params["reblogs"] = "true" + } else { + params["reblogs"] = "false" + } + } + rel, err := mc.updateRelationship("follow", accountID, params) + if err != nil { + return nil, err + } + if rel == nil { + return nil, ErrEntityNotFound + } + return rel, nil +} + +// UnfollowAccount unfollows an account +func (mc *Client) UnfollowAccount(accountID int64) (*Relationship, error) { + rel, err := mc.updateRelationship("unfollow", accountID, nil) + if err != nil { + return nil, err + } + if rel == nil { + return nil, ErrEntityNotFound + } + return rel, nil +} + +// FollowRemoteAccount follows a remote account +// The parameter 'uri' is a URI (e.g. "username@domain"). +func (mc *Client) FollowRemoteAccount(uri string) (*Account, error) { + if uri == "" { + return nil, ErrInvalidID + } + + params := make(apiCallParams) + params["uri"] = uri + + var account Account + if err := mc.apiCall("v1/follows", rest.Post, params, nil, nil, &account); err != nil { + return nil, err + } + if account.ID == 0 { + return nil, ErrEntityNotFound + } + return &account, nil +} + +// BlockAccount blocks an account +func (mc *Client) BlockAccount(accountID int64) (*Relationship, error) { + rel, err := mc.updateRelationship("block", accountID, nil) + if err != nil { + return nil, err + } + if rel == nil { + return nil, ErrEntityNotFound + } + return rel, nil +} + +// UnblockAccount unblocks an account +func (mc *Client) UnblockAccount(accountID int64) (*Relationship, error) { + rel, err := mc.updateRelationship("unblock", accountID, nil) + if err != nil { + return nil, err + } + if rel == nil { + return nil, ErrEntityNotFound + } + return rel, nil +} + +// MuteAccount mutes an account +// Note that with current Mastodon API, muteNotifications defaults to true +// when it is not provided. +func (mc *Client) MuteAccount(accountID int64, muteNotifications *bool) (*Relationship, error) { + var params apiCallParams + + if muteNotifications != nil { + params = make(apiCallParams) + if *muteNotifications { + params["notifications"] = "true" + } else { + params["notifications"] = "false" + } + } + + rel, err := mc.updateRelationship("mute", accountID, params) + if err != nil { + return nil, err + } + if rel == nil { + return nil, ErrEntityNotFound + } + return rel, nil +} + +// UnmuteAccount unmutes an account +func (mc *Client) UnmuteAccount(accountID int64) (*Relationship, error) { + rel, err := mc.updateRelationship("unmute", accountID, nil) + if err != nil { + return nil, err + } + if rel == nil { + return nil, ErrEntityNotFound + } + return rel, nil +} + +// SearchAccounts returns a list of accounts matching the query string +// The lopt parameter is optional (can be nil) or can be used to set a limit. +func (mc *Client) SearchAccounts(query string, following bool, lopt *LimitParams) ([]Account, error) { + o := &getAccountsOptions{Q: query, Limit: lopt, Following: following} + return mc.getMultipleAccountsHelper("search", o) +} + +// GetBlockedAccounts returns the list of blocked accounts +// The lopt parameter is optional (can be nil). +func (mc *Client) GetBlockedAccounts(lopt *LimitParams) ([]Account, error) { + o := &getAccountsOptions{Limit: lopt} + return mc.getMultipleAccountsHelper("blocks", o) +} + +// GetMutedAccounts returns the list of muted accounts +// The lopt parameter is optional (can be nil). +func (mc *Client) GetMutedAccounts(lopt *LimitParams) ([]Account, error) { + o := &getAccountsOptions{Limit: lopt} + return mc.getMultipleAccountsHelper("mutes", o) +} + +// GetAccountFollowRequests returns the list of follow requests accounts +// The lopt parameter is optional (can be nil). +func (mc *Client) GetAccountFollowRequests(lopt *LimitParams) ([]Account, error) { + o := &getAccountsOptions{Limit: lopt} + return mc.getMultipleAccountsHelper("follow_requests", o) +} + +// GetAccountRelationships returns a list of relationship entities for the given accounts +func (mc *Client) GetAccountRelationships(accountIDs []int64) ([]Relationship, error) { + if len(accountIDs) < 1 { + return nil, ErrInvalidID + } + + params := make(apiCallParams) + for i, id := range accountIDs { + if id < 1 { + return nil, ErrInvalidID + } + qID := fmt.Sprintf("[%d]id", i) + params[qID] = strconv.FormatInt(id, 10) + } + + var rl []Relationship + if err := mc.apiCall("v1/accounts/relationships", rest.Get, params, nil, nil, &rl); err != nil { + return nil, err + } + return rl, nil +} + +// GetAccountStatuses returns a list of status entities for the given account +// If onlyMedia is true, returns only statuses that have media attachments. +// If onlyPinned is true, returns only statuses that have been pinned. +// If excludeReplies is true, skip statuses that reply to other statuses. +// If lopt.All is true, several requests will be made until the API server +// has nothing to return. +// If lopt.Limit is set (and not All), several queries can be made until the +// limit is reached. +func (mc *Client) GetAccountStatuses(accountID int64, onlyPinned, onlyMedia, excludeReplies bool, lopt *LimitParams) ([]Status, error) { + if accountID < 1 { + return nil, ErrInvalidID + } + + endPoint := "accounts/" + strconv.FormatInt(accountID, 10) + "/" + "statuses" + params := make(apiCallParams) + if onlyMedia { + params["only_media"] = "true" + } + if onlyPinned { + params["pinned"] = "true" + } + if excludeReplies { + params["exclude_replies"] = "true" + } + + return mc.getMultipleStatuses(endPoint, params, lopt) +} + +// FollowRequestAuthorize authorizes or rejects an account follow-request +func (mc *Client) FollowRequestAuthorize(accountID int64, authorize bool) error { + endPoint := "follow_requests/reject" + if authorize { + endPoint = "follow_requests/authorize" + } + _, err := mc.getSingleAccount(endPoint, accountID) + return err +} + +// UpdateAccount updates the connected user's account data +// +// The fields avatar & headerImage are considered as file paths +// and their content will be uploaded. +// Please note that currently Mastodon leaks the avatar file name: +// https://github.com/tootsuite/mastodon/issues/5776 +// +// All fields can be nil, in which case they are not updated. +// 'DisplayName' and 'Note' can be set to "" to delete previous values. +// Setting 'Locked' to true means all followers should be approved. +// You can set 'Bot' to true to indicate this is a service (automated) account. +// I'm not sure images can be deleted -- only replaced AFAICS. +func (mc *Client) UpdateAccount(cmdParams UpdateAccountParams) (*Account, error) { + const endPoint = "accounts/update_credentials" + params := make(apiCallParams) + + if cmdParams.DisplayName != nil { + params["display_name"] = *cmdParams.DisplayName + } + if cmdParams.Note != nil { + params["note"] = *cmdParams.Note + } + if cmdParams.Locked != nil { + if *cmdParams.Locked { + params["locked"] = "true" + } else { + params["locked"] = "false" + } + } + if cmdParams.Bot != nil { + if *cmdParams.Bot { + params["bot"] = "true" + } else { + params["bot"] = "false" + } + } + if cmdParams.FieldsAttributes != nil { + if len(*cmdParams.FieldsAttributes) > 4 { + return nil, errors.New("too many fields (max=4)") + } + for i, attr := range *cmdParams.FieldsAttributes { + qName := fmt.Sprintf("fields_attributes[%d][name]", i) + qValue := fmt.Sprintf("fields_attributes[%d][value]", i) + params[qName] = attr.Name + params[qValue] = attr.Value + } + } + if cmdParams.Source != nil { + s := cmdParams.Source + + if s.Privacy != nil { + params["source[privacy]"] = *s.Privacy + } + if s.Language != nil { + params["source[language]"] = *s.Language + } + if s.Sensitive != nil { + params["source[sensitive]"] = fmt.Sprintf("%v", *s.Sensitive) + } + } + + var err error + var avatar, headerImage []byte + + avatar, err = readFile(cmdParams.AvatarImagePath) + if err != nil { + return nil, err + } + + headerImage, err = readFile(cmdParams.HeaderImagePath) + if err != nil { + return nil, err + } + + var formBuf bytes.Buffer + w := multipart.NewWriter(&formBuf) + + if avatar != nil { + formWriter, err := w.CreateFormFile("avatar", filepath.Base(*cmdParams.AvatarImagePath)) + if err != nil { + return nil, errors.Wrap(err, "avatar upload") + } + formWriter.Write(avatar) + } + if headerImage != nil { + formWriter, err := w.CreateFormFile("header", filepath.Base(*cmdParams.HeaderImagePath)) + if err != nil { + return nil, errors.Wrap(err, "header upload") + } + formWriter.Write(headerImage) + } + w.Close() + + // Prepare the request + req, err := mc.prepareRequest("v1/"+endPoint, rest.Patch, params) + if err != nil { + return nil, errors.Wrap(err, "prepareRequest failed") + } + req.Headers["Content-Type"] = w.FormDataContentType() + req.Body = formBuf.Bytes() + + // Make API call + r, err := restAPI(req) + if err != nil { + return nil, errors.Wrap(err, "account update failed") + } + + // Check for error reply + var errorResult Error + if err := json.Unmarshal([]byte(r.Body), &errorResult); err == nil { + // The empty object is not an error + if errorResult.Text != "" { + return nil, errors.New(errorResult.Text) + } + } + + // Not an error reply; let's unmarshal the data + var account Account + if err := json.Unmarshal([]byte(r.Body), &account); err != nil { + return nil, errors.Wrap(err, "cannot decode API response") + } + return &account, nil +} + +// readFile is a helper function to read a file's contents. +func readFile(filename *string) ([]byte, error) { + if filename == nil || *filename == "" { + return nil, nil + } + + file, err := os.Open(*filename) + if err != nil { + return nil, err + } + defer file.Close() + + fStat, err := file.Stat() + if err != nil { |
