Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rudamentary support for signald #1433

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bridge/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ type BridgeValues struct {
Zulip map[string]Protocol
Keybase map[string]Protocol
Mumble map[string]Protocol
Signald map[string]Protocol
General Protocol
Tengo Tengo
Gateway []Gateway
Expand Down
352 changes: 352 additions & 0 deletions bridge/signald/signald.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,352 @@
package bsignald

import (
"bufio"
"encoding/json"
"net"

"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
)

type JSONCMD map[string]interface{}

const (
cfgNumber = "Number"
cfgSocket = "UnixSocket"
)

type signaldMessage struct {
ID string
Type string
Error json.RawMessage
Data json.RawMessage
}

type signaldUnexpectedError struct {
Message string
}

type signaldMessageData struct {
ID string `json:",omitempty"`
Data signaldData `json:",omitempty"`
Type string `json:",omitempty"`
}

type signaldData struct {
CallMessage json.RawMessage `json:"callMessage,omitempty"`
DataMessage *signaldDataMessage `json:"dataMessage,omitempty"`
HasContent bool `json:"hasContent,omitempty"`
HasLegacyMessage bool `json:"hasLegacyMessage,omitempty"`
IsUnidentifiedSender bool `json:"isUnidentifiedSender,omitempty"`
Receipt json.RawMessage `json:"receipt,omitempty"`
Relay string `json:"relay,omitempty"`
ServerDeliveredTimestamp int64 `json:"serverDeliveredTimestamp,omitempty"`
ServerTimestamp int64 `json:"serverTimestamp,omitempty"`
Source *signaldAccount `json:"source,omitempty"`
SourceDevice int32 `json:"sourceDevice,omitempty"`
SyncMessage json.RawMessage `json:"syncMessage,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
TimestampISO string `json:"timestampISO,omitempty"`
Type string `json:"type,omitempty"`
Typing json.RawMessage `json:"typing,omitempty"`
Username string `json:"username,omitempty"`
UUID string `json:"uuid,omitempty"`
}

type signaldAccount struct {
Number string `json:"number,omitempty"`
Relay string `json:"relay,omitempty"`
UUID string `json:"uuid,omitempty"`
}

type signaldDataMessage struct {
Attachments json.RawMessage `json:"attachments,omitempty"`
Body string `json:"body,omitempty"`
Contacts json.RawMessage `json:"contacts,omitempty"`
EndSession bool `json:"endSession,omitempty"`
ExpiresInSeconds int32 `json:"expiresInSeconds,omitempty"`
Group *signaldGroupInfo `json:"group,omitempty"`
GroupV2 *signaldGroupV2Info `json:"groupV2,omitempty"`
Mentions json.RawMessage `json:"mentions,omitempty"`
Previews json.RawMessage `json:"previews,omitempty"`
ProfileKeyUpdate bool `json:"profileKeyUpdate,omitempty"`
Quote json.RawMessage `json:"quote,omitempty"`
Reaction json.RawMessage `json:"reaction,omitempty"`
RemoteDelete json.RawMessage `json:"remoteDelete,omitempty"`
Sticker json.RawMessage `json:"sticker,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
ViewOnce bool `json:"viewOnce,omitempty"`
}

type signaldGroupInfo struct {
AvatarID int64 `json:"avatarId,omitempty"`
ID string `json:"groupId,omitempty"`
Members json.RawMessage `json:"members,omitempty"`
Name string `json:"name,omitempty"`
Type string `json:"type,omitempty"`
}

type signaldGroupV2Info struct {
AccessControl json.RawMessage `json:"accessControl,omitempty"`
Avatar string `json:"avatar,omitempty"`
ID string `json:"id,omitempty"`
InviteLink string `json:"inviteLink,omitempty"`
MemberDetail json.RawMessage `json:"memberDetail,omitempty"`
Members json.RawMessage `json:"members,omitempty"`
PendingMemberDetail json.RawMessage `json:"pendingMemberDetail,omitempty"`
PendingMembers json.RawMessage `json:"pendingMembers,omitempty"`
RequestingMembers json.RawMessage `json:"requestingMembers,omitempty"`
Revision int32 `json:"revision,omitempty"`
Timer int32 `json:"timer,omitempty"`
Title string `son:"title,omitempty"`
}

type signaldSendMessage struct {
Type string `json:"type,omitempty"`
Username string `json:"username,omitempty"`
RecipientGroupID string `json:"recipientGroupId,omitempty"`
MessageBody string `json:"messageBody,omitempty"`
}

type signaldContact struct {
Name string `json:"name,omitempty"`
ProfileName string `json:"profile_name,omitempty"`
Account *signaldAccount `json:"address,omitempty"`
Avatar string `json:"avatar,omitempty"`
Color string `json:"color,omitempty"`
ProfileKey string `json:"profileKey,omitempty"`
MessageExpirationTime int32 `json:"messageExpirationTime,omitempty"`
InboxPosition int32 `json:"inboxPosition,omitempty"`
}

type Bsignald struct {
*bridge.Config
socketpath string
socket net.Conn
subscribed bool
reader *bufio.Scanner
groupid string
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making this a single string leads to the problem that the bridge can only work with one Signal group.
(And has weird side effects when configuring more than one group)

I think a map-as-hashset construct could work here.

Suggested change
groupid string
groupids map[string]bool

contacts map[string]signaldContact
}

func New(cfg *bridge.Config) bridge.Bridger {
number := cfg.GetString(cfgNumber)
if number == "" {
cfg.Log.Fatalf("Missing configuration for Signald bridge: Number")
}

socketpath := cfg.GetString(cfgSocket)
if socketpath == "" {
socketpath = "/var/run/signald/signald.sock"
}

return &Bsignald{
Config: cfg,
socketpath: socketpath,
subscribed: false,
contacts: make(map[string]signaldContact),
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
groupids: make(map[string]bool),
}

}

func (b *Bsignald) Connect() error {
b.Log.Infof("Connecting %s", b.socketpath)

s, err := net.Dial("unix", b.socketpath)
if err != nil {
b.Log.Fatalf(err.Error())
}
b.socket = s
r := bufio.NewScanner(s)
b.reader = r
go b.Listen()
if err := b.Login(); err != nil {
b.Log.Errorln("Error logging in: ", err.Error())
}
return nil
}

func (b *Bsignald) JoinChannel(channel config.ChannelInfo) error {
b.groupid = channel.Name
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add the groupid to the bridge

Suggested change
b.groupid = channel.Name
b.groupids[channel.Name] = true

return nil
}

func (b *Bsignald) HandleUnexpectedErrorMessage(msg signaldMessage) {
var errorResponse signaldUnexpectedError
if err := json.Unmarshal(msg.Data, &errorResponse); err != nil {
b.Log.Errorln("Error unmarshaling error response:", err.Error())
}
b.Log.Errorln("Unexpected error from signald: ", errorResponse.Message)
}

func (b *Bsignald) HandleSubscribeMessage() {
b.Log.Debugln("subscribe successful", b.GetString(cfgNumber))
b.subscribed = true
if err := b.GetContacts(); err != nil {
b.Log.Errorln("Error getting contacts: ", err.Error())
}
}

func (b *Bsignald) HandleListenStoppedMessage() {
b.Log.Errorln("got listen stopped, trying to re-subscribe")
b.subscribed = false
if err := b.Login(); err != nil {
b.Log.Errorln("Error logging in: ", err.Error())
}
}

func (b *Bsignald) HandleContactList(msg signaldMessage) {
var contacts []signaldContact
if err := json.Unmarshal(msg.Data, &contacts); err != nil {
b.Log.Errorln("failed to parse contact_list: ", err)
} else {
for _, contact := range contacts {
b.contacts[contact.Account.UUID] = contact
}
}
}

func (b *Bsignald) GetUsername(uuid string) string {
username := ""
if v, found := b.contacts[uuid]; found {
if v.ProfileName != "" {
username = v.ProfileName
} else if v.Name != "" {
username = v.Name
}
}

return username
}

func (b *Bsignald) HandleMessage(msg signaldMessage) {
response := signaldMessageData{ID: msg.ID, Type: msg.Type}
if err := json.Unmarshal(msg.Data, &response.Data); err != nil {
b.Log.Errorln("receive error: ", err)
return
}

if response.Data.DataMessage == nil {
return
}

groupMatched := false
if response.Data.DataMessage.GroupV2 != nil {
if b.groupid == response.Data.DataMessage.GroupV2.ID {
groupMatched = true
}
} else if response.Data.DataMessage.Group != nil {
if b.groupid == response.Data.DataMessage.Group.ID {
groupMatched = true
}
}

if !groupMatched {
b.Log.Debugln("skipping non-group message")
return
}
Comment on lines +233 to +247
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking can be done against the hashset.
Additionally, we now need the current groupid, because b.groupid is not unique anymore.

Suggested change
groupMatched := false
if response.Data.DataMessage.GroupV2 != nil {
if b.groupid == response.Data.DataMessage.GroupV2.ID {
groupMatched = true
}
} else if response.Data.DataMessage.Group != nil {
if b.groupid == response.Data.DataMessage.Group.ID {
groupMatched = true
}
}
if !groupMatched {
b.Log.Debugln("skipping non-group message")
return
}
groupid := ""
if response.Data.DataMessage.GroupV2 != nil {
groupid = response.Data.DataMessage.GroupV2.ID
} else if response.Data.DataMessage.Group != nil {
groupid = response.Data.DataMessage.Group.ID
}
groupMatched := b.groupids[groupid]
if !groupMatched {
b.Log.Debugln("skipping not addressed to one of the configured groups")
return
}


username := b.GetUsername(response.Data.Source.UUID)
if username == "" {
username = response.Data.Source.Number
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in the WhatsApp-Bridge, I would suggest not putting the Number in the username.

Suggested change
username = response.Data.Source.Number
username = "Someone"

}

rmsg := config.Message{
UserID: response.Data.Source.UUID,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer the Users Number as ID instead of the UUID, because this for me seems like a more usable identifier (and I have a specific use case that works better this way).

Suggested change
UserID: response.Data.Source.UUID,
UserID: response.Data.Source.Number,

Username: username,
Text: response.Data.DataMessage.Body,
Channel: b.groupid,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we need the current groupid

Suggested change
Channel: b.groupid,
Channel: groupid,

Account: b.Account,
Protocol: b.Protocol,
}

b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}

func (b *Bsignald) Listen() {
for {
for b.reader.Scan() {
var err error
if err = b.reader.Err(); err != nil {
b.Log.Errorf(err.Error())
continue
}

raw := b.reader.Text()

var msg signaldMessage
if err = json.Unmarshal([]byte(raw), &msg); err != nil {
b.Log.Errorln("Error unmarshaling raw response:", err.Error())
continue
}

switch msg.Type {
case "unexpected_error":
b.HandleUnexpectedErrorMessage(msg)
case "subscribed":
b.HandleSubscribeMessage()
case "listen_stopped":
b.HandleListenStoppedMessage()
case "contact_list":
b.HandleContactList(msg)
case "message":
b.HandleMessage(msg)
default:
b.Log.Debugln("unsupported signald data received, skipping it")
}
}
}
}

func (b *Bsignald) GetContacts() error {
cmd := JSONCMD{
"type": "list_contacts",
"username": b.GetString(cfgNumber),
}
return b.SendRawJSON(cmd)
}

func (b *Bsignald) Login() error {
var err error
if !b.subscribed {
cmd := JSONCMD{
"type": "subscribe",
"username": b.GetString(cfgNumber),
}
err = b.SendRawJSON(cmd)
}
return err
}

func (b *Bsignald) SendRawJSON(cmd JSONCMD) error {
err := json.NewEncoder(b.socket).Encode(cmd)
if err != nil {
b.Log.Errorln(err.Error())
}
return err
}

func (b *Bsignald) Disconnect() error {
b.Log.Debugln("Disconnecting..")
b.socket.Close()
return nil
}

func (b *Bsignald) Send(msg config.Message) (string, error) {
b.Log.Debugf("message to forward into signal: %#v", msg)

msgJSON := signaldSendMessage{
Type: "send",
Username: b.GetString(cfgNumber),
RecipientGroupID: b.groupid,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The groupid to send to now depends on the channel of the message, not the bridge configuration.

Suggested change
RecipientGroupID: b.groupid,
RecipientGroupID: msg.Channel,

MessageBody: msg.Username + msg.Text,
}

err := json.NewEncoder(b.socket).Encode(msgJSON)
if err != nil {
b.Log.Errorln(err.Error())
}
return "", err
}
11 changes: 11 additions & 0 deletions gateway/bridgemap/bsignald.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// +build !nosignald

package bridgemap

import (
bsignald "github.com/42wim/matterbridge/bridge/signald"
)

func init() {
FullMap["signald"] = bsignald.New
}