vendor/github.com/McKael/madon/v3/account.go
changeset 265 05c40b36d3b2
parent 242 2a9ec03fe5a1
child 268 4dd196a4ee7c
equal deleted inserted replaced
264:8f478162d991 265:05c40b36d3b2
       
     1 /*
       
     2 Copyright 2017-2018 Mikael Berthe
       
     3 
       
     4 Licensed under the MIT license.  Please see the LICENSE file is this directory.
       
     5 */
       
     6 
       
     7 package madon
       
     8 
       
     9 import (
       
    10 	"bytes"
       
    11 	"encoding/json"
       
    12 	"fmt"
       
    13 	"mime/multipart"
       
    14 	"os"
       
    15 	"path/filepath"
       
    16 	"strconv"
       
    17 
       
    18 	"github.com/pkg/errors"
       
    19 	"github.com/sendgrid/rest"
       
    20 )
       
    21 
       
    22 // getAccountsOptions contains option fields for POST and DELETE API calls
       
    23 type getAccountsOptions struct {
       
    24 	// The ID is used for most commands
       
    25 	ID int64
       
    26 
       
    27 	// Following can be set to true to limit a search to "following" accounts
       
    28 	Following bool
       
    29 
       
    30 	// The Q field (query) is used when searching for accounts
       
    31 	Q string
       
    32 
       
    33 	Limit *LimitParams
       
    34 }
       
    35 
       
    36 // UpdateAccountParams contains option fields for the UpdateAccount command
       
    37 type UpdateAccountParams struct {
       
    38 	DisplayName      *string
       
    39 	Note             *string
       
    40 	AvatarImagePath  *string
       
    41 	HeaderImagePath  *string
       
    42 	Locked           *bool
       
    43 	Bot              *bool
       
    44 	FieldsAttributes *[]Field
       
    45 	Source           *SourceParams
       
    46 }
       
    47 
       
    48 // updateRelationship returns a Relationship entity
       
    49 // The operation 'op' can be "follow", "unfollow", "block", "unblock",
       
    50 // "mute", "unmute".
       
    51 // The id is optional and depends on the operation.
       
    52 func (mc *Client) updateRelationship(op string, id int64, params apiCallParams) (*Relationship, error) {
       
    53 	var endPoint string
       
    54 	method := rest.Post
       
    55 	strID := strconv.FormatInt(id, 10)
       
    56 
       
    57 	switch op {
       
    58 	case "follow", "unfollow", "block", "unblock", "mute", "unmute", "pin", "unpin":
       
    59 		endPoint = "accounts/" + strID + "/" + op
       
    60 	default:
       
    61 		return nil, ErrInvalidParameter
       
    62 	}
       
    63 
       
    64 	var rel Relationship
       
    65 	if err := mc.apiCall("v1/"+endPoint, method, params, nil, nil, &rel); err != nil {
       
    66 		return nil, err
       
    67 	}
       
    68 	return &rel, nil
       
    69 }
       
    70 
       
    71 // getSingleAccount returns an account entity
       
    72 // The operation 'op' can be "account", "verify_credentials",
       
    73 // "follow_requests/authorize" or // "follow_requests/reject".
       
    74 // The id is optional and depends on the operation.
       
    75 func (mc *Client) getSingleAccount(op string, id int64) (*Account, error) {
       
    76 	var endPoint string
       
    77 	method := rest.Get
       
    78 	strID := strconv.FormatInt(id, 10)
       
    79 
       
    80 	switch op {
       
    81 	case "account":
       
    82 		endPoint = "accounts/" + strID
       
    83 	case "verify_credentials":
       
    84 		endPoint = "accounts/verify_credentials"
       
    85 	case "follow_requests/authorize", "follow_requests/reject":
       
    86 		// The documentation is incorrect, the endpoint actually
       
    87 		// is "follow_requests/:id/{authorize|reject}"
       
    88 		endPoint = op[:16] + strID + "/" + op[16:]
       
    89 		method = rest.Post
       
    90 	default:
       
    91 		return nil, ErrInvalidParameter
       
    92 	}
       
    93 
       
    94 	var account Account
       
    95 	if err := mc.apiCall("v1/"+endPoint, method, nil, nil, nil, &account); err != nil {
       
    96 		return nil, err
       
    97 	}
       
    98 	return &account, nil
       
    99 }
       
   100 
       
   101 // getMultipleAccounts returns a list of account entities
       
   102 // If lopt.All is true, several requests will be made until the API server
       
   103 // has nothing to return.
       
   104 func (mc *Client) getMultipleAccounts(endPoint string, params apiCallParams, lopt *LimitParams) ([]Account, error) {
       
   105 	var accounts []Account
       
   106 	var links apiLinks
       
   107 	if err := mc.apiCall("v1/"+endPoint, rest.Get, params, lopt, &links, &accounts); err != nil {
       
   108 		return nil, err
       
   109 	}
       
   110 	if lopt != nil { // Fetch more pages to reach our limit
       
   111 		var accountSlice []Account
       
   112 		for (lopt.All || lopt.Limit > len(accounts)) && links.next != nil {
       
   113 			newlopt := links.next
       
   114 			links = apiLinks{}
       
   115 			if err := mc.apiCall("v1/"+endPoint, rest.Get, params, newlopt, &links, &accountSlice); err != nil {
       
   116 				return nil, err
       
   117 			}
       
   118 			accounts = append(accounts, accountSlice...)
       
   119 			accountSlice = accountSlice[:0] // Clear struct
       
   120 		}
       
   121 	}
       
   122 	return accounts, nil
       
   123 }
       
   124 
       
   125 // getMultipleAccountsHelper returns a list of account entities
       
   126 // The operation 'op' can be "followers", "following", "search", "blocks",
       
   127 // "mutes", "follow_requests".
       
   128 // The id is optional and depends on the operation.
       
   129 // If opts.All is true, several requests will be made until the API server
       
   130 // has nothing to return.
       
   131 func (mc *Client) getMultipleAccountsHelper(op string, opts *getAccountsOptions) ([]Account, error) {
       
   132 	var endPoint string
       
   133 	var lopt *LimitParams
       
   134 
       
   135 	if opts != nil {
       
   136 		lopt = opts.Limit
       
   137 	}
       
   138 
       
   139 	switch op {
       
   140 	case "followers", "following":
       
   141 		if opts == nil || opts.ID < 1 {
       
   142 			return []Account{}, ErrInvalidID
       
   143 		}
       
   144 		endPoint = "accounts/" + strconv.FormatInt(opts.ID, 10) + "/" + op
       
   145 	case "follow_requests", "blocks", "mutes":
       
   146 		endPoint = op
       
   147 	case "search":
       
   148 		if opts == nil || opts.Q == "" {
       
   149 			return []Account{}, ErrInvalidParameter
       
   150 		}
       
   151 		endPoint = "accounts/" + op
       
   152 	case "reblogged_by", "favourited_by":
       
   153 		if opts == nil || opts.ID < 1 {
       
   154 			return []Account{}, ErrInvalidID
       
   155 		}
       
   156 		endPoint = "statuses/" + strconv.FormatInt(opts.ID, 10) + "/" + op
       
   157 	default:
       
   158 		return nil, ErrInvalidParameter
       
   159 	}
       
   160 
       
   161 	// Handle target-specific query parameters
       
   162 	params := make(apiCallParams)
       
   163 	if op == "search" {
       
   164 		params["q"] = opts.Q
       
   165 		if opts.Following {
       
   166 			params["following"] = "true"
       
   167 		}
       
   168 	}
       
   169 
       
   170 	return mc.getMultipleAccounts(endPoint, params, lopt)
       
   171 }
       
   172 
       
   173 // GetAccount returns an account entity
       
   174 // The returned value can be nil if there is an error or if the
       
   175 // requested ID does not exist.
       
   176 func (mc *Client) GetAccount(accountID int64) (*Account, error) {
       
   177 	account, err := mc.getSingleAccount("account", accountID)
       
   178 	if err != nil {
       
   179 		return nil, err
       
   180 	}
       
   181 	if account != nil && account.ID == 0 {
       
   182 		return nil, ErrEntityNotFound
       
   183 	}
       
   184 	return account, nil
       
   185 }
       
   186 
       
   187 // GetCurrentAccount returns the current user account
       
   188 func (mc *Client) GetCurrentAccount() (*Account, error) {
       
   189 	account, err := mc.getSingleAccount("verify_credentials", 0)
       
   190 	if err != nil {
       
   191 		return nil, err
       
   192 	}
       
   193 	if account != nil && account.ID == 0 {
       
   194 		return nil, ErrEntityNotFound
       
   195 	}
       
   196 	return account, nil
       
   197 }
       
   198 
       
   199 // GetAccountFollowers returns the list of accounts following a given account
       
   200 func (mc *Client) GetAccountFollowers(accountID int64, lopt *LimitParams) ([]Account, error) {
       
   201 	o := &getAccountsOptions{ID: accountID, Limit: lopt}
       
   202 	return mc.getMultipleAccountsHelper("followers", o)
       
   203 }
       
   204 
       
   205 // GetAccountFollowing returns the list of accounts a given account is following
       
   206 func (mc *Client) GetAccountFollowing(accountID int64, lopt *LimitParams) ([]Account, error) {
       
   207 	o := &getAccountsOptions{ID: accountID, Limit: lopt}
       
   208 	return mc.getMultipleAccountsHelper("following", o)
       
   209 }
       
   210 
       
   211 // FollowAccount follows an account
       
   212 // 'reblogs' can be used to specify if boots should be displayed or hidden.
       
   213 func (mc *Client) FollowAccount(accountID int64, reblogs *bool) (*Relationship, error) {
       
   214 	var params apiCallParams
       
   215 	if reblogs != nil {
       
   216 		params = make(apiCallParams)
       
   217 		if *reblogs {
       
   218 			params["reblogs"] = "true"
       
   219 		} else {
       
   220 			params["reblogs"] = "false"
       
   221 		}
       
   222 	}
       
   223 	rel, err := mc.updateRelationship("follow", accountID, params)
       
   224 	if err != nil {
       
   225 		return nil, err
       
   226 	}
       
   227 	if rel == nil {
       
   228 		return nil, ErrEntityNotFound
       
   229 	}
       
   230 	return rel, nil
       
   231 }
       
   232 
       
   233 // UnfollowAccount unfollows an account
       
   234 func (mc *Client) UnfollowAccount(accountID int64) (*Relationship, error) {
       
   235 	rel, err := mc.updateRelationship("unfollow", accountID, nil)
       
   236 	if err != nil {
       
   237 		return nil, err
       
   238 	}
       
   239 	if rel == nil {
       
   240 		return nil, ErrEntityNotFound
       
   241 	}
       
   242 	return rel, nil
       
   243 }
       
   244 
       
   245 // FollowRemoteAccount follows a remote account
       
   246 // The parameter 'uri' is a URI (e.g. "username@domain").
       
   247 func (mc *Client) FollowRemoteAccount(uri string) (*Account, error) {
       
   248 	if uri == "" {
       
   249 		return nil, ErrInvalidID
       
   250 	}
       
   251 
       
   252 	params := make(apiCallParams)
       
   253 	params["uri"] = uri
       
   254 
       
   255 	var account Account
       
   256 	if err := mc.apiCall("v1/follows", rest.Post, params, nil, nil, &account); err != nil {
       
   257 		return nil, err
       
   258 	}
       
   259 	if account.ID == 0 {
       
   260 		return nil, ErrEntityNotFound
       
   261 	}
       
   262 	return &account, nil
       
   263 }
       
   264 
       
   265 // BlockAccount blocks an account
       
   266 func (mc *Client) BlockAccount(accountID int64) (*Relationship, error) {
       
   267 	rel, err := mc.updateRelationship("block", accountID, nil)
       
   268 	if err != nil {
       
   269 		return nil, err
       
   270 	}
       
   271 	if rel == nil {
       
   272 		return nil, ErrEntityNotFound
       
   273 	}
       
   274 	return rel, nil
       
   275 }
       
   276 
       
   277 // UnblockAccount unblocks an account
       
   278 func (mc *Client) UnblockAccount(accountID int64) (*Relationship, error) {
       
   279 	rel, err := mc.updateRelationship("unblock", accountID, nil)
       
   280 	if err != nil {
       
   281 		return nil, err
       
   282 	}
       
   283 	if rel == nil {
       
   284 		return nil, ErrEntityNotFound
       
   285 	}
       
   286 	return rel, nil
       
   287 }
       
   288 
       
   289 // MuteAccount mutes an account
       
   290 // Note that with current Mastodon API, muteNotifications defaults to true
       
   291 // when it is not provided.
       
   292 func (mc *Client) MuteAccount(accountID int64, muteNotifications *bool) (*Relationship, error) {
       
   293 	var params apiCallParams
       
   294 
       
   295 	if muteNotifications != nil {
       
   296 		params = make(apiCallParams)
       
   297 		if *muteNotifications {
       
   298 			params["notifications"] = "true"
       
   299 		} else {
       
   300 			params["notifications"] = "false"
       
   301 		}
       
   302 	}
       
   303 
       
   304 	rel, err := mc.updateRelationship("mute", accountID, params)
       
   305 	if err != nil {
       
   306 		return nil, err
       
   307 	}
       
   308 	if rel == nil {
       
   309 		return nil, ErrEntityNotFound
       
   310 	}
       
   311 	return rel, nil
       
   312 }
       
   313 
       
   314 // UnmuteAccount unmutes an account
       
   315 func (mc *Client) UnmuteAccount(accountID int64) (*Relationship, error) {
       
   316 	rel, err := mc.updateRelationship("unmute", accountID, nil)
       
   317 	if err != nil {
       
   318 		return nil, err
       
   319 	}
       
   320 	if rel == nil {
       
   321 		return nil, ErrEntityNotFound
       
   322 	}
       
   323 	return rel, nil
       
   324 }
       
   325 
       
   326 // SearchAccounts returns a list of accounts matching the query string
       
   327 // The lopt parameter is optional (can be nil) or can be used to set a limit.
       
   328 func (mc *Client) SearchAccounts(query string, following bool, lopt *LimitParams) ([]Account, error) {
       
   329 	o := &getAccountsOptions{Q: query, Limit: lopt, Following: following}
       
   330 	return mc.getMultipleAccountsHelper("search", o)
       
   331 }
       
   332 
       
   333 // GetBlockedAccounts returns the list of blocked accounts
       
   334 // The lopt parameter is optional (can be nil).
       
   335 func (mc *Client) GetBlockedAccounts(lopt *LimitParams) ([]Account, error) {
       
   336 	o := &getAccountsOptions{Limit: lopt}
       
   337 	return mc.getMultipleAccountsHelper("blocks", o)
       
   338 }
       
   339 
       
   340 // GetMutedAccounts returns the list of muted accounts
       
   341 // The lopt parameter is optional (can be nil).
       
   342 func (mc *Client) GetMutedAccounts(lopt *LimitParams) ([]Account, error) {
       
   343 	o := &getAccountsOptions{Limit: lopt}
       
   344 	return mc.getMultipleAccountsHelper("mutes", o)
       
   345 }
       
   346 
       
   347 // GetAccountFollowRequests returns the list of follow requests accounts
       
   348 // The lopt parameter is optional (can be nil).
       
   349 func (mc *Client) GetAccountFollowRequests(lopt *LimitParams) ([]Account, error) {
       
   350 	o := &getAccountsOptions{Limit: lopt}
       
   351 	return mc.getMultipleAccountsHelper("follow_requests", o)
       
   352 }
       
   353 
       
   354 // GetAccountRelationships returns a list of relationship entities for the given accounts
       
   355 func (mc *Client) GetAccountRelationships(accountIDs []int64) ([]Relationship, error) {
       
   356 	if len(accountIDs) < 1 {
       
   357 		return nil, ErrInvalidID
       
   358 	}
       
   359 
       
   360 	params := make(apiCallParams)
       
   361 	for i, id := range accountIDs {
       
   362 		if id < 1 {
       
   363 			return nil, ErrInvalidID
       
   364 		}
       
   365 		qID := fmt.Sprintf("[%d]id", i)
       
   366 		params[qID] = strconv.FormatInt(id, 10)
       
   367 	}
       
   368 
       
   369 	var rl []Relationship
       
   370 	if err := mc.apiCall("v1/accounts/relationships", rest.Get, params, nil, nil, &rl); err != nil {
       
   371 		return nil, err
       
   372 	}
       
   373 	return rl, nil
       
   374 }
       
   375 
       
   376 // GetAccountStatuses returns a list of status entities for the given account
       
   377 // If onlyMedia is true, returns only statuses that have media attachments.
       
   378 // If onlyPinned is true, returns only statuses that have been pinned.
       
   379 // If excludeReplies is true, skip statuses that reply to other statuses.
       
   380 // If lopt.All is true, several requests will be made until the API server
       
   381 // has nothing to return.
       
   382 // If lopt.Limit is set (and not All), several queries can be made until the
       
   383 // limit is reached.
       
   384 func (mc *Client) GetAccountStatuses(accountID int64, onlyPinned, onlyMedia, excludeReplies bool, lopt *LimitParams) ([]Status, error) {
       
   385 	if accountID < 1 {
       
   386 		return nil, ErrInvalidID
       
   387 	}
       
   388 
       
   389 	endPoint := "accounts/" + strconv.FormatInt(accountID, 10) + "/" + "statuses"
       
   390 	params := make(apiCallParams)
       
   391 	if onlyMedia {
       
   392 		params["only_media"] = "true"
       
   393 	}
       
   394 	if onlyPinned {
       
   395 		params["pinned"] = "true"
       
   396 	}
       
   397 	if excludeReplies {
       
   398 		params["exclude_replies"] = "true"
       
   399 	}
       
   400 
       
   401 	return mc.getMultipleStatuses(endPoint, params, lopt)
       
   402 }
       
   403 
       
   404 // FollowRequestAuthorize authorizes or rejects an account follow-request
       
   405 func (mc *Client) FollowRequestAuthorize(accountID int64, authorize bool) error {
       
   406 	endPoint := "follow_requests/reject"
       
   407 	if authorize {
       
   408 		endPoint = "follow_requests/authorize"
       
   409 	}
       
   410 	_, err := mc.getSingleAccount(endPoint, accountID)
       
   411 	return err
       
   412 }
       
   413 
       
   414 // UpdateAccount updates the connected user's account data
       
   415 //
       
   416 // The fields avatar & headerImage are considered as file paths
       
   417 // and their content will be uploaded.
       
   418 // Please note that currently Mastodon leaks the avatar file name:
       
   419 // https://github.com/tootsuite/mastodon/issues/5776
       
   420 //
       
   421 // All fields can be nil, in which case they are not updated.
       
   422 // 'DisplayName' and 'Note' can be set to "" to delete previous values.
       
   423 // Setting 'Locked' to true means all followers should be approved.
       
   424 // You can set 'Bot' to true to indicate this is a service (automated) account.
       
   425 // I'm not sure images can be deleted -- only replaced AFAICS.
       
   426 func (mc *Client) UpdateAccount(cmdParams UpdateAccountParams) (*Account, error) {
       
   427 	const endPoint = "accounts/update_credentials"
       
   428 	params := make(apiCallParams)
       
   429 
       
   430 	if cmdParams.DisplayName != nil {
       
   431 		params["display_name"] = *cmdParams.DisplayName
       
   432 	}
       
   433 	if cmdParams.Note != nil {
       
   434 		params["note"] = *cmdParams.Note
       
   435 	}
       
   436 	if cmdParams.Locked != nil {
       
   437 		if *cmdParams.Locked {
       
   438 			params["locked"] = "true"
       
   439 		} else {
       
   440 			params["locked"] = "false"
       
   441 		}
       
   442 	}
       
   443 	if cmdParams.Bot != nil {
       
   444 		if *cmdParams.Bot {
       
   445 			params["bot"] = "true"
       
   446 		} else {
       
   447 			params["bot"] = "false"
       
   448 		}
       
   449 	}
       
   450 	if cmdParams.FieldsAttributes != nil {
       
   451 		if len(*cmdParams.FieldsAttributes) > 4 {
       
   452 			return nil, errors.New("too many fields (max=4)")
       
   453 		}
       
   454 		for i, attr := range *cmdParams.FieldsAttributes {
       
   455 			qName := fmt.Sprintf("fields_attributes[%d][name]", i)
       
   456 			qValue := fmt.Sprintf("fields_attributes[%d][value]", i)
       
   457 			params[qName] = attr.Name
       
   458 			params[qValue] = attr.Value
       
   459 		}
       
   460 	}
       
   461 	if cmdParams.Source != nil {
       
   462 		s := cmdParams.Source
       
   463 
       
   464 		if s.Privacy != nil {
       
   465 			params["source[privacy]"] = *s.Privacy
       
   466 		}
       
   467 		if s.Language != nil {
       
   468 			params["source[language]"] = *s.Language
       
   469 		}
       
   470 		if s.Sensitive != nil {
       
   471 			params["source[sensitive]"] = fmt.Sprintf("%v", *s.Sensitive)
       
   472 		}
       
   473 	}
       
   474 
       
   475 	var err error
       
   476 	var avatar, headerImage []byte
       
   477 
       
   478 	avatar, err = readFile(cmdParams.AvatarImagePath)
       
   479 	if err != nil {
       
   480 		return nil, err
       
   481 	}
       
   482 
       
   483 	headerImage, err = readFile(cmdParams.HeaderImagePath)
       
   484 	if err != nil {
       
   485 		return nil, err
       
   486 	}
       
   487 
       
   488 	var formBuf bytes.Buffer
       
   489 	w := multipart.NewWriter(&formBuf)
       
   490 
       
   491 	if avatar != nil {
       
   492 		formWriter, err := w.CreateFormFile("avatar", filepath.Base(*cmdParams.AvatarImagePath))
       
   493 		if err != nil {
       
   494 			return nil, errors.Wrap(err, "avatar upload")
       
   495 		}
       
   496 		formWriter.Write(avatar)
       
   497 	}
       
   498 	if headerImage != nil {
       
   499 		formWriter, err := w.CreateFormFile("header", filepath.Base(*cmdParams.HeaderImagePath))
       
   500 		if err != nil {
       
   501 			return nil, errors.Wrap(err, "header upload")
       
   502 		}
       
   503 		formWriter.Write(headerImage)
       
   504 	}
       
   505 	w.Close()
       
   506 
       
   507 	// Prepare the request
       
   508 	req, err := mc.prepareRequest("v1/"+endPoint, rest.Patch, params)
       
   509 	if err != nil {
       
   510 		return nil, errors.Wrap(err, "prepareRequest failed")
       
   511 	}
       
   512 	req.Headers["Content-Type"] = w.FormDataContentType()
       
   513 	req.Body = formBuf.Bytes()
       
   514 
       
   515 	// Make API call
       
   516 	r, err := restAPI(req)
       
   517 	if err != nil {
       
   518 		return nil, errors.Wrap(err, "account update failed")
       
   519 	}
       
   520 
       
   521 	// Check for error reply
       
   522 	var errorResult Error
       
   523 	if err := json.Unmarshal([]byte(r.Body), &errorResult); err == nil {
       
   524 		// The empty object is not an error
       
   525 		if errorResult.Text != "" {
       
   526 			return nil, errors.New(errorResult.Text)
       
   527 		}
       
   528 	}
       
   529 
       
   530 	// Not an error reply; let's unmarshal the data
       
   531 	var account Account
       
   532 	if err := json.Unmarshal([]byte(r.Body), &account); err != nil {
       
   533 		return nil, errors.Wrap(err, "cannot decode API response")
       
   534 	}
       
   535 	return &account, nil
       
   536 }
       
   537 
       
   538 // readFile is a helper function to read a file's contents.
       
   539 func readFile(filename *string) ([]byte, error) {
       
   540 	if filename == nil || *filename == "" {
       
   541 		return nil, nil
       
   542 	}
       
   543 
       
   544 	file, err := os.Open(*filename)
       
   545 	if err != nil {
       
   546 		return nil, err
       
   547 	}
       
   548 	defer file.Close()
       
   549 
       
   550 	fStat, err := file.Stat()
       
   551 	if err != nil {
       
   552 		return nil, err
       
   553 	}
       
   554 
       
   555 	buffer := make([]byte, fStat.Size())
       
   556 	_, err = file.Read(buffer)
       
   557 	if err != nil {
       
   558 		return nil, err
       
   559 	}
       
   560 
       
   561 	return buffer, nil
       
   562 }