Skip to main content

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 Hello sends 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 🟢 WA or Emy 🟢).
  • 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: false to 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 ffmpeg on 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 register followed by the content of registration.yaml in a code block, in the same message.
  • Synapse: add the file to app_service_config_files in homeserver.yaml and 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 -R above).
  • 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-personas and systemctl restart matrix-personas. The same command with matrix-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 of labels in the config, lower-cased and without spaces: wa or whatsapp, ms or messenger, ig or instagram, gm or googlemessages by 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 :server suffix.
  • 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:

  1. a reply to a message that came through a channel goes through that channel (which becomes the current one);
  2. !<channel> <text> goes through that channel (the current one doesn't change);
  3. 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:

  1. set __version__ in matrix_personas/__init__.py, commit and push;
  2. to try it first, run the Publish workflow by hand (Actions → Publish → Run workflow): it publishes to TestPyPI;
  3. tag it and push the tag: git tag -a v0.2.0 -m "matrix-personas 0.2.0", then git push origin v0.2.0. The Publish workflow checks that the tag matches __version__, runs the simulations and publishes to PyPI. Installations with auto_update: true pick it up within update_check_interval hours.

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)

Source distribution for matrix-personas 0.1.0
File Size Uploaded
matrix_personas-0.1.0.tar.gz 33.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for matrix-personas 0.1.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page