Opens an external site in a new window
Mental Health Awareness Month
“Community”
RODNEY LAB
  • Home
  • Plus +
  • Newsletter
  • Links
  • Profile
RODNEY LAB
  • Home
  • Plus +
  • Newsletter
  • Links

Matrix Message Relay Bot: API with Deno & Rust 🦀️ # Matrix Message Relay Bot: API with Deno & Rust 🦀️ #

blurry low resolution placeholder image Deno Fresh Getting Started
  1. Home Rodney Lab Home
  2. Blog Posts Rodney Lab Blog Posts
  3. Deno Deno Blog Posts
<PREVIOUS POST
NEXT POST >
LATEST POST >>

Matrix Message Relay Bot: API with Deno & Rust 🦀️ #

Updated 2 weeks ago
9 minute read
Gunning Fog Index: 5.5
Content by Rodney
blurry low resolution placeholder image Author Image: Rodney from Rodney Lab
SHARE:

🤖 Creating a Matrix Message Relay Bot with Deno and WASM #

In this post, we see how you can build a Matrix message relay bot API with Deno. Matrix is a secure messaging protocol, which Element and other message apps use. Element has similar features to apps like Telegram. It offers some enhancements over Telegram, such as end-to-end encrypted group chat within rooms. Another difference is that you can use Element to message peers who use other Matrix messaging apps. This is analogous to you being able to use your favourite email app to email contacts who prefer another app.

Although messaging bots today have some quite sophisticated applications, we will keep things simple. Our bot will relay messages via a Deno API to an existing Matrix Element chat room. As an example, you might use this API to let you know each time someone sends a contact form message on one of the sites you are managing. Using the Element smartphone app, you or your team can get the alert even if away from the desk.

What are we Building? #

We will create a Deno API to listen for new messages. The messages (sent from our contact form server code, for example) will arrive at the API as REST requests. Under the hood, to interface with the Matrix API, we will use the Rust Matrix SDK. WASM will provide the link between the Rust Matrix code and the Deno serverless app. If that sounds interesting to you, then let’s get going!

⚙️ Creating a Deno Server App #

We will start by writing the HTTP server code for the Deno app. The server will listen for POST HTTP requests on the /matrix-message route. To get going, create a new directory for the project and in there add a main.ts file with this content:

main.ts
typescript
    
1 import { load } from "@std/dotenv";
2 import { Temporal } from "js-temporal";
3
4 await load({ export: true });
5
6 const port = 8080;
7
8 function logRequest(request: Request, response: Response) {
9 const { method, url } = request;
10 const { hostname, pathname } = new URL(url);
11 const dateTime = Temporal.Now.plainDateTimeISO();
12 const dateTimeString = `${dateTime.toPlainDate().toString()} ${
13 dateTime.toLocaleString("en-GB", { timeStyle: "short" })
14 }`;
15 console.log(
16 `[${dateTimeString}] ${method} ${hostname}${pathname} ${response.status} ${response.statusText}`,
17 );
18 }
19
20 const handler: Deno.ServeHandler = async (request, _info): Promise<Response> => {
21 const { method, url } = request;
22 const { pathname } = new URL(url);
23
24 if (pathname === "/matrix-message" && method === "POST") {
25 const body = await request.text();
26
27 try {
28 // const messageSent: boolean = TODO: Call WASM function to send the message here
29
30 if (!messageSent) {
31 const response = new Response("Bad request", { status: 400 });
32 logRequest(request, response);
33
34 return response;
35 }
36 } catch (error) {
37 console.error(`Error sending message: ${error}`);
38 return new Response("Server error", { status: 500 });
39 }
40
41 const response = new Response(null, { status: 204 });
42 logRequest(request, response);
43 return response;
44 }
45 const response = new Response("Bad request", { status: 400 });
46 logRequest(request, response);
47
48 return response;
49 };
50
51 Deno.serve({ port }, handler);

Deno has an HTTP server included in its standard library. We import it in line 2 and start the server running in the last line of the code (line 49). You see, that Deno.serve call takes a handler function as an argument. We define handler from line 18 down. Deno uses standard JavaScript APIs, so the handler function probably does not contain anything too surprising to you. That will especially be the case if you have already read through the getting started with Deno Fresh posts which runs through Deno APIs.

We have a logRequest utility function defined in the code above. It uses the Temporal API to add a timestamp to the message it creates. We add a deno.json file next, with an import map for the Temporal module and the others we used above.

🛠️ Deno Config: deno.json #

Create a deno.json file in the project root directory and add the following content:

deno.json
json
    
1 {
2 "tasks": {
3 "start": "deno run -A --watch=utils/,main.ts main.ts",
4 "test": "deno test --allow-env --allow-read --allow-net --allow-run --allow-write dev.ts",
5 "wasmbuild": "RUSTFLAGS='--cfg getrandom_backend="wasm_js"' deno run -A jsr:@deno/[email protected]"
6 },
7 "imports": {
8 "@/": "./",
9 "@std/dotenv": "jsr:@std/dotenv@^0.225.3",
10 "js-temporal": "npm:@js-temporal/polyfill@^0.5.1",
11 }
12 }

This includes paths for imports as well as the tasks we will run to build WASM and start up the app. The tasks here provide a similar function to the scripts you would find in a package.json file. You will see, in a moment, that we use the matrix-sdk Rust crate in the project. This has a transitive dependency on getrandom. There are two steps to ensure getrandom compiles for WASM ). The first is to specify wasm_js as the back-end (we do that using a environment variable in line 5 above). The second step is performed on the Cargo.toml file, below.

Setting up the App to run WASM #

We can use the wasmbuild task straight away to set up our project for Deno WASM. Run this command in the Terminal:

    
deno task wasmbuild new

If this is your first time running WASM in Deno take a look at the Deno Fresh WASM post where we walk through a few details. The last command should have created an rs_lib directory in your project for our Rust code. We will update the Cargo.toml file, in there, next.

🦀 Rusting Up #

Disclaimer time! I’m still learning Rust. I tested the code, and it works, though you might have a better way of doing things here. Keen to hear about improvements. You can drop a comment below or reach out on Twitter or via other channels.

Cargo.toml #

Update the content in rs_lib/Cargo.toml:

rs_lib/Cargo.toml
toml
    
1 [package]
2 name = "deno-matrix-element-bot"
3 version = "0.0.1"
4 edition = "2021"
5 authors = ["Rodney Johnson <[email protected]>"]
6 license = "BSD-3-Clause"
7 repository = "https://github.com/rodneylab/deno/tree/main/demos/deno-fresh-rss-feed/deno-matrix-element-bot"
8 description = "Matrix Element bot WASM Rust code"
9
10 [lib]
11 crate-type = ["cdylib"]
12
13 [profile.release]
14 codegen-units = 1
15 incremental = true
16 lto = true
17 opt-level = "z"
18
19 [dependencies]
20 matrix-sdk = { version = "0.10", default-features = false, features = ["e2e-encryption", "js", "native-tls"] }
21 getrandom = { version = "0.3.2", features = ["wasm_js"] }
22 wasm-bindgen = "=0.2.100"
23 wasm-bindgen-futures = "=0.4.50"

Although getrandom is a transitive dependency, because we need to enable the wasm_js feature, we include it in the dependency list (line 21). This completes the second, final step for ensuring getrandom compiles for WASM.

lib.rs #

Next, update the content in rs_lib/src/lib.rs:

rs_lib/src/lib.rs
rust
    
1 mod matrix_client;
2
3 use matrix_client::MatrixClient;
4 use wasm_bindgen::prelude::*;
5
6 #[wasm_bindgen]
7 pub async fn matrix_message(
8 element_room_id: &str,
9 body: &str,
10 element_username: &str,
11 element_password: &str,
12 ) -> bool {
13 let matrix_client =
14 MatrixClient::new(element_username, element_password, element_room_id, None);
15 matrix_client.send_message(body).await;
16 true
17 }

You will create the matrix_client module referenced here next.

matrix_client.rs #

Finally, create rs_lib/src/matrix_client.rs with the following content:

rs_lib/src/matrix_client.rs — click to expand code.
rs_lib/src/matrix_client.rs
rust
    
1 use matrix_sdk::{
2 config::SyncSettings,
3 ruma::{events::room::message::RoomMessageEventContent, RoomId},
4 Client,
5 };
6 use wasm_bindgen::prelude::*;
7
8 #[wasm_bindgen]
9 extern "C" {
10 // Use `js_namespace` here to bind `console.log(..)` instead of just
11 // `log(..)`
12 #[wasm_bindgen(js_namespace = console)]
13 fn log(s: &str);
14 }
15
16 macro_rules! console_log {
17 ($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
18 }
19
20 pub struct MatrixClient<'a> {
21 homeserver_url: &'a str,
22 username: &'a str,
23 password: &'a str,
24 room_id: &'a str,
25 }
26
27 impl<'a> MatrixClient<'a> {
28 pub fn new(
29 username: &'a str,
30 password: &'a str,
31 room_id: &'a str,
32 homeserver_url: Option<&'a str>,
33 ) -> MatrixClient<'a> {
34 let actual_homeserver_url: &str = match homeserver_url {
35 Some(value) => value,
36 None => "https://matrix-client.matrix.org",
37 };
38
39 MatrixClient {
40 homeserver_url: actual_homeserver_url,
41 username,
42 password,
43 room_id,
44 }
45 }
46
47 pub async fn send_message(&self, message: &str) -> bool {
48 let Ok(client) = Client::builder()
49 .homeserver_url(self.homeserver_url)
50 .build()
51 .await
52 else {
53 return false;
54 };
55 if client
56 .matrix_auth()
57 .login_username(self.username, self.password)
58 .initial_device_display_name("deno-matrix-element-bot app")
59 .await
60 .is_err()
61 {
62 return false;
63 }
64 let _ = if let Ok(value) = client.sync_once(SyncSettings::default()).await {
65 value.next_batch
66 } else {
67 let _ = client.matrix_auth().logout().await;
68 return false;
69 };
70
71 let content = RoomMessageEventContent::text_plain(message);
72 let Ok(owned_room_id) = RoomId::parse(self.room_id) else {
73 return false;
74 };
75 let room = match client.join_room_by_id(&owned_room_id).await {
76 Ok(value) => value,
77 Err(error) => {
78 console_log!("Error joining room: {error}");
79 let _ = client.matrix_auth().logout().await;
80 return false;
81 }
82 };
83 let send_result = room.send(content).await.is_ok();
84 let _ = client.matrix_auth().logout().await;
85 send_result
86 }
87 }

Rust Summary #

In Cargo.toml, we add the matrix-sdk crate as well as wasm-bindgen-futures. You need that last one because we will call an async Rust function from the WASM in our Deno app. The function, we call from the WASM is matrix_message, defined in lib.rs. It just sets up a Matrix client instance for us, then uses the instance to send the message. The parameters we give in our Deno code will feed through.

Finally, turning to matrix_client.rs. The 'a’s sprinkled throughout the file are Rust lifetime annotations. I added them here because when we create a new instance of MatrixClient we will not reserve new memory for username, password and other parameters then copy them across. Instead, we “borrow” them. Effectively, we just store their existing memory address and read their value directly when we need to.

The lifetime annotations help make our code memory safe, avoiding us accidentally trying to read the values after that memory has been freed, for example. For more explanation on the lifetime annotations, see the Rust Book .

🧱 Building WASM #

To build the WASM module, just run the command:

    
deno task wasmbuild

This will create all the WASM code we need to contact the Matrix room from our Deno app. You can find the module in the lib folder of your project.

Updating Deno App with WASM #

Let’s add the missing logic (for sending messages) to the main server file main.ts now:

main.ts — click to expand code.
main.ts
typescript
    
1 import {
2 instantiate,
3 matrix_message as matrixMessage,
4 } from "@/lib/deno_matrix_element_bot.generated.js";
5 import { load } from "@std/dotenv";
6 import { Temporal } from "js-temporal/polyfill/?dts";
7
8 await load({ export: true });
9
10 const ELEMENT_ROOM_ID = Deno.env.get("ELEMENT_ROOM_ID");
11 const ELEMENT_BOT_USERNAME = Deno.env.get("ELEMENT_BOT_USERNAME");
12 const ELEMENT_BOT_PASSWORD = Deno.env.get("ELEMENT_BOT_PASSWORD");
13
14 const port = 8080;
15
16 function logRequest(request: Request, response: Response) {
17 const { method, url } = request;
18 const { hostname, pathname } = new URL(url);
19 const dateTime = Temporal.Now.plainDateTimeISO();
20 const dateTimeString = `${dateTime.toPlainDate().toString()} ${
21 dateTime.toLocaleString("en-GB", { timeStyle: "short" })
22 }`;
23 console.log(
24 `[${dateTimeString}] ${method} ${hostname}${pathname} ${response.status} ${response.statusText}`,
25 );
26 }
27
28 const handler: Deno.ServeHandler = async (request, _info): Promise<Response> => {
29 if (typeof ELEMENT_ROOM_ID === "undefined") {
30 throw new Error("env `ELEMENT_ROOM_ID` must be set");
31 }
32 if (typeof ELEMENT_BOT_USERNAME === "undefined") {
33 throw new Error("env `ELEMENT_BOT_USERNAME` must be set");
34 }
35 if (typeof ELEMENT_BOT_PASSWORD === "undefined") {
36 throw new Error("env `ELEMENT_BOT_PASSWORD` must be set");
37 }
38
39 const { method, url } = request;
40 const { pathname } = new URL(url);
41
42 if (pathname === "/matrix-message" && method === "POST") {
43 const body = await request.text();
44
45 await instantiate();
46
47 // remember to authenticate request before sending a message in a real-world app
48 try {
49 const messageSent = await matrixMessage(
50 ELEMENT_ROOM_ID,
51 body,
52 ELEMENT_BOT_USERNAME,
53 ELEMENT_BOT_PASSWORD,
54 );
55
56 if (!messageSent) {
57 const response = new Response("Bad request", { status: 400 });
58 logRequest(request, response);
59
60 return response;
61 }
62 } catch (error) {
63 console.error(`Error sending message: ${error}`);
64 return new Response("Server error", { status: 500 });
65 }
66
67 const response = new Response(null, { status: 204 });
68 logRequest(request, response);
69 return response;
70 }
71 const response = new Response("Bad request", { status: 400 });
72 logRequest(request, response);
73
74 return response;
75 };
76
77 Deno.serve({ port }, handler);

The WASM module import statement is in lines 2 – 5. Notice, we have an instantiate function (generated for us) as well as the matrix_message function we wrote in Rust. Before we can call matrix_message we need to make a one-off call to this instantiate function. That initializes the WASM module.

The only thing we are missing now is the environment variables. We can add those next, then run a test.

🤐 Environment Variables #

Create a .env file in the project root directory. Remember to add it to your .gitignore file, so your secrets will not get committed to your remote repo.

    
ELEMENT_ROOM_ID="!abcdefghijklmonpqr:matrix.org"
ELEMENT_BOT_USERNAME="your-bot-username"
ELEMENT_BOT_PASSWORD='YOUR_BOT_ACCOUNT_PASSWORD'

To get the ELEMENT_ROOM_ID in Element Matrix desktop:

  1. Select the room the bot will post to.
  2. Click the info button in the top right corner.
  3. Select Room settings then Advanced.
  4. Use the Internal room ID displayed here.
  5. You can create a new account for your bot at https://app.element.io/#/welcome. Use your bot account username and password in your .env file.

    🗳 Poll #

    What’s your preferred messaging app right now?
    Voting reveals latest results.

    💯 Matrix Message Relay Bot: Testing #

    To spin up your app, run this command from the Terminal:

        
    deno task start

    The server should now be listening on http://localhost:8080. We will use curl from another Terminal tab to test it with this command (sending a POST request with plaintext message in the request body):

        
    curl -H "Content-Type: text/plain" --data "Good morning" \
    http://localhost:8080/matrix-message

    Check back in the Deno app Terminal tab. You should see a log message showing a 204 response code.

    blurry low resolution placeholder image Matrix Message Relay Bot: screen capture shows the Terminal with a Deno server running at http://localhost:8080.  Log shows the date and time a POST request was received.
    Matrix Message Relay Bot: Server running in Terminal

    Also, if you open up Element Matrix, you should see the message:

    blurry low resolution placeholder image Matrix Message Relay Bot: screen capture shows the message window in the Matrix Element app with a recent message reading 'Good morning'
    Matrix Message Relay Bot: Matrix Element App

    🙌🏽 Matrix Message Relay Bot: Wrapping Up #

    We had an introduction to how you can create a Matrix message relay bot. In particular, you saw:

    • how to create an HTTP server in Deno;
    • how to use the Matrix SDK to send messages into a Matrix chat room; and
    • an example of how you can generate WASM code from Rust with Deno tooling.

    The complete code for this project is in the Rodney Lab GitHub repo . I do hope the post has either helped you with an existing project or provided some inspiration for a new project. You might want to explore the Matrix Rust SDK further, considering extensions like:

    • listening for messages in the Element room and using AI to respond
    • moderating messages received on the REST API (filtering out inappropriate content, for example) before relaying them

    Be sure to add authentication before pushing the code to your live server.

    Get in touch if you have some tips on how I can improve my Rust. Also, let me know if you have suggestions for other content.

    🏁 Matrix Message Relay Bot: Summary #

    Which HTTP server should you use with Deno? #

    Deno has a built-in, fast HTTP server. You can access it via the Deno standard library. Unless you have a special reason, this would be your first choice. There is no need to add a third party HTTP sever module like you would in Node or Rust.

    Does the Matrix Rust SDK compile to WASM? #

    We have seen you can compile an app using the Matrix Rust SDK into WASM. To get it to work, disable the default features, then enable the `js` and `native-tls` features. The generated WASM module can be quite large. This might be an issue if you want to serve your WASM from a Rust Cloudflare Worker. That said, it should fit comfortably within the 20 MB limit that Deno deploy offer.

    How is Element Matrix different to Telegram? #

    Element is a secure messaging app implementing the Matrix protocol. Like Telegram, it has mobile and desktop apps. A key difference is Element offers end-to-end encryption in rooms as well as one-to-one conversations. The interface is intuitive and not too different to other message apps. You can also use Element to message peers who use other message apps but still run using the Matrix protocol.

    🙏🏽 Matrix Message Relay Bot: Feedback #

    Have you found the post useful? Would you prefer to see posts on another topic instead? Get in touch with ideas for new posts. Also, if you like my writing style, get in touch if I can write some posts for your company site on a consultancy basis. Read on to find ways to get in touch, further below. If you want to support posts similar to this one and can spare a few dollars, euros or pounds, then please consider supporting me through Buy me a Coffee.

    blurry low resolution placeholder image ask Rodney X (formerly Twitter) avatar

    Rodney

    @askRodney

    Just dropped a post on how you can create an Element Matrix bot REST API with 🦖 Deno to relay messages to your chat room.

    We use a spot of 🦀 Rust WASM to work with the Matrix SDK.

    Hope you find it useful!

    #learndeno #takeTheRustPill #askRodneyhttps://t.co/wUfQzdnNTj

    — Rodney (@askRodney) February 17, 2023

    Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via @askRodney on Twitter, @[email protected]  on Mastodon and also the #rodney  Element Matrix room. Also, see further ways to get in touch with Rodney Lab. I post regularly on Astro as well as Deno. Also, subscribe to the newsletter to keep up-to-date with our latest projects.

Thanks for reading this post. I hope you found it valuable. Please get in touch with your feedback and suggestions for posts you would like to see. Read more about me …

blurry low resolution placeholder image Rodney from Rodney Lab
TAGS:
DENORUST

Related Posts

blurry low resolution placeholder image Get Started with SvelteKit Headless WordPress

Get Started with SvelteKit Headless WordPress

plus
sveltekit
<PREVIOUS POST
NEXT POST >
LATEST POST >>

Leave a comment …

Your information will be handled in line with our Privacy Policy .

Ask for more

1 Nov 2022 — Astro Server-Side Rendering: Edge Search Site
3 Oct 2022 — Svelte eCommerce Site: SvelteKit Snipcart Storefront
1 Sept 2022 — Get Started with SvelteKit Headless WordPress

Copyright © 2020 – 2025 Rodney Johnson. All Rights Reserved. Please read important copyright and intellectual property information.

  • Home
  • Profile
  • Plus +
  • Newsletter
  • Contact
  • Links
  • Terms of Use
  • Privacy Policy
We use cookies  to enhance visitors’ experience. Please click the “Options” button to make your choice.  Learn more here.