Skip to content

Commit

Permalink
Merge pull request #147 from systemli/add-support-for-mastodon
Browse files Browse the repository at this point in the history
✨ Add Support for Mastodon
  • Loading branch information
0x46616c6b authored Oct 14, 2022
2 parents 12ded07 + b0d9781 commit 1a54ef5
Show file tree
Hide file tree
Showing 12 changed files with 356 additions and 1 deletion.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ require (
github.com/goccy/go-json v0.9.11
github.com/golang/snappy v0.0.4 // indirect
github.com/gorilla/feeds v1.1.1
github.com/mattn/go-mastodon v0.0.5
github.com/onsi/ginkgo/v2 v2.2.0
github.com/onsi/gomega v1.20.2
github.com/swaggo/swag v1.8.6
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0=
Expand Down Expand Up @@ -374,6 +376,8 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-mastodon v0.0.5 h1:P0e1/R2v3ho6kM7BUW0noQm8gAqHE0p8Gq1TMapIVAc=
github.com/mattn/go-mastodon v0.0.5/go.mod h1:cg7RFk2pcUfHZw/IvKe1FUzmlq5KnLFqs7eV2PHplV8=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
Expand Down Expand Up @@ -515,6 +519,8 @@ github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
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/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f h1:oqdnd6OGlOUu1InG37hWcCB3a+Jy3fwjylyVboaNMwY=
github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f/go.mod h1:X3Dd1SB8Gt1V968NTzpKFjMM6O8ccta2NPC6MprOxZQ=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
Expand Down
2 changes: 2 additions & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ func API(config config.Config, storage storage.TickerStorage) *gin.Engine {
admin.DELETE(`/tickers/:tickerID/twitter`, ticker.PrefetchTicker(storage), handler.DeleteTickerTwitter)
admin.PUT(`/tickers/:tickerID/telegram`, ticker.PrefetchTicker(storage), handler.PutTickerTelegram)
admin.DELETE(`/tickers/:tickerID/telegram`, ticker.PrefetchTicker(storage), handler.DeleteTickerTelegram)
admin.PUT(`/tickers/:tickerID/mastodon`, ticker.PrefetchTicker(storage), handler.PutTickerMastodon)
admin.DELETE(`/tickers/:tickerID/mastodon`, ticker.PrefetchTicker(storage), handler.DeleteTickerMastodon)
admin.DELETE(`/tickers/:tickerID`, user.NeedAdmin(), ticker.PrefetchTicker(storage), handler.DeleteTicker)
admin.PUT(`/tickers/:tickerID/reset`, ticker.PrefetchTicker(storage), ticker.PrefetchTicker(storage), handler.ResetTicker)
admin.GET(`/tickers/:tickerID/users`, ticker.PrefetchTicker(storage), handler.GetTickerUsers)
Expand Down
1 change: 1 addition & 0 deletions internal/api/response/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const (
FormError ErrorMessage = "invalid form values"
StorageError ErrorMessage = "failed to save"
UploadsNotFound ErrorMessage = "uploads not found"
MastodonError ErrorMessage = "unable to connect to mastodon"

StatusSuccess Status = `success`
StatusError Status = `error`
Expand Down
19 changes: 19 additions & 0 deletions internal/api/response/ticker.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Ticker struct {
Information Information `json:"information"`
Twitter Twitter `json:"twitter"`
Telegram Telegram `json:"telegram"`
Mastodon Mastodon `json:"mastodon"`
Location Location `json:"location"`
}

Expand Down Expand Up @@ -44,6 +45,16 @@ type Telegram struct {
ChannelName string `json:"channel_name"`
}

type Mastodon struct {
Active bool `json:"active"`
Connected bool `json:"connected"`
Name string `json:"name"`
Server string `json:"server"`
ScreenName string `json:"screen_name"`
Description string `json:"description"`
ImageURL string `json:"image_url"`
}

type Location struct {
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
Expand Down Expand Up @@ -78,6 +89,14 @@ func TickerResponse(t storage.Ticker, config config.Config) Ticker {
BotUsername: config.TelegramBotUser.UserName,
ChannelName: t.Telegram.ChannelName,
},
Mastodon: Mastodon{
Active: t.Mastodon.Active,
Connected: t.Mastodon.Connected(),
Name: t.Mastodon.User.Username,
Server: t.Mastodon.Server,
ScreenName: t.Mastodon.User.DisplayName,
ImageURL: t.Mastodon.User.Avatar,
},
Location: Location{
Lat: t.Location.Lat,
Lon: t.Location.Lon,
Expand Down
65 changes: 65 additions & 0 deletions internal/api/tickers.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"strconv"

"github.com/gin-gonic/gin"
"github.com/mattn/go-mastodon"
"github.com/systemli/ticker/internal/api/helper"
"github.com/systemli/ticker/internal/api/response"
"github.com/systemli/ticker/internal/storage"
Expand Down Expand Up @@ -223,6 +224,70 @@ func (h *handler) DeleteTickerTelegram(c *gin.Context) {
c.JSON(http.StatusOK, response.SuccessResponse(map[string]interface{}{"ticker": response.TickerResponse(ticker, h.config)}))
}

func (h *handler) PutTickerMastodon(c *gin.Context) {
ticker, err := helper.Ticker(c)
if err != nil {
c.JSON(http.StatusNotFound, response.ErrorResponse(response.CodeDefault, response.TickerNotFound))
return
}

var body storage.Mastodon
err = c.Bind(&body)
if err != nil {
c.JSON(http.StatusBadRequest, response.ErrorResponse(response.CodeNotFound, response.FormError))
return
}

if body.Secret != "" || body.Token != "" || body.AccessToken != "" || body.Server != "" {
client := mastodon.NewClient(&mastodon.Config{
Server: body.Server,
ClientID: body.Token,
ClientSecret: body.Secret,
AccessToken: body.AccessToken,
})

account, err := client.GetAccountCurrentUser(c.Request.Context())
if err != nil {
c.JSON(http.StatusBadRequest, response.ErrorResponse(response.CodeBadCredentials, response.MastodonError))
return
}

ticker.Mastodon.Server = body.Server
ticker.Mastodon.Secret = body.Secret
ticker.Mastodon.Token = body.Token
ticker.Mastodon.AccessToken = body.AccessToken
ticker.Mastodon.User = *account
}

ticker.Mastodon.Active = body.Active

err = h.storage.SaveTicker(&ticker)
if err != nil {
c.JSON(http.StatusBadRequest, response.ErrorResponse(response.CodeDefault, response.StorageError))
return
}

c.JSON(http.StatusOK, response.SuccessResponse(map[string]interface{}{"ticker": response.TickerResponse(ticker, h.config)}))
}

func (h *handler) DeleteTickerMastodon(c *gin.Context) {
ticker, err := helper.Ticker(c)
if err != nil {
c.JSON(http.StatusNotFound, response.ErrorResponse(response.CodeDefault, response.TickerNotFound))
return
}

ticker.Mastodon.Reset()

err = h.storage.SaveTicker(&ticker)
if err != nil {
c.JSON(http.StatusBadRequest, response.ErrorResponse(response.CodeDefault, response.StorageError))
return
}

c.JSON(http.StatusOK, response.SuccessResponse(map[string]interface{}{"ticker": response.TickerResponse(ticker, h.config)}))
}

func (h *handler) DeleteTicker(c *gin.Context) {
ticker, err := helper.Ticker(c)
if err != nil {
Expand Down
149 changes: 149 additions & 0 deletions internal/api/tickers_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package api

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/gin-gonic/gin"
"github.com/mattn/go-mastodon"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/systemli/ticker/internal/config"
Expand Down Expand Up @@ -545,6 +548,152 @@ func TestDeleteTickerTelegram(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
}

func TestPutTickerMastodonTickerNotFound(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
s := &storage.MockTickerStorage{}
h := handler{
storage: s,
config: config.NewConfig(),
}

h.PutTickerMastodon(c)

assert.Equal(t, http.StatusNotFound, w.Code)
}

func TestPutTickerMastodonFormError(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Set("ticker", storage.Ticker{})
c.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1/mastodon", nil)
c.Request.Header.Add("Content-Type", "application/json")
s := &storage.MockTickerStorage{}
h := handler{
storage: s,
config: config.NewConfig(),
}

h.PutTickerMastodon(c)

assert.Equal(t, http.StatusBadRequest, w.Code)
}

func TestPutTickerMastodonConnectError(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Set("ticker", storage.Ticker{})
body := `{"active":true,"server":"http://localhost","secret":"secret","token":"token","access_token":"access_token"}`
c.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1/mastodon", strings.NewReader(body))
c.Request.Header.Add("Content-Type", "application/json")
s := &storage.MockTickerStorage{}
s.On("SaveTicker", mock.Anything).Return(nil)
h := handler{
storage: s,
config: config.NewConfig(),
}

h.PutTickerMastodon(c)

assert.Equal(t, http.StatusBadRequest, w.Code)
}

func TestPutTickerMastodonStorageError(t *testing.T) {
w := httptest.NewRecorder()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
account := mastodon.Account{}
json, _ := json.Marshal(account)
w.Write(json)
}))
defer server.Close()
c, _ := gin.CreateTestContext(w)
c.Set("ticker", storage.Ticker{})
body := fmt.Sprintf(`{"server":"%s","token":"token","secret":"secret","access_token":"access_toklen"}`, server.URL)
c.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1/mastodon", strings.NewReader(body))
c.Request.Header.Add("Content-Type", "application/json")
s := &storage.MockTickerStorage{}
s.On("SaveTicker", mock.Anything).Return(errors.New("storage error"))
h := handler{
storage: s,
config: config.NewConfig(),
}

h.PutTickerMastodon(c)

assert.Equal(t, http.StatusBadRequest, w.Code)
}

func TestPutTickerMastodon(t *testing.T) {
w := httptest.NewRecorder()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
account := mastodon.Account{}
json, _ := json.Marshal(account)
w.Write(json)
}))
defer server.Close()
c, _ := gin.CreateTestContext(w)
c.Set("ticker", storage.Ticker{})
body := fmt.Sprintf(`{"server":"%s","token":"token","secret":"secret","access_token":"access_toklen"}`, server.URL)
c.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1/mastodon", strings.NewReader(body))
c.Request.Header.Add("Content-Type", "application/json")
s := &storage.MockTickerStorage{}
s.On("SaveTicker", mock.Anything).Return(nil)
h := handler{
storage: s,
config: config.NewConfig(),
}

h.PutTickerMastodon(c)

assert.Equal(t, http.StatusOK, w.Code)
}

func TestDeleteTickerMastodonTickerNotFound(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
s := &storage.MockTickerStorage{}
h := handler{
storage: s,
config: config.NewConfig(),
}

h.DeleteTickerMastodon(c)

assert.Equal(t, http.StatusNotFound, w.Code)
}

func TestDeleteTickerMastodonStorageError(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Set("ticker", storage.Ticker{})
s := &storage.MockTickerStorage{}
s.On("SaveTicker", mock.Anything).Return(errors.New("storage error"))
h := handler{
storage: s,
config: config.NewConfig(),
}

h.DeleteTickerMastodon(c)

assert.Equal(t, http.StatusBadRequest, w.Code)
}

func TestDeleteTickerMastodon(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Set("ticker", storage.Ticker{})
s := &storage.MockTickerStorage{}
s.On("SaveTicker", mock.Anything).Return(nil)
h := handler{
storage: s,
config: config.NewConfig(),
}

h.DeleteTickerMastodon(c)

assert.Equal(t, http.StatusOK, w.Code)
}

func TestDeleteTickerTickerNotFound(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
Expand Down
3 changes: 2 additions & 1 deletion internal/bridge/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ type Bridges map[string]Bridge
func RegisterBridges(config config.Config, storage storage.TickerStorage) Bridges {
twitter := TwitterBridge{config, storage}
telegram := TelegramBridge{config, storage}
mastodon := MastodonBridge{config, storage}

return Bridges{"twitter": &twitter, "telegram": &telegram}
return Bridges{"twitter": &twitter, "telegram": &telegram, "mastodon": &mastodon}
}

func (b *Bridges) Send(ticker storage.Ticker, message *storage.Message) error {
Expand Down
Loading

0 comments on commit 1a54ef5

Please sign in to comment.