I've created Matrix bots before, and sending simple unencrypted messages is so easy it doesn't even require a library. Typically, you'd get your room ID, username, and password, then perform two HTTP requests: one for login, and one for sending the message.
But recently, I wanted to do things the proper way. We're migrating from Slack to Matrix for a project I'm working on with some friends, and we've decided that all rooms, including our server notifications channel, should be encrypted. This meant I had to find a suitable library with end-to-end encryption support in a language I'm comfortable with. Eventually, I settled on mautrix-go.
Setting Up Your Matrix Bot
We'll create a straightforward proof-of-concept bot that logs in, sends a single message, and exits. Later, we'll enhance it by adding encryption support.
Installation
First, install the mautrix-go library:
go get maunium.net/go/mautrix
Defining Constants
We'll use some constants for simplicity in this example. Remember: never store sensitive credentials like this in production code.
const homeserver = "https://matrix.exapmle.com/" // replace with your server
const username = "test_bot"
const password = "super-secret-cool-password"
const roomID = "!okfsAqlvVqyZZRgPWy:example.com"
const userId = ""
const accessToken = ""
const deviceId = ""
Initially, the user ID, access token, and device ID are empty because the bot needs to log in and retrieve these values. Usually, you'd store them securely in a database or similar storage.
Initializing the Client
Now, let's create the Matrix client:
func main() {
client, err := mautrix.NewClient(homeserver, userId, accessToken)
if err != nil {
panic(err)
}
}
Logging In
If your credentials aren't set, log in to obtain them:
if deviceId == "" || userId == "" || accessToken == "" {
resp, err := client.Login(context.Background(), &mautrix.ReqLogin{
Type: mautrix.AuthTypePassword,
Identifier: mautrix.UserIdentifier{
User: username,
Type: mautrix.IdentifierTypeUser,
},
Password: password,
StoreCredentials: true,
})
if err != nil {
panic(err)
}
log.Println(resp.DeviceID)
log.Println(resp.AccessToken)
log.Println(resp.UserID)
return
}
The printed values will look something like this:
2025/04/19 15:57:50 AQWFKLSBNJ
2025/04/19 15:57:50 syt_dgVzdF7ibFQ_GurkyhAWzEpTGgSBemjL_2JdxlO
2025/04/19 15:57:50 @test_bot:example.com
Copy these values back into your constants.
Sending an Unencrypted Message
Now we can send a basic message:
client.DeviceID = deviceId
content := event.MessageEventContent{
MsgType: event.MsgText,
Body: "Hello world from Go!",
}
_, err = client.SendMessageEvent(context.Background(), roomID, event.EventMessage, content)
if err != nil {
panic(err)
}
At this stage, your message will arrive in the Matrix room—but it's not encrypted yet:
Here's the full code so far:
import (
"context"
"log"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
)
const homeserver = "https://matrix.exapmle.com/" // replace with your server
const username = "test_bot"
const password = "super-secret-cool-password"
const roomID = "!okfsAqlvVqyZZRgPWy:example.com"
const userId = "@test_bot:example.com"
const accessToken = "syt_dgVzdF7ibFQ_GurkyhAWzEpTGgSBemjL_2JdxlO"
const deviceId = "AQWFKLSBNJ"
func main() {
client, err := mautrix.NewClient(homeserver, userId, accessToken)
if err != nil {
panic(err)
}
if deviceId == "" || userId == "" || accessToken == "" {
resp, err := client.Login(context.Background(), &mautrix.ReqLogin{
Type: mautrix.AuthTypePassword,
Identifier: mautrix.UserIdentifier{
User: username,
Type: mautrix.IdentifierTypeUser,
},
Password: password,
StoreCredentials: true,
})
if err != nil {
panic(err)
}
log.Println(resp.DeviceID)
log.Println(resp.AccessToken)
log.Println(resp.UserID)
return
}
client.DeviceID = deviceId
content := event.MessageEventContent{
MsgType: event.MsgText,
Body: "Hello world from Go!",
}
_, err = client.SendMessageEvent(context.Background(), roomID, event.EventMessage, content)
if err != nil {
panic(err)
}
}
Sending Encrypted Messages
Encrypting messages involves syncing with the server and setting up cryptography, but don't worry—it's still quite straightforward. Let's see how easily this can be done using mautrix-go.
Create a Cryptography Helper
We'll first create a secure key ("pickle key") and helper function. Make sure to keep this key completely secret and never share it publicly:
// note that the key doesn't have to be a string, you can directly generate random bytes and store them somewhere in a binary form
const pickleKeyString = "NnSHJguDSW7vtSshQJh2Yny4zQHc6Wyf"
func setupCryptoHelper(cli *mautrix.Client) (*cryptohelper.CryptoHelper, error) {
// remember to use a secure key for the pickle key in production
pickleKey := []byte(pickleKeyString)
// this is a path to the SQLite database you will use to store various data about your bot
dbPath := "crypto.db"
helper, err := cryptohelper.NewCryptoHelper(cli, pickleKey, dbPath)
if err != nil {
return nil, err
}
// initialize the database and other stuff
err = helper.Init(context.Background())
if err != nil {
return nil, err
}
return helper, nil
}
Syncing the Client
First, we create the syncer and assign it to the client:
syncer := mautrix.NewDefaultSyncer()
client.Syncer = syncer
Then we create and assign the crypto helper:
cryptoHelper, err := setupCryptoHelper(client)
if err != nil {
panic(err)
}
client.Crypto = cryptoHelper
The syncer is needed to listen to events from synchronization, which is what we'll implement next:
go func() {
if err := client.Sync(); err != nil {
panic(err)
}
}()
The Sync()
method is a blocking call and runs until an error occurs, so we run it in a goroutine. Now we'll use a channel to wait for the first event from the syncer to make sure everything's initialized:
readyChan := make(chan bool)
var once sync.Once
syncer.OnSync(func(ctx context.Context, resp *mautrix.RespSync, since string) bool {
once.Do(func() {
close(readyChan)
})
return true
})
The sync.Once
ensures the channel gets closed only once, even if multiple sync events come in in different threads. Finally, we wait for the first sync:
log.Println("Waiting for sync to receive first event from the encrypted room...")
<-readyChan
log.Println("Sync received")
Now your client is ready to send encrypted messages! The full section we just created looks like this:
readyChan := make(chan bool)
var once sync.Once
syncer.OnSync(func(ctx context.Context, resp *mautrix.RespSync, since string) bool {
once.Do(func() {
close(readyChan)
})
return true
})
go func() {
if err := client.Sync(); err != nil {
panic(err)
}
}()
log.Println("Waiting for sync to receive first event from the encrypted room...")
<-readyChan
log.Println("Sync received")
And just to confirm everything worked, here's what the message looks like in the Matrix room:
As you can see, the message was encrypted successfully, but the session still isn't verified yet—hence the warning. We'll fix that next.
Here's the full source code so far:
import (
"context"
"log"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/cryptohelper"
"maunium.net/go/mautrix/event"
"sync"
)
const homeserver = "https://matrix.exapmle.com/" // replace with your server
const username = "test_bot"
const password = "super-secret-cool-password"
const roomID = "!okfsAqlvVqyZZRgPWy:example.com"
const userId = "@test_bot:example.com"
const accessToken = "syt_dgVzdF7ibFQ_GurkyhAWzEpTGgSBemjL_2JdxlO"
const deviceId = "AQWFKLSBNJ"
const pickleKeyString = "NnSHJguDSW7vtSshQJh2Yny4zQHc6Wyf"
func setupCryptoHelper(cli *mautrix.Client) (*cryptohelper.CryptoHelper, error) {
// remember to use a secure key for the pickle key in production
pickleKey := []byte(pickleKeyString)
// this is a path to the SQLite database you will use to store various data about your bot
dbPath := "crypto.db"
helper, err := cryptohelper.NewCryptoHelper(cli, pickleKey, dbPath)
if err != nil {
return nil, err
}
// initialize the database and other stuff
err = helper.Init(context.Background())
if err != nil {
return nil, err
}
return helper, nil
}
func main() {
client, err := mautrix.NewClient(homeserver, userId, accessToken)
if err != nil {
panic(err)
}
if deviceId == "" || userId == "" || accessToken == "" {
resp, err := client.Login(context.Background(), &mautrix.ReqLogin{
Type: mautrix.AuthTypePassword,
Identifier: mautrix.UserIdentifier{
User: username,
Type: mautrix.IdentifierTypeUser,
},
Password: password,
StoreCredentials: true,
})
if err != nil {
panic(err)
}
log.Println(resp.DeviceID)
log.Println(resp.AccessToken)
log.Println(resp.UserID)
return
}
client.DeviceID = deviceId
syncer := mautrix.NewDefaultSyncer()
client.Syncer = syncer
cryptoHelper, err := setupCryptoHelper(client)
if err != nil {
panic(err)
}
client.Crypto = cryptoHelper
readyChan := make(chan bool)
var once sync.Once
syncer.OnSync(func(ctx context.Context, resp *mautrix.RespSync, since string) bool {
once.Do(func() {
close(readyChan)
})
return true
})
go func() {
if err := client.Sync(); err != nil {
panic(err)
}
}()
log.Println("Waiting for sync to receive first event from the encrypted room...")
<-readyChan
log.Println("Sync received")
content := event.MessageEventContent{
MsgType: event.MsgText,
Body: "Hello world from Go!",
}
_, err = client.SendMessageEvent(context.Background(), roomID, event.EventMessage, content)
if err != nil {
panic(err)
}
}
Verifying the Session
For verified encryption, you'll need a recovery key (obtainable via Element). Store it securely. I have to admit, this part wasn't as intuitive for me—I had to look at some existing projects because it dives a bit deeper into Matrix internals than I usually go. Still, the method names are quite descriptive, so even without deep knowledge, it's not too hard to follow:
const recoveryKey = "EsUF NQce e4BW teUM Kf7W iZqD Nj3f 56qj GuN5 s3aw aut7 div2"
Just like the pickle key, the recovery key should be treated as highly sensitive—do not share or hardcode it in production environments.
Then, create this helper function:
func verifyWithRecoveryKey(machine *crypto.OlmMachine) (err error) {
ctx := context.Background()
keyId, keyData, err := machine.SSSS.GetDefaultKeyData(ctx)
if err != nil {
return
}
key, err := keyData.VerifyRecoveryKey(keyId, recoveryKey)
if err != nil {
return
}
err = machine.FetchCrossSigningKeysFromSSSS(ctx, key)
if err != nil {
return
}
err = machine.SignOwnDevice(ctx, machine.OwnIdentity())
if err != nil {
return
}
err = machine.SignOwnMasterKey(ctx)
return
}
Call this function after synchronization—back in the main()
function:
err = verifyWithRecoveryKey(cryptoHelper.Machine())
if err != nil {
panic(err)
}
Now, your messages will be encrypted, verified, and free of security warnings.
And just to confirm, here's what that looks like in the Matrix room—notice that the warning icon is gone:
Here's the full source code:
import (
"context"
"log"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto"
"maunium.net/go/mautrix/crypto/cryptohelper"
"maunium.net/go/mautrix/event"
"sync"
)
const homeserver = "https://matrix.exapmle.com/" // replace with your server
const username = "test_bot"
const password = "super-secret-cool-password"
const roomID = "!okfsAqlvVqyZZRgPWy:example.com"
const userId = "@test_bot:example.com"
const accessToken = "syt_dgVzdF7ibFQ_GurkyhAWzEpTGgSBemjL_2JdxlO"
const deviceId = "AQWFKLSBNJ"
const pickleKeyString = "NnSHJguDSW7vtSshQJh2Yny4zQHc6Wyf"
func setupCryptoHelper(cli *mautrix.Client) (*cryptohelper.CryptoHelper, error) {
// remember to use a secure key for the pickle key in production
pickleKey := []byte(pickleKeyString)
// this is a path to the SQLite database you will use to store various data about your bot
dbPath := "crypto.db"
helper, err := cryptohelper.NewCryptoHelper(cli, pickleKey, dbPath)
if err != nil {
return nil, err
}
// initialize the database and other stuff
err = helper.Init(context.Background())
if err != nil {
return nil, err
}
return helper, nil
}
func verifyWithRecoveryKey(machine *crypto.OlmMachine) (err error) {
ctx := context.Background()
keyId, keyData, err := machine.SSSS.GetDefaultKeyData(ctx)
if err != nil {
return
}
key, err := keyData.VerifyRecoveryKey(keyId, recoveryKey)
if err != nil {
return
}
err = machine.FetchCrossSigningKeysFromSSSS(ctx, key)
if err != nil {
return
}
err = machine.SignOwnDevice(ctx, machine.OwnIdentity())
if err != nil {
return
}
err = machine.SignOwnMasterKey(ctx)
return
}
func main() {
client, err := mautrix.NewClient(homeserver, userId, accessToken)
if err != nil {
panic(err)
}
if deviceId == "" || userId == "" || accessToken == "" {
resp, err := client.Login(context.Background(), &mautrix.ReqLogin{
Type: mautrix.AuthTypePassword,
Identifier: mautrix.UserIdentifier{
User: username,
Type: mautrix.IdentifierTypeUser,
},
Password: password,
StoreCredentials: true,
})
if err != nil {
panic(err)
}
log.Println(resp.DeviceID)
log.Println(resp.AccessToken)
log.Println(resp.UserID)
return
}
client.DeviceID = deviceId
syncer := mautrix.NewDefaultSyncer()
client.Syncer = syncer
cryptoHelper, err := setupCryptoHelper(client)
if err != nil {
panic(err)
}
client.Crypto = cryptoHelper
readyChan := make(chan bool)
var once sync.Once
syncer.OnSync(func(ctx context.Context, resp *mautrix.RespSync, since string) bool {
once.Do(func() {
close(readyChan)
})
return true
})
go func() {
if err := client.Sync(); err != nil {
panic(err)
}
}()
log.Println("Waiting for sync to receive first event from the encrypted room...")
<-readyChan
log.Println("Sync received")
err = verifyWithRecoveryKey(cryptoHelper.Machine())
if err != nil {
panic(err)
}
content := event.MessageEventContent{
MsgType: event.MsgText,
Body: "Hello world from Go!",
}
_, err = client.SendMessageEvent(context.Background(), roomID, event.EventMessage, content)
if err != nil {
panic(err)
}
}
Conclusion
With this approach, your Matrix bot securely communicates within encrypted rooms. Remember to securely store credentials, use secure keys, and manage device verification properly in production. Happy coding!

Asi bude lepší použít můj Lemmy účet (@[email protected]), tohle je můj blog a vesměs není použitelný na komentáře mimo moje vlastní příspěvky, nic jiného se mi tu nezobrazuje.