Inklusiv.Matrix
0.1.3
dotnet add package Inklusiv.Matrix --version 0.1.3
NuGet\Install-Package Inklusiv.Matrix -Version 0.1.3
<PackageReference Include="Inklusiv.Matrix" Version="0.1.3" />
<PackageVersion Include="Inklusiv.Matrix" Version="0.1.3" />
<PackageReference Include="Inklusiv.Matrix" />
paket add Inklusiv.Matrix --version 0.1.3
#r "nuget: Inklusiv.Matrix, 0.1.3"
#:package Inklusiv.Matrix@0.1.3
#addin nuget:?package=Inklusiv.Matrix&version=0.1.3
#tool nuget:?package=Inklusiv.Matrix&version=0.1.3
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 |
BootstrapCrossSigning |
false |
publish an identity at start-up so the device is not flagged unverified |
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.
Cross-signing
Set BootstrapCrossSigning and the account publishes a cross-signing identity the first time it
starts, so clients stop marking its messages as coming from an unverified device.
This leans hard on StorePath being persistent. The identity's private half lives only in the crypto
store; the homeserver keeps the public half forever. If the store is lost, start-up will not mint a
replacement, because that would void every signature made under the old key and reads in every client
as the account having been taken over. It logs and runs unverified instead. Restore the store.
Roadmap
- 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.