aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristine Dodrill <me@christine.website>2017-04-22 19:29:54 -0700
committerChristine Dodrill <me@christine.website>2017-04-22 19:29:54 -0700
commit09446399be0faeee09b7a323e57434f69d428826 (patch)
tree19a8134fdad4dbe5c063836afd129ab4e1c6e882
parent729bbe26deac8975f2b31e4c3ddaae938f078fa5 (diff)
downloadx-09446399be0faeee09b7a323e57434f69d428826.tar.xz
x-09446399be0faeee09b7a323e57434f69d428826.zip
furrybb: an experimental #furry boost bot
-rw-r--r--mastodon/furrybb/.gitignore2
-rw-r--r--mastodon/furrybb/README.md36
-rw-r--r--mastodon/furrybb/main.go74
-rw-r--r--mastodon/furrybb/mkapp.go38
-rw-r--r--mastodon/furrybb/vendor-log8
-rw-r--r--mastodon/furrybb/vendor/github.com/McKael/madon/account.go430
-rw-r--r--mastodon/furrybb/vendor/github.com/McKael/madon/api.go137
-rw-r--r--mastodon/furrybb/vendor/github.com/McKael/madon/app.go96
-rw-r--r--mastodon/furrybb/vendor/github.com/McKael/madon/favourites.go21
-rw-r--r--mastodon/furrybb/vendor/github.com/McKael/madon/instance.go20
-rw-r--r--mastodon/furrybb/vendor/github.com/McKael/madon/login.go88
-rw-r--r--mastodon/furrybb/vendor/github.com/McKael/madon/madon.go36
-rw-r--r--mastodon/furrybb/vendor/github.com/McKael/madon/media.go75
-rw-r--r--mastodon/furrybb/vendor/github.com/McKael/madon/notifications.go47
-rw-r--r--mastodon/furrybb/vendor/github.com/McKael/madon/report.go47
-rw-r--r--mastodon/furrybb/vendor/github.com/McKael/madon/search.go30
-rw-r--r--mastodon/furrybb/vendor/github.com/McKael/madon/status.go229
-rw-r--r--mastodon/furrybb/vendor/github.com/McKael/madon/streams.go169
-rw-r--r--mastodon/furrybb/vendor/github.com/McKael/madon/timelines.go46
-rw-r--r--mastodon/furrybb/vendor/github.com/McKael/madon/types.go157
-rw-r--r--mastodon/furrybb/vendor/github.com/Xe/ln/filter.go66
-rw-r--r--mastodon/furrybb/vendor/github.com/Xe/ln/formatter.go110
-rw-r--r--mastodon/furrybb/vendor/github.com/Xe/ln/logger.go141
-rw-r--r--mastodon/furrybb/vendor/github.com/Xe/ln/stack.go44
-rw-r--r--mastodon/furrybb/vendor/github.com/caarlos0/env/env.go275
-rw-r--r--mastodon/furrybb/vendor/github.com/gorilla/websocket/client.go420
-rw-r--r--mastodon/furrybb/vendor/github.com/gorilla/websocket/compression.go85
-rw-r--r--mastodon/furrybb/vendor/github.com/gorilla/websocket/conn.go1031
-rw-r--r--mastodon/furrybb/vendor/github.com/gorilla/websocket/conn_read.go18
-rw-r--r--mastodon/furrybb/vendor/github.com/gorilla/websocket/conn_read_legacy.go21
-rw-r--r--mastodon/furrybb/vendor/github.com/gorilla/websocket/doc.go173
-rw-r--r--mastodon/furrybb/vendor/github.com/gorilla/websocket/json.go55
-rw-r--r--mastodon/furrybb/vendor/github.com/gorilla/websocket/mask.go61
-rw-r--r--mastodon/furrybb/vendor/github.com/gorilla/websocket/server.go292
-rw-r--r--mastodon/furrybb/vendor/github.com/gorilla/websocket/util.go214
-rw-r--r--mastodon/furrybb/vendor/github.com/joho/godotenv/autoload/autoload.go15
-rw-r--r--mastodon/furrybb/vendor/github.com/joho/godotenv/godotenv.go235
-rw-r--r--mastodon/furrybb/vendor/github.com/pkg/errors/errors.go269
-rw-r--r--mastodon/furrybb/vendor/github.com/pkg/errors/stack.go178
-rw-r--r--mastodon/furrybb/vendor/github.com/sendgrid/rest/rest.go140
40 files changed, 5629 insertions, 0 deletions
diff --git a/mastodon/furrybb/.gitignore b/mastodon/furrybb/.gitignore
new file mode 100644
index 0000000..73c1161
--- /dev/null
+++ b/mastodon/furrybb/.gitignore
@@ -0,0 +1,2 @@
+.env
+furrybb
diff --git a/mastodon/furrybb/README.md b/mastodon/furrybb/README.md
new file mode 100644
index 0000000..c64d0eb
--- /dev/null
+++ b/mastodon/furrybb/README.md
@@ -0,0 +1,36 @@
+furrybb
+======
+
+This boosts any toots with the tag `#furry`, but this can be used for other
+hashtags too. Usage is simple:
+
+```console
+$ go get github.com/Xe/x/mastodon/furrybb
+$ cd $GOPATH/src/github.com/Xe/x/mastodon/furrybb
+$ go run mkapp.go -help
+Usage of [mkapp.go]:
+ -app-name string
+ app name for mastodon (default "Xe/x bot")
+ -instance string
+ mastodon instance
+ -password string
+ password to generate token
+ -redirect-uri string
+ redirect URI for app users (default "urn:ietf:wg:oauth:2.0:oob")
+ -username string
+ username to generate token
+ -website string
+ website for users that click the app name (default "https://github.com/Xe/x")
+exit status 2
+$ go run mkapp.go [your options here] > .env
+$ echo "HASHTAG=furry" >> .env
+$ go build && ./furrybb
+```
+
+once you see:
+
+```
+time="2017-04-22T19:25:28-07:00" action=streaming.toots
+```
+
+you're all good fam.
diff --git a/mastodon/furrybb/main.go b/mastodon/furrybb/main.go
new file mode 100644
index 0000000..cfaf9bb
--- /dev/null
+++ b/mastodon/furrybb/main.go
@@ -0,0 +1,74 @@
+package main
+
+import (
+ "github.com/McKael/madon"
+ "github.com/Xe/ln"
+ "github.com/caarlos0/env"
+ _ "github.com/joho/godotenv/autoload"
+)
+
+var cfg = &struct {
+ Instance string `env:"INSTANCE,required"`
+ AppID string `env:"APP_ID,required"`
+ AppSecret string `env:"APP_SECRET,required"`
+ Token string `env:"TOKEN,required"`
+ Hashtag string `env:"HASHTAG,required"`
+}{}
+
+var scopes = []string{"read", "write", "follow"}
+
+func main() {
+ err := env.Parse(cfg)
+ if err != nil {
+ ln.Fatal(ln.F{"err": err, "action": "startup"})
+ }
+
+ c, err := madon.RestoreApp("furry boostbot", cfg.Instance, cfg.AppID, cfg.AppSecret, &madon.UserToken{AccessToken: cfg.Token})
+ if err != nil {
+ ln.Fatal(ln.F{"err": err, "action": "madon.RestoreApp"})
+ }
+
+ evChan := make(chan madon.StreamEvent, 10)
+ stop := make(chan bool)
+ done := make(chan bool)
+
+ err = c.StreamListener("public", "", evChan, stop, done)
+ if err != nil {
+ ln.Fatal(ln.F{"err": err, "action": "c.StreamListener"})
+ }
+
+ ln.Log(ln.F{
+ "action": "streaming.toots",
+ })
+
+ for {
+ select {
+ case _, ok := <-done:
+ if !ok {
+ ln.Fatal(ln.F{"action": "stream.dead"})
+ }
+
+ case ev := <-evChan:
+ switch ev.Event {
+ case "error":
+ ln.Fatal(ln.F{"err": ev.Error, "action": "processing.event"})
+ case "update":
+ s := ev.Data.(madon.Status)
+
+ for _, tag := range s.Tags {
+ if tag.Name == cfg.Hashtag {
+ err = c.ReblogStatus(s.ID)
+ if err != nil {
+ ln.Fatal(ln.F{"err": err, "action": "c.ReblogStatus", "id": s.ID})
+ }
+
+ ln.Log(ln.F{
+ "action": "reblogged",
+ "id": s.ID,
+ })
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/mastodon/furrybb/mkapp.go b/mastodon/furrybb/mkapp.go
new file mode 100644
index 0000000..88ad5f8
--- /dev/null
+++ b/mastodon/furrybb/mkapp.go
@@ -0,0 +1,38 @@
+// +build ignore
+
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+
+ "github.com/McKael/madon"
+)
+
+var (
+ instance = flag.String("instance", "", "mastodon instance")
+ appName = flag.String("app-name", "Xe/x bot", "app name for mastodon")
+ redirectURI = flag.String("redirect-uri", "urn:ietf:wg:oauth:2.0:oob", "redirect URI for app users")
+ website = flag.String("website", "https://github.com/Xe/x", "website for users that click the app name")
+ username = flag.String("username", "", "username to generate token")
+ password = flag.String("password", "", "password to generate token")
+)
+
+var scopes = []string{"read", "write", "follow"}
+
+func main() {
+ flag.Parse()
+
+ c, err := madon.NewApp(*appName, scopes, *redirectURI, *instance)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ err = c.LoginBasic(*username, *password, scopes)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ fmt.Printf("APP_ID=%s\nAPP_SECRET=%s\nTOKEN=%s\nINSTANCE=%s", c.ID, c.Secret, c.UserToken.AccessToken, *instance)
+}
diff --git a/mastodon/furrybb/vendor-log b/mastodon/furrybb/vendor-log
new file mode 100644
index 0000000..c9c6acd
--- /dev/null
+++ b/mastodon/furrybb/vendor-log
@@ -0,0 +1,8 @@
+039943f9c38eb1f43c26e1ffd21ad691efc00657 github.com/McKael/madon
+f759b797c0ff6b2c514202198fe5e8ba90094c14 github.com/Xe/ln
+9474f19b515f52326c7d197d2d097caa7fc7485e github.com/caarlos0/env
+e8f0f8aaa98dfb6586cbdf2978d511e3199a960a github.com/gorilla/websocket
+726cc8b906e3d31c70a9671c90a13716a8d3f50d github.com/joho/godotenv
+726cc8b906e3d31c70a9671c90a13716a8d3f50d github.com/joho/godotenv/autoload
+248dadf4e9068a0b3e79f02ed0a610d935de5302 github.com/pkg/errors
+14de1ac72d9ae5c3c0d7c02164c52ebd3b951a4e github.com/sendgrid/rest
diff --git a/mastodon/furrybb/vendor/github.com/McKael/madon/account.go b/mastodon/furrybb/vendor/github.com/McKael/madon/account.go
new file mode 100644
index 0000000..e96aa59
--- /dev/null
+++ b/mastodon/furrybb/vendor/github.com/McKael/madon/account.go
@@ -0,0 +1,430 @@
+/*
+Copyright 2017 Mikael Berthe
+
+Licensed under the MIT license. Please see the LICENSE file is this directory.
+*/
+
+package madon
+
+import (
+ "bytes"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "mime/multipart"
+ "net/http"
+ "os"
+ "strconv"
+ "strings"
+
+ "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 int
+
+ // The following fields are used when searching for accounts
+ Q string
+ Limit int
+}
+
+// getSingleAccount returns an account entity
+// The operation 'op' can be "account", "verify_credentials", "follow",
+// "unfollow", "block", "unblock", "mute", "unmute",
+// "follow_requests/authorize" or // "follow_requests/reject".
+// The id is optional and depends on the operation.
+func (mc *Client) getSingleAccount(op string, id int) (*Account, error) {
+ var endPoint string
+ method := rest.Get
+ strID := strconv.Itoa(id)
+
+ switch op {
+ case "account":
+ endPoint = "accounts/" + strID
+ case "verify_credentials":
+ endPoint = "accounts/verify_credentials"
+ case "follow", "unfollow", "block", "unblock", "mute", "unmute":
+ endPoint = "accounts/" + strID + "/" + op
+ method = rest.Post
+ 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(endPoint, method, nil, &account); err != nil {
+ return nil, err
+ }
+ return &account, nil
+}
+
+// getMultipleAccounts 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.
+func (mc *Client) getMultipleAccounts(op string, opts *getAccountsOptions) ([]Account, error) {
+ var endPoint string
+
+ switch op {
+ case "followers", "following":
+ if opts == nil || opts.ID < 1 {
+ return []Account{}, ErrInvalidID
+ }
+ endPoint = "accounts/" + strconv.Itoa(opts.ID) + "/" + op
+ case "follow_requests", "blocks", "mutes":
+ endPoint = op
+ case "search":
+ if opts == nil || opts.Q == "" {
+ return []Account{}, ErrInvalidParameter
+ }
+ endPoint = "accounts/" + op
+ default:
+ return nil, ErrInvalidParameter
+ }
+
+ // Handle target-specific query parameters
+ params := make(apiCallParams)
+ if op == "search" {
+ params["q"] = opts.Q
+ if opts.Limit > 0 {
+ params["limit"] = strconv.Itoa(opts.Limit)
+ }
+ }
+
+ var accounts []Account
+ if err := mc.apiCall(endPoint, rest.Get, params, &accounts); err != nil {
+ return nil, err
+ }
+ return accounts, nil
+}
+
+// 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 int) (*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 int) ([]Account, error) {
+ o := &getAccountsOptions{ID: accountID}
+ return mc.getMultipleAccounts("followers", o)
+}
+
+// GetAccountFollowing returns the list of accounts a given account is following
+func (mc *Client) GetAccountFollowing(accountID int) ([]Account, error) {
+ o := &getAccountsOptions{ID: accountID}
+ return mc.getMultipleAccounts("following", o)
+}
+
+// FollowAccount follows an account
+func (mc *Client) FollowAccount(accountID int) error {
+ account, err := mc.getSingleAccount("follow", accountID)
+ if err != nil {
+ return err
+ }
+ if account != nil && account.ID != accountID {
+ return ErrEntityNotFound
+ }
+ return nil
+}
+
+// UnfollowAccount unfollows an account
+func (mc *Client) UnfollowAccount(accountID int) error {
+ account, err := mc.getSingleAccount("unfollow", accountID)
+ if err != nil {
+ return err
+ }
+ if account != nil && account.ID != accountID {
+ return ErrEntityNotFound
+ }
+ return nil
+}
+
+// FollowRemoteAccount follows a remote account
+// The parameter 'uri' is a URI (e.mc. "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("follows", rest.Post, params, &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 int) error {
+ account, err := mc.getSingleAccount("block", accountID)
+ if err != nil {
+ return err
+ }
+ if account != nil && account.ID != accountID {
+ return ErrEntityNotFound
+ }
+ return nil
+}
+
+// UnblockAccount unblocks an account
+func (mc *Client) UnblockAccount(accountID int) error {
+ account, err := mc.getSingleAccount("unblock", accountID)
+ if err != nil {
+ return err
+ }
+ if account != nil && account.ID != accountID {
+ return ErrEntityNotFound
+ }
+ return nil
+}
+
+// MuteAccount mutes an account
+func (mc *Client) MuteAccount(accountID int) error {
+ account, err := mc.getSingleAccount("mute", accountID)
+ if err != nil {
+ return err
+ }
+ if account != nil && account.ID != accountID {
+ return ErrEntityNotFound
+ }
+ return nil
+}
+
+// UnmuteAccount unmutes an account
+func (mc *Client) UnmuteAccount(accountID int) error {
+ account, err := mc.getSingleAccount("unmute", accountID)
+ if err != nil {
+ return err
+ }
+ if account != nil && account.ID != accountID {
+ return ErrEntityNotFound
+ }
+ return nil
+}
+
+// SearchAccounts returns a list of accounts matching the query string
+// The limit parameter is optional (can be 0).
+func (mc *Client) SearchAccounts(query string, limit int) ([]Account, error) {
+ o := &getAccountsOptions{Q: query, Limit: limit}
+ return mc.getMultipleAccounts("search", o)
+}
+
+// GetBlockedAccounts returns the list of blocked accounts
+func (mc *Client) GetBlockedAccounts() ([]Account, error) {
+ return mc.getMultipleAccounts("blocks", nil)
+}
+
+// GetMutedAccounts returns the list of muted accounts
+func (mc *Client) GetMutedAccounts() ([]Account, error) {
+ return mc.getMultipleAccounts("mutes", nil)
+}
+
+// GetAccountFollowRequests returns the list of follow requests accounts
+func (mc *Client) GetAccountFollowRequests() ([]Account, error) {
+ return mc.getMultipleAccounts("follow_requests", nil)
+}
+
+// GetAccountRelationships returns a list of relationship entities for the given accounts
+func (mc *Client) GetAccountRelationships(accountIDs []int) ([]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("id[%d]", i+1)
+ params[qID] = strconv.Itoa(id)
+ }
+
+ var rl []Relationship
+ if err := mc.apiCall("accounts/relationships", rest.Get, params, &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 excludeReplies is true, skip statuses that reply to other statuses.
+func (mc *Client) GetAccountStatuses(accountID int, onlyMedia, excludeReplies bool) ([]Status, error) {
+ if accountID < 1 {
+ return nil, ErrInvalidID
+ }
+
+ endPoint := "accounts/" + strconv.Itoa(accountID) + "/" + "statuses"
+ params := make(apiCallParams)
+ if onlyMedia {
+ params["only_media"] = "true"
+ }
+ if excludeReplies {
+ params["exclude_replies"] = "true"
+ }
+
+ var sl []Status
+ if err := mc.apiCall(endPoint, rest.Get, params, &sl); err != nil {
+ return nil, err
+ }
+ return sl, nil
+}
+
+// FollowRequestAuthorize authorizes or rejects an account follow-request
+func (mc *Client) FollowRequestAuthorize(accountID int, 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 can contain base64-encoded images; if
+// they do not (that is; if they don't contain ";base64,"), they are considered
+// as file paths and their content will be encoded.
+// All fields can be nil, in which case they are not updated.
+// displayName and note can be set to "" to delete previous values;
+// I'm not sure images can be deleted -- only replaced AFAICS.
+func (mc *Client) UpdateAccount(displayName, note, avatar, headerImage *string) (*Account, error) {
+ const endPoint = "accounts/update_credentials"
+ params := make(apiCallParams)
+
+ if displayName != nil {
+ params["display_name"] = *displayName
+ }
+ if note != nil {
+ params["note"] = *note
+ }
+
+ var err error
+ avatar, err = fileToBase64(avatar, nil)
+ if err != nil {
+ return nil, err
+ }
+ headerImage, err = fileToBase64(headerImage, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ var formBuf bytes.Buffer
+ w := multipart.NewWriter(&formBuf)
+
+ if avatar != nil {
+ w.WriteField("avatar", *avatar)
+ }
+ if headerImage != nil {
+ w.WriteField("header", *headerImage)
+ }
+ w.Close()
+
+ // Prepare the request
+ req, err := mc.prepareRequest(endPoint, rest.Patch, params)
+ if err != nil {
+ return nil, fmt.Errorf("prepareRequest failed: %s", err.Error())
+ }
+ req.Headers["Content-Type"] = w.FormDataContentType()
+ req.Body = formBuf.Bytes()
+
+ // Make API call
+ r, err := restAPI(req)
+ if err != nil {
+ return nil, fmt.Errorf("account update failed: %s", err.Error())
+ }
+
+ // 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, fmt.Errorf("%s", 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, fmt.Errorf("cannot decode API response: %s", err.Error())
+ }
+ return &account, nil
+}
+
+// fileToBase64 is a helper function to convert a file's contents to
+// base64-encoded data. Is the data string already contains base64 data, it
+// is not modified.
+// If contentType is nil, it is detected.
+func fileToBase64(data, contentType *string) (*string, error) {
+ if data == nil {
+ return nil, nil
+ }
+
+ if *data == "" {
+ return data, nil
+ }
+
+ if strings.Contains(*data, ";base64,") {
+ return data, nil
+ }
+
+ // We need to convert the file and file name to base64
+
+ file, err := os.Open(*data)
+ 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
+ }
+
+ var cType string
+ if contentType == nil || *contentType == "" {
+ cType = http.DetectContentType(buffer[:512])
+ } else {
+ cType = *contentType
+ }
+ contentData := base64.StdEncoding.EncodeToString(buffer)
+ newData := "data:" + cType + ";base64," + contentData
+ return &newData, nil
+}
diff --git a/mastodon/furrybb/vendor/github.com/McKael/madon/api.go b/mastodon/furrybb/vendor/github.com/McKael/madon/api.go
new file mode 100644
index 0000000..b232553
--- /dev/null
+++ b/mastodon/furrybb/vendor/github.com/McKael/madon/api.go
@@ -0,0 +1,137 @@
+/*
+Copyright 2017 Ollivier Robert
+Copyright 2017 Mikael Berthe
+
+Licensed under the MIT license. Please see the LICENSE file is this directory.
+*/
+
+package madon
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+
+ "github.com/sendgrid/rest"
+)
+
+// 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{}
+ 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 '^.+\[.*\]$'
+ klen := len(key)
+ if klen == 0 {
+ continue
+ }
+ i := strings.Index(key, "[")
+ if key[klen-1] == ']' && i > 0 {
+ // This is an array, let's remove the index number
+ key = key[:i] + "[]"
+ }
+ 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
+ }
+
+ // 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
+func (mc *Client) apiCall(endPoint string, method rest.Method, params apiCallParams, data interface{}) error {
+ if mc == nil {
+ return fmt.Errorf("use of uninitialized madon client")
+ }
+
+ // 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 fmt.Errorf("API query (%s) failed: %s", endPoint, err.Error())
+ }
+
+ // 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 fmt.Errorf("%s", errorResult.Text)
+ }
+ }
+
+ // Not an error reply; let's unmarshal the data
+ err = json.Unmarshal([]byte(r.Body), &data)
+ if err != nil {
+ return fmt.Errorf("cannot decode API response (%s): %s", method, err.Error())
+ }
+ return nil
+}
diff --git a/mastodon/furrybb/vendor/github.com/McKael/madon/app.go b/mastodon/furrybb/vendor/github.com/McKael/madon/app.go
new file mode 100644
index 0000000..d890224
--- /dev/null
+++ b/mastodon/furrybb/vendor/github.com/McKael/madon/app.go
@@ -0,0 +1,96 @@
+/*
+Copyright 2017 Ollivier Robert
+Copyright 2017 Mikael Berthe
+
+Licensed under the MIT license. Please see the LICENSE file is this directory.
+*/
+
+package madon
+
+import (
+ "errors"
+ "net/url"
+ "strings"
+
+ "github.com/sendgrid/rest"
+)
+
+type registerApp struct {
+ ID int `json:"id"`
+ 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 string, scopes []string, redirectURI, instanceName string) (mc *Client, err error) {
+ instanceURL, err := buildInstanceURL(instanceName)
+ if err != nil {
+ return nil, er