Skip to content

Commit

Permalink
Merge pull request #317 from systemli/feat/signal
Browse files Browse the repository at this point in the history
✨ Add a Signal group bridge
  • Loading branch information
doobry-systemli authored Jun 23, 2024
2 parents 73bd773 + 298f712 commit 4f65542
Show file tree
Hide file tree
Showing 24 changed files with 1,213 additions and 13 deletions.
5 changes: 5 additions & 0 deletions config.yml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ secret: "slorp-panfil-becall-dorp-hashab-incus-biter-lyra-pelage-sarraf-drunk"
# telegram configuration
telegram:
token: ""
# signal group configuration
signal_group:
api_url: ""
avatar: ""
account: ""
# listen port for prometheus metrics exporter
metrics_listen: ":8181"
upload:
Expand Down
10 changes: 9 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ database:
secret: "slorp-panfil-becall-dorp-hashab-incus-biter-lyra-pelage-sarraf-drunk"
# telegram configuration
telegram:
token: ""
token: "" # telegram bot token
# signal group configuration
signal_group:
api_url: "" # URL to your signal cli (https://github.com/AsamK/signal-cli)
avatar: "" # URL to the avatar for the signal group
account: "" # phone number for the signal account
# listen port for prometheus metrics exporter
metrics_listen: ":8181"
upload:
Expand All @@ -39,6 +44,9 @@ The following env vars can be used:
* `TICKER_INITIATOR`
* `TICKER_SECRET`
* `TICKER_TELEGRAM_TOKEN`
* `TICKER_SIGNAL_GROUP_API_URL`
* `TICKER_SIGNAL_GROUP_AVATAR`
* `TICKER_SIGNAL_GROUP_ACCOUNT`
* `TICKER_METRICS_LISTEN`
* `TICKER_UPLOAD_PATH`
* `TICKER_UPLOAD_URL`
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ require (
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
github.com/whyrusleeping/cbor-gen v0.1.1-0.20240311221002-68b9f235c302 // indirect
github.com/ybbus/jsonrpc/v3 v3.1.5 // indirect
go.etcd.io/bbolt v1.3.7 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
go.opentelemetry.io/otel v1.21.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSD
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
github.com/whyrusleeping/cbor-gen v0.1.1-0.20240311221002-68b9f235c302 h1:MhInbXe4SzcImAKktUvWBCWZgcw6MYf5NfumTj1BhAw=
github.com/whyrusleeping/cbor-gen v0.1.1-0.20240311221002-68b9f235c302/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so=
github.com/ybbus/jsonrpc/v3 v3.1.5 h1:0cC/QzS8OCuXYqqDbYnKKhsEe+IZLrNlDx8KPCieeW0=
github.com/ybbus/jsonrpc/v3 v3.1.5/go.mod h1:U1QbyNfL5Pvi2roT0OpRbJeyvGxfWYSgKJHjxWdAEeE=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
Expand Down
3 changes: 3 additions & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ func API(config config.Config, store storage.Storage, log *logrus.Logger) *gin.E
admin.DELETE(`/tickers/:tickerID/mastodon`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.DeleteTickerMastodon)
admin.PUT(`/tickers/:tickerID/bluesky`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.PutTickerBluesky)
admin.DELETE(`/tickers/:tickerID/bluesky`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.DeleteTickerBluesky)
admin.PUT(`/tickers/:tickerID/signal_group`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.PutTickerSignalGroup)
admin.DELETE(`/tickers/:tickerID/signal_group`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.DeleteTickerSignalGroup)
admin.PUT(`/tickers/:tickerID/signal_group/admin`, ticker.PrefetchTicker(store, storage.WithPreload()), handler.PutTickerSignalGroupAdmin)
admin.DELETE(`/tickers/:tickerID`, user.NeedAdmin(), ticker.PrefetchTicker(store), handler.DeleteTicker)
admin.PUT(`/tickers/:tickerID/reset`, ticker.PrefetchTicker(store, storage.WithPreload()), ticker.PrefetchTicker(store), handler.ResetTicker)
admin.GET(`/tickers/:tickerID/users`, ticker.PrefetchTicker(store), handler.GetTickerUsers)
Expand Down
3 changes: 2 additions & 1 deletion internal/api/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ type FeaturesResponse map[string]bool

func NewFeaturesResponse(config config.Config) FeaturesResponse {
return FeaturesResponse{
"telegramEnabled": config.Telegram.Enabled(),
"telegramEnabled": config.Telegram.Enabled(),
"signalGroupEnabled": config.SignalGroup.Enabled(),
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/api/features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func (s *FeaturesTestSuite) TestGetFeatures() {
h.GetFeatures(c)

s.Equal(http.StatusOK, w.Code)
s.Equal(`{"data":{"features":{"telegramEnabled":false}},"status":"success","error":{}}`, w.Body.String())
s.Equal(`{"data":{"features":{"signalGroupEnabled":false,"telegramEnabled":false}},"status":"success","error":{}}`, w.Body.String())
}

func TestFeaturesTestSuite(t *testing.T) {
Expand Down
2 changes: 2 additions & 0 deletions internal/api/response/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const (
UploadsNotFound ErrorMessage = "uploads not found"
MastodonError ErrorMessage = "unable to connect to mastodon"
BlueskyError ErrorMessage = "unable to connect to bluesky"
SignalGroupError ErrorMessage = "unable to connect to signal"
SignalGroupDeleteError ErrorMessage = "unable to delete signal group"
PasswordError ErrorMessage = "could not authenticate password"

StatusSuccess Status = `success`
Expand Down
18 changes: 18 additions & 0 deletions internal/api/response/ticker.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Ticker struct {
Telegram Telegram `json:"telegram"`
Mastodon Mastodon `json:"mastodon"`
Bluesky Bluesky `json:"bluesky"`
SignalGroup SignalGroup `json:"signalGroup"`
Location Location `json:"location"`
}

Expand Down Expand Up @@ -55,6 +56,15 @@ type Bluesky struct {
Handle string `json:"handle"`
}

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"`
}

type Location struct {
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
Expand Down Expand Up @@ -97,6 +107,14 @@ func TickerResponse(t storage.Ticker, config config.Config) Ticker {
Connected: t.Bluesky.Connected(),
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,
},
Location: Location{
Lat: t.Location.Lat,
Lon: t.Location.Lon,
Expand Down
12 changes: 12 additions & 0 deletions internal/api/response/ticker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ func (s *TickersResponseTestSuite) TestTickersResponse() {
Avatar: "https://example.com/avatar.png",
},
},
SignalGroup: storage.TickerSignalGroup{
Active: true,
GroupID: "example",
GroupName: "Example",
GroupDescription: "Example",
GroupInviteLink: "https://signal.group/#example",
},
Location: storage.TickerLocation{
Lat: 0.0,
Lon: 0.0,
Expand Down Expand Up @@ -86,6 +93,11 @@ func (s *TickersResponseTestSuite) TestTickersResponse() {
s.Equal(ticker.Mastodon.Server, tickerResponse[0].Mastodon.Server)
s.Equal(ticker.Mastodon.User.DisplayName, tickerResponse[0].Mastodon.ScreenName)
s.Equal(ticker.Mastodon.User.Avatar, tickerResponse[0].Mastodon.ImageURL)
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)
}
Expand Down
99 changes: 99 additions & 0 deletions internal/api/tickers.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/systemli/ticker/internal/api/helper"
"github.com/systemli/ticker/internal/api/response"
"github.com/systemli/ticker/internal/bluesky"
"github.com/systemli/ticker/internal/signal"
"github.com/systemli/ticker/internal/storage"
)

Expand Down Expand Up @@ -289,6 +290,104 @@ func (h *handler) DeleteTickerBluesky(c *gin.Context) {
c.JSON(http.StatusOK, response.SuccessResponse(map[string]interface{}{"ticker": response.TickerResponse(ticker, h.config)}))
}

func (h *handler) PutTickerSignalGroup(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.TickerSignalGroup
err = c.Bind(&body)
if err != nil {
c.JSON(http.StatusBadRequest, response.ErrorResponse(response.CodeNotFound, response.FormError))
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
}
}
ticker.SignalGroup.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) DeleteTickerSignalGroup(c *gin.Context) {
ticker, err := helper.Ticker(c)
if err != nil {
c.JSON(http.StatusNotFound, response.ErrorResponse(response.CodeDefault, response.TickerNotFound))
return
}

groupClient := signal.NewGroupClient(h.config)

// Remove all members except the account number
err = groupClient.RemoveAllMembers(ticker.SignalGroup.GroupID)
if err != nil {
log.WithError(err).Error("failed to remove members")
return
}

// Quit the group
err = groupClient.QuitGroup(ticker.SignalGroup.GroupID)
if err != nil {
log.WithError(err).Error("failed to quit group")
return
}

ticker.SignalGroup.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) PutTickerSignalGroupAdmin(c *gin.Context) {
ticker, err := helper.Ticker(c)
if err != nil {
c.JSON(http.StatusNotFound, response.ErrorResponse(response.CodeDefault, response.TickerNotFound))
return
}

var body struct {
Number string `json:"number" binding:"required"`
}

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

groupClient := signal.NewGroupClient(h.config)
err = groupClient.AddAdminMember(ticker.SignalGroup.GroupID, body.Number)
if err != nil {
log.WithError(err).Error("failed to add member")
c.JSON(http.StatusBadRequest, response.ErrorResponse(response.CodeDefault, response.SignalGroupError))
return
}

c.JSON(http.StatusOK, response.SuccessResponse(map[string]interface{}{}))
}

func (h *handler) DeleteTicker(c *gin.Context) {
ticker, err := helper.Ticker(c)
if err != nil {
Expand Down
Loading

0 comments on commit 4f65542

Please sign in to comment.