matrix-personas
A Matrix appservice that gives you a Beeper-style unified conversation per person. The bridged rooms of one contact (WhatsApp, Messenger, Instagram, Google Messages…) are merged into a single persona room: their messages are mirrored there, and your answers go back through the right channel.
Built with mautrix-python, developed against Tuwunel and the mautrix WhatsApp, Meta, Instagram and Google Messages bridges. It should work with any homeserver and bridge.
Features
- One room per person. A persona (e.g. Emy) is a Matrix user of its own, with a direct chat that shows up among your people, no invite to accept.
- Incoming messages of the contact, from every linked bridge room, are posted by the persona
in the color of their channel (and, by default, prefixed with
[WhatsApp]), then marked as read in their bridge room. Replies keep their link, or quote the replied message when it was never mirrored. - Your answers go through the channel of the message you reply to; otherwise through the
current channel: the one of the last mirrored message, or the last one you used or picked.
!ig Hellosends one message through Instagram without changing it. - The current channel is always visible: in a notice kept at the bottom of the room, and/or
in the room name (
Emy 🟢 WhatsApp,Emy 🟢 WAorEmy 🟢). - Commands in the persona room:
!channel,!ig,!avatar,!help… A mistyped command is refused, never sent to your contact. - Profile picture taken from the contact on the first linked channel; the persona asks which one to keep when another channel brings a different one.
- Only the contact's messages are mirrored, not the bridge bots' notices nor your messages sent from the native apps. The errors the bridges report about your messages (e.g. an unsupported media type) are shown in the persona room.
- Updates itself from PyPI (
auto_update, on by default). - Configurable: colors, labels, texts, indicators, command prefix, behaviors. See config.example.yaml.
Limitations
- Bridge rooms must be unencrypted: the service has to read the messages.
- It acts as you in the bridge rooms, with your access token, to send your answers and read
receipts. Your contacts see the read receipts (
mark_as_read: falseto disable). - Edits, reactions and deletions are not mirrored, in either direction. Your own messages sent from the native apps are not mirrored either.
- Voice messages to Messenger and Instagram need
ffmpegon the host of the mautrix-meta / mautrix-instagram bridge (it converts Matrix' ogg/opus voice messages); without it, the bridge rejects them.
Installation
Requires Python 3.10 or later, on a host that can reach the homeserver and that the homeserver can reach (the same host is simplest).
mkdir /opt/matrix-personas && cd /opt/matrix-personas
python3 -m venv .venv
.venv/bin/pip install matrix-personas
.venv/bin/matrix-personas -g config.yaml
-g writes the example configuration, with every setting documented.
Configuration
Edit config.yaml. The essential settings:
| Setting | Value |
|---|---|
homeserver.address |
URL of the homeserver from this host, e.g. http://127.0.0.1:8008 |
homeserver.domain |
server_name of the homeserver, e.g. example.com |
appservice.host, appservice.port |
where the service listens, e.g. 127.0.0.1 and 29400 |
appservice.as_token, appservice.hs_token |
two different random strings (see below) |
owner.mxid |
your account, the one logged in to the bridges, e.g. @you:example.com |
owner.access_token or owner.access_token_file |
the access token of that account, or a file holding it |
Generate each token with:
python3 -c "import secrets; print(secrets.token_urlsafe(48))"
Your access token is in Element under Settings → Help & About, in the Advanced section. Prefer a session dedicated to this service: logging out of it revokes the token.
The other settings (labels, colors, current channel indicator, commands…) are optional and
documented in config.yaml itself, with their default values (see also
config.example.yaml).
Registration
Generate the registration file of the homeserver from config.yaml:
.venv/bin/matrix-personas config.yaml -r registration.yaml
It holds the id, the tokens, the url built from appservice.host and appservice.port,
the bot (sender_localpart) and the persona users regex built from appservice.ghost_prefix
and your domain:
id: matrix-personas
url: http://127.0.0.1:29400
as_token: <appservice.as_token>
hs_token: <appservice.hs_token>
sender_localpart: personasbot
rate_limited: false
namespaces:
users:
- exclusive: true
regex: '@persona_.*:example\.com'
aliases: []
rooms: []
Generate it again after changing any of these settings, and register it again.
Then register it with your homeserver:
- Tuwunel / conduwuit: in the admin room, send
!admin appservices registerfollowed by the content ofregistration.yamlin a code block, in the same message. - Synapse: add the file to
app_service_config_filesinhomeserver.yamland restart Synapse. The file is only readable by its owner (it holds the tokens): copy it where Synapse can read it if Synapse runs as another user.
Service
Run it as a dedicated user with systemd:
useradd --system --home-dir /opt/matrix-personas --shell /usr/sbin/nologin matrix-personas
chown -R matrix-personas: /opt/matrix-personas
chmod 600 /opt/matrix-personas/config.yaml
/etc/systemd/system/matrix-personas.service:
[Unit]
Description=matrix-personas (unified conversations per person)
After=network.target
[Service]
Type=simple
User=matrix-personas
Group=matrix-personas
WorkingDirectory=/opt/matrix-personas
ExecStart=/opt/matrix-personas/.venv/bin/matrix-personas /opt/matrix-personas/config.yaml
Restart=on-failure
RestartSec=15
[Install]
WantedBy=multi-user.target
Add your homeserver's unit to After= when it runs on the same host (e.g.
After=network.target tuwunel.service). Then:
systemctl daemon-reload
systemctl enable --now matrix-personas
journalctl -u matrix-personas -f
Until the homeserver is reachable and knows the appservice, the logs say homeserver unreachable or as_token rejected every 30 seconds; the service starts by itself once the
registration is done. After a change to config.yaml,
systemctl restart matrix-personas.
To run it by hand instead: .venv/bin/matrix-personas config.yaml.
Updates
With auto_update: true (the default), the service checks PyPI once started and then every
update_check_interval hours (24 by default). When a new release is out, it installs it into
its own environment, finishes handling the messages it already received and restarts by
itself; the homeserver keeps the events of the meantime and sends them afterwards. The logs
say updating matrix-personas 0.1.0 -> 0.1.1, then installed: restarting.
- The service's user must be able to write its virtual environment (the
chown -Rabove). - It only installs releases (not pre-releases) and only this package; its dependencies are upgraded only when the new release requires it.
- It is ignored when the service was installed from a local copy (
pip install .) or a URL. - If you'd rather update by hand:
auto_update: false, then.venv/bin/pip install -U matrix-personasandsystemctl restart matrix-personas. The same command withmatrix-personas==<version>goes back to an earlier release.
Usage
Creating a persona
Start a direct chat with the bot (@personasbot:example.com) from your account:
!persona create Emy
!persona link Emy whatsapp !AbCdEf123
!persona link Emy ms https://matrix.to/#/!GhIjKl456?via=example.com
!persona list
| Command | Effect |
|---|---|
!persona create <name> |
create a persona and its room |
!persona link <name> <channel> <room> |
link a bridge room to it |
!persona link <name> <channel> <room> <@contact> |
the same, naming the contact |
!persona delete <name> |
delete it (leaving its room does the same) |
!persona list |
list the personas and their channels |
!persona help |
list the commands |
<channel>is a tag or a label oflabelsin the config, lower-cased and without spaces:waorwhatsapp,msormessenger,igorinstagram,gmorgooglemessagesby default.<room>is the ID of the bridge room (in its settings, Advanced) or a matrix.to link to it. Room IDs of room version 12 have no:serversuffix.- Linking invites the bot into the bridge room, as you: you need the right to invite there (see below for Google Messages).
- The contact is guessed from the members of the bridge room. Without double puppeting, the
bridges also represent you with an account of theirs in each room; the service recognises
it as the one present in most of your rooms of that bridge. When it can't decide, it lists the
candidates: name the contact, e.g.
@whatsapp_33612345678:example.com.
Rooms where you can't invite (Google Messages)
mautrix-gmessages locks its rooms: inviting needs power level 99 and your account has 0, so
linking fails with sender does not have enough power to invite (the bot then tells you what
to do). As an admin of the bridge (permissions in its config), give your account that level
with the bridge's set-pl command, sent in that Google Messages room:
!gm set-pl @you:example.com 99
then link again. Do it once per Google Messages room you link. Copy the command exactly: in a
bridge room, a message that doesn't start with the bridge's command prefix (!gm by default)
is sent to your contact. Other mautrix bridges have the same command with their own prefix.
To log the Google Messages bridge in to your account, QR code pairing no longer works: use the Google account login of Mautrix Manager, a desktop app that connects to the bridge (only needed for logging in).
In a persona's room
| Command | Effect |
|---|---|
!channel |
show the current channel |
!<channel> (or !channel <channel>) |
switch the current channel, e.g. !ig |
!<channel> <text> |
send this text through that channel, without changing the current one |
!avatar |
show the profile pictures of the channels (those without one are left out) |
!avatar <channel> |
use the profile picture of that channel |
!!<text> |
send a text starting with ! |
!help |
list the commands and the channel names |
Any other !word is refused instead of being sent. Your commands and the persona's answers
are shown in command_color.
Editing a message into a command runs it, e.g. to fix a typo: its answer replaces the answer
to that message's previous command, or is posted if there was none. A message edited into
!<channel> <text> is not sent (edits are not mirrored).
Where your message goes:
- a reply to a message that came through a channel goes through that channel (which becomes the current one);
!<channel> <text>goes through that channel (the current one doesn't change);- anything else goes through the current channel (the first linked one if there is none yet).
The current channel is also set by every incoming message and by !<channel>.
Testing
git clone https://github.com/quentinemusee/matrix-personas.git
cd matrix-personas
pip install .
python tests/simulate.py
tests/simulate.py sends real Matrix events to the event handler, sends back to it everything
it posts (as a homeserver would, so that loops show up), and prints what reaches the contact
and the persona room for a series of messages, replies, commands and edits. Settings can be
overridden: python tests/simulate.py '{"command_prefix": "."}'.
Releasing
The CI runs the simulations on every push. To publish a version:
- set
__version__inmatrix_personas/__init__.py, commit and push; - to try it first, run the Publish workflow by hand (Actions → Publish → Run workflow): it publishes to TestPyPI;
- tag it and push the tag:
git tag -a v0.2.0 -m "matrix-personas 0.2.0", thengit push origin v0.2.0. The Publish workflow checks that the tag matches__version__, runs the simulations and publishes to PyPI. Installations withauto_update: truepick it up withinupdate_check_intervalhours.
Roadmap
- Mirror edits, reactions and deletions, both ways
- Mirror your messages sent from the native apps
- Suggest personas (same phone number or name across bridges)
- Translatable texts
- Encrypted bridge rooms
License
MIT
Metadata
Release files for matrix-personas 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| matrix_personas-0.1.0.tar.gz | 33.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| matrix_personas-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 63.9 kB
Release files / matrix_personas-0.1.0.tar.gz
| Download URL | matrix_personas-0.1.0.tar.gz |
|---|---|
| Size | 33.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9e4007ab6b268e06b83c7d5a635b49b09ddd0ab3c98a710a706ead48119a1e09
|
|
BLAKE2b-256 checksum How to use checksums |
08ddb12db2c675cbf3fd10af385913b3ec6094c3f2da99ea1055bf00d35595b8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency logRelease files / matrix_personas-0.1.0-py3-none-any.whl
| Download URL | matrix_personas-0.1.0-py3-none-any.whl |
|---|---|
| Size | 30.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8bd98ca0c542648414762668e08260ec6487f9b5be44496a0dbb553ca9424653
|
|
BLAKE2b-256 checksum How to use checksums |
09750e7b4ea45f88c23448a7f82dea0fc511f43f6f7dc8409eb67bcceb7128ac
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency log