Skip to content

Matrix, through a plug-in on the matrix-rust-sdk (early version) - #13

Merged
KnutMann merged 14 commits into
KnutMann:masterfrom
iilonmasc:feature/purple-matrix
Oct 14, 2026
Merged

KnutMann merged 14 commits into
KnutMann:masterfrom
iilonmasc:feature/purple-matrix

Conversation

@iilonmasc

@iilonmasc iilonmasc commented Oct 13, 2026 •

Copy link
Copy Markdown

This adds Matrix as a service, through a new protocol plug-in, purple-matrix-rs, written for this bundle on the matrix-rust-sdk. It is an early version: it does what the list below says and nothing more, and it has been tested against a local Synapse instance, with three accounts in Adium and one in Element. I would rather have it in front of you now than after another month, because the Adium-side changes are small and the plug-in develops in its own repository.

Follows up on #12, where the licence question was raised.

What works

  • Signing in with an access token: the token goes in the password field, so Adium keeps it in the keychain, and a bool option says that it is a token. That covers homeservers that offer only SSO, such as Gitter's.
  • Joined rooms open as group chats with name, topic, member list and the last lines of history; invitations arrive as chat invitations and can be accepted or declined; /leave leaves a room, closing its window does not.
  • Text in both directions, with end-to-end encryption done by the SDK (vodozemac, no libolm anywhere) in encrypted rooms: a line typed in Adium is readable in Element, a line from Element is readable in Adium. Nothing is echoed locally; the line appears once, after the server confirmed it.
  • Bookmarks: a room can be bookmarked and reopens from its state, with members, while the account is still in it.
  • Reconnecting opens the rooms from the SDK's store before the first sync.

What does not work yet

  • Password and SSO sign-in: a token from another client is the only way in.
  • The device does not verify itself, so Element shows "encrypted by a device not verified by its owner" on every line from Adium. Verification and key backup are later specs in the plug-in.
  • No typing notifications, read receipts, media, presence, room or direct-chat creation.
  • A reopened room shows its history again (so Adium's log has it twice); on the plug-in's list.
  • The chat-invitation dialog is hard to read in Dark Mode; that is Adium's request window, not the plug-in, and I will send it separately.

Adium-side changes

  • AIPurpleServiceDescriptors.plist: the prpl-matrix entry (case-sensitive names, account name from the UID, EchoesOutgoingMessages, no reachability), and the service icons.
  • AIPurpleGenericService: chat names stay as they are when the service is case-sensitive; Matrix room ids are.
  • adiumPurpleConversation: a chat line with an empty sender is treated as a system line, as a missing sender already was.
  • Dependencies/patches/purple-matrix-rs/: build.sh clones the plug-in's repository at the pinned release (v0.2.1) and runs its own just plugin recipe against Frameworks/, the way purple-presage is built here; the README documents the pin and what the plug-in does. No adium.patch: what Adium needs goes into the plug-in. Building needs a Rust toolchain and just, which, as for the other plug-ins, is a developer's job and not a build step.
  • PurplePlugins/libmatrix.so: the plug-in built at that pin, 26 MB stripped, ad hoc signed; install.sh --build-only verifies it against the bundle's libpurple like the others.

Licence

The plug-in is GPL-2.0-or-later with the header in every source file; the SDK it links in statically is Apache 2.0, which combines with GPL v3 and not with GPL v2, so the shipped libmatrix.so is distributed under GPL v3, like several plug-ins in this bundle already. The README's licence section and binary table say so.

Closes #12

🤖 Generated with Claude Code

iilonmasc and others added 12 commits October 14, 2026 00:57
purple-matrix, built against the bundled frameworks like purple-presage and bound through the
descriptor like Teams. Upstream calls itself unmaintained and never finished encryption, which is
compiled out; what is left is plain text in rooms, and Gitter's rooms are exactly that.

The Gitter homeserver offers no password login, only SSO and token, and the plugin only asks for a
password. It already keeps and prefers an access token once it has one, so the patch makes that
token an account option: sign in once in a browser, paste the token, done. An empty option used to
pass as a token and fail at whoami; empty now means absent.

http-parser is compiled into the plugin rather than bundled: one source file from an archived
project that macOS does not ship.

Verified: the bundle's libpurple registers prpl-matrix, and a build with its own preference folder
lists Matrix among its services. Not yet tried against a real room.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… read

Account options are plain text on disk, in accounts.xml and in Adium's mirror of it, and the first
cut of the patch put the access token there. Now the password field carries it, which Adium keeps
in the keychain, and a bool option says that is what it is. A token the plugin stored before is
removed on the next connection.

On the way, _whoami_error read its user_data as the account while every caller hands it the
connection; a stale token used to send the password fallback off with the wrong pointer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
libpurple calls chat_leave when a chat conversation is destroyed, Adium destroys it when the
window closes, and upstream answered chat_leave by leaving the room on the server. Six windows
closed, six Gitter rooms gone; that is how it came to light.

chat_leave now frees what the conversation held and says nothing to the server; the room is back
with the next sync. Leaving for good is /leave, which tells the server and marks the conversation
as left.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t a room needs

chat_info_defaults returned an empty table whatever room it was asked about, and that table is
what Adium turns into a bookmark's join data; every bookmark on a Matrix room was therefore
"insufficient" and offered up for deletion. The room id is the one thing the protocol asks for,
so a name is a complete answer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… server spells it

Two things kept the window empty. Joining a room the account is already in changes nothing on the
server, so the sync never mentioned it and the plugin, which only logged the completed join, never
made a conversation; now it fetches the room's state and opens the room from that. And Adium
lowercased every chat name, where a Matrix room id is case sensitive and, since room version 12,
nothing but a hash: the generic service keeps chat names as typed when its descriptor says so.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… replies

Upstream dropped every encrypted event it could not decrypt with nothing but a debug line, and sent
into an encrypted room in the clear, which every other client then flagged. The room now announces
its encryption when it opens, an unreadable message leaves a line with its sender, and a message
typed into such a room is refused rather than sent unencrypted.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The refusal to send into an encrypted room was written with an empty sender and the ERROR flag
alone. Adium's bridge takes a line with a sender and without SYSTEM for a message from a contact,
and a contact with an empty UID is one the logger will not store: it threw, and Adium went down
with the first message typed into an encrypted room. The plugin now writes its lines with no
sender and SYSTEM set, and the bridge treats an empty sender as none, so the next protocol that
does this is told off by a log line rather than a crash.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
purple_conv_chat_write asserts on a NULL sender and writes nothing, so the three notes of the
previous commit never reached the room. "" is what libpurple takes, and with SYSTEM set it is
what Adium treats as a line from nobody.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The plugin writes its own outgoing messages into the room, so Adium's local copy was a second one,
and for a message the plugin refused it was the only one: it sat in the window as if it had gone
out. The descriptor now says the protocol echoes, and Adium shows what the plugin writes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
purple-matrix stopped upstream in 2023 and never got past decrypting, so an
encrypted room on it was a room that swallowed messages. The plug-in that replaces
it is written for this bundle on the matrix-rust-sdk: sync, rooms, invitations,
history and end-to-end encryption in both directions come from the SDK, a small
C file speaks libpurple. It keeps the protocol id, so the service entry and every
account made for purple-matrix carry on unchanged, token in the keychain included.

The plug-in lives in its own public repository and is taken the way purple-presage
is: pinned to the v0.2.1 release, built by Dependencies/patches/purple-matrix-rs/
build.sh, which clones the pin and runs the plug-in's own just recipe against
Frameworks/, and shipped as PurplePlugins/libmatrix.so (26 MB, stripped). There is
no adium.patch any more; what Adium needs goes upstream into the plug-in.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VFfMvsZKVa4n9djqHZRT1
@iilonmasc iilonmasc mentioned this pull request Oct 13, 2026
iilonmasc and others added 2 commits October 14, 2026 01:55
… its own

The rebase brought the licence check that refuses to ship a binary without the
terms it came with, and libmatrix.so was the first binary it did not know. The
plug-in is GPL-2.0-or-later, its text read from the pinned source tree like
purple-presage's; the matrix-rust-sdk linked into it statically is Apache 2.0,
fetched at the tag the plug-in was built against, the way tdlib is listed beside
the Telegram plug-in. License.txt names both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VFfMvsZKVa4n9djqHZRT1
…trix

# Conflicts:
#	Utilities/collect-licenses.sh
@KnutMann
KnutMann merged commit fdcf757 into KnutMann:master Oct 14, 2026
1 check passed
@KnutMann

Copy link
Copy Markdown
Owner

Merged, and thank you. This is the third time you have sent something that was better checked than the thing it was fixing, and it shows: the licence tooling grew two entries rather than being worked around, the deployment target matches the floor that moved yesterday, and the plug-in links nothing outside the bundle.

Two things I did before merging, because a 26 MB binary in a repository deserves them.

I rebuilt it. cargo and just, five minutes, from the pinned commit. The result carries exactly the same exported symbols and exactly the same dependencies as the one you committed, and the same minimum OS. It differs by 1552 bytes, which is Rust writing absolute paths and a build id into the object, and it registers prpl-matrix against the bundle's own libpurple. So nobody has to take the binary on trust, and the build script earned that.

And I read the two changes that reach past Matrix. The empty-sender one is a crash fix for every protocol, not a Matrix detail, and it is the kind of thing that is easy to write off as somebody else's problem. Thank you for not doing that.

One thing I have added on top, and I want to be straight about why rather than let you find it in a diff.

The service is marked Experimental in its descriptor, and an experimental service stays out of every service menu until somebody sets a preference:

defaults write com.adiumX.adiumX AIShowExperimentalServices -bool YES

This is not a comment on the work. It follows from what your own list says: no password or SSO sign-in, the device does not verify itself so Element warns about every line from Adium, no media, no receipts, and a reopened room logs its history twice. Somebody who came to try Matrix will happily live with all of that. Somebody who opened the account list to add a Jabber account did not ask to meet it, and they would judge the whole application by the roughest thing they saw.

It hides the offer, not the service. An account already configured keeps working and keeps connecting whatever the preference says, so anybody testing Matrix does not lose it at the next launch, and the switch is one line in a terminal. When sign-in and device verification are in and the history stops doubling, the key comes out of the descriptor and Matrix stands beside the others.

AIService has had an unused isHidden since long before either of us; it only wanted a caller. The next early protocol needs the key and no new code.

@iilonmasc

Copy link
Copy Markdown
Author

That's perfectly fine, I didn't know about the isHidden or experimental descriptor and it is justified enough by the current state of the implementation and also the current state of purple-matrix-rs which is also the reason I called it an "early version"

I already identified some tasks and technical details on how to implement sso and verification which will lead to more PRs for adium and also more versions of purple-matrix-rs in the future. Happy to contribute.

@iilonmasc
iilonmasc deleted the feature/purple-matrix branch October 14, 2026 01:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plans for matrix?

2 participants