Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 249 additions & 0 deletions proposals/4144-per-message-profile.md
Comment thread
tulir marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
# MSC4144: Per-message profiles
Currently profiles in Matrix are defined by `m.room.member` state events, and
there is no easy way to have different profiles per message.

## Proposal
The proposed solution is a new field called `m.per_message_profile`, which
contains a displayname and/or avatar URL to override the default profile,
Comment thread
tulir marked this conversation as resolved.
plus an ID to identify different profiles within the same Matrix user.
Comment thread
tulir marked this conversation as resolved.

Comment thread
tulir marked this conversation as resolved.
```json
{
"msgtype": "m.text",
"body": "Hello, World!",
"m.per_message_profile": {
Comment thread
tulir marked this conversation as resolved.
Comment thread
tulir marked this conversation as resolved.
"id": "meow",
"displayname": "cat",
"avatar_url": "mxc://maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo"
}
}
```

The `id` field is required and is an opaque string. Clients may use it to group
messages with the same ID like they would group messages from the same sender.
For example, bridges would likely set it to the immutable remote user ID.
Comment thread
clokep marked this conversation as resolved.
Comment thread
tulir marked this conversation as resolved.
Comment thread
tulir marked this conversation as resolved.

This ID is scoped to the real MXID used to send the event, but is otherwise
global. In other words, the same ID in different rooms from the same account
can be considered to be the same user, but the same ID in the same room from
different accounts are considered to be different users.

The field is allowed for all message events, which currently means
`m.room.message` and `m.sticker`. A future MSC may expand the field to other
events, such as reactions.

Modifying per-message profiles using edits is allowed, i.e. clients SHOULD
include the per-message profile object in `m.new_content` when sending edits
and read it when receiving edits. Including the per-message profile object in
the edit fallback at the top level is not necessary, as all clients that support
per-message profiles can be assumed to support edits.

### Field limits
After JSON decoding, `id` and `displayname` MUST be at most 255 bytes of UTF-8
and every code point MUST be a Unicode scalar. Null bytes (`\u0000`) are not
allowed.

Like with other images and avatars, clients should ignore any avatar URL that
is not a `mxc://` URI.

### Encrypted avatars
Because the profile is inside the ciphertext in encrypted events, the entire
profile can be hidden from the server, as long as the avatar is also encrypted.
Encrypted avatars are placed under `avatar_file` instead of `avatar_url`.
The `avatar_file` field has the same schema as the `file` field in
`m.room.message` events.

<details>
<summary>Encrypted avatar example</summary>

```json
{
"msgtype": "m.text",
"body": "Hello, World!",
"m.per_message_profile": {
"id": "meow",
"displayname": "cat",
"avatar_file": {
"v": "v2",
"key": {
"alg": "A256CTR",
"ext": true,
"k": "8dXeBMBMthuXGY5zmUh9Mi0aqC1kndMZ4NCa-0RhELc",
"key_ops": [
"encrypt",
"decrypt"
],
"kty": "oct"
},
"iv": "L6zup2cR570AAAAAAAAAAA",
"hashes": {
"sha256": "/cTs+PajUcznbV3h1w5gh1AHnLjrKQVl2jU3xLCqoBI"
},
"url": "mxc://maunium.net/eKLhozQduElYSgBkWjtwSXoi"
}
}
}
```

</details>

### Behavior of omitted and empty fields
If the `displayname` field is omitted, null, or an empty string, the
displayname from the member event should be used instead. Setting an empty
displayname using a per-message profile is not supported, as there aren't any
clear use cases for it.

However, there are use cases for setting an empty avatar, so `avatar_url` being
an empty string should be treated as clearing the avatar and falling back to
the client's default blank avatar behavior (e.g. generating one based on the
displayname). If both `avatar_url` and `avatar_file` are omitted or null, the
avatar from the member event should be used instead. If the member event does
not have an avatar defined either, and the client uses the displayname to
generate fallback avatars, it should use the per-message displayname for the
fallback avatar rather than the global one.

### Extensible profiles
This MSC is not related to extensible profiles and does not attempt to
implement them. However, in case extensible profiles are implemented as
something that can be referenced (e.g. room IDs), the MSC adding them could
allow per-message profiles to specify which extensible profile is used.

### Fallbacks for old clients
In order for users on old clients to see the per-message profile data, sending
clients SHOULD include a fallback containing the per-message displayname.
When a fallback is used, the per-message profile object MUST include
`"has_fallback": true`.

Fallbacks are intentionally optional. Clients MUST NOT assume the message has
a fallback unless `has_fallback` is set to `true`,

#### Plaintext fallback
The fallback MUST be at the beginning of the plaintext `body` and consist of
the exact value of the `displayname` property, followed by a colon (`:`) and
a space (`\x20`), e.g. `cat: original message`.

To remove the plaintext fallback, trim the prefix `{displayname}: ` from the body.

#### HTML fallback
If a `formatted_body` of type `org.matrix.custom.html` is present, it SHOULD
include the same fallback text inside a `strong` tag with the `data-mx-profile-fallback`
Comment thread
tulir marked this conversation as resolved.
attribute, e.g. `<strong data-mx-profile-fallback>cat: </strong>original message`.

The displayname MUST be HTML-escaped and there MUST NOT be any HTML tags inside
the fallback. The tag MUST NOT have any other attributes. The attribute MAY
have an empty string as its value (`=""`) for compatibility with all HTML
generators, but MUST NOT have any other value. The HTML fallback is not mandated
to be at the beginning of the string, as there may be valid reasons to put it
inside another tag, such as a `<p>` tag.

To remove the HTML fallback, either use a HTML parser to drop the entire `strong`
tag with a `data-mx-profile-fallback` attribute, or replace matches of the
following regex with an empty string: `<strong\s+data-mx-profile-fallback(?:="")?\s*>([^<]+): </strong\s*>`

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Stealthii said outside of a thread:

To address one of the main criticisms I have with this MSC: the suggestion of matching specifically this tag: <strong\s+data-mx-profile-fallback(?:="")?\s*>([^<]+): </strong\s*>

If we are recommending regular expressions, I would suggest the following, which is safer, handling multi-line input, other tags, matching the same closing tag, etc.

input = input.replace(/<(\w+)[^>]*\bdata-mx-profile-fallback\b[^>]*>[\s\S]*?<\/\1>/g, "")

This however is not 100% safe, no regular expression is. Personally I would like to argue for non-fallback content to be explicitly stated in that MSC in a separate property field, because regex should be used for matching, not content manipulation in matrix client or bridge implementations.

Similarly, suggesting trimming the plaintext component by "PMP name length + 2" also assumes that clients implement the spec accurately as it stands today, and do not deviate. This also cannot be guaranteed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I would like to argue for non-fallback content to be explicitly stated in that MSC in a separate property field

Duplicating the entire content is a really bad idea. Each fallback doubles the content, so if there are 3 different fallbacks (edits, formatting, per-message profiles), that'd result in 2*2*2 = 8 duplicates of the content. It might be "only" 6 duplicates if the non-fallback per-message profile is only inside m.new_content, but if anyone decided to invent a new thing needing a fallback copy, it'd jump to 12 or more

which is safer, handling multi-line input, other tags, matching the same closing tag, etc.

The regex is intentionally fairly strict, other tags are not meant to be allowed. It could maybe slightly be relaxed in terms of the trailing space inside the fallback to allow \n there (<br> is obviously not allowed), perhaps :\s* instead of :

Alternatively, I guess the fallback itself could be put in another field, such that clients can simply do formatted_body.replace(fallback, "") rather than using a regex 🤔

Similarly, suggesting trimming the plaintext component by "PMP name length + 2" also assumes that clients implement the spec accurately as it stands today, and do not deviate. This also cannot be guaranteed.

The only consequence of deviating from the spec is that the message will render incorrectly. If clients don't implement the spec correctly and their messages render differently than they intended, that's their problem, not the spec's.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Blackilykat posted outside of a thread:

I'm not active in matrix spec proposals but am developing a bot which benefits from this feature and I'd like to give my 2 cents.

I think the sender should have more freedom on how to format the HTML fallback. Currently, it is required to be in exactly a <strong> tag with the data-mx-profile-fallback attribute containing exactly the text <profile>: . I think it would be beneficial to allow any tag (i.e. span) with the same attribute and any contents, including HTML formatted contents. This would include the original fallback but allow bots to customize the fallback if needed.

My use case: I am bridging a Minecraft chat over matrix. Different players have different chat formats which I would like to include in the fallback so outdated clients will see chat exactly as shown in-game. Clients which support this feature would instead show the message as if it was sent by that user, ignoring the HTML content inside the fallback.

If a client supports HTML-formatted content it should already have the logic to correctly parse HTML tags and ignoring a first tag with the data-mx-profile-fallback attribute, regardless of which tag it is or its contents, should not be difficult to implement.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clients may want to remove fallbacks at a different stage than HTML parsing, so requiring the use of a parser is probably a bad idea. Several clients had trouble with reply fallbacks too.

A separate field containing the exact fallback string to remove should be easy and safe to remove, but it also feels slightly weird and has never been done before. Maybe I should just do that anyway

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you could include the length of the formatted fallback string instead of the whole thing to avoid sending/storing duplicate data.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would require it to strictly be a prefix, which may not be the case if the message uses paragraph tags or other such things


#### Other details
Comment thread
tulir marked this conversation as resolved.
If a fallback is used in a media message, the `filename` property MUST be set
to ensure the `body` is treated as a caption rather than a file name.

Fallbacks MUST only be used when the `displayname` property is a non-empty
string, i.e. they are not used when falling back to the member event name.

#### Example with fallback
```json
{
"msgtype": "m.text",
"body": "cat: Hello, World!",
"format": "org.matrix.custom.html",
"formatted_body": "<p><strong data-mx-profile-fallback>cat: </strong>Hello, World!</p>",
"m.per_message_profile": {
"id": "meow",
"displayname": "cat",
"avatar_url": "mxc://maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo",
"has_fallback": true
}
}
```

## Use cases

### Bridging
Per-message profiles will allow making "lighter-weight" bridges that don't need
appservice access. Currently the only option for such bridges is to prepend the
displayname to the message, which is extremely ugly. Even though they're ugly,
there are still rooms that use bot-based bridges like matterbridge, which shows
there's demand for bridging without requiring server admin access.

Such bridges would obviously have downsides, like not being able to start chats
via standard mechanisms, and not being able to see the member list on Matrix.
However, those may be acceptable compromises for non-puppeting bridges that
only operate in specific predetermined rooms. Non-message events like reactions
are also not supported by this MSC, but they could be allowed in the future.

This method also allows encrypting profile info, which reduces metadata leaked
by bridging.

Comment thread
tulir marked this conversation as resolved.
### Feature-parity with other platforms
Other chat applications such as Slack and Discord have "webhooks" which allow
per-message profile overrides. This MSC effectively enables the same on Matrix.
Comment thread
clokep marked this conversation as resolved.

For example, Discord's [execute webhook](https://discord.com/developers/docs/resources/webhook#execute-webhook)
API takes `username` and `avatar_url` as optional parameters.

### Roleplaying, plural users, etc
Some users want to be able to switch between profiles quickly, which would be
much easier using this MSC. Currently easiest way is to have multiple accounts,
which has other benefits, but is much more cumbersome to manage.

## Potential issues
Implementing encrypted avatars could cause difficulty for clients that assume
that avatars are always unencrypted mxc URIs.

Comment thread
tulir marked this conversation as resolved.
Comment thread
tulir marked this conversation as resolved.
When per-message profiles are used for bridging, there's no way to mention
specific users, as mentions will just target the single bridge bot. A future
MSC could define a way to specify which profile to mention.

## Alternatives
Comment thread
tulir marked this conversation as resolved.
Comment thread
tulir marked this conversation as resolved.
### New state events
Per-message profiles could be transmitted more compactly by defining the profile
Comment thread
tulir marked this conversation as resolved.
in a new state event and only referencing the state key in the message event.
However, that approach wouldn't enable encrypting per-message profiles without
Comment thread
tulir marked this conversation as resolved.
inventing encrypted state events. Additionally, even with encrypted state
events, some kind of sender identifiers would be leaked via state keys.

State events also have the downside that they have to be sent into the room
separately, which means extra work for everyone. They would also pollute the
room state if a profile is only used once or a few times.

The main benefit of state events is that it makes it easy to retroactively
modify the per-message profile of already sent messages. This MSC considers
retroactive modifications to be sufficiently low-priority that state events
are not worth it considering the other downsides.

### Appservices
Appservices work perfectly fine for bridging already now, but they require
admin access to a server, which is not available for everyone. Additionally,
they have similar metadata issues as the "New state events" alternative above.

For use cases involving a single human user, having multiple mxids (regardless
of whether they're registered manually or via an appservice) complicates things
unnecessarily.

## Security considerations

### Preventing impersonation
To prevent impersonation using per-message profiles, clients MUST somehow
indicate to the user that the message has a per-message profile with an easy
way to see the user's MXID or default profile. For example, a client could have
a small `via @user:example.com` text next to the per-message displayname.

Comment thread
tulir marked this conversation as resolved.
To improve user experience, clients MAY omit the indicator when the sender
account has sufficiently high power level, and the displayname is unique among
members of the room (i.e. it does not require disambiguation in the
["Calculating the display name for a user" spec](https://spec.matrix.org/v1.12/client-server-api/#calculating-the-display-name-for-a-user)).

The recommended power level to check is the level for a `m.per_message_profile`
state event (i.e. check `events` -> `m.per_message_profile` and fall back to
`state_default` if not set).

## Unstable prefix
`com.beeper.per_message_profile` should be used instead of `m.per_message_profile`
until this MSC is accepted.
Loading