Metadata-Version: 2.4
Name: matrix-personas
Version: 0.1.0
Summary: Matrix appservice merging the bridged rooms of one contact (WhatsApp, Messenger, Instagram...) into a single Beeper-style persona room.
Author-email: quentinemusee <quentinemusee@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/quentinemusee/matrix-personas
Project-URL: Source, https://github.com/quentinemusee/matrix-personas
Project-URL: Issues, https://github.com/quentinemusee/matrix-personas/issues
Keywords: matrix,appservice,mautrix,bridge,beeper,whatsapp,messenger,instagram
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: No Input/Output (Daemon)
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Communications :: Chat
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: mautrix>=0.21
Requires-Dist: aiosqlite>=0.20
Requires-Dist: pyyaml>=6
Dynamic: license-file

# 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](https://github.com/mautrix/python), developed against
[Tuwunel](https://github.com/matrix-construct/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](https://github.com/quentinemusee/matrix-personas/blob/master/matrix_personas/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).

```bash
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:

```bash
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](https://github.com/quentinemusee/matrix-personas/blob/master/matrix_personas/config.example.yaml)).

### Registration

Generate the registration file of the homeserver from `config.yaml`:

```bash
.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:

```yaml
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:

```bash
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`:

```ini
[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:

```bash
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](https://github.com/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

```bash
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](https://test.pypi.org/p/matrix-personas);
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
