From 78c1bf58fe30f0660de32c05edc2d60d74214636 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Wed, 10 Oct 2018 05:16:15 +0000 Subject: sona-pi-toki-pona: only trigger if the user has a magic field --- vendor/github.com/McKael/madon/v2/.gitignore | 20 + vendor/github.com/McKael/madon/v2/.travis.yml | 19 + vendor/github.com/McKael/madon/v2/LICENSE | 22 + vendor/github.com/McKael/madon/v2/README.md | 49 ++ vendor/github.com/McKael/madon/v2/account.go | 562 +++++++++++++++++++++ vendor/github.com/McKael/madon/v2/api.go | 261 ++++++++++ vendor/github.com/McKael/madon/v2/app.go | 99 ++++ vendor/github.com/McKael/madon/v2/domain.go | 52 ++ vendor/github.com/McKael/madon/v2/emoji.go | 20 + vendor/github.com/McKael/madon/v2/endorsements.go | 46 ++ vendor/github.com/McKael/madon/v2/go.mod | 14 + vendor/github.com/McKael/madon/v2/go.sum | 24 + vendor/github.com/McKael/madon/v2/instance.go | 42 ++ vendor/github.com/McKael/madon/v2/lists.go | 136 +++++ vendor/github.com/McKael/madon/v2/login.go | 129 +++++ vendor/github.com/McKael/madon/v2/madon.go | 41 ++ vendor/github.com/McKael/madon/v2/media.go | 108 ++++ vendor/github.com/McKael/madon/v2/notifications.go | 90 ++++ vendor/github.com/McKael/madon/v2/report.go | 48 ++ vendor/github.com/McKael/madon/v2/search.go | 68 +++ vendor/github.com/McKael/madon/v2/status.go | 309 +++++++++++ vendor/github.com/McKael/madon/v2/streams.go | 177 +++++++ vendor/github.com/McKael/madon/v2/suggestions.go | 31 ++ vendor/github.com/McKael/madon/v2/timelines.go | 58 +++ vendor/github.com/McKael/madon/v2/types.go | 265 ++++++++++ 25 files changed, 2690 insertions(+) create mode 100644 vendor/github.com/McKael/madon/v2/.gitignore create mode 100644 vendor/github.com/McKael/madon/v2/.travis.yml create mode 100644 vendor/github.com/McKael/madon/v2/LICENSE create mode 100644 vendor/github.com/McKael/madon/v2/README.md create mode 100644 vendor/github.com/McKael/madon/v2/account.go create mode 100644 vendor/github.com/McKael/madon/v2/api.go create mode 100644 vendor/github.com/McKael/madon/v2/app.go create mode 100644 vendor/github.com/McKael/madon/v2/domain.go create mode 100644 vendor/github.com/McKael/madon/v2/emoji.go create mode 100644 vendor/github.com/McKael/madon/v2/endorsements.go create mode 100644 vendor/github.com/McKael/madon/v2/go.mod create mode 100644 vendor/github.com/McKael/madon/v2/go.sum create mode 100644 vendor/github.com/McKael/madon/v2/instance.go create mode 100644 vendor/github.com/McKael/madon/v2/lists.go create mode 100644 vendor/github.com/McKael/madon/v2/login.go create mode 100644 vendor/github.com/McKael/madon/v2/madon.go create mode 100644 vendor/github.com/McKael/madon/v2/media.go create mode 100644 vendor/github.com/McKael/madon/v2/notifications.go create mode 100644 vendor/github.com/McKael/madon/v2/report.go create mode 100644 vendor/github.com/McKael/madon/v2/search.go create mode 100644 vendor/github.com/McKael/madon/v2/status.go create mode 100644 vendor/github.com/McKael/madon/v2/streams.go create mode 100644 vendor/github.com/McKael/madon/v2/suggestions.go create mode 100644 vendor/github.com/McKael/madon/v2/timelines.go create mode 100644 vendor/github.com/McKael/madon/v2/types.go (limited to 'vendor/github.com') 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 + +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 + +[![godoc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/McKael/madon) +[![license](https://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/McKael/madon/master/LICENSE) +[![build](https://travis-ci.org/McKael/madon.svg?branch=master)](https://travis-ci.org/McKael/madon) +[![Go Report Card](https://goreportcard.com/badge/github.com/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 { + return nil, err + } + + buffer := make([]byte, fStat.Size()) + _, err = file.Read(buffer) + if err != nil { + return nil, err + } + + return buffer, nil +} diff --git a/vendor/github.com/McKael/madon/v2/api.go b/vendor/github.com/McKael/madon/v2/api.go new file mode 100644 index 0000000..77802bd --- /dev/null +++ b/vendor/github.com/McKael/madon/v2/api.go @@ -0,0 +1,261 @@ +/* +Copyright 2017-2018 Mikael Berthe +Copyright 2017 Ollivier Robert + +Licensed under the MIT license. Please see the LICENSE file is this directory. +*/ + +package madon + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" + "time" + + "github.com/pkg/errors" + "github.com/sendgrid/rest" +) + +type apiLinks struct { + next, prev *LimitParams +} + +func parseLink(links []string) (*apiLinks, error) { + if len(links) == 0 { + return nil, nil + } + + al := new(apiLinks) + linkRegex := regexp.MustCompile(`<([^>]+)>; rel="([^"]+)`) + for _, l := range links { + m := linkRegex.FindAllStringSubmatch(l, -1) + for _, submatch := range m { + if len(submatch) != 3 { + continue + } + // Parse URL + u, err := url.Parse(submatch[1]) + if err != nil { + return al, err + } + var lp *LimitParams + since := u.Query().Get("since_id") + max := u.Query().Get("max_id") + lim := u.Query().Get("limit") + if since == "" && max == "" { + continue + } + lp = new(LimitParams) + if since != "" { + lp.SinceID, err = strconv.ParseInt(since, 10, 64) + if err != nil { + return al, err + } + } + if max != "" { + lp.MaxID, err = strconv.ParseInt(max, 10, 64) + if err != nil { + return al, err + } + } + if lim != "" { + lp.Limit, err = strconv.Atoi(lim) + if err != nil { + return al, err + } + } + switch submatch[2] { + case "prev": + al.prev = lp + case "next": + al.next = lp + } + } + } + return al, nil +} + +// restAPI actually does the HTTP query +// It is a copy of rest.API with better handling of parameters with multiple values +func restAPI(request rest.Request) (*rest.Response, error) { + c := &rest.Client{HTTPClient: http.DefaultClient} + + // Build the HTTP request object. + if len(request.QueryParams) != 0 { + // Add parameters to the URL + request.BaseURL += "?" + urlp := url.Values{} + arrayRe := regexp.MustCompile(`^\[\d+\](.*)$`) + for key, value := range request.QueryParams { + // It seems Mastodon doesn't like parameters with index + // numbers, but it needs the brackets. + // Let's check if the key matches '^.+\[.*\]$' + // Do not proceed if there's another bracket pair. + klen := len(key) + if klen == 0 { + continue + } + if m := arrayRe.FindStringSubmatch(key); len(m) > 0 { + // This is an array, let's remove the index number + key = m[1] + "[]" + } + urlp.Add(key, value) + } + urlpstr := urlp.Encode() + request.BaseURL += urlpstr + } + + req, err := http.NewRequest(string(request.Method), request.BaseURL, bytes.NewBuffer(request.Body)) + if err != nil { + return nil, err + } + + for key, value := range request.Headers { + req.Header.Set(key, value) + } + _, exists := req.Header["Content-Type"] + if len(request.Body) > 0 && !exists { + req.Header.Set("Content-Type", "application/json") + } + + // Build the HTTP client and make the request. + res, err := c.MakeRequest(req) + if err != nil { + return nil, err + } + if res.StatusCode < 200 || res.StatusCode >= 300 { + // Please note that the error string code is used by Search() + // to check the error cause. + const errFormatString = "bad server status code (%d)" + return nil, errors.Errorf(errFormatString+": %s", + res.StatusCode, http.StatusText(res.StatusCode)) + } + + // Build Response object. + response, err := rest.BuildResponse(res) + if err != nil { + return nil, err + } + + return response, nil +} + +// prepareRequest inserts all pre-defined stuff +func (mc *Client) prepareRequest(target string, method rest.Method, params apiCallParams) (rest.Request, error) { + var req rest.Request + + if mc == nil { + return req, ErrUninitializedClient + } + + endPoint := mc.APIBase + "/" + target + + // Request headers + hdrs := make(map[string]string) + hdrs["User-Agent"] = fmt.Sprintf("madon/%s", MadonVersion) + if mc.UserToken != nil { + hdrs["Authorization"] = fmt.Sprintf("Bearer %s", mc.UserToken.AccessToken) + } + + req = rest.Request{ + BaseURL: endPoint, + Headers: hdrs, + Method: method, + QueryParams: params, + } + return req, nil +} + +// apiCall makes a call to the Mastodon API server +// If links is not nil, the prev/next links from the API response headers +// will be set (if they exist) in the structure. +func (mc *Client) apiCall(endPoint string, method rest.Method, params apiCallParams, limitOptions *LimitParams, links *apiLinks, data interface{}) error { + if mc == nil { + return errors.New("use of uninitialized madon client") + } + + if limitOptions != nil { + if params == nil { + params = make(apiCallParams) + } + if limitOptions.Limit > 0 { + params["limit"] = strconv.Itoa(limitOptions.Limit) + } + if limitOptions.SinceID > 0 { + params["since_id"] = strconv.FormatInt(limitOptions.SinceID, 10) + } + if limitOptions.MaxID > 0 { + params["max_id"] = strconv.FormatInt(limitOptions.MaxID, 10) + } + } + + // Prepare query + req, err := mc.prepareRequest(endPoint, method, params) + if err != nil { + return err + } + + // Make API call + r, err := restAPI(req) + if err != nil { + return errors.Wrapf(err, "API query (%s) failed", endPoint) + } + + if links != nil { + pLinks, err := parseLink(r.Headers["Link"]) + if err != nil { + return errors.Wrapf(err, "cannot decode header links (%s)", method) + } + if pLinks != nil { + *links = *pLinks + } + } + + // 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 errors.New(errorResult.Text) + } + } + + // Not an error reply; let's unmarshal the data + err = json.Unmarshal([]byte(r.Body), &data) + if err != nil { + return errors.Wrapf(err, "cannot decode API response (%s)", method) + } + return nil +} + +/* Mastodon timestamp handling */ + +// MastodonDate is a custom type for the timestamps returned by some API calls +// It is used, for example, by 'v1/instance/activity' and 'v2/search'. +// The date returned by those Mastodon API calls is a string containing a +// timestamp in seconds... + +// UnmarshalJSON handles deserialization for custom MastodonDate type +func (act *MastodonDate) UnmarshalJSON(b []byte) error { + s, err := strconv.ParseInt(strings.Trim(string(b), "\""), 10, 64) + if err != nil { + return err + } + if s == 0 { + act.Time = time.Time{} + return nil + } + act.Time = time.Unix(s, 0) + return nil +} + +// MarshalJSON handles serialization for custom MastodonDate type +func (act *MastodonDate) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf("\"%d\"", act.Unix())), nil +} diff --git a/vendor/github.com/McKael/madon/v2/app.go b/vendor/github.com/McKael/madon/v2/app.go new file mode 100644 index 0000000..922d802 --- /dev/null +++ b/vendor/github.com/McKael/madon/v2/app.go @@ -0,0 +1,99 @@ +/* +Copyright 2017-2018 Mikael Berthe +Copyright 2017 Ollivier Robert + +Licensed under the MIT license. Please see the LICENSE file is this directory. +*/ + +package madon + +import ( + "net/url" + "strings" + + "github.com/pkg/errors" + "github.com/sendgrid/rest" +) + +type registerApp struct { + ID int64 `json:"id,string"` + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` +} + +// buildInstanceURL creates the URL from the instance name or cleans up the +// provided URL +func buildInstanceURL(instanceName string) (string, error) { + if instanceName == "" { + return "", errors.New("no instance provided") + } + + instanceURL := instanceName + if !strings.Contains(instanceURL, "/") { + instanceURL = "https://" + instanceName + } + + u, err := url.ParseRequestURI(instanceURL) + if err != nil { + return "", err + } + + u.Path = "" + u.RawPath = "" + u.RawQuery = "" + u.Fragment = "" + return u.String(), nil +} + +// NewApp registers a new application with a given instance +func NewApp(name, website string, scopes []string, redirectURI, instanceName string) (mc *Client, err error) { + instanceURL, err := buildInstanceURL(instanceName) + if err != nil { + return nil, err + } + + mc = &Client{ + Name: name, + InstanceURL: instanceURL, + APIBase: instanceURL + currentAPIPath, + } + + params := make(apiCallParams) + params["client_name"] = name + if website != "" { + params["website"] = website + } + params["scopes"] = strings.Join(scopes, " ") + if redirectURI != "" { + params["redirect_uris"] = redirectURI + } else { + params["redirect_uris"] = NoRedirect + } + + var app registerApp + if err := mc.apiCall("v1/apps", rest.Post, params, nil, nil, &app); err != nil { + return nil, err + } + + mc.ID = app.ClientID + mc.Secret = app.ClientSecret + + return +} + +// RestoreApp recreates an application client with existing secrets +func RestoreApp(name, instanceName, appID, appSecret string, userToken *UserToken) (mc *Client, err error) { + instanceURL, err := buildInstanceURL(instanceName) + if err != nil { + return nil, err + } + + return &Client{ + Name: name, + InstanceURL: instanceURL, + APIBase: instanceURL + currentAPIPath, + ID: appID, + Secret: appSecret, + UserToken: userToken, + }, nil +} diff --git a/vendor/github.com/McKael/madon/v2/domain.go b/vendor/github.com/McKael/madon/v2/domain.go new file mode 100644 index 0000000..d662446 --- /dev/null +++ b/vendor/github.com/McKael/madon/v2/domain.go @@ -0,0 +1,52 @@ +/* +Copyright 2017-2018 Mikael Berthe + +Licensed under the MIT license. Please see the LICENSE file is this directory. +*/ + +package madon + +import ( + "github.com/sendgrid/rest" +) + +// GetBlockedDomains returns the current user blocked domains +// If lopt.All is true, several requests will be made until the API server +// has nothing to return. +func (mc *Client) GetBlockedDomains(lopt *LimitParams) ([]DomainName, error) { + const endPoint = "domain_blocks" + var links apiLinks + var domains []DomainName + if err := mc.apiCall("v1/"+endPoint, rest.Get, nil, lopt, &links, &domains); err != nil { + return nil, err + } + if lopt != nil { // Fetch more pages to reach our limit + var domainSlice []DomainName + for (lopt.All || lopt.Limit > len(domains)) && links.next != nil { + newlopt := links.next + links = apiLinks{} + if err := mc.apiCall("v1/"+endPoint, rest.Get, nil, newlopt, &links, &domainSlice); err != nil { + return nil, err + } + domains = append(domains, domainSlice...) + domainSlice = domainSlice[:0] // Clear struct + } + } + return domains, nil +} + +// BlockDomain blocks the specified domain +func (mc *Client) BlockDomain(domain DomainName) error { + const endPoint = "domain_blocks" + params := make(apiCallParams) + params["domain"] = string(domain) + return mc.apiCall("v1/"+endPoint, rest.Post, params, nil, nil, nil) +} + +// UnblockDomain unblocks the specified domain +func (mc *Client) UnblockDomain(domain DomainName) error { + const endPoint = "domain_blocks" + params := make(apiCallParams) + params["domain"] = string(domain) + return mc.apiCall("v1/"+endPoint, rest.Delete, params, nil, nil, nil) +} diff --git a/vendor/github.com/McKael/madon/v2/emoji.go b/vendor/github.com/McKael/madon/v2/emoji.go new file mode 100644 index 0000000..7ae8e78 --- /dev/null +++ b/vendor/github.com/McKael/madon/v2/emoji.go @@ -0,0 +1,20 @@ +/* +Copyright 2018 Mikael Berthe + +Licensed under the MIT license. Please see the LICENSE file is this directory. +*/ + +package madon + +import ( + "github.com/sendgrid/rest" +) + +// GetCustomEmojis returns a list with the server custom emojis +func (mc *Client) GetCustomEmojis(lopt *LimitParams) ([]Emoji, error) { + var emojiList []Emoji + if err := mc.apiCall("v1/custom_emojis", rest.Get, nil, lopt, nil, &emojiList); err != nil { + return nil, err + } + return emojiList, nil +} diff --git a/vendor/github.com/McKael/madon/v2/endorsements.go b/vendor/github.com/McKael/madon/v2/endorsements.go new file mode 100644 index 0000000..e8682bd --- /dev/null +++ b/vendor/github.com/McKael/madon/v2/endorsements.go @@ -0,0 +1,46 @@ +/* +Copyright 2018 Mikael Berthe + +Licensed under the MIT license. Please see the LICENSE file is this directory. +*/ + +package madon + +import ( + "github.com/sendgrid/rest" +) + +// GetEndorsements returns the list of user's endorsements +func (mc *Client) GetEndorsements(lopt *LimitParams) ([]Account, error) { + endPoint := "endorsements" + method := rest.Get + var accountList []Account + if err := mc.apiCall("v1/"+endPoint, method, nil, lopt, nil, &accountList); err != nil { + return nil, err + } + return accountList, nil +} + +// PinAccount adds the account to the endorsement list +func (mc *Client) PinAccount(accountID int64) (*Relationship, error) { + rel, err := mc.updateRelationship("pin", accountID, nil) + if err != nil { + return nil, err + } + if rel == nil { + return nil, ErrEntityNotFound + } + return rel, nil +} + +// UnpinAccount removes the account from the endorsement list +func (mc *Client) UnpinAccount(accountID int64) (*Relationship, error) { + rel, err := mc.updateRelationship("unpin", accountID, nil) + if err != nil { + return nil, err + } + if rel == nil { + return nil, ErrEntityNotFound + } + return rel, nil +} diff --git a/vendor/github.com/McKael/madon/v2/go.mod b/vendor/github.com/McKael/madon/v2/go.mod new file mode 100644 index 0000000..609c35b --- /dev/null +++ b/vendor/github.com/McKael/madon/v2/go.mod @@ -0,0 +1,14 @@ +module github.com/McKael/madon/v2 + +require ( + github.com/davecgh/go-spew v1.1.1 + github.com/gorilla/websocket v1.4.0 + github.com/pkg/errors v0.8.0 + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sendgrid/rest v2.4.1+incompatible + github.com/stretchr/testify v1.2.2 + golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3 + golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be + golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f // indirect + google.golang.org/appengine v1.2.0 // indirect +) diff --git a/vendor/github.com/McKael/madon/v2/go.sum b/vendor/github.com/McKael/madon/v2/go.sum new file mode 100644 index 0000000..5ce0724 --- /dev/null +++ b/vendor/github.com/McKael/madon/v2/go.sum @@ -0,0 +1,24 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sendgrid/rest v2.4.1+incompatible h1:HDib/5xzQREPq34lN3YMhQtMkdXxS/qLp5G3k9a5++4= +github.com/sendgrid/rest v2.4.1+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/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-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +google.golang.org/appengine v1.2.0 h1:S0iUepdCWODXRvtE+gcRDd15L+k+k1AiHlMiMjefH24= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/vendor/github.com/McKael/madon/v2/instance.go b/vendor/github.com/McKael/madon/v2/instance.go new file mode 100644 index 0000000..baa8354 --- /dev/null +++ b/vendor/github.com/McKael/madon/v2/instance.go @@ -0,0 +1,42 @@ +/* +Copyright 2017-2018 Mikael Berthe + +Licensed under the MIT license. Please see the LICENSE file is this directory. +*/ + +package madon + +import ( + "github.com/sendgrid/rest" +) + +// GetCurrentInstance returns current instance information +func (mc *Client) GetCurrentInstance() (*Instance, error) { + var i Instance + if err := mc.apiCall("v1/instance", rest.Get, nil, nil, nil, &i); err != nil { + return nil, err + } + return &i, nil +} + +// GetInstancePeers returns current instance peers +// The peers are defined as the domains of users the instance has previously +// resolved. +func (mc *Client) GetInstancePeers() ([]InstancePeer, error) { + var peers []InstancePeer + if err := mc.apiCall("v1/instance/peers", rest.Get, nil, nil, nil, &peers); err != nil { + return nil, err + } + return peers, nil +} + +// GetInstanceActivity returns current instance activity +// The activity contains the counts of active users, locally posted statuses, +// and new registrations in weekly buckets. +func (mc *Client) GetInstanceActivity() ([]WeekActivity, error) { + var activity []WeekActivity + if err := mc.apiCall("v1/instance/activity", rest.Get, nil, nil, nil, &activity); err != nil { + return nil, err + } + return activity, nil +} diff --git a/vendor/github.com/McKael/madon/v2/lists.go b/vendor/github.com/McKael/madon/v2/lists.go new file mode 100644 index 0000000..0420e44 --- /dev/null +++ b/vendor/github.com/McKael/madon/v2/lists.go @@ -0,0 +1,136 @@ +/* +Copyright 2018 Mikael Berthe + +Licensed under the MIT license. Please see the LICENSE file is this directory. +*/ + +package madon + +import ( + "fmt" + "strconv" + + "github.com/pkg/errors" + "github.com/sendgrid/rest" +) + +// GetList returns a List entity +func (mc *Client) GetList(listID int64) (*List, error) { + if listID <= 0 { + return nil, errors.New("invalid list ID") + } + endPoint := "lists/" + strconv.FormatInt(listID, 10) + var list List + if err := mc.apiCall("v1/"+endPoint, rest.Get, nil, nil, nil, &list); err != nil { + return nil, err + } + return &list, nil +} + +// GetLists returns a list of List entities +// If accountID is > 0, this will return the lists containing this account. +// If lopt.All is true, several requests will be made until the API server +// has nothing to return. +func (mc *Client) GetLists(accountID int64, lopt *LimitParams) ([]List, error) { + endPoint := "lists" + + if accountID > 0 { + endPoint = "accounts/" + strconv.FormatInt(accountID, 10) + "/lists" + } + + var lists []List + var links apiLinks + if err := mc.apiCall("v1/"+endPoint, rest.Get, nil, lopt, &links, &lists); err != nil { + return nil, err + } + if lopt != nil { // Fetch more pages to reach our limit + var listSlice []List + for (lopt.All || lopt.Limit > len(lists)) && links.next != nil { + newlopt := links.next + links = apiLinks{} + if err := mc.apiCall("v1/"+endPoint, rest.Get, nil, newlopt, &links, &listSlice); err != nil { + return nil, err + } + lists = append(lists, listSlice...) + listSlice = listSlice[:0] // Clear struct + } + } + return lists, nil +} + +// CreateList creates a List +func (mc *Client) CreateList(title string) (*List, error) { + params := apiCallParams{"title": title} + method := rest.Post + return mc.setSingleList(method, 0, params) +} + +// UpdateList updates an existing List +func (mc *Client) UpdateList(listID int64, title string) (*List, error) { + if listID <= 0 { + return nil, errors.New("invalid list ID") + } + params := apiCallParams{"title": title} + method := rest.Put + return mc.setSingleList(method, listID, params) +} + +// DeleteList deletes a list +func (mc *Client) DeleteList(listID int64) error { + if listID <= 0 { + return errors.New("invalid list ID") + } + method := rest.Delete + _, err := mc.setSingleList(method, listID, nil) + return err +} + +// GetListAccounts returns the accounts belonging to a given list +func (mc *Client) GetListAccounts(listID int64, lopt *LimitParams) ([]Account, error) { + endPoint := "lists/" + strconv.FormatInt(listID, 10) + "/accounts" + return mc.getMultipleAccounts(endPoint, nil, lopt) +} + +// AddListAccounts adds the accounts to a given list +func (mc *Client) AddListAccounts(listID int64, accountIDs []int64) error { + endPoint := "lists/" + strconv.FormatInt(listID, 10) + "/accounts" + method := rest.Post + params := make(apiCallParams) + for i, id := range accountIDs { + if id < 1 { + return ErrInvalidID + } + qID := fmt.Sprintf("[%d]account_ids", i) + params[qID] = strconv.FormatInt(id, 10) + } + return mc.apiCall("v1/"+endPoint, method, params, nil, nil, nil) +} + +// RemoveListAccounts removes the accounts from the given list +func (mc *Client) RemoveListAccounts(listID int64, accountIDs []int64) error { + endPoint := "lists/" + strconv.FormatInt(listID, 10) + "/accounts" + method := rest.Delete + params := make(apiCallParams) + for i, id := range accountIDs { + if id < 1 { + return ErrInvalidID + } + qID := fmt.Sprintf("[%d]account_ids", i) + params[qID] = strconv.FormatInt(id, 10) + } + return mc.apiCall("v1/"+endPoint, method, params, nil, nil, nil) +} + +func (mc *Client) setSingleList(method rest.Method, listID int64, params apiCallParams) (*List, error) { + var endPoint string + if listID > 0 { + endPoint = "lists/" + strconv.FormatInt(listID, 10) + } else { + endPoint = "lists" + } + var list List + if err := mc.apiCall("v1/"+endPoint, method, params, nil, nil, &list); err != nil { + return nil, err + } + return &list, nil +} diff --git a/vendor/github.com/McKael/madon/v2/login.go b/vendor/github.com/McKael/madon/v2/login.go new file mode 100644 index 0000000..3c443a6 --- /dev/null +++ b/vendor/github.com/McKael/madon/v2/login.go @@ -0,0 +1,129 @@ +/* +Copyright 2017-2018 Mikael Berthe + +Licensed under the MIT license. Please see the LICENSE file is this directory. +*/ + +package madon + +import ( + "encoding/json" + "strings" + + "golang.org/x/net/context" + "golang.org/x/oauth2" + + "github.com/pkg/errors" + "github.com/sendgrid/rest" +) + +const oAuthRelPath = "/oauth/" + +// UserToken represents a user token as returned by the Mastodon API +type UserToken struct { + AccessToken string `json:"access_token"` + CreatedAt int64 `json:"created_at"` + Scope string `json:"scope"` + TokenType string `json:"token_type"` +} + +// LoginBasic does basic user authentication +func (mc *Client) LoginBasic(username, password string, scopes []string) error { + if mc == nil { + return ErrUninitializedClient + } + + if username == "" { + return errors.New("missing username") + } + if password == "" { + return errors.New("missing password") + } + + hdrs := make(map[string]string) + opts := make(map[string]string) + + hdrs["User-Agent"] = "madon/" + MadonVersion + + opts["grant_type"] = "password" + opts["client_id"] = mc.ID + opts["client_secret"] = mc.Secret + opts["username"] = username + opts["password"] = password + if len(scopes) > 0 { + opts["scope"] = strings.Join(scopes, " ") + } + + req := rest.Request{ + BaseURL: mc.InstanceURL + oAuthRelPath + "token", + Headers: hdrs, + QueryParams: opts, + Method: rest.Post, + } + + r, err := restAPI(req) + if err != nil { + return err + } + + var resp UserToken + + err = json.Unmarshal([]byte(r.Body), &resp) + if err != nil { + return errors.Wrap(err, "cannot unmarshal server response") + } + + mc.UserToken = &resp + return nil +} + +// SetUserToken sets an existing user credentials +// No verification of the arguments is made. +func (mc *Client) SetUserToken(token, username, password string, scopes []string) error { + if mc == nil { + return ErrUninitializedClient + } + + mc.UserToken = &UserToken{ + AccessToken: token, + Scope: strings.Join(scopes, " "), + TokenType: "bearer", + } + return nil +} + +// LoginOAuth2 handles OAuth2 authentication +// If code is empty, the URL to the server consent page will be returned; +// if not, the user token is set. +func (mc *Client) LoginOAuth2(code string, scopes []string) (string, error) { + if mc == nil { + return "", ErrUninitializedClient + } + + conf := &oauth2.Config{ + ClientID: mc.ID, + ClientSecret: mc.Secret, + Scopes: scopes, + Endpoint: oauth2.Endpoint{ + AuthURL: mc.InstanceURL + oAuthRelPath + "authorize", + TokenURL: mc.InstanceURL + oAuthRelPath + "token", + }, + RedirectURL: NoRedirect, + } + + if code == "" { + // URL to consent page to ask for permission + // for the scopes specified above. + return conf.AuthCodeURL("state", oauth2.AccessTypeOffline), nil + } + + // Return token + t, err := conf.Exchange(context.TODO(), code) + if err != nil { + return "", errors.Wrap(err, "cannot convert code into a token") + } + if t == nil || t.AccessToken == "" { + return "", errors.New("empty token") + } + return "", mc.SetUserToken(t.AccessToken, "", "", scopes) +} diff --git a/vendor/github.com/McKael/madon/v2/madon.go b/vendor/github.com/McKael/madon/v2/madon.go new file mode 100644 index 0000000..cc23fba --- /dev/null +++ b/vendor/github.com/McKael/madon/v2/madon.go @@ -0,0 +1,41 @@ +/* +Copyright 2017-2018 Mikael Berthe +Copyright 2017 Ollivier Robert + +Licensed under the MIT license. Please see the LICENSE file is this directory. +*/ + +package madon + +import ( + "github.com/pkg/errors" +) + +// LimitParams contains common limit/paging options for the Mastodon REST API +type LimitParams struct { + Limit int // Number of items per query + SinceID, MaxID int64 // Boundaries + All bool // Get as many items as possible +} + +// apiCallParams is a map with the parameters for an API call +type apiCallParams map[string]string + +const ( + // MadonVersion contains the version of the Madon library + MadonVersion = "2.4.0-dev" + + currentAPIPath = "/api" + + // NoRedirect is the URI for no redirection in the App registration + NoRedirect = "urn:ietf:wg:oauth:2.0:oob" +) + +// Error codes +var ( + ErrUninitializedClient = errors.New("use of uninitialized madon client") + ErrAlreadyRegistered = errors.New("app already registered") + ErrEntityNotFound = errors.New("entity not found") + ErrInvalidParameter = errors.New("incorrect parameter") + ErrInvalidID = errors.New("incorrect entity ID") +) diff --git a/vendor/github.com/McKael/madon/v2/media.go b/vendor/github.com/McKael/madon/v2/media.go new file mode 100644 index 0000000..8c5f6d5 --- /dev/null +++ b/vendor/github.com/McKael/madon/v2/media.go @@ -0,0 +1,108 @@ +/* +Copyright 2017-2018 Mikael Berthe + +Licensed under the MIT license. Please see the LICENSE file is this directory. +*/ + +package madon + +import ( + "bytes" + "encoding/json" + "io" + "mime/multipart" + "os" + "path/filepath" + "strconv" + + "github.com/pkg/errors" + "github.com/sendgrid/rest" +) + +// UploadMedia uploads the given file and returns an attachment +// The description and focus arguments can be empty strings. +// 'focus' is the "focal point", written as two comma-delimited floating points. +func (mc *Client) UploadMedia(filePath, description, focus string) (*Attachment, error) { + var b bytes.Buffer + + if filePath == "" { + return nil, ErrInvalidParameter + } + + f, err := os.Open(filePath) + if err != nil { + return nil, errors.Wrap(err, "cannot read file") + } + defer f.Close() + + w := multipart.NewWriter(&b) + formWriter, err := w.CreateFormFile("file", filepath.Base(filePath)) + if err != nil { + return nil, errors.Wrap(err, "media upload") + } + if _, err = io.Copy(formWriter, f); err != nil { + return nil, errors.Wrap(err, "media upload") + } + + w.Close() + + var params apiCallParams + if description != "" || focus != "" { + params = make(apiCallParams) + if description != "" { + params["description"] = description + } + if focus != "" { + params["focus"] = focus + } + } + + req, err := mc.prepareRequest("media", rest.Post, params) + if err != nil { + return nil, errors.Wrap(err, "media prepareRequest failed") + } + req.Headers["Content-Type"] = w.FormDataContentType() + req.Body = b.Bytes() + + // Make API call + r, err := restAPI(req) + if err != nil { + return nil, errors.Wrap(err, "media upload 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 attachment Attachment + err = json.Unmarshal([]byte(r.Body), &attachment) + if err != nil { + return nil, errors.Wrap(err, "cannot decode API response (media)") + } + return &attachment, nil +} + +// UpdateMedia updates the description and focal point of a media +// One of the description and focus arguments can be nil to not be updated. +func (mc *Client) UpdateMedia(mediaID int64, description, focus *string) (*Attachment, error) { + params := make(apiCallParams) + if description != nil { + params["description"] = *description + } + if focus != nil { + params["focus"] = *focus + } + + endPoint := "media/" + strconv.FormatInt(mediaID, 10) + var attachment Attachment + if err := mc.apiCall("v1/"+endPoint, rest.Put, params, nil, nil, &attachment); err != nil { + return nil, err + } + return &attachment, nil +} diff --git a/vendor/github.com/McKael/madon/v2/notifications.go b/vendor/github.com/McKael/madon/v2/notifications.go new file mode 100644 index 0000000..e0e33a9 --- /dev/null +++ b/vendor/github.com/McKael/madon/v2/notifications.go @@ -0,0 +1,90 @@ +/* +Copyright 2017-2018 Mikael Berthe + +Licensed under the MIT license. Please see the LICENSE file is this directory. +*/ + +package madon + +import ( + "fmt" + "strconv" + + "github.com/sendgrid/rest" +) + +// GetNotifications returns the list of the user's notifications +// excludeTypes is an array of notifications to exclude ("follow", "favourite", +// "reblog", "mention"). It can be nil. +// 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) GetNotifications(excludeTypes []string, lopt *LimitParams) ([]Notification, error) { + var notifications []Notification + var links apiLinks + var params apiCallParams + + if len(excludeTypes) > 0 { + params = make(apiCallParams) + for i, eType := range excludeTypes { + qID := fmt.Sprintf("[%d]exclude_types", i) + params[qID] = eType + } + } + + if err := mc.apiCall("v1/notifications", rest.Get, params, lopt, &links, ¬ifications); err != nil { + return nil, err + } + if lopt != nil { // Fetch more pages to reach our limit + var notifSlice []Notification + for (lopt.All || lopt.Limit > len(notifications)) && links.next != nil { + newlopt := links.next + links = apiLinks{} + if err := mc.apiCall("v1/notifications", rest.Get, nil, newlopt, &links, ¬ifSlice); err != nil { + return nil, err + } + notifications = append(notifications, notifSlice...) + notifSlice = notifSlice[:0] // Clear struct + } + } + return notifications, nil +} + +// GetNotification returns a notification +// The returned notification can be nil if there is an error or if the +// requested notification does not exist. +func (mc *Client) GetNotification(notificationID int64) (*Notification, error) { + if notificationID < 1 { + return nil, ErrInvalidID + } + + var endPoint = "notifications/" + strconv.FormatInt(notificationID, 10) + var notification Notification + if err := mc.apiCall("v1/"+endPoint, rest.Get, nil, nil, nil, ¬ification); err != nil { + return nil, err + } + if notification.ID == 0 { + return nil, ErrEntityNotFound + } + return ¬ification, nil +} + +// DismissNotification deletes a notification +func (mc *Client) DismissNotification(notificationID int64) error { + if notificationID < 1 { + return ErrInvalidID + } + + endPoint := "notifications/dismiss" + params := apiCallParams{"id": strconv.FormatInt(notificationID, 10)} + err := mc.apiCall("v1/"+endPoint, rest.Post, params, nil, nil, &Notification{}) + return err +} + +// ClearNotifications deletes all notifications from the Mastodon server for +// the authenticated user +func (mc *Client) ClearNotifications() error { + err := mc.apiCall("v1/notifications/clear", rest.Post, nil, nil, nil, &Notification{}) + return err +} diff --git a/vendor/github.com/McKael/madon/v2/report.go b/vendor/github.com/McKael/madon/v2/report.go new file mode 100644 index 0000000..976bb74 --- /dev/null +++ b/vendor/github.com/McKael/madon/v2/report.go @@ -0,0 +1,48 @@ +/* +Copyright 2017-2018 Mikael Berthe + +Licensed under the MIT license. Please see the LICENSE file is this directory. +*/ + +package madon + +import ( + "fmt" + "strconv" + + "github.com/sendgrid/rest" +) + +// GetReports returns the current user's reports +// (I don't know if the limit options are used by the API server.) +func (mc *Client) GetReports(lopt *LimitParams) ([]Report, error) { + var reports []Report + if err := mc.apiCall("v1/reports", rest.Get, nil, lopt, nil, &reports); err != nil { + return nil, err + } + return reports, nil +} + +// ReportUser reports the user account +func (mc *Client) ReportUser(accountID int64, statusIDs []int64, comment string) (*Report, error) { + if accountID < 1 || comment == "" || len(statusIDs) < 1 { + return nil, ErrInvalidParameter + } + + params := make(apiCallParams) + params["account_id"] = strconv.FormatInt(accountID, 10) + params["comment"] = comment + for i, id := range statusIDs { + if id < 1 { + return nil, ErrInvalidID + } + qID := fmt.Sprintf("[%d]status_ids", i) + params[qID] = strconv.FormatInt(id, 10) + } + + var report Report + if err := mc.apiCall("v1/reports", rest.Post, params, nil, nil, &report); err != nil { + return nil, err + } + return &report, nil +} diff --git a/vendor/github.com/McKael/madon/v2/search.go b/vendor/github.com/McKael/madon/v2/search.go new file mode 100644 index 0000000..417a921 --- /dev/null +++ b/vendor/github.com/McKael/madon/v2/search.go @@ -0,0 +1,68 @@ +/* +Copyright 2017-2018 Mikael Berthe + +Licensed under the MIT license. Please see the LICENSE file is this directory. +*/ + +package madon + +import ( + "strings" + + "github.com/sendgrid/rest" +) + +// Search search for contents (accounts or statuses) and returns a Results +func (mc *Client) searchV1(params apiCallParams) (*Results, error) { + // We use a custom structure with shadowed Hashtags field, + // since the v1 version only returns strings. + var resultsV1 struct { + Results + Hashtags []string `json:"hashtags"` + } + if err := mc.apiCall("v1/"+"search", rest.Get, params, nil, nil, &resultsV1); err != nil { + return nil, err + } + + var results Results + results.Accounts = resultsV1.Accounts + results.Statuses = resultsV1.Statuses + for _, t := range resultsV1.Hashtags { + results.Hashtags = append(results.Hashtags, Tag{Name: t}) + } + + return &results, nil +} + +func (mc *Client) searchV2(params apiCallParams) (*Results, error) { + var results Results + if err := mc.apiCall("v2/"+"search", rest.Get, params, nil, nil, &results); err != nil { + return nil, err + } + + return &results, nil +} + +// Search search for contents (accounts or statuses) and returns a Results +func (mc *Client) Search(query string, resolve bool) (*Results, error) { + if query == "" { + return nil, ErrInvalidParameter + } + + // The parameters are the same in both v1 & v2 API versions + params := make(apiCallParams) + params["q"] = query + if resolve { + params["resolve"] = "true" + } + + r, err := mc.searchV2(params) + + // This is not a very beautiful way to check the error cause, I admit. + if err != nil && strings.Contains(err.Error(), "bad server status code (404)") { + // Fall back to v1 API endpoint + r, err = mc.searchV1(params) + } + + return r, err +} diff --git a/vendor/github.com/McKael/madon/v2/status.go b/vendor/github.com/McKael/madon/v2/status.go new file mode 100644 index 0000000..644a917 --- /dev/null +++ b/vendor/github.com/McKael/madon/v2/status.go @@ -0,0 +1,309 @@ +/* +Copyright 2017-2018 Mikael Berthe + +Licensed under the MIT license. Please see the LICENSE file is this directory. +*/ + +package madon + +import ( + "fmt" + "strconv" + + "github.com/pkg/errors" + "github.com/sendgrid/rest" +) + +// PostStatusParams contains option fields for the PostStatus command +type PostStatusParams struct { + Text string + InReplyTo int64 + MediaIDs []int64 + Sensitive bool + SpoilerText string + Visibility string +} + +// updateStatusOptions contains option fields for POST and DELETE API calls +type updateStatusOptions struct { + // The ID is used for most commands + ID int64 + + // The following fields are used for posting a new status + Status string + InReplyToID int64 + MediaIDs []int64 + Sensitive bool + SpoilerText string + Visibility string // "direct", "private", "unlisted" or "public" +} + +// getMultipleStatuses returns a list of status entities +// If lopt.All is true, several requests will be made until the API server +// has nothing to return. +func (mc *Client) getMultipleStatuses(endPoint string, params apiCallParams, lopt *LimitParams) ([]Status, error) { + var statuses []Status + var links apiLinks + if err := mc.apiCall("v1/"+endPoint, rest.Get, params, lopt, &links, &statuses); err != nil { + return nil, err + } + if lopt != nil { // Fetch more pages to reach our limit + var statusSlice []Status + for (lopt.All || lopt.Limit > len(statuses)) && links.next != nil { + newlopt := links.next + links = apiLinks{} + if err := mc.apiCall("v1/"+endPoint, rest.Get, params, newlopt, &links, &statusSlice); err != nil { + return nil, err + } + statuses = append(statuses, statusSlice...) + statusSlice = statusSlice[:0] // Clear struct + } + } + return statuses, nil +} + +// queryStatusData queries the statuses API +// The operation 'op' can be empty or "status" (the status itself), "context", +// "card", "reblogged_by", "favourited_by". +// The data argument will receive the object(s) returned by the API server. +func (mc *Client) queryStatusData(statusID int64, op string, data interface{}) error { + if statusID < 1 { + return ErrInvalidID + } + + endPoint := "statuses/" + strconv.FormatInt(statusID, 10) + + if op != "" && op != "status" { + switch op { + case "context", "card", "reblogged_by", "favourited_by": + default: + return ErrInvalidParameter + } + + endPoint += "/" + op + } + + return mc.apiCall("v1/"+endPoint, rest.Get, nil, nil, nil, data) +} + +// updateStatusData updates the statuses +// The operation 'op' can be empty or "status" (to post a status), "delete" +// (for deleting a status), "reblog"/"unreblog", "favourite"/"unfavourite", +// "mute"/"unmute" (for conversations) or "pin"/"unpin". +// The data argument will receive the object(s) returned by the API server. +func (mc *Client) updateStatusData(op string, opts updateStatusOptions, data interface{}) error { + method := rest.Post + endPoint := "statuses" + params := make(apiCallParams) + + switch op { + case "", "status": + op = "status" + if opts.Status == "" { + return ErrInvalidParameter + } + switch opts.Visibility { +