threema

package module
v0.0.0-...-e8f2fa5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 7, 2023 License: BSD-3-Clause Imports: 32 Imported by: 1

README

Threema messaging from Go

This is a Threema bot library written in Go. It uses personal - properly licensed - Threema accounts to send and receive messages.

This library will never have feature parity with the Threema wire protocol as the goal is personal use (notifications, alerts, commands). If you wish to send Threema messages in production settings with quality of service assurances, please use the Threema Gateway.

Disclaimer: This library is completely unrelated to the Threema company and project. It may break at any point in time if the protocol changes and there might be no update coming to fix it. Only use in settings where this risk is acceptable.

Threema license and account

This project uses personal Threema accounts. It is forbidden to use the same personal account on multiple devices, but running a personal bot with its own dedicated license is tolerated (don't blame me if they terminate your license/account due to abuse).

To obtain a dedicated license for your bot, go to the Threema Shop and buy a license key for Threema for Android. You should get a key in the form of XXXXX-XXXXX. This is not yet a Threema account, just a key allowing you to create a Threema account on their servers.

Creating the Threema account has its own funky REST workflow against the Threema API servers. The authorization and registration API is not public. Although we could implement the flow in this library, it's asking for trouble wrt compatibility issues long term.

We're going to side track this issue by requesting you to install the standalone Threema for Android either into an extra phone or an Android emulator. Through the app, you will be able to complete the signup workflow no matter how the APIs evolve in the coming years. After completing the signup, you can export your live identity.

You should end up with an encrypted backup key consisting of 20x4 characters in the shape of XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX and your chosen password. You will need both to use this library.

Threema user directory service

When you attempt to contact an unknown user through Threema (via their 8 character ID) - or when you yourself are contacted by an unknown user - communication is not possible until the public key associated with the account is retrieved. This does not happen within the Threema chat protocol, rather relies on a REST API operated by Threema.

Similarly to how this library avoided implementing the registration workflow, public key retrievals are also delegated to the user. This ensures that the library itself - focusing on communication through the Threema chat protocol - will not break due to some REST API change, and will continue to function even if the directory service is offline.

To retrieve the public key of a user identified by their 8 character Threema ID, make an HTTP GET request to https://api.threema.ch/identity/XXXXXXXX. The response will be a JSON struct containing, among other fields, {"publicKey": "..."}. This is the base64 encoded 32byte public key you'll need to contact a specific user.

How to use this library

Before you can start messaging, you'll need to load your exported backup key into a threema.Identity.

// We assume you already have an exported identity (don't get your hopes up,
// this is a fake identity).
var (
    backup   = "A4G3-BF25-JEN4-EA7Q-XSMG-AIYL-A2W6-CCTW-VYGW-HT3L-KVA7-TTG7-VF2G-RHMY-YB5I-ER7S-WQMU-XF4Y-PZLU-XJFN"
    password = "1337speak"
)
// Loading an exported identity is as simple as providing the exported backup
// string and the password it was encrypted with.
id, err := threema.Identify(backup, password)
if err != nil {
    panic(err)
}
fmt.Printf("Loaded Threema identity: %s\n", id.Self())

This identity is enough to establish a connection to the Threema chat servers, a threema.Connection.

// We assume you already loaded an exported identity though this library, as
// well as a handler that reacts to events. Mode on this later.
var (
    id      *threema.Identity
    handler *threema.Handler
)
// With a real identity and an event handler (you can use nil for a dry run),
// it's already enough to authenticate into the Threema network.
conn, err := threema.Connect(id, handler)
if err != nil {
    panic(err)
}
defer conn.Close()

fmt.Printf("Connected to the Threema network\n")

Before sending the first message, you'll need to create a threema.Handler to react to inbound events. The handler is constructed as an "abstract interface" so you can provide only the methods you're interested in and drop everything else onto the floor.

// There are various events that a user might want to react to. These are
// most commonly messages received from others, but there are also a few
// Threema protocol events too.
handler := &threema.Handler{
    Message: func(from string, nick string, when time.Time, msg string) {
        fmt.Printf("%v] %s(%s): %s\n", when, from, nick, msg)
    },
}
fmt.Printf("Handler methods implemented: %+v\n", handler)

With all the setup in place, we can send - and receive - our very first Threema message from Go!

// We assume you already loaded an exported identity though this library, as
// well as established a live connection to the Threema servers.
var (
    id   *threema.Identity
    conn *threema.Connection
)
// Sending a message will block until it is delivered to the Threema servers
// and it is acknowledged by it (i.e. no data loss). There is no waiting for
// the remote side to receive nor read it!
if err := conn.SendText(id.Self(), "Hello Threema!"); err != nil {
    panic(err)
}
fmt.Printf("We've just sent out first message!\n")

Before you can send (or receive) messages from a different user, you need to know their Threema ID and public key. Although it is possible to seamlessly retrieve the key from Threema's directory service, this library will not do it for you, sorry.

// We assume you already loaded an exported identity though this library, as
// well as retrieved a known user's base64 encoded public key from Threema's
// user directory service.
var (
    id *threema.Identity

    friend = "DEADBEEF"
    pubkey = "1qEnvgAm59YN0VUQqjOWHF3TymgIcIdMDpH7p1GajQU="
)
// Add the friend's key mapped to their Threema ID.
if err := id.Trust(friend, pubkey); err != nil {
    panic(err)
}
fmt.Printf("We've just trusted %s to message with\n", friend)

Quick sends from the CLI

This library also has a small CLI utility to do some initial testing or quick-and-dirty integrations. You can install it directly via Go. If you're not using Go regularly, you might need to add the install path to your PATH end var.

$ go install github.com/karalabe/go-threema/cmd/threema@latest

Before you can send a message, you need the sender's exported identity and its decryption key. You can either provide them as --id and --id.secret, but it might be simpler and safer to use env vars:

# Do note again, these are fake credentials
$ export THREEMA_ID_BACKUP=A4G3-BF25-JEN4-EA7Q-XSMG-AIYL-A2W6-CCTW-VYGW-HT3L-KVA7-TTG7-VF2G-RHMY-YB5I-ER7S-WQMU-XF4Y-PZLU-XJFN
$ export THREEMA_ID_SECRET=1337speak

After injecting the sender's credentials, you can fire away with sending messages to your desired recipients. Providing the recipient public key --to.pubkey is optional, but highly recommended to avoid hitting the Threema directory service at every invocation.

$ threema send text --to DEADBEEF --msg "Hello Threema!"
$ threema send image --to DEADBEEF --img /path/to/duck.png --msg "Dang it's a nice duck"

Note, the CLI sender will not have any inbound message handlers set, so anything that is received during connectivity will be dropped on the floor. They will get acked to the Threema server and lost!

Contributing

Don't.

I do not have the capacity to maintain a project that tracks a constantly evolving product (Threema). The more feature parity this library has with the protocol, the higher the probability of breakages.

My goal is to use a very limited subset of features for personal notifications and maybe some automations. I may in the future add more things I use, but I will definitely not add anything I don't.

Last but not least, Threema made an amazing product and I don't want this library to compete with - or be to the detriment of - their commercial offerings. I feel that running a fully licenced personal bot with very limited traffic is within the limits of good faith; and I don't want to push the envelope too far.

License

3-Clause BSD

Documentation

Overview

Package threema implements messaging through Threema.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Connection

type Connection struct {
	// contains filtered or unexported fields
}

Connection represents a live connection to the Threema servers, authenticated with a pre-loaded Threema personal user.

func Connect

func Connect(id *Identity, handler *Handler) (*Connection, error)

Connect dials the Threema servers and runs the authentication handshake.

Example
// We assume you already loaded an exported identity though this library, as
// well as a handler that reacts to events. Mode on this later.
var (
	id      *threema.Identity
	handler *threema.Handler
)
// With a real identity and an event handler (you can use nil for a dry run),
// it's already enough to authenticate into the Threema network.
conn, err := threema.Connect(id, handler)
if err != nil {
	panic(err)
}
defer conn.Close()

fmt.Printf("Connected to the Threema network\n")
Output:

func (*Connection) Close

func (c *Connection) Close() error

Close tears down the network connection.

func (*Connection) SendImage

func (c *Connection) SendImage(to string, blob []byte, caption string) error

SendImage sends an image to the given recipient.

func (*Connection) SendText

func (c *Connection) SendText(to string, text string) error

SendText sends a text message to the given recipient.

type Handler

type Handler struct {
	// Spam is called when we receive a message from a Threema user not on the
	// local contact list. The expectation is that no unexpected messages should
	// be consumed, so we notify the handler, but don't act further on it.
	Spam func(from string, nick string, when time.Time)

	// Message is called when the account receives a remote message.
	Message func(from string, nick string, when time.Time, msg string)

	// Image is called when the account receives a remote image transfer.
	Image func(from string, nick string, when time.Time, image image.Image, thumb image.Image, caption string)

	// Alert is called when the Threema server sends a warning to the user.
	Alert func(reason string)

	// Error is called when the Threema server sends us an error, specifying if
	// it is allowed to reconnect or not.
	Error func(reason string, reconnect bool)

	// Closed is called when the connection to the Threema server terminates.
	Closed func()
}

Handler defines the various events that the user's code needs to handle (or ignore if so they chose).

Example
// There are various events that a user might want to react to. These are
// most commonly messages received from others, but there are also a few
// Threema protocol events too.
handler := &threema.Handler{
	Message: func(from string, nick string, when time.Time, msg string) {
		fmt.Printf("%v] %s(%s): %s\n", when, from, nick, msg)
	},
}
fmt.Printf("Handler methods implemented: %+v\n", handler)
Output:

type Identity

type Identity struct {
	// contains filtered or unexported fields
}

Identity contains the Threema specific user identifier as well as the crypto keys associated with it.

func Identify

func Identify(export string, pass string) (*Identity, error)

Identify decrypts and loads an identity exported from Threema. It is in the form of `XXXX-XXXX-...-XXXX` with 20 grouping of 4 characters each.

Example
// We assume you already have an exported identity (don't get your hopes up,
// this is a fake identity).
var (
	backup   = "A4G3-BF25-JEN4-EA7Q-XSMG-AIYL-A2W6-CCTW-VYGW-HT3L-KVA7-TTG7-VF2G-RHMY-YB5I-ER7S-WQMU-XF4Y-PZLU-XJFN"
	password = "1337speak"
)
// Loading an exported identity is as simple as providing the exported backup
// string and the password it was encrypted with.
id, err := threema.Identify(backup, password)
if err != nil {
	panic(err)
}
fmt.Printf("Loaded Threema identity: %s\n", id.Self())
Output:

func (*Identity) Export

func (t *Identity) Export(pass string) (string, error)

Export generates a new Threema backup from the existing identity with the given secret passphrase.

func (*Identity) Self

func (i *Identity) Self() string

Self retrieves the Threema ID of the loaded identity.

func (*Identity) Trust

func (i *Identity) Trust(id string, pubkey string) error

Trust injects a public key for a specific Threema id. Although we could retrieve this from the Threema REST API servers, it's up to the user to obtain the credentials.

Hint: curl -k https://api.threema.ch/identity/XXXXXXXX

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL