Initial commit
This commit is contained in:
commit
5e4da01c3a
|
@ -0,0 +1,2 @@
|
|||
web
|
||||
database.db
|
|
@ -0,0 +1,14 @@
|
|||
.POSIX:
|
||||
|
||||
GO=go
|
||||
#GOFLAGS=-mod=vendor
|
||||
|
||||
all: web
|
||||
|
||||
PHONY:
|
||||
|
||||
web: main.go PHONY
|
||||
$(GO) build $(GOFLAGS) -o web main.go
|
||||
|
||||
run: web
|
||||
./web
|
|
@ -0,0 +1,113 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
ListenAddress string
|
||||
ClientName string
|
||||
ClientScope string
|
||||
ClientWebsite string
|
||||
StaticDirectory string
|
||||
TemplatesGlobPattern string
|
||||
DatabasePath string
|
||||
Logfile string
|
||||
}
|
||||
|
||||
func (c *config) IsValid() bool {
|
||||
if len(c.ListenAddress) < 1 ||
|
||||
len(c.ClientName) < 1 ||
|
||||
len(c.ClientScope) < 1 ||
|
||||
len(c.ClientWebsite) < 1 ||
|
||||
len(c.StaticDirectory) < 1 ||
|
||||
len(c.TemplatesGlobPattern) < 1 ||
|
||||
len(c.DatabasePath) < 1 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getDefaultConfig() *config {
|
||||
return &config{
|
||||
ListenAddress: ":8080",
|
||||
ClientName: "web",
|
||||
ClientScope: "read write follow",
|
||||
ClientWebsite: "http://localhost:8080",
|
||||
StaticDirectory: "static",
|
||||
TemplatesGlobPattern: "templates/*",
|
||||
DatabasePath: "database.db",
|
||||
Logfile: "",
|
||||
}
|
||||
}
|
||||
|
||||
func Parse(r io.Reader) (c *config, err error) {
|
||||
c = getDefaultConfig()
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
if len(line) < 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
index := strings.IndexRune(line, '#')
|
||||
if index == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
index = strings.IndexRune(line, '=')
|
||||
if index < 1 {
|
||||
return nil, errors.New("invalid config key")
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(line[:index])
|
||||
val := strings.TrimSpace(line[index+1 : len(line)])
|
||||
|
||||
switch key {
|
||||
case "listen_address":
|
||||
c.ListenAddress = val
|
||||
case "client_name":
|
||||
c.ClientName = val
|
||||
case "client_scope":
|
||||
c.ClientScope = val
|
||||
case "client_website":
|
||||
c.ClientWebsite = val
|
||||
case "static_directory":
|
||||
c.StaticDirectory = val
|
||||
case "templates_glob_pattern":
|
||||
c.TemplatesGlobPattern = val
|
||||
case "database_path":
|
||||
c.DatabasePath = val
|
||||
case "logfile":
|
||||
c.Logfile = val
|
||||
default:
|
||||
return nil, errors.New("invliad config key " + key)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func ParseFile(file string) (c *config, err error) {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil, errors.New("invalid config file")
|
||||
}
|
||||
|
||||
return Parse(f)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
listen_address=:8080
|
||||
client_name=web
|
||||
client_scope=read write follow
|
||||
client_website=http://localhost:8080
|
||||
static_directory=static
|
||||
templates_glob_pattern=templates/*
|
||||
database_path=database.db
|
|
@ -0,0 +1,11 @@
|
|||
module web
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/mattn/go-sqlite3 v2.0.1+incompatible
|
||||
mastodon v0.0.0-00010101000000-000000000000
|
||||
)
|
||||
|
||||
replace mastodon => ./mastodon
|
|
@ -0,0 +1,8 @@
|
|||
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw=
|
||||
github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y=
|
||||
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE=
|
|
@ -0,0 +1,76 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"web/config"
|
||||
"web/renderer"
|
||||
"web/repository"
|
||||
"web/service"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().Unix())
|
||||
}
|
||||
|
||||
func main() {
|
||||
config, err := config.ParseFile("default.conf")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if !config.IsValid() {
|
||||
log.Fatal("invalid config")
|
||||
}
|
||||
|
||||
renderer, err := renderer.NewRenderer(config.TemplatesGlobPattern)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
db, err := sql.Open("sqlite3", config.DatabasePath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
sessionRepo, err := repository.NewSessionRepository(db)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
appRepo, err := repository.NewAppRepository(db)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var logger *log.Logger
|
||||
if len(config.Logfile) < 1 {
|
||||
logger = log.New(os.Stdout, "", log.LstdFlags)
|
||||
} else {
|
||||
lf, err := os.Open(config.Logfile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer lf.Close()
|
||||
logger = log.New(lf, "", log.LstdFlags)
|
||||
}
|
||||
|
||||
s := service.NewService(config.ClientName, config.ClientScope, config.ClientWebsite, renderer, sessionRepo, appRepo)
|
||||
s = service.NewAuthService(sessionRepo, appRepo, s)
|
||||
s = service.NewLoggingService(logger, s)
|
||||
handler := service.NewHandler(s, config.StaticDirectory)
|
||||
|
||||
log.Println("listening on", config.ListenAddress)
|
||||
err = http.ListenAndServe(config.ListenAddress, handler)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017 Yasuhiro Matsumoto
|
||||
|
||||
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.
|
|
@ -0,0 +1,142 @@
|
|||
# go-mastodon
|
||||
|
||||
[![Build Status](https://travis-ci.org/mattn/go-mastodon.svg?branch=master)](https://travis-ci.org/mattn/go-mastodon)
|
||||
[![Coverage Status](https://coveralls.io/repos/github/mattn/go-mastodon/badge.svg?branch=master)](https://coveralls.io/github/mattn/go-mastodon?branch=master)
|
||||
[![GoDoc](https://godoc.org/github.com/mattn/go-mastodon?status.svg)](http://godoc.org/github.com/mattn/go-mastodon)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/mattn/go-mastodon)](https://goreportcard.com/report/github.com/mattn/go-mastodon)
|
||||
|
||||
## Usage
|
||||
|
||||
### Application
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/mattn/go-mastodon"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app, err := mastodon.RegisterApp(context.Background(), &mastodon.AppConfig{
|
||||
Server: "https://mstdn.jp",
|
||||
ClientName: "client-name",
|
||||
Scopes: "read write follow",
|
||||
Website: "https://github.com/mattn/go-mastodon",
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("client-id : %s\n", app.ClientID)
|
||||
fmt.Printf("client-secret: %s\n", app.ClientSecret)
|
||||
}
|
||||
```
|
||||
|
||||
### Client
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/mattn/go-mastodon"
|
||||
)
|
||||
|
||||
func main() {
|
||||
c := mastodon.NewClient(&mastodon.Config{
|
||||
Server: "https://mstdn.jp",
|
||||
ClientID: "client-id",
|
||||
ClientSecret: "client-secret",
|
||||
})
|
||||
err := c.Authenticate(context.Background(), "your-email", "your-password")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
timeline, err := c.GetTimelineHome(context.Background(), nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for i := len(timeline) - 1; i >= 0; i-- {
|
||||
fmt.Println(timeline[i])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Status of implementations
|
||||
|
||||
* [x] GET /api/v1/accounts/:id
|
||||
* [x] GET /api/v1/accounts/verify_credentials
|
||||
* [x] PATCH /api/v1/accounts/update_credentials
|
||||
* [x] GET /api/v1/accounts/:id/followers
|
||||
* [x] GET /api/v1/accounts/:id/following
|
||||
* [x] GET /api/v1/accounts/:id/statuses
|
||||
* [x] POST /api/v1/accounts/:id/follow
|
||||
* [x] POST /api/v1/accounts/:id/unfollow
|
||||
* [x] GET /api/v1/accounts/:id/block
|
||||
* [x] GET /api/v1/accounts/:id/unblock
|
||||
* [x] GET /api/v1/accounts/:id/mute
|
||||
* [x] GET /api/v1/accounts/:id/unmute
|
||||
* [x] GET /api/v1/accounts/:id/lists
|
||||
* [x] GET /api/v1/accounts/relationships
|
||||
* [x] GET /api/v1/accounts/search
|
||||
* [x] POST /api/v1/apps
|
||||
* [x] GET /api/v1/blocks
|
||||
* [x] GET /api/v1/favourites
|
||||
* [x] GET /api/v1/follow_requests
|
||||
* [x] POST /api/v1/follow_requests/:id/authorize
|
||||
* [x] POST /api/v1/follow_requests/:id/reject
|
||||
* [x] POST /api/v1/follows
|
||||
* [x] GET /api/v1/instance
|
||||
* [x] GET /api/v1/instance/activity
|
||||
* [x] GET /api/v1/instance/peers
|
||||
* [x] GET /api/v1/lists
|
||||
* [x] GET /api/v1/lists/:id/accounts
|
||||
* [x] GET /api/v1/lists/:id
|
||||
* [x] POST /api/v1/lists
|
||||
* [x] PUT /api/v1/lists/:id
|
||||
* [x] DELETE /api/v1/lists/:id
|
||||
* [x] POST /api/v1/lists/:id/accounts
|
||||
* [x] DELETE /api/v1/lists/:id/accounts
|
||||
* [x] POST /api/v1/media
|
||||
* [x] GET /api/v1/mutes
|
||||
* [x] GET /api/v1/notifications
|
||||
* [x] GET /api/v1/notifications/:id
|
||||
* [x] POST /api/v1/notifications/clear
|
||||
* [x] GET /api/v1/reports
|
||||
* [x] POST /api/v1/reports
|
||||
* [x] GET /api/v1/search
|
||||
* [x] GET /api/v1/statuses/:id
|
||||
* [x] GET /api/v1/statuses/:id/context
|
||||
* [x] GET /api/v1/statuses/:id/card
|
||||
* [x] GET /api/v1/statuses/:id/reblogged_by
|
||||
* [x] GET /api/v1/statuses/:id/favourited_by
|
||||
* [x] POST /api/v1/statuses
|
||||
* [x] DELETE /api/v1/statuses/:id
|
||||
* [x] POST /api/v1/statuses/:id/reblog
|
||||
* [x] POST /api/v1/statuses/:id/unreblog
|
||||
* [x] POST /api/v1/statuses/:id/favourite
|
||||
* [x] POST /api/v1/statuses/:id/unfavourite
|
||||
* [x] GET /api/v1/timelines/home
|
||||
* [x] GET /api/v1/timelines/public
|
||||
* [x] GET /api/v1/timelines/tag/:hashtag
|
||||
* [x] GET /api/v1/timelines/list/:id
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
$ go get github.com/mattn/go-mastodon
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Author
|
||||
|
||||
Yasuhiro Matsumoto (a.k.a. mattn)
|
|
@ -0,0 +1,314 @@
|
|||
package mastodon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Account hold information for mastodon account.
|
||||
type Account struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Acct string `json:"acct"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Locked bool `json:"locked"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
FollowersCount int64 `json:"followers_count"`
|
||||
FollowingCount int64 `json:"following_count"`
|
||||
StatusesCount int64 `json:"statuses_count"`
|
||||
Note string `json:"note"`
|
||||
URL string `json:"url"`
|
||||
Avatar string `json:"avatar"`
|
||||
AvatarStatic string `json:"avatar_static"`
|
||||
Header string `json:"header"`
|
||||
HeaderStatic string `json:"header_static"`
|
||||
Emojis []Emoji `json:"emojis"`
|
||||
Moved *Account `json:"moved"`
|
||||
Fields []Field `json:"fields"`
|
||||
Bot bool `json:"bot"`
|
||||
}
|
||||
|
||||
// Field is a Mastodon account profile field.
|
||||
type Field struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
VerifiedAt time.Time `json:"verified_at"`
|
||||
}
|
||||
|
||||
// AccountSource is a Mastodon account profile field.
|
||||
type AccountSource struct {
|
||||
Privacy *string `json:"privacy"`
|
||||
Sensitive *bool `json:"sensitive"`
|
||||
Language *string `json:"language"`
|
||||
Note *string `json:"note"`
|
||||
Fields *[]Field `json:"fields"`
|
||||
}
|
||||
|
||||
// GetAccount return Account.
|
||||
func (c *Client) GetAccount(ctx context.Context, id string) (*Account, error) {
|
||||
var account Account
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s", url.PathEscape(string(id))), nil, &account, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
// GetAccountCurrentUser return Account of current user.
|
||||
func (c *Client) GetAccountCurrentUser(ctx context.Context) (*Account, error) {
|
||||
var account Account
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/accounts/verify_credentials", nil, &account, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
// Profile is a struct for updating profiles.
|
||||
type Profile struct {
|
||||
// If it is nil it will not be updated.
|
||||
// If it is empty, update it with empty.
|
||||
DisplayName *string
|
||||
Note *string
|
||||
Locked *bool
|
||||
Fields *[]Field
|
||||
Source *AccountSource
|
||||
|
||||
// Set the base64 encoded character string of the image.
|
||||
Avatar string
|
||||
Header string
|
||||
}
|
||||
|
||||
// AccountUpdate updates the information of the current user.
|
||||
func (c *Client) AccountUpdate(ctx context.Context, profile *Profile) (*Account, error) {
|
||||
params := url.Values{}
|
||||
if profile.DisplayName != nil {
|
||||
params.Set("display_name", *profile.DisplayName)
|
||||
}
|
||||
if profile.Note != nil {
|
||||
params.Set("note", *profile.Note)
|
||||
}
|
||||
if profile.Locked != nil {
|
||||
params.Set("locked", strconv.FormatBool(*profile.Locked))
|
||||
}
|
||||
if profile.Fields != nil {
|
||||
for idx, field := range *profile.Fields {
|
||||
params.Set(fmt.Sprintf("fields_attributes[%d][name]", idx), field.Name)
|
||||
params.Set(fmt.Sprintf("fields_attributes[%d][value]", idx), field.Value)
|
||||
}
|
||||
}
|
||||
if profile.Source != nil {
|
||||
if profile.Source.Privacy != nil {
|
||||
params.Set("source[privacy]", *profile.Source.Privacy)
|
||||
}
|
||||
if profile.Source.Sensitive != nil {
|
||||
params.Set("source[sensitive]", strconv.FormatBool(*profile.Source.Sensitive))
|
||||
}
|
||||
if profile.Source.Language != nil {
|
||||
params.Set("source[language]", *profile.Source.Language)
|
||||
}
|
||||
}
|
||||
if profile.Avatar != "" {
|
||||
params.Set("avatar", profile.Avatar)
|
||||
}
|
||||
if profile.Header != "" {
|
||||
params.Set("header", profile.Header)
|
||||
}
|
||||
|
||||
var account Account
|
||||
err := c.doAPI(ctx, http.MethodPatch, "/api/v1/accounts/update_credentials", params, &account, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
// GetAccountStatuses return statuses by specified accuont.
|
||||
func (c *Client) GetAccountStatuses(ctx context.Context, id string, pg *Pagination) ([]*Status, error) {
|
||||
var statuses []*Status
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s/statuses", url.PathEscape(string(id))), nil, &statuses, pg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
// GetAccountFollowers return followers list.
|
||||
func (c *Client) GetAccountFollowers(ctx context.Context, id string, pg *Pagination) ([]*Account, error) {
|
||||
var accounts []*Account
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s/followers", url.PathEscape(string(id))), nil, &accounts, pg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
// GetAccountFollowing return following list.
|
||||
func (c *Client) GetAccountFollowing(ctx context.Context, id string, pg *Pagination) ([]*Account, error) {
|
||||
var accounts []*Account
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s/following", url.PathEscape(string(id))), nil, &accounts, pg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
// GetBlocks return block list.
|
||||
func (c *Client) GetBlocks(ctx context.Context, pg *Pagination) ([]*Account, error) {
|
||||
var accounts []*Account
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/blocks", nil, &accounts, pg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
// Relationship hold information for relation-ship to the account.
|
||||
type Relationship struct {
|
||||
ID string `json:"id"`
|
||||
Following bool `json:"following"`
|
||||
FollowedBy bool `json:"followed_by"`
|
||||
Blocking bool `json:"blocking"`
|
||||
Muting bool `json:"muting"`
|
||||
MutingNotifications bool `json:"muting_notifications"`
|
||||
Requested bool `json:"requested"`
|
||||
DomainBlocking bool `json:"domain_blocking"`
|
||||
ShowingReblogs bool `json:"showing_reblogs"`
|
||||
Endorsed bool `json:"endorsed"`
|
||||
}
|
||||
|
||||
// AccountFollow follow the account.
|
||||
func (c *Client) AccountFollow(ctx context.Context, id string) (*Relationship, error) {
|
||||
var relationship Relationship
|
||||
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/follow", url.PathEscape(string(id))), nil, &relationship, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &relationship, nil
|
||||
}
|
||||
|
||||
// AccountUnfollow unfollow the account.
|
||||
func (c *Client) AccountUnfollow(ctx context.Context, id string) (*Relationship, error) {
|
||||
var relationship Relationship
|
||||
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/unfollow", url.PathEscape(string(id))), nil, &relationship, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &relationship, nil
|
||||
}
|
||||
|
||||
// AccountBlock block the account.
|
||||
func (c *Client) AccountBlock(ctx context.Context, id string) (*Relationship, error) {
|
||||
var relationship Relationship
|
||||
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/block", url.PathEscape(string(id))), nil, &relationship, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &relationship, nil
|
||||
}
|
||||
|
||||
// AccountUnblock unblock the account.
|
||||
func (c *Client) AccountUnblock(ctx context.Context, id string) (*Relationship, error) {
|
||||
var relationship Relationship
|
||||
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/unblock", url.PathEscape(string(id))), nil, &relationship, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &relationship, nil
|
||||
}
|
||||
|
||||
// AccountMute mute the account.
|
||||
func (c *Client) AccountMute(ctx context.Context, id string) (*Relationship, error) {
|
||||
var relationship Relationship
|
||||
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/mute", url.PathEscape(string(id))), nil, &relationship, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &relationship, nil
|
||||
}
|
||||
|
||||
// AccountUnmute unmute the account.
|
||||
func (c *Client) AccountUnmute(ctx context.Context, id string) (*Relationship, error) {
|
||||
var relationship Relationship
|
||||
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/unmute", url.PathEscape(string(id))), nil, &relationship, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &relationship, nil
|
||||
}
|
||||
|
||||
// GetAccountRelationships return relationship for the account.
|
||||
func (c *Client) GetAccountRelationships(ctx context.Context, ids []string) ([]*Relationship, error) {
|
||||
params := url.Values{}
|
||||
for _, id := range ids {
|
||||
params.Add("id[]", id)
|
||||
}
|
||||
|
||||
var relationships []*Relationship
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/accounts/relationships", params, &relationships, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return relationships, nil
|
||||
}
|
||||
|
||||
// AccountsSearch search accounts by query.
|
||||
func (c *Client) AccountsSearch(ctx context.Context, q string, limit int64) ([]*Account, error) {
|
||||
params := url.Values{}
|
||||
params.Set("q", q)
|
||||
params.Set("limit", fmt.Sprint(limit))
|
||||
|
||||
var accounts []*Account
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/accounts/search", params, &accounts, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
// FollowRemoteUser send follow-request.
|
||||
func (c *Client) FollowRemoteUser(ctx context.Context, uri string) (*Account, error) {
|
||||
params := url.Values{}
|
||||
params.Set("uri", uri)
|
||||
|
||||
var account Account
|
||||
err := c.doAPI(ctx, http.MethodPost, "/api/v1/follows", params, &account, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
// GetFollowRequests return follow-requests.
|
||||
func (c *Client) GetFollowRequests(ctx context.Context, pg *Pagination) ([]*Account, error) {
|
||||
var accounts []*Account
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/follow_requests", nil, &accounts, pg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
// FollowRequestAuthorize is authorize the follow request of user with id.
|
||||
func (c *Client) FollowRequestAuthorize(ctx context.Context, id string) error {
|
||||
return c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/follow_requests/%s/authorize", url.PathEscape(string(id))), nil, nil, nil)
|
||||
}
|
||||
|
||||
// FollowRequestReject is rejects the follow request of user with id.
|
||||
func (c *Client) FollowRequestReject(ctx context.Context, id string) error {
|
||||
return c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/follow_requests/%s/reject", url.PathEscape(string(id))), nil, nil, nil)
|
||||
}
|
||||
|
||||
// GetMutes returns the list of users muted by the current user.
|
||||
func (c *Client) GetMutes(ctx context.Context, pg *Pagination) ([]*Account, error) {
|
||||
var accounts []*Account
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/mutes", nil, &accounts, pg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package mastodon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AppConfig is a setting for registering applications.
|
||||
type AppConfig struct {
|
||||
http.Client
|
||||
Server string
|
||||
ClientName string
|
||||
|
||||
// Where the user should be redirected after authorization (for no redirect, use urn:ietf:wg:oauth:2.0:oob)
|
||||
RedirectURIs string
|
||||
|
||||
// This can be a space-separated list of items listed on the /settings/applications/new page of any Mastodon
|
||||
// instance. "read", "write", and "follow" are top-level scopes that include all the permissions of the more
|
||||
// specific scopes like "read:favourites", "write:statuses", and "write:follows".
|
||||
Scopes string
|
||||
|
||||
// Optional.
|
||||
Website string
|
||||
}
|
||||
|
||||
// Application is mastodon application.
|
||||
type Application struct {
|
||||
ID string `json:"id"`
|
||||
RedirectURI string `json:"redirect_uri"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
|
||||
// AuthURI is not part of the Mastodon API; it is generated by go-mastodon.
|
||||
AuthURI string `json:"auth_uri,omitempty"`
|
||||
}
|
||||
|
||||
// RegisterApp returns the mastodon application.
|
||||
func RegisterApp(ctx context.Context, appConfig *AppConfig) (*Application, error) {
|
||||
params := url.Values{}
|
||||
params.Set("client_name", appConfig.ClientName)
|
||||
if appConfig.RedirectURIs == "" {
|
||||
params.Set("redirect_uris", "urn:ietf:wg:oauth:2.0:oob")
|
||||
} else {
|
||||
params.Set("redirect_uris", appConfig.RedirectURIs)
|
||||
}
|
||||
params.Set("scopes", appConfig.Scopes)
|
||||
params.Set("website", appConfig.Website)
|
||||
|
||||
u, err := url.Parse(appConfig.Server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.Path = path.Join(u.Path, "/api/v1/apps")
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(params.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
resp, err := appConfig.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, parseAPIError("bad request", resp)
|
||||
}
|
||||
|
||||
var app Application
|
||||
err = json.NewDecoder(resp.Body).Decode(&app)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u, err = url.Parse(appConfig.Server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.Path = path.Join(u.Path, "/oauth/authorize")
|
||||
u.RawQuery = url.Values{
|
||||
"scope": {appConfig.Scopes},
|
||||
"response_type": {"code"},
|
||||
"redirect_uri": {app.RedirectURI},
|
||||
"client_id": {app.ClientID},
|
||||
}.Encode()
|
||||
|
||||
app.AuthURI = u.String()
|
||||
|
||||
return &app, nil
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
module mastodon
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
|
||||
)
|
|
@ -0,0 +1,4 @@
|
|||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y=
|
||||
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE=
|
|
@ -0,0 +1,55 @@
|
|||
package mastodon
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Base64EncodeFileName returns the base64 data URI format string of the file with the file name.
|
||||
func Base64EncodeFileName(filename string) (string, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
return Base64Encode(file)
|
||||
}
|
||||
|
||||
// Base64Encode returns the base64 data URI format string of the file.
|
||||
func Base64Encode(file *os.File) (string, error) {
|
||||
fi, err := file.Stat()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
d := make([]byte, fi.Size())
|
||||
_, err = file.Read(d)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "data:" + http.DetectContentType(d) +
|
||||
";base64," + base64.StdEncoding.EncodeToString(d), nil
|
||||
}
|
||||
|
||||
// String is a helper function to get the pointer value of a string.
|
||||
func String(v string) *string { return &v }
|
||||
|
||||
func parseAPIError(prefix string, resp *http.Response) error {
|
||||
errMsg := fmt.Sprintf("%s: %s", prefix, resp.Status)
|
||||
var e struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
json.NewDecoder(resp.Body).Decode(&e)
|
||||
if e.Error != "" {
|
||||
errMsg = fmt.Sprintf("%s: %s", errMsg, e.Error)
|
||||
}
|
||||
|
||||
return errors.New(errMsg)
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package mastodon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Instance hold information for mastodon instance.
|
||||
type Instance struct {
|
||||
URI string `json:"uri"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
EMail string `json:"email"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Thumbnail string `json:"thumbnail,omitempty"`
|
||||
URLs map[string]string `json:"urls,omitempty"`
|
||||
Stats *InstanceStats `json:"stats,omitempty"`
|
||||
Languages []string `json:"languages"`
|
||||
ContactAccount *Account `json:"account"`
|
||||
}
|
||||
|
||||
// InstanceStats hold information for mastodon instance stats.
|
||||
type InstanceStats struct {
|
||||
UserCount int64 `json:"user_count"`
|
||||
StatusCount int64 `json:"status_count"`
|
||||
DomainCount int64 `json:"domain_count"`
|
||||
}
|
||||
|
||||
// GetInstance return Instance.
|
||||
func (c *Client) GetInstance(ctx context.Context) (*Instance, error) {
|
||||
var instance Instance
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/instance", nil, &instance, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &instance, nil
|
||||
}
|
||||
|
||||
// WeeklyActivity hold information for mastodon weekly activity.
|
||||
type WeeklyActivity struct {
|
||||
Week Unixtime `json:"week"`
|
||||
Statuses int64 `json:"statuses,string"`
|
||||
Logins int64 `json:"logins,string"`
|
||||
Registrations int64 `json:"registrations,string"`
|
||||
}
|
||||
|
||||
// GetInstanceActivity return instance activity.
|
||||
func (c *Client) GetInstanceActivity(ctx context.Context) ([]*WeeklyActivity, error) {
|
||||
var activity []*WeeklyActivity
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/instance/activity", nil, &activity, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return activity, nil
|
||||
}
|
||||
|
||||
// GetInstancePeers return instance peers.
|
||||
func (c *Client) GetInstancePeers(ctx context.Context) ([]string, error) {
|
||||
var peers []string
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/instance/peers", nil, &peers, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return peers, nil
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
package mastodon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// List is metadata for a list of users.
|
||||
type List struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
// GetLists returns all the lists on the current account.
|
||||
func (c *Client) GetLists(ctx context.Context) ([]*List, error) {
|
||||
var lists []*List
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/lists", nil, &lists, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return lists, nil
|
||||
}
|
||||
|
||||
// GetAccountLists returns the lists containing a given account.
|
||||
func (c *Client) GetAccountLists(ctx context.Context, id string) ([]*List, error) {
|
||||
var lists []*List
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s/lists", url.PathEscape(string(id))), nil, &lists, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return lists, nil
|
||||
}
|
||||
|
||||
// GetListAccounts returns the accounts in a given list.
|
||||
func (c *Client) GetListAccounts(ctx context.Context, id string) ([]*Account, error) {
|
||||
var accounts []*Account
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/lists/%s/accounts", url.PathEscape(string(id))), url.Values{"limit": {"0"}}, &accounts, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
// GetList retrieves a list by string.
|
||||
func (c *Client) GetList(ctx context.Context, id string) (*List, error) {
|
||||
var list List
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/lists/%s", url.PathEscape(string(id))), nil, &list, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &list, nil
|
||||
}
|
||||
|
||||
// CreateList creates a new list with a given title.
|
||||
func (c *Client) CreateList(ctx context.Context, title string) (*List, error) {
|
||||
params := url.Values{}
|
||||
params.Set("title", title)
|
||||
|
||||
var list List
|
||||
err := c.doAPI(ctx, http.MethodPost, "/api/v1/lists", params, &list, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &list, nil
|
||||
}
|
||||
|
||||
// RenameList assigns a new title to a list.
|
||||
func (c *Client) RenameList(ctx context.Context, id string, title string) (*List, error) {
|
||||
params := url.Values{}
|
||||
params.Set("title", title)
|
||||
|
||||
var list List
|
||||
err := c.doAPI(ctx, http.MethodPut, fmt.Sprintf("/api/v1/lists/%s", url.PathEscape(string(id))), params, &list, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &list, nil
|
||||
}
|
||||
|
||||
// DeleteList removes a list.
|
||||
func (c *Client) DeleteList(ctx context.Context, id string) error {
|
||||
return c.doAPI(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/lists/%s", url.PathEscape(string(id))), nil, nil, nil)
|
||||
}
|
||||
|
||||
// AddToList adds accounts to a list.
|
||||
//
|
||||
// Only accounts already followed by the user can be added to a list.
|
||||
func (c *Client) AddToList(ctx context.Context, list string, accounts ...string) error {
|
||||
params := url.Values{}
|
||||
for _, acct := range accounts {
|
||||
params.Add("account_ids", string(acct))
|
||||
}
|
||||
|
||||
return c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/lists/%s/accounts", url.PathEscape(string(list))), params, nil, nil)
|
||||
}
|
||||
|
||||
// RemoveFromList removes accounts from a list.
|
||||
func (c *Client) RemoveFromList(ctx context.Context, list string, accounts ...string) error {
|
||||
params := url.Values{}
|
||||
for _, acct := range accounts {
|
||||
params.Add("account_ids", string(acct))
|
||||
}
|
||||
|
||||
return c.doAPI(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/lists/%s/accounts", url.PathEscape(string(list))), params, nil, nil)
|
||||
}
|
|
@ -0,0 +1,388 @@
|
|||
// Package mastodon provides functions and structs for accessing the mastodon API.
|
||||
package mastodon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tomnomnom/linkheader"
|
||||
)
|
||||
|
||||
// Config is a setting for access mastodon APIs.
|
||||
type Config struct {
|
||||
Server string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
AccessToken string
|
||||
}
|
||||
|
||||
// Client is a API client for mastodon.
|
||||
type Client struct {
|
||||
http.Client
|
||||
config *Config
|
||||
}
|
||||
|
||||
func (c *Client) doAPI(ctx context.Context, method string, uri string, params interface{}, res interface{}, pg *Pagination) error {
|
||||
u, err := url.Parse(c.config.Server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Path = path.Join(u.Path, uri)
|
||||
|
||||
var req *http.Request
|
||||
ct := "application/x-www-form-urlencoded"
|
||||
if values, ok := params.(url.Values); ok {
|
||||
var body io.Reader
|
||||
if method == http.MethodGet {
|
||||
if pg != nil {
|
||||
values = pg.setValues(values)
|
||||
}
|
||||
u.RawQuery = values.Encode()
|
||||
} else {
|
||||
body = strings.NewReader(values.Encode())
|
||||
}
|
||||
req, err = http.NewRequest(method, u.String(), body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if file, ok := params.(string); ok {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var buf bytes.Buffer
|
||||
mw := multipart.NewWriter(&buf)
|
||||
part, err := mw.CreateFormFile("file", filepath.Base(file))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(part, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = mw.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err = http.NewRequest(method, u.String(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ct = mw.FormDataContentType()
|
||||
} else if reader, ok := params.(io.Reader); ok {
|
||||
var buf bytes.Buffer
|
||||
mw := multipart.NewWriter(&buf)
|
||||
part, err := mw.CreateFormFile("file", "upload")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(part, reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = mw.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err = http.NewRequest(method, u.String(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ct = mw.FormDataContentType()
|
||||
} else {
|
||||
if method == http.MethodGet && pg != nil {
|
||||
u.RawQuery = pg.toValues().Encode()
|
||||
}
|
||||
req, err = http.NewRequest(method, u.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Set("Authorization", "Bearer "+c.config.AccessToken)
|
||||
if params != nil {
|
||||
req.Header.Set("Content-Type", ct)
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
backoff := 1000 * time.Millisecond
|
||||
for {
|
||||
resp, err = c.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// handle status code 429, which indicates the server is throttling
|
||||
// our requests. Do an exponential backoff and retry the request.
|
||||
if resp.StatusCode == 429 {
|
||||
if backoff > time.Hour {
|
||||
break
|
||||
}
|
||||
backoff *= 2
|
||||
|
||||
select {
|
||||
case <-time.After(backoff):
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return parseAPIError("bad request", resp)
|
||||
} else if res == nil {
|
||||
return nil
|
||||
} else if pg != nil {
|
||||
if lh := resp.Header.Get("Link"); lh != "" {
|
||||
pg2, err := newPagination(lh)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*pg = *pg2
|
||||
}
|
||||
}
|
||||
return json.NewDecoder(resp.Body).Decode(&res)
|
||||
}
|
||||
|
||||
// NewClient return new mastodon API client.
|
||||
func NewClient(config *Config) *Client {
|
||||
return &Client{
|
||||
Client: *http.DefaultClient,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// Authenticate get access-token to the API.
|
||||
func (c *Client) Authenticate(ctx context.Context, username, password string) error {
|
||||
params := url.Values{
|
||||
"client_id": {c.config.ClientID},
|
||||
"client_secret": {c.config.ClientSecret},
|
||||
"grant_type": {"password"},
|
||||
"username": {username},
|
||||
"password": {password},
|
||||
"scope": {"read write follow"},
|
||||
}
|
||||
|
||||
return c.authenticate(ctx, params)
|
||||
}
|
||||
|
||||
// AuthenticateToken logs in using a grant token returned by Application.AuthURI.
|
||||
//
|
||||
// redirectURI should be the same as Application.RedirectURI.
|
||||
func (c *Client) AuthenticateToken(ctx context.Context, authCode, redirectURI string) error {
|
||||
params := url.Values{
|
||||
"client_id": {c.config.ClientID},
|
||||
"client_secret": {c.config.ClientSecret},
|
||||
"grant_type": {"authorization_code"},
|
||||
"code": {authCode},
|
||||
"redirect_uri": {redirectURI},
|
||||
}
|
||||
|
||||
return c.authenticate(ctx, params)
|
||||
}
|
||||
|
||||
func (c *Client) authenticate(ctx context.Context, params url.Values) error {
|
||||
u, err := url.Parse(c.config.Server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Path = path.Join(u.Path, "/oauth/token")
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(params.Encode()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
resp, err := c.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return parseAPIError("bad authorization", resp)
|
||||
}
|
||||
|
||||
var res struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
err = json.NewDecoder(resp.Body).Decode(&res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.config.AccessToken = res.AccessToken
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) GetAccessToken(ctx context.Context) string {
|
||||
if c == nil || c.config == nil {
|
||||
return ""
|
||||
}
|
||||
return c.config.AccessToken
|
||||
}
|
||||
|
||||
// Toot is struct to post status.
|
||||
type Toot struct {
|
||||
Status string `json:"status"`
|
||||
InReplyToID string `json:"in_reply_to_id"`
|
||||
MediaIDs []string `json:"media_ids"`
|
||||
Sensitive bool `json:"sensitive"`
|
||||
SpoilerText string `json:"spoiler_text"`
|
||||
Visibility string `json:"visibility"`
|
||||
}
|
||||
|
||||
// Mention hold information for mention.
|
||||
type Mention struct {
|
||||
URL string `json:"url"`
|
||||
Username string `json:"username"`
|
||||
Acct string `json:"acct"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// Tag hold information for tag.
|
||||
type Tag struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
History []History `json:"history"`
|
||||
}
|
||||
|
||||
// History hold information for history.
|
||||
type History struct {
|
||||
Day string `json:"day"`
|
||||
Uses int64 `json:"uses"`
|
||||
Accounts int64 `json:"accounts"`
|
||||
}
|
||||
|
||||
// Attachment hold information for attachment.
|
||||
type Attachment struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
URL string `json:"url"`
|
||||
RemoteURL string `json:"remote_url"`
|
||||
PreviewURL string `json:"preview_url"`
|
||||
TextURL string `json:"text_url"`
|
||||
Description string `json:"description"`
|
||||
Meta AttachmentMeta `json:"meta"`
|
||||
}
|
||||
|
||||
// AttachmentMeta holds information for attachment metadata.
|
||||
type AttachmentMeta struct {
|
||||
Original AttachmentSize `json:"original"`
|
||||
Small AttachmentSize `json:"small"`
|
||||
}
|
||||
|
||||
// AttachmentSize holds information for attatchment size.
|
||||
type AttachmentSize struct {
|
||||
Width int64 `json:"width"`
|
||||
Height int64 `json:"height"`
|
||||
Size string `json:"size"`
|
||||
Aspect float64 `json:"aspect"`
|
||||
}
|
||||
|
||||
// Emoji hold information for CustomEmoji.
|
||||
type Emoji struct {
|
||||
ShortCode string `json:"shortcode"`
|
||||
StaticURL string `json:"static_url"`
|
||||
URL string `json:"url"`
|
||||
VisibleInPicker bool `json:"visible_in_picker"`
|
||||
}
|
||||
|
||||
// Results hold information for search result.
|
||||
type Results struct {
|
||||
Accounts []*Account `json:"accounts"`
|
||||
Statuses []*Status `json:"statuses"`
|
||||
Hashtags []string `json:"hashtags"`
|
||||
}
|
||||
|
||||
// Pagination is a struct for specifying the get range.
|
||||
type Pagination struct {
|
||||
MaxID string
|
||||
SinceID string
|
||||
MinID string
|
||||
Limit int64
|
||||
}
|
||||
|
||||
func newPagination(rawlink string) (*Pagination, error) {
|
||||
if rawlink == "" {
|
||||
return nil, errors.New("empty link header")
|
||||
}
|
||||
|
||||
p := &Pagination{}
|
||||
for _, link := range linkheader.Parse(rawlink) {
|
||||
switch link.Rel {
|
||||
case "next":
|
||||
maxID, err := getPaginationID(link.URL, "max_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.MaxID = maxID
|
||||
case "prev":
|
||||
sinceID, err := getPaginationID(link.URL, "since_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.SinceID = sinceID
|
||||
|
||||
minID, err := getPaginationID(link.URL, "min_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.MinID = minID
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func getPaginationID(rawurl, key string) (string, error) {
|
||||
u, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
val := u.Query().Get(key)
|
||||
if val == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return string(val), nil
|
||||
}
|
||||
|
||||
func (p *Pagination) toValues() url.Values {
|
||||
return p.setValues(url.Values{})
|
||||
}
|
||||
|
||||
func (p *Pagination) setValues(params url.Values) url.Values {
|
||||
if p.MaxID != "" {
|
||||
params.Set("max_id", string(p.MaxID))
|
||||
}
|
||||
if p.SinceID != "" {
|
||||
params.Set("since_id", string(p.SinceID))
|
||||
}
|
||||
if p.MinID != "" {
|
||||
params.Set("min_id", string(p.MinID))
|
||||
}
|
||||
if p.Limit > 0 {
|
||||
params.Set("limit", fmt.Sprint(p.Limit))
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package mastodon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Notification hold information for mastodon notification.
|
||||
type Notification struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Account Account `json:"account"`
|
||||
Status *Status `json:"status"`
|
||||
}
|
||||
|
||||
// GetNotifications return notifications.
|
||||
func (c *Client) GetNotifications(ctx context.Context, pg *Pagination) ([]*Notification, error) {
|
||||
var notifications []*Notification
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/notifications", nil, ¬ifications, pg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return notifications, nil
|
||||
}
|
||||
|
||||
// GetNotification return notification.
|
||||
func (c *Client) GetNotification(ctx context.Context, id string) (*Notification, error) {
|
||||
var notification Notification
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/notifications/%v", id), nil, ¬ification, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ¬ification, nil
|
||||
}
|
||||
|
||||
// ClearNotifications clear notifications.
|
||||
func (c *Client) ClearNotifications(ctx context.Context) error {
|
||||
return c.doAPI(ctx, http.MethodPost, "/api/v1/notifications/clear", nil, nil, nil)
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package mastodon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Report hold information for mastodon report.
|
||||
type Report struct {
|
||||
ID int64 `json:"id"`
|
||||
ActionTaken bool `json:"action_taken"`
|
||||
}
|
||||
|
||||
// GetReports return report of the current user.
|
||||
func (c *Client) GetReports(ctx context.Context) ([]*Report, error) {
|
||||
var reports []*Report
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/reports", nil, &reports, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||