Skip to content

Commit

Permalink
✨ Add a Signal group bridge
Browse files Browse the repository at this point in the history
* Creates a Signal group when the bridge is configured, with following
  settings:
  - name & description as configured
  - predefined avatar (file stored in the signal-cli container)
  - invite link enabled
  - add member permissions: every member
  - edit details permissions: only admin
  - send messages: only admin
* Sends messages (with attachments) to the Signal group
* Tries to leave the Signal group when bridge is reset. This doesnt work
  though if the Signal account is the only admin (which is almost always
  the case).
* Tries to delete message when it's deleted in ticker.
  • Loading branch information
doobry-systemli committed May 19, 2024
1 parent fe8f2bc commit 21d11f1
Show file tree
Hide file tree
Showing 14 changed files with 460 additions and 9 deletions.
4 changes: 4 additions & 0 deletions config.yml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ secret: "slorp-panfil-becall-dorp-hashab-incus-biter-lyra-pelage-sarraf-drunk"
# telegram configuration
telegram:
token: ""
# signal group configuration
signal_group:
api_url: ""
account: ""
# listen port for prometheus metrics exporter
metrics_listen: ":8181"
upload:
Expand Down
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 @@ -297,6 +297,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
2 changes: 2 additions & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ 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.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
1 change: 1 addition & 0 deletions internal/api/response/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ 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"
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
61 changes: 61 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,66 @@ 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
err = signal.CreateOrUpdateGroup(&ticker.SignalGroup, h.config)
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
}

err = signal.QuitGroup(h.config, ticker.SignalGroup.GroupID)
if err != nil {
log.WithError(err).Error("failed to quit group")
// c.JSON(http.StatusNotFound, response.ErrorResponse(response.CodeDefault, response.SignalGroupError))
// 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) DeleteTicker(c *gin.Context) {
ticker, err := helper.Ticker(c)
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion internal/bridge/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ func RegisterBridges(config config.Config, storage storage.Storage) Bridges {
telegram := TelegramBridge{config, storage}
mastodon := MastodonBridge{config, storage}
bluesky := BlueskyBridge{config, storage}
signalGroup := SignalGroupBridge{config, storage}

return Bridges{"telegram": &telegram, "mastodon": &mastodon, "bluesky": &bluesky}
return Bridges{"telegram": &telegram, "mastodon": &mastodon, "bluesky": &bluesky, "signalGroup": &signalGroup}
}

func (b *Bridges) Send(ticker storage.Ticker, message *storage.Message) error {
Expand Down
39 changes: 39 additions & 0 deletions internal/bridge/signal_group.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package bridge

import (
"github.com/systemli/ticker/internal/config"
"github.com/systemli/ticker/internal/signal"
"github.com/systemli/ticker/internal/storage"
)

type SignalGroupBridge struct {
config config.Config
storage storage.Storage
}

func (sb *SignalGroupBridge) Send(ticker storage.Ticker, message *storage.Message) error {
if !sb.config.SignalGroup.Enabled() || !ticker.SignalGroup.Connected() || !ticker.SignalGroup.Active {
return nil
}

err := signal.SendGroupMessage(sb.config, sb.storage, ticker.SignalGroup.GroupID, message)
if err != nil {
return err
}

return nil
}

func (sb *SignalGroupBridge) Delete(ticker storage.Ticker, message *storage.Message) error {
if !sb.config.SignalGroup.Enabled() || !ticker.SignalGroup.Connected() || !ticker.SignalGroup.Active || message.SignalGroup.Timestamp == nil {
return nil
}

err := signal.DeleteMessage(sb.config, ticker.SignalGroup.GroupID, message)
if err != nil {
return err
}

return nil

}
33 changes: 25 additions & 8 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ import (
var log = logrus.WithField("package", "config")

type Config struct {
Listen string `yaml:"listen"`
LogLevel string `yaml:"log_level"`
LogFormat string `yaml:"log_format"`
Secret string `yaml:"secret"`
Database Database `yaml:"database"`
Telegram Telegram `yaml:"telegram"`
MetricsListen string `yaml:"metrics_listen"`
Upload Upload `yaml:"upload"`
Listen string `yaml:"listen"`
LogLevel string `yaml:"log_level"`
LogFormat string `yaml:"log_format"`
Secret string `yaml:"secret"`
Database Database `yaml:"database"`
Telegram Telegram `yaml:"telegram"`
SignalGroup SignalGroup `yaml:"signal_group"`
MetricsListen string `yaml:"metrics_listen"`
Upload Upload `yaml:"upload"`
FileBackend afero.Fs
}

Expand All @@ -35,6 +36,11 @@ type Telegram struct {
User tgbotapi.User
}

type SignalGroup struct {
ApiUrl string `yaml:"api_url"`
Account string
}

type Upload struct {
Path string `yaml:"path"`
URL string `yaml:"url"`
Expand Down Expand Up @@ -63,6 +69,11 @@ func (t *Telegram) Enabled() bool {
return t.Token != ""
}

// Enabled returns true if requried API URL and account are set.
func (t *SignalGroup) Enabled() bool {
return t.ApiUrl != "" && t.Account != ""
}

// LoadConfig loads config from file.
func LoadConfig(path string) Config {
c := defaultConfig()
Expand Down Expand Up @@ -108,6 +119,12 @@ func LoadConfig(path string) Config {
if os.Getenv("TICKER_TELEGRAM_TOKEN") != "" {
c.Telegram.Token = os.Getenv("TICKER_TELEGRAM_TOKEN")
}
if os.Getenv("TICKER_SIGNAL_GROUP_API_URL") != "" {
c.SignalGroup.ApiUrl = os.Getenv("TICKER_SIGNAL_GROUP_API_URL")
}
if os.Getenv("TICKER_SIGNAL_GROUP_ACCOUNT") != "" {
c.SignalGroup.ApiUrl = os.Getenv("TICKER_SIGNAL_GROUP_ACCOUNT")
}

return c
}
Loading

0 comments on commit 21d11f1

Please sign in to comment.