This commit is contained in:
Paul Waldo 2023-06-25 20:17:17 +09:00 committed by GitHub
commit 9bb271a14a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 322 additions and 6 deletions

25
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,25 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${fileDirname}",
"console": "integratedTerminal"
},
{
"name": "mstdn",
"type": "go",
"request": "launch",
"mode": "auto",
// "cwd": "${workspaceFolder}/cmd/mstdn",
"program": "${workspaceFolder}/cmd/mstdn",
"console": "integratedTerminal"
}
]
}

View file

@ -69,6 +69,65 @@ func main() {
}
```
### Client with Token
This option lets the user avoid storing login credentials in the application. Instead, the user's Mastodon server
provides an access token which is used to authenticate. This token can be stored in the application, but should be guarded.
```
package main
import (
"context"
"fmt"
"log"
"net/url"
"github.com/mattn/go-mastodon"
)
func main() {
appConfig := &mastodon.AppConfig{
Server: "https://stranger.social",
ClientName: "client-name",
Scopes: "read write follow",
Website: "https://github.com/mattn/go-mastodon",
RedirectURIs: "urn:ietf:wg:oauth:2.0:oob",
}
app, err := mastodon.RegisterApp(context.Background(), appConfig)
if err != nil {
log.Fatal(err)
}
// Have the user manually get the token and send it back to us
u, err := url.Parse(app.AuthURI)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Open your browser to \n%s\n and copy/paste the given token\n", u)
var token string
fmt.Print("Paste the token here:")
fmt.Scanln(&token)
config := &mastodon.Config{
Server: "https://stranger.social",
ClientID: app.ClientID,
ClientSecret: app.ClientSecret,
AccessToken: token,
}
c := mastodon.NewClient(config)
err = c.AuthenticateToken(context.Background(), token, "urn:ietf:wg:oauth:2.0:oob")
if err != nil {
log.Fatal((err)
}
acct, err := c.GetAccountCurrentUser(context.Background())
if err != nil {
log.Fatal((err)
}
fmt.Printf("Account is %v\n", acct)
}
```
## Status of implementations
* [x] GET /api/v1/accounts/:id
@ -102,6 +161,7 @@ func main() {
* [x] GET /api/v1/follow_requests
* [x] POST /api/v1/follow_requests/:id/authorize
* [x] POST /api/v1/follow_requests/:id/reject
* [x] GET /api/v1/followed_tags
* [x] POST /api/v1/follows
* [x] GET /api/v1/instance
* [x] GET /api/v1/instance/activity

View file

@ -2,6 +2,7 @@ package mastodon
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
@ -32,6 +33,7 @@ type Account struct {
Bot bool `json:"bot"`
Discoverable bool `json:"discoverable"`
Source *AccountSource `json:"source"`
FollowedTag []FollowedTag `json:"followed_tags"`
}
// Field is a Mastodon account profile field.
@ -50,6 +52,40 @@ type AccountSource struct {
Fields *[]Field `json:"fields"`
}
// UnixTimeString represents a time in a Unix Epoch string
type UnixTimeString struct {
time.Time
}
func (u *UnixTimeString) UnmarshalJSON(b []byte) error {
var timestampSring string
err := json.Unmarshal(b, &timestampSring)
if err != nil {
return err
}
timestamp, err := strconv.ParseInt(timestampSring, 0, 0)
if err != nil {
return err
}
u.Time = time.Unix(timestamp, 0)
return nil
}
// History is the history of a followed tag
type FollowedTagHistory struct {
Day UnixTimeString `json:"day,omitempty"`
Accounts int `json:"accounts,string,omitempty"`
Uses int `json:"uses,string,omitempty"`
}
// FollowedTag is a Hash Tag followed by the user
type FollowedTag struct {
Name string `json:"name,omitempty"`
URL string `json:"url,omitempty"`
History []FollowedTagHistory `json:"history,omitempty"`
Following bool `json:"following,omitempty"`
}
// GetAccount return Account.
func (c *Client) GetAccount(ctx context.Context, id ID) (*Account, error) {
var account Account
@ -326,3 +362,13 @@ func (c *Client) GetMutes(ctx context.Context, pg *Pagination) ([]*Account, erro
}
return accounts, nil
}
// GetFollowedTags returns the list of Hashtags followed by the user.
func (c *Client) GetFollowedTags(ctx context.Context, pg *Pagination) ([]*FollowedTag, error) {
var followedTags []*FollowedTag
err := c.doAPI(ctx, http.MethodGet, "/api/v1/followed_tags", nil, &followedTags, pg)
if err != nil {
return nil, err
}
return followedTags, nil
}

View file

@ -697,3 +697,93 @@ func TestGetMutes(t *testing.T) {
t.Fatalf("want %q but %q", "bar", mutes[1].Username)
}
}
func TestGetFollowedTags(t *testing.T) {
t.Parallel()
canErr := true
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if canErr {
canErr = false
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
fmt.Fprintln(w, `[
{
"name": "Test1",
"url": "http://mastodon.example/tags/test1",
"history": [
{
"day": "1668211200",
"accounts": "0",
"uses": "0"
},
{
"day": "1668124800",
"accounts": "0",
"uses": "0"
},
{
"day": "1668038400",
"accounts": "0",
"uses": "0"
}
],
"following": true
},
{
"name": "Test2",
"url": "http://mastodon.example/tags/test2",
"history": [
{
"day": "1668211200",
"accounts": "0",
"uses": "0"
}
],
"following": true
}
]`)
}))
defer ts.Close()
client := NewClient(&Config{
Server: ts.URL,
ClientID: "foo",
ClientSecret: "bar",
AccessToken: "zoo",
})
_, err := client.GetFollowedTags(context.Background(), nil)
if err == nil {
t.Fatalf("should be fail: %v", err)
}
followedTags, err := client.GetFollowedTags(context.Background(), nil)
if err != nil {
t.Fatalf("should not be fail: %v", err)
}
if len(followedTags) != 2 {
t.Fatalf("result should be two: %d", len(followedTags))
}
if followedTags[0].Name != "Test1" {
t.Fatalf("want %q but %q", "Test1", followedTags[0].Name)
}
if followedTags[0].URL != "http://mastodon.example/tags/test1" {
t.Fatalf("want %q but got %q", "http://mastodon.example/tags/test1", followedTags[0].URL)
}
if !followedTags[0].Following {
t.Fatalf("want following, but got false")
}
if 3 != len(followedTags[0].History) {
t.Fatalf("expecting first tag history length to be %d but got %d", 3, len(followedTags[0].History))
}
if followedTags[1].Name != "Test2" {
t.Fatalf("want %q but %q", "Test2", followedTags[1].Name)
}
if followedTags[1].URL != "http://mastodon.example/tags/test2" {
t.Fatalf("want %q but got %q", "http://mastodon.example/tags/test2", followedTags[1].URL)
}
if !followedTags[1].Following {
t.Fatalf("want following, but got false")
}
if 1 != len(followedTags[1].History) {
t.Fatalf("expecting first tag history length to be %d but got %d", 1, len(followedTags[1].History))
}
}

View file

@ -9,7 +9,6 @@ require (
github.com/fatih/color v1.13.0
github.com/mattn/go-mastodon v0.0.4
github.com/mattn/go-tty v0.0.4
github.com/urfave/cli v1.13.0
github.com/urfave/cli/v2 v2.23.5 // indirect
github.com/urfave/cli/v2 v2.23.5
golang.org/x/net v0.0.0-20220531201128-c960675eff93
)

View file

@ -24,8 +24,6 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
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=
github.com/urfave/cli v1.13.0 h1:kkpCmfxnnnWIie2rCljcvaVrNYmsFq1ynTJH5kn1Ip4=
github.com/urfave/cli v1.13.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli/v2 v2.23.5 h1:xbrU7tAYviSpqeR3X4nEFWUdB/uDZ6DE+HxmRU7Xtyw=
github.com/urfave/cli/v2 v2.23.5/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=

View file

@ -258,7 +258,7 @@ func makeApp() *cli.App {
{
Name: "timeline-tag",
Flags: []cli.Flag{
cli.BoolFlag{
&cli.BoolFlag{
Name: "local",
Usage: "local tags only",
},

6
go.work Normal file
View file

@ -0,0 +1,6 @@
go 1.19
use (
.
./cmd/mstdn
)

View file

@ -729,7 +729,7 @@ func TestSearch(t *testing.T) {
t.Fatalf("Hashtags have %q entries, but %q", "3", len(ret.Hashtags))
}
if ret.Hashtags[2].Name != "tag3" {
t.Fatalf("Hashtags[2] should %q , but %q", "tag3", ret.Hashtags[2])
t.Fatalf("Hashtags[2] should %v , but %v", "tag3", ret.Hashtags[2])
}
}

17
tags.go Normal file
View file

@ -0,0 +1,17 @@
package mastodon
import (
"context"
"fmt"
"net/http"
)
// TagUnfollow unfollows a hashtag.
func (c *Client) TagUnfollow(ctx context.Context, ID string) (*FollowedTag, error) {
var tag FollowedTag
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/tags/%s/unfollow", ID), nil, &tag, nil)
if err != nil {
return nil, err
}
return &tag, nil
}

75
tags_test.go Normal file
View file

@ -0,0 +1,75 @@
package mastodon
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
func TestTagUnfollow(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, `{
"name": "Test",
"url": "http://mastodon.example/tags/test",
"history": [
{
"day": "1668556800",
"accounts": "0",
"uses": "0"
},
{
"day": "1668470400",
"accounts": "0",
"uses": "0"
},
{
"day": "1668384000",
"accounts": "0",
"uses": "0"
},
{
"day": "1668297600",
"accounts": "1",
"uses": "1"
},
{
"day": "1668211200",
"accounts": "0",
"uses": "0"
},
{
"day": "1668124800",
"accounts": "0",
"uses": "0"
},
{
"day": "1668038400",
"accounts": "0",
"uses": "0"
}
],
"following": false
}`)
}))
defer ts.Close()
client := NewClient(&Config{
Server: ts.URL,
ClientID: "foo",
ClientSecret: "bar",
AccessToken: "zoo",
})
tag, err := client.TagUnfollow(context.Background(), "Test")
if err != nil {
t.Fatalf("should not be fail: %v", err)
}
if tag.Name != "Test" {
t.Fatalf("want %q but %q", "Test", tag.Name)
}
if tag.Following {
t.Fatalf("want %t but %t", false, tag.Following)
}
}