Inklusiv.Matrix
0.1.1
See the version list below for details.
dotnet add package Inklusiv.Matrix --version 0.1.1
NuGet\Install-Package Inklusiv.Matrix -Version 0.1.1
<PackageReference Include="Inklusiv.Matrix" Version="0.1.1" />
<PackageVersion Include="Inklusiv.Matrix" Version="0.1.1" />
<PackageReference Include="Inklusiv.Matrix" />
paket add Inklusiv.Matrix --version 0.1.1
#r "nuget: Inklusiv.Matrix, 0.1.1"
#:package Inklusiv.Matrix@0.1.1
#addin nuget:?package=Inklusiv.Matrix&version=0.1.1
#tool nuget:?package=Inklusiv.Matrix&version=0.1.1
Inklusiv.Matrix
A headless Matrix client for bots, with end-to-end encryption built in. Register it at startup with the bot's account and it takes it from there: login, the sync loop, key management, encrypting what you send, decrypting what arrives. Your code only ever sees plaintext events and calls plain send methods.
Encryption is Element's own engine, matrix-sdk-crypto, embedded as a native library for
linux-x64, which is where bots built on this run: inside a Linux container. No sidecar, no proxy,
no extra process.
Quick start
builder.Services.AddMatrixBot(o =>
{
o.HomeServer = "https://matrix.example.com";
o.UserId = "@bot:matrix.example.com";
o.AccessToken = builder.Configuration["Matrix:AccessToken"]; // or o.Password
o.DeviceId = "bot-main"; // stable, see below
o.StorePath = "/app/data/matrix"; // persistent volume
});
builder.Services.AddMatrixEventHandler<MyHandler>();
sealed class MyHandler(IMatrixBotClient client) : IMatrixEventHandler
{
public async Task HandleEventAsync(MatrixRoomEvent e, CancellationToken ct)
{
if (e.Type == "m.room.message" && e.MessageType == "m.text")
await client.SendTextAsync(e.RoomId, $"you said: {e.Body}", ct: ct);
}
}
That handler runs identically in plain and encrypted rooms. e.WasEncrypted tells you which,
e.Shield tells you how much to trust the sender's device.
What the package does for you
| Concern | Behaviour |
|---|---|
| Login | Pre-issued token validated with whoami, password fallback, silent re-login on 401/403 |
| Sync | Long-poll loop as a hosted service; sync token persisted after each fully processed response |
| Invites | Joined automatically (AutoJoinInvites), handlers get HandleJoinedAsync |
| Receiving | m.room.encrypted decrypted before dispatch; events whose key arrives late are retried across the next syncs, then delivered with DecryptionFailed = true |
| Sending | In a room carrying m.room.encryption, every Send* call tracks members, claims Olm sessions, shares the Megolm key and encrypts. Elsewhere it sends as-is |
| Files | SendFileAsync encrypts attachments client-side in encrypted rooms (EncryptedFile v2) |
| Rooms | CreateRoomAsync(new CreateRoomOptions { Encrypted = true }), EnableRoomEncryptionAsync for existing rooms (irreversible) |
| History | GetRoomEventsAsync pages backwards and decrypts what the device has keys for |
| Escape hatch | RawAsync(method, path, body) for anything not wrapped, using the account's session |
Rules that keep encryption healthy
- Stable
DeviceId, persistentStorePath. The store is the device's cryptographic identity. Delete it after keys were uploaded and every client will refuse the device. - One process per store. The package takes a lock file and refuses to start a second instance. Deployments must stop the old container before starting the new one.
- Encryption is per room and permanent. Once a room has
m.room.encryptionit cannot go back.
Configuration
MatrixBotOptions can also be bound from configuration:
builder.Services.AddMatrixBot(builder.Configuration.GetSection("Matrix"));
| Option | Default | Notes |
|---|---|---|
HomeServer |
URL or bare host (https assumed) | |
UserId / Username |
full MXID, or localpart combined with the homeserver host | |
AccessToken / Password |
at least one | |
DeviceId |
required for encryption | |
StorePath |
matrix-store |
sync token, crypto store, lock |
Encryption |
Auto |
Off disables the crypto engine entirely |
EncryptNewRooms |
false |
default for CreateRoomOptions.Encrypted |
AutoJoinInvites |
true |
|
DispatchOwnEvents |
false |
deliver the account's own events to handlers |
SyncTimeoutMs |
30000 |
Native library and bindings
src/Inklusiv.Matrix/Bindings/*.cs is generated by
uniffi-bindgen-cs from
matrix-sdk-crypto-ffi at the commit pinned in build/matrix-rust-sdk.commit; the generator
tag is pinned in build/uniffi-bindgen-cs.tag. The two uniffi versions must stay on the same
line. CI builds the Linux native on Debian 12 (the same glibc as the
mcr.microsoft.com/dotnet/aspnet images), regenerates the bindings and fails if they differ
from what is committed. The build is cached on the pinned commit, so only a pin bump recompiles.
Nobody needs a Rust toolchain to consume or contribute to the package. The scripts under build/
exist for CI and for the rare pin bump:
docker run --rm -v "$PWD":/repo -w /repo rust:1-bookworm build/build-native.sh # Linux, Debian 12 ABI
build\build-native.ps1 -GenerateBindings # Windows, only if you ever need a local DLL for experiments
Other platforms (win-x64, osx-arm64, linux-arm64) are deliberately not shipped.
Publishing to nuget.org (Trusted Publishing, no API key)
The workflow publishes on every v* tag through nuget.org Trusted Publishing: the job's GitHub
OIDC token is exchanged for a one-hour API key at push time, so no long-lived secret exists.
One-time setup by the nuget.org account that will own the package:
- nuget.org → your username → Trusted Publishing → add a policy:
- Repository owner: the GitHub owner of this repository (for example
inklusiv-team) - Repository: this repository's name
- Workflow file:
publish.yml(file name only) - Environment: leave empty
- Repository owner: the GitHub owner of this repository (for example
- In the GitHub repository add the secret
NUGET_USER= that nuget.org profile name (not the email). - Push a tag:
git tag v0.1.1 && git push origin v0.1.1.
For a private repository the policy starts as temporarily active for 7 days and becomes permanent after the first successful publish. Command-line publishing still works with a classic API key if ever needed; the workflow does not use one.
Samples
samples/EchoBot is a complete bot in one file and doubles as the integration test: give it a
throwaway account, put a real client in the room, and watch both directions decrypt.
Roadmap
- Cross-signing bootstrap for the bot identity, so clients stop flagging its device as unverified.
- Server-side key backup, so additional sessions of the same account can read history.
- Additional native runtimes if a deployment ever needs one (linux-arm64 first).
License
Apache-2.0. Bundles matrix-rust-sdk (Apache-2.0).
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 9.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.