← Projects

Matrix Bot

In progress Jun 2026 matrixpythonbots

What it is

A self-hosted bot for Matrix, the open protocol for decentralized chat. The goal is a small, dependable assistant that lives in a room, responds to commands, and can be extended without fighting the framework.

Why build it

As we shifted towards the matrix synapse for our day to day communication there is one specific task that we perform and its quite useful to us because of the different langauges we use we needed translation and for that purpose we had to find a way to generate translations and bots are obvious solution. We wanted to start with something very simple and thought of the approach that is similar to discord.js and found an offical sdk called matrix-bot-sdk. The bot is written in Python on top of matrix-nio, the async client library. It uses a matrix user account that needs access to your channels and server and is a normal user account but can be used with the sdk to create automations for matrix and reply to messages send or recieve audio, video and other file formats.

The making of bot

first thing that you have to do while configuring our bot is to obtain a token that’ll act as user user account between your server api and element and allow the sdk to verify the user account and recieve the back and forth communication from user on element to this bot


require("dotenv").config();
const { MatrixAuth } = require("matrix-bot-sdk");

const homeserverUrl = process.env.HOMESERVER_URL;
const username = process.env.MATRIX_USERNAME;
const password = process.env.MATRIX_PASSWORD;

if (!username || !password) {
  console.error("❌ MATRIX_USERNAME and MATRIX_PASSWORD must be set in .env");
  process.exit(1);
}

(async () => {
  const auth = new MatrixAuth(homeserverUrl);
  const client = await auth.passwordLogin(username, password);

  console.log(
    "Copy this access token to your bot's config: ",
    client.accessToken,
  );
})();

As told earlier you need a simple user account created which we would refer to bot account and then use this script to generate a token you would also need your homeserver url as well. Once that is done we need to move towards the core logic and getting the bot up and running

Main configuration to get bot running

The simple configuration and use case: log in, join a room or auto join if invited, and reply to commands via command handler.


const {
  MatrixClient,
  SimpleFsStorageProvider,
  AutojoinRoomsMixin,
  RustSdkCryptoStorageProvider,
} = require("matrix-bot-sdk");
require("dotenv").config();

// Configuration
const homeserverUrl =
  process.env.HOMESERVER_URL || "https://matrix.studiofritzgnad.de";
const accessToken = process.env.ACCESS_TOKEN || process.env.access_token;

if (!accessToken) {
  console.error("❌ ACCESS_TOKEN is required in .env file");
  process.exit(1);
}


const client = new MatrixClient(
  homeserverUrl,
  accessToken,
  storage,
);

// Auto-join rooms
AutojoinRoomsMixin.setupOnClient(client);

// Now import handlers AFTER client is created
const fileCommandHandler = require("./commandHandler/fileCommandHandler");
const textCommandHandler = require("./commandHandler/textCommandHandler");

// Pass client to handlers
client.on("room.message", (roomId, event) =>
  textCommandHandler(client, roomId, event),
);
client.on("room.message", (roomId, event) =>
  fileCommandHandler(client, roomId, event),
);

// Start the bot
client
  .start()
  .then(() => {
    console.log("✅ Bot started!");
    console.log(`🤖 Bot ID: ${client.userId}`);
  })
  .catch((err) => {
    console.error("❌ Failed to start bot:", err);
    process.exit(1);
  });

module.exports = { client };

If we look at the code thats our main configuration which we can refer to core logic of the bot as well and that is where all commands are handled and we provide env stored information of user account token which is generated earlier and provide that token to bot client and then start the client to run our bot. If we look closer we also have two client handlers configured one is that we use for general text commands and the other we use for file handling of any type.

A command handler could be similar to ours but you can create according to your own use case,


async function textCommandHandler(client, roomId, event) {
  // Don't handle unhelpful events (ones that aren't text messages, are redacted, or sent by us)
  console.log("📝 Message type:", event["content"]?.["msgtype"]);

  // Skip if not text message
  if (event["content"]?.["msgtype"] !== "m.text") return;

  // Skip if message is from bot itself
  if (event["sender"] === (await client.getUserId())) return;

  const body = event["content"]["body"];

  // Only process commands starting with "!"
  if (!body.startsWith("!")) {
    console.log("Ignoring non-command message");
    return;
  }

  try {
    let { command, string } = parseString(body);
    console.log("🎯 Command:", command, "Args:", string);
  }catch{
    console.log('error')
  }

This command handler is simple yet so powerfull that it can be used to extend the commands and extend the logic of your bot and file command handler would work in a similar manner but for handling files. If we recall from our main configuration of the bot we added this handler and provided parameters to our handler then implement specific filters because we dont want to be replying to every type of text except its a text message even after that we have to check that who is the sender if its bot himself or any other bot we dont need to reply to it since this could result in spaming the whole channel. Once we are done with this we get the content of the body and use simple parse string function to get the command prefix and the acutal command sinc this is intended for the translation our main objective is to get command & string which means what do we really want to do with this interaction either translate or summarize then the actual textual content which we refer to as string.

Handling encryption

Matrix rooms are often end-to-end encrypted, which means the bot needs to manage device keys and verify sessions. If we add bot to an encrypted channel without the encryption bot wont be able to handle the decryption and wont respond to overcome this we utilize bot’s function

const storage = new SimpleFsStorageProvider("hello-bot.json");
const cryptoProvider = new RustSdkCryptoStorageProvider("cryptostorage.json");
const client = new MatrixClient(
  homeserverUrl,
  accessToken,
  storage,
  cryptoProvider,
);

This allows us to initialize the cryptographic storage and that helps bot to do the encryption-decryption and respond to commands inside end-to-end encrypted channels. This needs to be configured and provided at the time of initializing the bot and one thing to remember is that if somehow your encryption doesn’t work or bot does not starts end-to-end encryption simply deleting the storage.json and json file re-creating bot token and starting fresh should resolve this issue since it refreshes the session and creates a new cryptographic store allows bot to enable end-to-end encryption.

Deploying

We’r running this bot via pm2 with specific configuration and this keeps it running with other applications that we run and manage it uses matrix-bot-sdk and is build using nodejs so we have deployed this on our own cloud server and moniter with pm2 as well.

What’s next

Following along or want to suggest a feature? Get in touch.