Using the signal-cli dbus interface in golang

The signal-cli tool profieds a dbus interface to get and send messages from other programms. The Documentation for that is avalible in the signal-cli wiki but that not so easy to understand if you never work with dbus before.

Setup signal-cli

To setup signal-cli and pair it as secound device to an existing account you first need to install signal-cli, that steps based on your os. After you install it you can create a new “Pairing-Code” by runnuning signal-cli link -n "mybot". The given code can used on the webpage goqr.me to create an qr code, which you can scann with the signal app on your mobile device.

After that you can start signal-cli as deamon, for example with the following command with also print all incomming events as json on your cli:

signal-cli -u +49176XXXXXXX -o json daemon 

Getting Messages

First we need to connect to the DBus system, thats possible with the godbus/dbus package. The connection based on the example from the package is really easy:

conn, err := dbus.ConnectSessionBus()
if err != nil {
  fmt.Fprintln(os.Stderr, "Failed to connect to session bus:", err)
  return err
}
defer conn.Close()

After that we need to told the dbus package which messages we want to recive and create a channel where we can revice all signales. To got the signal messages the follwoing code works for me:

if err = conn.AddMatchSignal(
  dbus.WithMatchInterface("org.asamk.Signal"),
); err != nil {
  return err
}
c := make(chan *dbus.Signal, 10)
conn.Signal(c)

than we can “listen” to the channel and got the messages the signal-cli deamon sends:

for v := range c {
  fmt.Println(v)
}

That will create mainly two signales im interested in, first getting a message from another conversation partner:

&{:1.34 /org/asamk/Signal org.asamk.Signal.MessageReceived [1615064176455 +49176XXXXXXXX [] Durchgefallen []] 11}

and secound getting a Sync Message, it will be send if yourself send a message from another device to a conversation partner:

&{:1.34 /org/asamk/Signal org.asamk.Signal.SyncMessageReceived [1615064055775 +49176XXXXXXXXX +49176XXXXXXX [] Ich bin ein test []] 8}

Parsing Messages

If i just focus on 1:1 chats i could parse both kinds of events. Here two examples without error handling. First on the “Incomming Messages” :

type IncommingMessage struct {
	Timestamp int64
	Source string
	Message string
	Attachments []string
}

func parseMessageReceived(v *dbus.Signal) IncommingMessage {
	msg := IncommingMessage{}
	msg.Timestamp, _ = v.Body[0].(int64)
	msg.Source = v.Body[1].(string)
	msg.Message = v.Body[3].(string)
	msg.Attachments = v.Body[4].([]string)
	return msg
}

and for the Snyc Events:

type SyncMessage struct {
	Timestamp int64
	Source string
	Destination string
	Message string
	Attachments []string
}

func parseSyncMessageReceived(v *dbus.Signal) SyncMessage {
	msg := SyncMessage{}
	msg.Timestamp, _ = v.Body[0].(int64)
	msg.Source = v.Body[1].(string)
	msg.Destination = v.Body[2].(string)
	msg.Message = v.Body[4].(string)
	msg.Attachments = v.Body[5].([]string)
	return msg
}

That functions will return strucs you can easy use for your application.

Sending Messages

For the sending we also need a dbus connection, but unlike in the receving message part we don’t subscribe to the events we produse one which will be consumed by the signal-cli deamon and send as chat messages to the conversation. All other devices (like your mobile phone) will get a SyncMessage event and show that “new” message too.

First you need to create the connection like before:

conn, err := dbus.ConnectSessionBus()
if err != nil {
  fmt.Fprintln(os.Stderr, "Failed to connect to session bus:", err)
  os.Exit(1)
}
defer conn.Close()

after that just send a call like this:

obj := conn.Object("org.asamk.Signal", "/org/asamk/Signal")
call := obj.Call("org.asamk.Signal.sendMessage",0,  "Your really cool message", []string{}, "+49176XXXXXXX")
if call.Err != nil {
  panic(call.Err)
}

Example

Here an “full” example of getting messages (without error handling)

func listenToDbus() error  {
  conn, err := dbus.ConnectSessionBus()
  if err != nil {
    fmt.Fprintln(os.Stderr, "Failed to connect to session bus:", err)
    return err
  }
  defer conn.Close()

  if err = conn.AddMatchSignal(
    dbus.WithMatchInterface("org.asamk.Signal"),
  ); err != nil {
    return err
  }

  c := make(chan *dbus.Signal, 10)
  conn.Signal(c)
  for v := range c {
    if v.Name == "org.asamk.Signal.SyncMessageReceived" {
      msg := parseSyncMessageReceived(v)
      // do something with msg like a callback
    }
    if v.Name == "org.asamk.Signal.MessageReceived" {
      msg := parseMessageReceived(v)
      // do something with msg like a callback
    }
  }

  return nil
}

type SyncMessage struct {
  Timestamp int64
  Source string
  Destination string
  Message string
  Attachments []string
}

func parseSyncMessageReceived(v *dbus.Signal) SyncMessage {
  msg := SyncMessage{}
  msg.Timestamp, _ = v.Body[0].(int64)
  msg.Source = v.Body[1].(string)
  msg.Destination = v.Body[2].(string)
  msg.Message = v.Body[4].(string)
  msg.Attachments = v.Body[5].([]string)
  return msg
}

type IncommingMessage struct {
  Timestamp int64
  Source string
  Message string
  Attachments []string
}

func parseMessageReceived(v *dbus.Signal) IncommingMessage {
  msg := IncommingMessage{}
  msg.Timestamp, _ = v.Body[0].(int64)
  msg.Source = v.Body[1].(string)
  msg.Message = v.Body[3].(string)
  msg.Attachments = v.Body[4].([]string)
  return msg
}

and another one to sending messages:

func SendMessage(to string, msg string)  {
  conn, err := dbus.ConnectSessionBus()
  if err != nil {
    fmt.Fprintln(os.Stderr, "Failed to connect to session bus:", err)
    os.Exit(1)
	}
  defer conn.Close()

  obj := conn.Object("org.asamk.Signal", "/org/asamk/Signal")
  call := obj.Call("org.asamk.Signal.sendMessage",0,  msg, []string{}, to)
  if call.Err != nil {
    panic(call.Err)
  }
}