From 93f6bea588a3b9bb5e000539831c1f239f9386e6 Mon Sep 17 00:00:00 2001 From: doobry Date: Fri, 5 Jul 2024 13:02:21 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Use=20ticker=20title=20and=20descri?= =?UTF-8?q?ption=20for=20signal=20group=20details?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/api/response/response.go | 1 + internal/api/response/ticker.go | 20 +++---- internal/api/response/ticker_test.go | 10 ++-- internal/api/tickers.go | 23 ++++---- internal/api/tickers_test.go | 80 +++++++++++++++++----------- internal/bridge/bluesky.go | 4 ++ internal/bridge/bluesky_test.go | 9 ++++ internal/bridge/bridge.go | 13 +++++ internal/bridge/bridge_test.go | 29 ++++++++-- internal/bridge/mastodon.go | 4 ++ internal/bridge/mastodon_test.go | 9 ++++ internal/bridge/mock_Bridge.go | 17 ++++++ internal/bridge/signal_group.go | 14 +++++ internal/bridge/telegram.go | 4 ++ internal/bridge/telegram_test.go | 9 ++++ internal/signal/signal.go | 21 ++++---- internal/storage/ticker.go | 18 +++---- 17 files changed, 199 insertions(+), 86 deletions(-) diff --git a/internal/api/response/response.go b/internal/api/response/response.go index 968c7d21..12492ef8 100644 --- a/internal/api/response/response.go +++ b/internal/api/response/response.go @@ -20,6 +20,7 @@ const ( FormError ErrorMessage = "invalid form values" StorageError ErrorMessage = "failed to save" UploadsNotFound ErrorMessage = "uploads not found" + BridgeError ErrorMessage = "unable to update ticker in bridges" MastodonError ErrorMessage = "unable to connect to mastodon" BlueskyError ErrorMessage = "unable to connect to bluesky" SignalGroupError ErrorMessage = "unable to connect to signal" diff --git a/internal/api/response/ticker.go b/internal/api/response/ticker.go index d4533177..7dd7eeb0 100644 --- a/internal/api/response/ticker.go +++ b/internal/api/response/ticker.go @@ -57,12 +57,10 @@ type Bluesky struct { } type SignalGroup struct { - Active bool `json:"active"` - Connected bool `json:"connected"` - GroupID string `json:"groupID"` - GroupName string `json:"groupName"` - GroupDescription string `json:"groupDescription"` - GroupInviteLink string `json:"groupInviteLink"` + Active bool `json:"active"` + Connected bool `json:"connected"` + GroupID string `json:"groupID"` + GroupInviteLink string `json:"groupInviteLink"` } type Location struct { @@ -108,12 +106,10 @@ func TickerResponse(t storage.Ticker, config config.Config) Ticker { Handle: t.Bluesky.Handle, }, SignalGroup: SignalGroup{ - Active: t.SignalGroup.Active, - Connected: t.SignalGroup.Connected(), - GroupID: t.SignalGroup.GroupID, - GroupName: t.SignalGroup.GroupName, - GroupDescription: t.SignalGroup.GroupDescription, - GroupInviteLink: t.SignalGroup.GroupInviteLink, + Active: t.SignalGroup.Active, + Connected: t.SignalGroup.Connected(), + GroupID: t.SignalGroup.GroupID, + GroupInviteLink: t.SignalGroup.GroupInviteLink, }, Location: Location{ Lat: t.Location.Lat, diff --git a/internal/api/response/ticker_test.go b/internal/api/response/ticker_test.go index de2045be..8959c85f 100644 --- a/internal/api/response/ticker_test.go +++ b/internal/api/response/ticker_test.go @@ -46,11 +46,9 @@ func (s *TickersResponseTestSuite) TestTickersResponse() { }, }, SignalGroup: storage.TickerSignalGroup{ - Active: true, - GroupID: "example", - GroupName: "Example", - GroupDescription: "Example", - GroupInviteLink: "https://signal.group/#example", + Active: true, + GroupID: "example", + GroupInviteLink: "https://signal.group/#example", }, Location: storage.TickerLocation{ Lat: 0.0, @@ -96,8 +94,6 @@ func (s *TickersResponseTestSuite) TestTickersResponse() { s.Equal(ticker.SignalGroup.Active, tickerResponse[0].SignalGroup.Active) s.Equal(ticker.SignalGroup.Connected(), tickerResponse[0].SignalGroup.Connected) s.Equal(ticker.SignalGroup.GroupID, tickerResponse[0].SignalGroup.GroupID) - s.Equal(ticker.SignalGroup.GroupName, tickerResponse[0].SignalGroup.GroupName) - s.Equal(ticker.SignalGroup.GroupDescription, tickerResponse[0].SignalGroup.GroupDescription) s.Equal(ticker.Location.Lat, tickerResponse[0].Location.Lat) s.Equal(ticker.Location.Lon, tickerResponse[0].Location.Lon) } diff --git a/internal/api/tickers.go b/internal/api/tickers.go index c27e4362..4f072f01 100644 --- a/internal/api/tickers.go +++ b/internal/api/tickers.go @@ -84,6 +84,13 @@ func (h *handler) PutTicker(c *gin.Context) { return } + err = h.bridges.UpdateTicker(ticker) + if err != nil { + log.WithError(err).Error("failed to update ticker in bridges") + c.JSON(http.StatusBadRequest, response.ErrorResponse(response.CodeDefault, response.BridgeError)) + return + } + err = h.storage.SaveTicker(&ticker) if err != nil { c.JSON(http.StatusBadRequest, response.ErrorResponse(response.CodeDefault, response.StorageError)) @@ -304,16 +311,12 @@ func (h *handler) PutTickerSignalGroup(c *gin.Context) { return } - if body.GroupName != "" && body.GroupDescription != "" { - ticker.SignalGroup.GroupName = body.GroupName - ticker.SignalGroup.GroupDescription = body.GroupDescription - groupClient := signal.NewGroupClient(h.config) - err = groupClient.CreateOrUpdateGroup(&ticker.SignalGroup) - if err != nil { - log.WithError(err).Error("failed to create or update group") - c.JSON(http.StatusBadRequest, response.ErrorResponse(response.CodeDefault, response.SignalGroupError)) - return - } + groupClient := signal.NewGroupClient(h.config) + err = groupClient.CreateOrUpdateGroup(&ticker) + if err != nil { + log.WithError(err).Error("failed to create or update group") + c.JSON(http.StatusBadRequest, response.ErrorResponse(response.CodeDefault, response.SignalGroupError)) + return } ticker.SignalGroup.Active = body.Active diff --git a/internal/api/tickers_test.go b/internal/api/tickers_test.go index 3745c934..7224a554 100644 --- a/internal/api/tickers_test.go +++ b/internal/api/tickers_test.go @@ -594,44 +594,48 @@ func (s *TickerTestSuite) TestPutTickerSignalGroup() { s.store.AssertExpectations(s.T()) }) - s.Run("when storage returns error", func() { - s.ctx.Set("ticker", storage.Ticker{}) - body := `{"active":true}` - s.ctx.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1/signal_group", strings.NewReader(body)) - s.ctx.Request.Header.Add("Content-Type", "application/json") - s.store.On("SaveTicker", mock.Anything).Return(errors.New("storage error")).Once() - - h := s.handler() - h.PutTickerSignalGroup(s.ctx) - - s.Equal(http.StatusBadRequest, s.w.Code) - s.store.AssertExpectations(s.T()) - }) + s.Run("when signal-cli API call updateGroup returns error", func() { + // updateGroup + gock.New("https://signal-cli.example.org"). + Post("/api/v1/rpc"). + MatchHeader("Content-Type", "application/json"). + Reply(500) - s.Run("when storage returns ticker", func() { - s.ctx.Set("ticker", storage.Ticker{}) + s.ctx.Set("ticker", storage.Ticker{Title: "Example", Description: "Example"}) body := `{"active":true}` s.ctx.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1/signal_group", strings.NewReader(body)) s.ctx.Request.Header.Add("Content-Type", "application/json") - s.store.On("SaveTicker", mock.Anything).Return(nil).Once() h := s.handler() h.PutTickerSignalGroup(s.ctx) - s.Equal(http.StatusOK, s.w.Code) - s.Equal(gock.IsDone(), true) + s.Equal(http.StatusBadRequest, s.w.Code) + s.True(gock.IsDone()) s.store.AssertExpectations(s.T()) }) - s.Run("when signal-cli API call updateGroup returns error", func() { + s.Run("when signal-cli API call getGroups returns error", func() { // updateGroup + gock.New("https://signal-cli.example.org"). + Post("/api/v1/rpc"). + MatchHeader("Content-Type", "application/json"). + Reply(200). + JSON(map[string]interface{}{ + "jsonrpc": "2.0", + "result": map[string]interface{}{ + "groupId": "sample-group-id", + "timestamp": 1, + }, + "id": 1, + }) + // listGroups gock.New("https://signal-cli.example.org"). Post("/api/v1/rpc"). MatchHeader("Content-Type", "application/json"). Reply(500) - s.ctx.Set("ticker", storage.Ticker{}) - body := `{"active":true,"GroupName":"Example","GroupDescription":"Example"}` + s.ctx.Set("ticker", storage.Ticker{Title: "Example", Description: "Example"}) + body := `{"active":true}` s.ctx.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1/signal_group", strings.NewReader(body)) s.ctx.Request.Header.Add("Content-Type", "application/json") @@ -643,7 +647,7 @@ func (s *TickerTestSuite) TestPutTickerSignalGroup() { s.store.AssertExpectations(s.T()) }) - s.Run("when signal-cli API call getGroups returns error", func() { + s.Run("when storage returns error", func() { // updateGroup gock.New("https://signal-cli.example.org"). Post("/api/v1/rpc"). @@ -661,22 +665,34 @@ func (s *TickerTestSuite) TestPutTickerSignalGroup() { gock.New("https://signal-cli.example.org"). Post("/api/v1/rpc"). MatchHeader("Content-Type", "application/json"). - Reply(500) + Reply(200). + JSON(map[string]interface{}{ + "jsonrpc": "2.0", + "result": []map[string]interface{}{ + { + "id": "sample-group-id", + "name": "Example", + "description": "Example", + "groupInviteLink": "https://signal.group/#example", + }, + }, + "id": 1, + }) - s.ctx.Set("ticker", storage.Ticker{}) - body := `{"active":true,"GroupName":"Example","GroupDescription":"Example"}` + s.ctx.Set("ticker", storage.Ticker{Title: "Example", Description: "Example"}) + body := `{"active":true}` s.ctx.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1/signal_group", strings.NewReader(body)) s.ctx.Request.Header.Add("Content-Type", "application/json") + s.store.On("SaveTicker", mock.Anything).Return(errors.New("storage error")).Once() h := s.handler() h.PutTickerSignalGroup(s.ctx) s.Equal(http.StatusBadRequest, s.w.Code) - s.True(gock.IsDone()) s.store.AssertExpectations(s.T()) }) - s.Run("when enabling signal group successfully", func() { + s.Run("when storage returns ticker", func() { // updateGroup gock.New("https://signal-cli.example.org"). Post("/api/v1/rpc"). @@ -708,8 +724,8 @@ func (s *TickerTestSuite) TestPutTickerSignalGroup() { "id": 1, }) - s.ctx.Set("ticker", storage.Ticker{}) - body := `{"active":true,"GroupName":"Example","GroupDescription":"Example"}` + s.ctx.Set("ticker", storage.Ticker{Title: "Example", Description: "Example"}) + body := `{"active":true}` s.ctx.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1/signal_group", strings.NewReader(body)) s.ctx.Request.Header.Add("Content-Type", "application/json") s.store.On("SaveTicker", mock.Anything).Return(nil).Once() @@ -718,7 +734,7 @@ func (s *TickerTestSuite) TestPutTickerSignalGroup() { h.PutTickerSignalGroup(s.ctx) s.Equal(http.StatusOK, s.w.Code) - s.True(gock.IsDone()) + s.Equal(gock.IsDone(), true) s.store.AssertExpectations(s.T()) }) @@ -754,8 +770,8 @@ func (s *TickerTestSuite) TestPutTickerSignalGroup() { "id": 1, }) - s.ctx.Set("ticker", storage.Ticker{}) - body := `{"active":true,"GroupId":"sample-group-id","GroupName":"Example","GroupDescription":"Example"}` + s.ctx.Set("ticker", storage.Ticker{Title: "Example", Description: "Example"}) + body := `{"active":true}` s.ctx.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1/signal_group", strings.NewReader(body)) s.ctx.Request.Header.Add("Content-Type", "application/json") s.store.On("SaveTicker", mock.Anything).Return(nil).Once() diff --git a/internal/bridge/bluesky.go b/internal/bridge/bluesky.go index 456b77ef..7d0c3d2d 100644 --- a/internal/bridge/bluesky.go +++ b/internal/bridge/bluesky.go @@ -22,6 +22,10 @@ type BlueskyBridge struct { storage storage.Storage } +func (bb *BlueskyBridge) UpdateTicker(ticker storage.Ticker) error { + return nil +} + func (bb *BlueskyBridge) Send(ticker storage.Ticker, message *storage.Message) error { if !ticker.Bluesky.Connected() || !ticker.Bluesky.Active { return nil diff --git a/internal/bridge/bluesky_test.go b/internal/bridge/bluesky_test.go index 072d25a7..8a292266 100644 --- a/internal/bridge/bluesky_test.go +++ b/internal/bridge/bluesky_test.go @@ -8,6 +8,15 @@ import ( "github.com/systemli/ticker/internal/storage" ) +func (s *BridgeTestSuite) TestBlueskyUpdateTicker() { + s.Run("does nothing", func() { + bridge := s.blueskyBridge(config.Config{}, &storage.MockStorage{}) + + err := bridge.UpdateTicker(tickerWithBridges) + s.NoError(err) + }) +} + func (s *BridgeTestSuite) TestBlueskySend() { s.Run("when bluesky is inactive", func() { bridge := s.blueskyBridge(config.Config{}, &storage.MockStorage{}) diff --git a/internal/bridge/bridge.go b/internal/bridge/bridge.go index a4e73490..43523c6d 100644 --- a/internal/bridge/bridge.go +++ b/internal/bridge/bridge.go @@ -9,6 +9,7 @@ import ( var log = logrus.WithField("package", "bridge") type Bridge interface { + UpdateTicker(ticker storage.Ticker) error Send(ticker storage.Ticker, message *storage.Message) error Delete(ticker storage.Ticker, message *storage.Message) error } @@ -24,6 +25,18 @@ func RegisterBridges(config config.Config, storage storage.Storage) Bridges { return Bridges{"telegram": &telegram, "mastodon": &mastodon, "bluesky": &bluesky, "signalGroup": &signalGroup} } +func (b *Bridges) UpdateTicker(ticker storage.Ticker) error { + var err error + for name, bridge := range *b { + err := bridge.UpdateTicker(ticker) + if err != nil { + log.WithError(err).WithField("bridge_name", name).Error("failed to update ticker") + } + } + + return err +} + func (b *Bridges) Send(ticker storage.Ticker, message *storage.Message) error { var err error for name, bridge := range *b { diff --git a/internal/bridge/bridge_test.go b/internal/bridge/bridge_test.go index 837c2956..64c66a43 100644 --- a/internal/bridge/bridge_test.go +++ b/internal/bridge/bridge_test.go @@ -54,10 +54,8 @@ func (s *BridgeTestSuite) SetupTest() { AppKey: "app_key", }, SignalGroup: storage.TickerSignalGroup{ - Active: true, - GroupID: "group_id", - GroupName: "group_name", - GroupDescription: "group_description", + Active: true, + GroupID: "group_id", }, } messageWithoutBridges = storage.Message{ @@ -102,6 +100,29 @@ func (s *BridgeTestSuite) SetupTest() { } } +func (s *BridgeTestSuite) TestUpdateTicker() { + s.Run("when successful", func() { + ticker := storage.Ticker{} + bridge := MockBridge{} + bridge.On("UpdateTicker", ticker).Return(nil).Once() + + bridges := Bridges{"mock": &bridge} + err := bridges.UpdateTicker(ticker) + s.NoError(err) + s.True(bridge.AssertExpectations(s.T())) + }) + + s.Run("when failed", func() { + ticker := storage.Ticker{} + bridge := MockBridge{} + bridge.On("UpdateTicker", ticker).Return(errors.New("failed to update ticker")).Once() + + bridges := Bridges{"mock": &bridge} + _ = bridges.UpdateTicker(ticker) + s.True(bridge.AssertExpectations(s.T())) + }) +} + func (s *BridgeTestSuite) TestSend() { s.Run("when successful", func() { ticker := storage.Ticker{} diff --git a/internal/bridge/mastodon.go b/internal/bridge/mastodon.go index 507f82c7..b4af2ddd 100644 --- a/internal/bridge/mastodon.go +++ b/internal/bridge/mastodon.go @@ -14,6 +14,10 @@ type MastodonBridge struct { storage storage.Storage } +func (mb *MastodonBridge) UpdateTicker(ticker storage.Ticker) error { + return nil +} + func (mb *MastodonBridge) Send(ticker storage.Ticker, message *storage.Message) error { if !ticker.Mastodon.Active { return nil diff --git a/internal/bridge/mastodon_test.go b/internal/bridge/mastodon_test.go index bf8a8c45..807a132d 100644 --- a/internal/bridge/mastodon_test.go +++ b/internal/bridge/mastodon_test.go @@ -8,6 +8,15 @@ import ( "github.com/systemli/ticker/internal/storage" ) +func (s *BridgeTestSuite) TestMastodonUpdateTicker() { + s.Run("does nothing", func() { + bridge := s.mastodonBridge(config.Config{}, &storage.MockStorage{}) + + err := bridge.UpdateTicker(tickerWithBridges) + s.NoError(err) + }) +} + func (s *BridgeTestSuite) TestMastodonSend() { s.Run("when mastodon is inactive", func() { bridge := s.mastodonBridge(config.Config{}, &storage.MockStorage{}) diff --git a/internal/bridge/mock_Bridge.go b/internal/bridge/mock_Bridge.go index 97645092..95da5d4e 100644 --- a/internal/bridge/mock_Bridge.go +++ b/internal/bridge/mock_Bridge.go @@ -12,6 +12,23 @@ type MockBridge struct { mock.Mock } +func (_m *MockBridge) UpdateTicker(ticker storage.Ticker) error { + ret := _m.Called(ticker) + + if len(ret) == 0 { + panic("no return value specified for UpdateTicker") + } + + var r0 error + if rf, ok := ret.Get(0).(func(storage.Ticker) error); ok { + r0 = rf(ticker) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // Delete provides a mock function with given fields: ticker, message func (_m *MockBridge) Delete(ticker storage.Ticker, message *storage.Message) error { ret := _m.Called(ticker, message) diff --git a/internal/bridge/signal_group.go b/internal/bridge/signal_group.go index 2438c60f..92391187 100644 --- a/internal/bridge/signal_group.go +++ b/internal/bridge/signal_group.go @@ -21,6 +21,20 @@ type SignalGroupResponse struct { Timestamp int `json:"timestamp"` } +func (sb *SignalGroupBridge) UpdateTicker(ticker storage.Ticker) error { + if !sb.config.SignalGroup.Enabled() || !ticker.SignalGroup.Connected() { + return nil + } + + groupClient := signal.NewGroupClient(sb.config) + err := groupClient.CreateOrUpdateGroup(&ticker) + if err != nil { + return err + } + + return nil +} + func (sb *SignalGroupBridge) Send(ticker storage.Ticker, message *storage.Message) error { if !sb.config.SignalGroup.Enabled() || !ticker.SignalGroup.Connected() || !ticker.SignalGroup.Active { return nil diff --git a/internal/bridge/telegram.go b/internal/bridge/telegram.go index dcae4284..26b868de 100644 --- a/internal/bridge/telegram.go +++ b/internal/bridge/telegram.go @@ -11,6 +11,10 @@ type TelegramBridge struct { storage storage.Storage } +func (tb *TelegramBridge) UpdateTicker(ticker storage.Ticker) error { + return nil +} + func (tb *TelegramBridge) Send(ticker storage.Ticker, message *storage.Message) error { if ticker.Telegram.ChannelName == "" || !tb.config.Telegram.Enabled() || !ticker.Telegram.Active { return nil diff --git a/internal/bridge/telegram_test.go b/internal/bridge/telegram_test.go index 5ad8f39b..d208c299 100644 --- a/internal/bridge/telegram_test.go +++ b/internal/bridge/telegram_test.go @@ -9,6 +9,15 @@ import ( "github.com/systemli/ticker/internal/storage" ) +func (s *BridgeTestSuite) TestTelegramUpdateTicker() { + s.Run("does nothing", func() { + bridge := s.telegramBridge(config.Config{}, &storage.MockStorage{}) + + err := bridge.UpdateTicker(tickerWithBridges) + s.NoError(err) + }) +} + func (s *BridgeTestSuite) TestTelegramSend() { s.Run("when telegram is inactive", func() { bridge := s.telegramBridge(config.Config{}, &storage.MockStorage{}) diff --git a/internal/signal/signal.go b/internal/signal/signal.go index ebdd862f..4acfdb43 100644 --- a/internal/signal/signal.go +++ b/internal/signal/signal.go @@ -33,11 +33,11 @@ func NewGroupClient(cfg config.Config) *GroupClient { return &GroupClient{cfg, client} } -func (gc *GroupClient) CreateOrUpdateGroup(ts *storage.TickerSignalGroup) error { +func (gc *GroupClient) CreateOrUpdateGroup(t *storage.Ticker) error { params := map[string]interface{}{ "account": gc.cfg.SignalGroup.Account, - "name": ts.GroupName, - "description": ts.GroupDescription, + "name": t.Title, + "description": t.Description, "avatar": gc.cfg.SignalGroup.Avatar, "link": "enabled", "setPermissionAddMember": "every-member", @@ -45,8 +45,8 @@ func (gc *GroupClient) CreateOrUpdateGroup(ts *storage.TickerSignalGroup) error "setPermissionSendMessages": "only-admins", "expiration": 86400, } - if ts.GroupID != "" { - params["group-id"] = ts.GroupID + if t.SignalGroup.GroupID != "" { + params["group-id"] = t.SignalGroup.GroupID } var response struct { @@ -57,15 +57,16 @@ func (gc *GroupClient) CreateOrUpdateGroup(ts *storage.TickerSignalGroup) error if err != nil { return err } - if ts.GroupID == "" { - ts.GroupID = response.GroupID + if t.SignalGroup.GroupID == "" { + // Set GroupID for newly created group + t.SignalGroup.GroupID = response.GroupID } - if ts.GroupID == "" { + if t.SignalGroup.GroupID == "" { return errors.New("unable to create or update group") } - g, err := gc.getGroup(ts.GroupID) + g, err := gc.getGroup(t.SignalGroup.GroupID) if err != nil { return err } @@ -73,7 +74,7 @@ func (gc *GroupClient) CreateOrUpdateGroup(ts *storage.TickerSignalGroup) error return errors.New("unable to get group invite link") } - ts.GroupInviteLink = g.GroupInviteLink + t.SignalGroup.GroupInviteLink = g.GroupInviteLink return nil } diff --git a/internal/storage/ticker.go b/internal/storage/ticker.go index 8aab99ab..89c4100c 100644 --- a/internal/storage/ticker.go +++ b/internal/storage/ticker.go @@ -144,15 +144,13 @@ func (b *TickerBluesky) Reset() { } type TickerSignalGroup struct { - ID int `gorm:"primaryKey"` - CreatedAt time.Time - UpdatedAt time.Time - TickerID int `gorm:"index"` - Active bool - GroupName string - GroupDescription string - GroupID string - GroupInviteLink string + ID int `gorm:"primaryKey"` + CreatedAt time.Time + UpdatedAt time.Time + TickerID int `gorm:"index"` + Active bool + GroupID string + GroupInviteLink string } func (s *TickerSignalGroup) Connected() bool { @@ -161,8 +159,6 @@ func (s *TickerSignalGroup) Connected() bool { func (s *TickerSignalGroup) Reset() { s.Active = false - s.GroupName = "" - s.GroupDescription = "" s.GroupID = "" s.GroupInviteLink = "" }