From 48e090e744589103ea7177a1e8f446510a1e6124 Mon Sep 17 00:00:00 2001 From: q <41203525+q-TheDog@users.noreply.github.com> Date: Mon, 6 Jul 2026 08:48:58 -0400 Subject: [PATCH 1/7] Add MSCXXXX: Rooms as Social Media Pages Spiritual successor to the abandoned MSC3639, defining room types for social media profiles (m.social.profile) and groups (m.social.group), a profile-discoverability field built on MSC4133, a post event type reusing m.room.message's schema, a single mechanism for reposts/boosts/ quote-posts, liking via standard m.reaction, room-membership-as-following, and a phased migration plan for treating m.room.message as an interchangeable stand-in for m.social.post during rollout. --- proposals/0000-rooms-as-social-media-pages.md | 392 ++++++++++++++++++ 1 file changed, 392 insertions(+) create mode 100644 proposals/0000-rooms-as-social-media-pages.md diff --git a/proposals/0000-rooms-as-social-media-pages.md b/proposals/0000-rooms-as-social-media-pages.md new file mode 100644 index 00000000000..ee2ee5fd386 --- /dev/null +++ b/proposals/0000-rooms-as-social-media-pages.md @@ -0,0 +1,392 @@ +# MSCXXXX: Rooms as Social Media Pages + +*Note: this proposal is a spiritual successor to +[MSC3639: Matrix for the Social Media Use Case](https://github.com/Henri2h/matrix-doc/blob/henri2h-matrix-for-social-media/proposals/3639-matrix-for-the-social-media-use-case.md), +which has been abandoned. It keeps MSC3639's core idea, dedicated room types for social-media-style +profiles and groups, but narrows and simplifies the rest: it drops the bespoke comment event type in +favor of Matrix's own thread relations, reuses `m.room.message`'s content schema directly for posts +instead of defining a new one, and adds a profile-discoverability mechanism and a phased migration +plan that MSC3639 did not cover.* + +Matrix has no standardized convention for building a social-media-style application (one organized +around personal profiles, group pages, a following/discovery model, and an aggregated "feed") on top +of rooms and events. + +This proposal defines a small, focused set of conventions intended to be the common ground any Matrix +client can build a social media experience on top of: + +- Two new room types for the two kinds of "pages" a social network needs (personal profiles, and + groups/pages with no single owner). +- A profile field that lets any client discover which room is a given user's profile, plus a way to + discover new profiles/groups in general via the existing public room directory. +- Room membership doubling as "following", with no new event needed to represent it. +- A post event type that is structurally identical to `m.room.message`, so existing message-handling + code can be reused with no new parsing logic. +- A minimal, single mechanism for reposts, boosts, retweets, and quote-posts. +- Reusing the existing `m.reaction` mechanism for "liking" a post, rather than a new event type. +- Guidance for how a client builds a "feed" out of these rooms. +- A phased rollout plan for when clients should treat `m.room.message` as an interchangeable stand-in + for a real post, so that the ecosystem doesn't have to flag-day cut over and break interoperability + with clients that haven't adopted this MSC yet. + +## Proposal + +### Terminology + +This proposal uses "post" to mean a piece of social-media content submitted to a profile or group (the +equivalent of a tweet/X post, a Bluesky post, or a Facebook status update), as distinct from a "message" +sent in an ordinary Matrix room (a chat message). Structurally, under this proposal, a post is simply a +message-shaped event sent into a room of a particular type: the distinction is about the room and +event *type*, not the content shape. + +### Profile rooms + +A new room type, `m.social.profile`, set via the existing `type` field of the room's `m.room.create` +event content (the same generic room-type mechanism `m.space` already uses): + +```json +{ + "creator": "@alice:example.org", + "room_version": "11", + "type": "m.social.profile" +} +``` + +A profile room represents a single person's social media profile: a place other users join (follow) +to see that person's posts and interact with them (reply, react, repost). This is the Matrix analogue +of a personal profile/timeline on X, Bluesky, or a Fediverse instance. + +A profile room's display name, avatar, topic, and banner image are set with the ordinary +`m.room.name`, `m.room.avatar`, `m.room.topic`, and `m.room.banner` (see +[MSC4221](https://github.com/matrix-org/matrix-spec-proposals/pull/4221); see Dependencies) state +events, same as any other room. There is no new state event for "profile bio" or similar; the room's +existing metadata *is* the profile's metadata. + +This proposal does not mandate how a client enforces "single ownership" of a profile room (e.g., via +power levels restricting who can post); that is left to the creating client's own conventions, the +same way it already is for any other room today. + +### Profile/Group discoverability + +A new user profile key, `m.social.profile_room_id`, whose value is the room ID of the user's profile +room: + +```json +PUT /_matrix/client/v3/profile/{userId}/m.social.profile_room_id + +{ "m.social.profile_room_id": "!theirprofileroom:example.org" } +``` + +This relies on [MSC4133](https://github.com/matrix-org/matrix-spec-proposals/pull/4133) (Extensible +Profiles), which is what makes arbitrary per-key profile fields like this possible, see Dependencies. + +Setting this field ties a user's Matrix account directly to their profile room, the same way +`avatar_url` and `displayname` are already discoverable from any account, without needing a directory +service, a room alias convention, or a shared space to find it. It also gives a user a clean way to: + +- Change which room represents their profile (e.g. starting over), by simply updating the pointer, and +- Explicitly disassociate a room from their profile (without leaving the room, redacting anything, or + otherwise disrupting it) by clearing the field. + +A room being of type `m.social.profile` does **not** by itself mean it is "the" official profile of +its creator. `m.social.profile_room_id` is the authoritative signal a client should use to decide +"this is user X's current profile." A profile-typed room's own type and creator are only a secondary, +best-effort fallback signal (useful for legacy clients, or while this field hasn't propagated yet), not +a substitute for it. + +`m.social.profile_room_id` only helps discover *a specific person's* profile once you already know who +you're looking for. To discover new profiles and groups more generally, this proposal relies on the +public room directory: profile and group rooms can be published to a server's room directory (via the +existing `PUT /_matrix/client/v3/directory/list/room/{roomId}` visibility mechanism, same as any other +publishable room), and the directory search endpoint (`POST /_matrix/client/v3/publicRooms`) +already supports filtering results by room type via its `filter.room_types` field (added by +[MSC3827](https://github.com/matrix-org/matrix-spec-proposals/pull/3827), which has already been +merged into the stable spec). A social media client can therefore let a user browse or search for new +profiles/groups to follow simply by querying the directory with +`filter.room_types: ["m.social.profile"]` or `["m.social.group"]`, without needing any new +server-side mechanism of its own. + +### Following + +Matrix's own room membership is used to represent "following": joining a profile or group room *is* +following it, with no separate follow/subscription event needed. A room's member count is therefore +also its follower count: clients can read it directly off the room (e.g. via `m.room.member` state, or +a server-provided summary/heroes count) without any new counter or state event. + +For a profile room specifically, clients SHOULD subtract the profile's own owner (its creator) from the +displayed follower count, so the owner isn't counted as following themself just by virtue of being a +member of their own profile room. Group rooms have no single owner, so no such subtraction applies: +every member of a group room, including whoever created it, counts toward its member/follower count. + +### Group rooms + +A new room type, `m.social.group`, using the same mechanism as profile rooms: + +```json +{ + "creator": "@alice:example.org", + "room_version": "11", + "type": "m.social.group" +} +``` + +A group room represents a shared social media space where multiple people post and interact with each +other's posts, with no single owner implied: the Matrix analogue of an X Community or a public +Facebook Page. As with profile rooms, `m.room.name`/`m.room.avatar`/`m.room.topic`/`m.room.banner` +carry the group's displayed metadata, and posting/moderation permissions are governed by ordinary power +levels. + +### Posts + +A new event type, `m.social.post`. Its content is **structurally identical to `m.room.message`**: +same `msgtype`, `body`, `formatted_body`, media fields, etc. It is not a new content schema, only a +different top-level event `type`: + +```json +{ + "type": "m.social.post", + "content": { + "msgtype": "m.text", + "body": "Hello, Social Matrix!" + } +} +``` + +Reusing `m.room.message`'s schema verbatim means a compliant client's existing message +composing/rendering code (every `msgtype`, media attachment, formatted body, etc.) already works for +posts with zero new parsing logic; only the event `type` used to distinguish "this is a post" from +"this is an ordinary message" is new. Replies to a post use the existing `m.thread` relation +(`m.relates_to` with `rel_type: "m.thread"`) exactly as they would for a threaded reply to any other +event. This proposal does not define a separate "comment" event type, unlike MSC3639. + +### Reposting / Boosting / Retweeting / Quote Posting + +To mark a post as a repost of another post, its content includes an `m.social.repost_of` block: + +```json +{ + "type": "m.social.post", + "content": { + "msgtype": "m.text", + "body": "", + "m.social.repost_of": { + "event_id": "$original:example.org", + "room_id": "!originalroom:example.org", + "body": "This is the original post being reposted" + } + } +} +``` + +`m.social.repost_of` contains the `event_id` and `room_id` of the original post, and a copy of its +`body` at the time of reposting. + +The body is embedded (rather than requiring viewers to fetch the original event) because the person +viewing a repost in their feed may not share a room with the original poster at all, e.g., reposting a +post from a public group into your own profile, seen by followers who aren't members of that group. +Embedding the body guarantees the repost is renderable without the viewer's client needing to join or +peek the original room. Clients SHOULD still attempt to resolve the live original event (via +`event_id` + `room_id`) where accessible, to support "view original", live reaction counts, or +detecting that the original has since been edited or redacted, falling back to the embedded body when +the original isn't reachable. + +**Quote-posting** is a repost with the reposting user's own added commentary in the *outer* post's +`body`, with the quoted post held entirely inside `m.social.repost_of`: + +```json +{ + "type": "m.social.post", + "content": { + "msgtype": "m.text", + "body": "This is exactly what I was talking about:", + "m.social.repost_of": { + "event_id": "$original:example.org", + "room_id": "!originalroom:example.org", + "body": "This is the original post being reposted" + } + } +} +``` + +**Boosting/Retweeting** is the same structure with an empty (or absent) outer `body`, i.e., a repost +with no added commentary. A boost and a quote-post are the same event shape; only whether the outer +`body` is empty differs. Clients SHOULD render an empty-body repost distinctly in the UI (e.g. "Alice +boosted this" rather than an empty comment bubble), but no new event type is needed to distinguish +them. This deliberately avoids a combinatorial explosion of near-identical event types for what is +fundamentally one relationship (this post reshares that post, with or without commentary). + +### Liking + +Posts can be reacted to with any `m.reaction` event, the same as messages in any other Matrix room +today. This proposal does not define a new event type for "liking" a post. Instead, clients SHOULD +treat an `m.reaction` whose `key` is "👍" as a like: showing it in a dedicated like count/button rather +than (or in addition to) the room's normal reaction display, incrementing a "like" affordance when the +current user has sent one, and so on. + +A dedicated `m.social.like` event type was considered and rejected: it would add a new event type that +carries no information a plain `m.reaction` doesn't already carry, since "reacting with 👍" and "liking" +are the same user intent for the purposes of this proposal. The one real tradeoff is that a client +cannot distinguish "this user actually meant to *like* the post" from "this user just happened to react +with a thumbs-up emoji". This proposal considers that an acceptable ambiguity, given the alternative is +an entire new event type whose only job is to disambiguate an edge case most users will never notice. +Using the standard `m.reaction`/`m.annotation` relation also means a like sent by a compliant client is +still a completely ordinary, renderable reaction to any non-compliant Matrix client, and a 👍 reaction +sent by a non-compliant client is automatically recognized as a like by compliant ones, keeping the +whole feature interoperable for free, the same rationale as reusing `m.room.message` for posts. + +### Feeds + +Constructing a "feed" is a client-side behavior, not a new room or event type. A client builds a +user's feed by walking every room the user has joined that is of type `m.social.profile` or +`m.social.group`, and merging their `m.social.post` events (see Phased rollout, below, for +`m.room.message`) into a single timeline sorted by timestamp. + +Clients MAY additionally let a user opt in to including other, non-social-typed rooms in their feed, +for cases where a room has organically become a source of "posts" a user wants to follow without +formally reclassifying it. This is a client-local preference, not a protocol-level concept; this +proposal does not define a state or account data event for it, leaving it to individual client +implementations (which may already have their own account-data-backed settings sync). + +### Handling `m.room.message` in social rooms + +`m.social.post` will not be supported by every Matrix client immediately, and profile/group rooms are +ordinary rooms that any Matrix client, compliant with this MSC or not, can already join and post +`m.room.message` events into. To preserve interoperability with non-compliant clients during rollout, +this proposal defines three phases for how a compliant client should treat `m.room.message` events +found in `m.social.profile`/`m.social.group` rooms: + +- **Phase 1 (initial rollout).** Compliant clients render `m.room.message` and `m.social.post` + identically in the feed. When composing a post, clients default to sending `m.room.message`, with an + option to send `m.social.post` instead. This maximizes compatibility: any existing Matrix client, + compliant or not, can already fully participate in a profile/group room's content as ordinary + messages, while the new type is introduced. +- **Phase 2 (majority adoption).** Once a majority of active Matrix users report using at least one + MSCXXXX-compliant client, compliant clients should switch their default posting behavior to send + `m.social.post`, with an option to fall back to `m.room.message`. Rendering in the feed is unchanged + from Phase 1; both event types are still shown identically. (This proposal does not define how + "majority of active users" is measured; that is left to the ecosystem and spec core team to judge, + the same way other adoption-gated transitions in Matrix already are.) +- **Phase 3 (post-migration).** Once a majority of posts sent by compliant clients use `m.social.post`, + clients are free to stop treating `m.room.message` as a post at all. A profile or group room could + then use `m.room.message` for something other than posts, e.g. an internal chat thread alongside its + public posts, without that content leaking into anyone's feed. + +This phased approach is intended to avoid a hard cutover that would break interoperability for any user +still on a non-compliant or Phase-1 client, at the cost of a longer transition period before +`m.room.message` and `m.social.post` can be treated as meaningfully different things. + +## Potential issues + +- **Repost body duplication.** Because `m.social.repost_of.body` is a copy taken at repost time, an + edit to the original post does not propagate to reposts that already copied the old body, the same + limitation any "forwarded message" feature already has. Clients can mitigate this by re-fetching the + live original where accessible (see Security considerations for the more serious version of this + concern). +- **Feed construction doesn't distinguish "my posts" from "posts I follow" beyond room + type/membership.** This proposal only specifies which *rooms* feed into a feed, not how a client + should organize multiple joined profile/group rooms into, say, a "Home" feed versus an individual + profile's own timeline. That is left to client implementations (similar to how room lists and space + membership are already client-organized today). +- **No encryption guidance.** This proposal takes no position on whether profile/group rooms should be + encrypted, public, or otherwise configured; that is left entirely to the room creator, same as any + room today. Given that the whole point of a profile/group room is usually to be broadly readable, + most deployments are expected to be unencrypted and public/knock-restricted in practice, but this MSC + does not mandate it. +- **Depends on an MSC that hasn't landed.** Profile discoverability depends entirely on MSC4133. If + MSC4133 stalls or changes shape significantly, this proposal's discoverability mechanism has no + fallback (a profile-typed room a client happens to be a member of and was created by the target user + remains usable as a weaker, best-effort substitute; see Profile/Group discoverability, above). +- **Phase transition timing is deliberately unmeasured.** "Majority of active users"/"majority of + posts" in the phased rollout are not tied to any concrete, verifiable metric, since Matrix does not + currently have a reliable mechanism for measuring client adoption share across the network. This is + intentionally left as a judgment call rather than invented for this proposal, but is flagged here as + a real gap in how the phase transitions would actually be triggered in practice. + +## Alternatives + +- **Keep reviving MSC3639 as-is.** Rejected because MSC3639 is abandoned (no active author or + momentum), and this proposal intentionally narrows its scope in ways that aren't just a straight + continuation: dropping the bespoke `org.matrix.msc3639.social.comment` event type in favor of + standard thread relations (which did not yet exist when MSC3639 was written), and dropping + post-specific fields like `m.social.image-ref` in favor of reusing `m.room.message`'s existing media + `msgtype`s directly. +- **No new room types, treating "feed" as a purely client-side filter over ordinary rooms.** Rejected + because, without a room-level type marker, there is no reliable way for a client to know a room is + *meant* to be a social profile or group at all, e.g. to show a "Create Profile" versus "Create Room" + distinction in the UI, or to avoid accidentally treating a private DM as postable. Room types are + also an existing, established Matrix mechanism (the same one `m.space` already uses) for exactly this + kind of container classification, so reusing it is the smaller addition to the protocol. +- **Model reposts as an `m.relates_to` relation** (e.g. `rel_type: "m.social.repost"`) referencing the + original event, instead of an `m.social.repost_of` content block. Considered, but rejected: the + embedded original body is still needed regardless (see Proposal, above, for why), and a relation + doesn't carry that payload any more naturally than a plain content field would, so the relation form + adds an extra concept without removing anything. +- **Separate event types for boosts versus quote-reposts** (e.g. `m.social.boost` and + `m.social.quote_post`). Rejected in favor of a single mechanism (empty vs. non-empty outer `body`) + to minimize the number of new event types and avoid clients needing to special-case several + near-identical shapes. +- **A dedicated `m.social.like` event type.** Considered, but rejected for lack of value over reusing + `m.reaction`: a like and a 👍 reaction are the same user intent for this proposal's purposes, and a + new event type would only exist to disambiguate an edge case (someone reacting 👍 without "meaning" + to like it) that most users will never notice or care about. See Liking, above. + +## Security considerations + +- **`m.social.profile_room_id` is a self-asserted, unauthenticated claim.** Nothing stops a user from + pointing it at a room they don't own or aren't even a member of. Clients SHOULD treat the field as a + hint rather than proof of ownership, and MAY additionally check that the referenced room's own + `m.room.create` sender matches the profile's owner as a weak corroborating signal, though this + proposal does not mandate any specific verification. This matches the existing trust model for + `avatar_url`/`displayname`, which are equally self-asserted today. +- **Reposts can misrepresent an already-edited or already-deleted original.** Because + `m.social.repost_of.body` is a snapshot taken at repost time, a malicious or careless repost could + keep an offensive or retracted statement circulating in others' feeds after the original author has + edited or redacted it, a "quote taken out of context after the fact" concern already familiar from + other social platforms. Clients SHOULD indicate when a live original event can no longer be found or + has been redacted, distinct from a repost whose original is unchanged, so viewers aren't misled into + thinking a still-live, unedited post says something it no longer says. +- **Public, joinable profile/group rooms carry the same abuse surface as any public Matrix room + today** (spam, unwanted joins, abusive content). This proposal introduces no new attack surface + beyond what already exists for public `m.room.message`-based rooms, and defers entirely to existing + moderation tooling (power levels, bans, policy-room-based moderation) rather than proposing anything + new. +- **No new client-server or server-server API surface.** This proposal only defines conventions layered + on existing mechanisms (room types, message-shaped events, and MSC4133's per-key profile fields), so + it introduces no new endpoints and no attack surface beyond what those existing/dependency mechanisms + already carry. + +## Unstable prefix + +While this proposal doesn't yet have an assigned MSC number, implementations should use the following +placeholder identifiers, all under the `org.matrix.mscXXXX.social.` namespace: + +| Stable (once accepted) | Unstable (for now) | +| -------------------------------- | ------------------------------------------------ | +| `m.social.profile` | `org.matrix.mscXXXX.social.profile` | +| `m.social.group` | `org.matrix.mscXXXX.social.group` | +| `m.social.post` | `org.matrix.mscXXXX.social.post` | +| `m.social.repost_of` | `org.matrix.mscXXXX.social.repost_of` | +| `m.social.profile_room_id` | `org.matrix.mscXXXX.social.profile_room_id` | + +*(`mscXXXX` is a placeholder for this proposal's real MSC number, to be filled in once assigned; +update every identifier above, and throughout this document, at that point. This mirrors how MSC3639 +itself moved from `org.matrix.msc3639.*` unstable identifiers to `m.social.*` on acceptance.)* + +The `m.social.profile_room_id` profile field additionally depends on whatever unstable identifier the +implementing homeserver uses for MSC4133 support itself (e.g. some Synapse deployments currently +advertise `uk.tcpip.msc4133`/`uk.tcpip.msc4133.stable` in `/_matrix/client/versions`); clients should +check for MSC4133 support (by whatever identifier the server advertises) before relying on +`m.social.profile_room_id` being writable/readable. + +## Dependencies + +This MSC builds on the following, neither of which has been accepted into the spec at the time of +writing: + +- [MSC4133](https://github.com/matrix-org/matrix-spec-proposals/pull/4133) (Extensible Profiles): the + profile discoverability mechanism (`m.social.profile_room_id`) has no fallback if MSC4133 does not + land in close to its current form. +- [MSC4221](https://github.com/matrix-org/matrix-spec-proposals/pull/4221) (Room Banners): used for + `m.room.banner` in Profile rooms and Group rooms, above. Without it, profile/group rooms still have a + name, avatar, and topic, just no banner image, so this dependency is not load-bearing for the rest of + this proposal. From a47cc2be2f48c09f1da0be70150834f4c70e891f Mon Sep 17 00:00:00 2001 From: q <41203525+q-TheDog@users.noreply.github.com> Date: Mon, 6 Jul 2026 09:12:36 -0400 Subject: [PATCH 2/7] Strengthen repost security consideration Note that a repost's embedded body isn't tied to the referenced event's actual content at all, so a malicious repost could fabricate something the original author never said, not just resurface an edited/retracted statement. --- proposals/0000-rooms-as-social-media-pages.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/proposals/0000-rooms-as-social-media-pages.md b/proposals/0000-rooms-as-social-media-pages.md index ee2ee5fd386..306e1d4df81 100644 --- a/proposals/0000-rooms-as-social-media-pages.md +++ b/proposals/0000-rooms-as-social-media-pages.md @@ -338,13 +338,16 @@ still on a non-compliant or Phase-1 client, at the cost of a longer transition p `m.room.create` sender matches the profile's owner as a weak corroborating signal, though this proposal does not mandate any specific verification. This matches the existing trust model for `avatar_url`/`displayname`, which are equally self-asserted today. -- **Reposts can misrepresent an already-edited or already-deleted original.** Because +- **Reposts can misrepresent, or entirely fabricate, what the original said.** Because `m.social.repost_of.body` is a snapshot taken at repost time, a malicious or careless repost could keep an offensive or retracted statement circulating in others' feeds after the original author has - edited or redacted it, a "quote taken out of context after the fact" concern already familiar from - other social platforms. Clients SHOULD indicate when a live original event can no longer be found or - has been redacted, distinct from a repost whose original is unchanged, so viewers aren't misled into - thinking a still-live, unedited post says something it no longer says. + edited or redacted it. Nothing ties `body` to the actual content of the referenced `event_id`/ + `room_id` at all: a malicious user can point those fields at a real post while writing any `body` + they want, fabricating something the original author never said. Clients SHOULD indicate when a live + original event can no longer be found or has been redacted, distinct from a repost whose original is + unchanged, and SHOULD verify the embedded `body` against the live original's actual content where + the original is accessible, flagging a mismatch as a fabricated or altered quote rather than + silently trusting the embedded copy. - **Public, joinable profile/group rooms carry the same abuse surface as any public Matrix room today** (spam, unwanted joins, abusive content). This proposal introduces no new attack surface beyond what already exists for public `m.room.message`-based rooms, and defers entirely to existing From 23b42f326018c4b220a3120c7f6b91196ccab631 Mon Sep 17 00:00:00 2001 From: q <41203525+q-TheDog@users.noreply.github.com> Date: Mon, 6 Jul 2026 09:34:15 -0400 Subject: [PATCH 3/7] Rename proposal to MSC4501 --- ...ial-media-pages.md => 4501-rooms-as-social-media-pages.md} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename proposals/{0000-rooms-as-social-media-pages.md => 4501-rooms-as-social-media-pages.md} (99%) diff --git a/proposals/0000-rooms-as-social-media-pages.md b/proposals/4501-rooms-as-social-media-pages.md similarity index 99% rename from proposals/0000-rooms-as-social-media-pages.md rename to proposals/4501-rooms-as-social-media-pages.md index 306e1d4df81..ecb93dd3789 100644 --- a/proposals/0000-rooms-as-social-media-pages.md +++ b/proposals/4501-rooms-as-social-media-pages.md @@ -1,4 +1,4 @@ -# MSCXXXX: Rooms as Social Media Pages +# MSC4501: Rooms as Social Media Pages *Note: this proposal is a spiritual successor to [MSC3639: Matrix for the Social Media Use Case](https://github.com/Henri2h/matrix-doc/blob/henri2h-matrix-for-social-media/proposals/3639-matrix-for-the-social-media-use-case.md), @@ -261,7 +261,7 @@ found in `m.social.profile`/`m.social.group` rooms: compliant or not, can already fully participate in a profile/group room's content as ordinary messages, while the new type is introduced. - **Phase 2 (majority adoption).** Once a majority of active Matrix users report using at least one - MSCXXXX-compliant client, compliant clients should switch their default posting behavior to send + MSC4501-compliant client, compliant clients should switch their default posting behavior to send `m.social.post`, with an option to fall back to `m.room.message`. Rendering in the feed is unchanged from Phase 1; both event types are still shown identically. (This proposal does not define how "majority of active users" is measured; that is left to the ecosystem and spec core team to judge, From 5e3000b2d7bc5694e164f18ceb4c61eb609d578f Mon Sep 17 00:00:00 2001 From: q <41203525+q-TheDog@users.noreply.github.com> Date: Mon, 6 Jul 2026 09:37:18 -0400 Subject: [PATCH 4/7] Fill in unstable prefix table with assigned MSC number --- proposals/4501-rooms-as-social-media-pages.md | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/proposals/4501-rooms-as-social-media-pages.md b/proposals/4501-rooms-as-social-media-pages.md index ecb93dd3789..f7d63b3f591 100644 --- a/proposals/4501-rooms-as-social-media-pages.md +++ b/proposals/4501-rooms-as-social-media-pages.md @@ -360,20 +360,19 @@ still on a non-compliant or Phase-1 client, at the cost of a longer transition p ## Unstable prefix -While this proposal doesn't yet have an assigned MSC number, implementations should use the following -placeholder identifiers, all under the `org.matrix.mscXXXX.social.` namespace: +Until this proposal is accepted into the spec, implementations should use the following identifiers, +all under the `org.matrix.msc4501.social.` namespace: | Stable (once accepted) | Unstable (for now) | | -------------------------------- | ------------------------------------------------ | -| `m.social.profile` | `org.matrix.mscXXXX.social.profile` | -| `m.social.group` | `org.matrix.mscXXXX.social.group` | -| `m.social.post` | `org.matrix.mscXXXX.social.post` | -| `m.social.repost_of` | `org.matrix.mscXXXX.social.repost_of` | -| `m.social.profile_room_id` | `org.matrix.mscXXXX.social.profile_room_id` | - -*(`mscXXXX` is a placeholder for this proposal's real MSC number, to be filled in once assigned; -update every identifier above, and throughout this document, at that point. This mirrors how MSC3639 -itself moved from `org.matrix.msc3639.*` unstable identifiers to `m.social.*` on acceptance.)* +| `m.social.profile` | `org.matrix.msc4501.social.profile` | +| `m.social.group` | `org.matrix.msc4501.social.group` | +| `m.social.post` | `org.matrix.msc4501.social.post` | +| `m.social.repost_of` | `org.matrix.msc4501.social.repost_of` | +| `m.social.profile_room_id` | `org.matrix.msc4501.social.profile_room_id` | + +*(This mirrors how MSC3639 itself moved from `org.matrix.msc3639.*` unstable identifiers to +`m.social.*` on acceptance; the same rename will happen here if/when this proposal is accepted.)* The `m.social.profile_room_id` profile field additionally depends on whatever unstable identifier the implementing homeserver uses for MSC4133 support itself (e.g. some Synapse deployments currently From 69db24c0e38654c544c9eee81b55837de55b7b05 Mon Sep 17 00:00:00 2001 From: q <41203525+q-TheDog@users.noreply.github.com> Date: Mon, 6 Jul 2026 17:14:50 -0400 Subject: [PATCH 5/7] Require boost body to be a permalink instead of blank A boost's outer body was empty/absent, which renders as a blank, confusing message bubble in any client without support for this proposal. Require a matrix.to/matrix: permalink to the reposted event instead, so non-compliant clients see an ordinary clickable link, and specify how a compliant client detects and re-renders that case. --- proposals/4501-rooms-as-social-media-pages.md | 87 +++++++++++++------ 1 file changed, 61 insertions(+), 26 deletions(-) diff --git a/proposals/4501-rooms-as-social-media-pages.md b/proposals/4501-rooms-as-social-media-pages.md index f7d63b3f591..ca79395a9f4 100644 --- a/proposals/4501-rooms-as-social-media-pages.md +++ b/proposals/4501-rooms-as-social-media-pages.md @@ -161,22 +161,9 @@ event. This proposal does not define a separate "comment" event type, unlike MSC ### Reposting / Boosting / Retweeting / Quote Posting -To mark a post as a repost of another post, its content includes an `m.social.repost_of` block: - -```json -{ - "type": "m.social.post", - "content": { - "msgtype": "m.text", - "body": "", - "m.social.repost_of": { - "event_id": "$original:example.org", - "room_id": "!originalroom:example.org", - "body": "This is the original post being reposted" - } - } -} -``` +To mark a post as a repost of another post, its content includes an `m.social.repost_of` block, with +the outer `body` holding either the reposting user's own commentary (a quote-post) or a permalink to +the reposted event (a boost); see below for which. `m.social.repost_of` contains the `event_id` and `room_id` of the original post, and a copy of its `body` at the time of reposting. @@ -208,12 +195,47 @@ the original isn't reachable. } ``` -**Boosting/Retweeting** is the same structure with an empty (or absent) outer `body`, i.e., a repost -with no added commentary. A boost and a quote-post are the same event shape; only whether the outer -`body` is empty differs. Clients SHOULD render an empty-body repost distinctly in the UI (e.g. "Alice -boosted this" rather than an empty comment bubble), but no new event type is needed to distinguish -them. This deliberately avoids a combinatorial explosion of near-identical event types for what is -fundamentally one relationship (this post reshares that post, with or without commentary). +**Boosting/Retweeting** is the same structure with no added commentary. Rather than an empty (or +absent) outer `body`, the outer `body` MUST contain only a permalink: a `matrix.to` URI +([MSC1704](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/1704-matrix.to-permalinks.md)) +or a `matrix:` URI +([MSC2312](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2312-matrix-uri.md)), +pointing at the same event referenced by `m.social.repost_of`: + +```json +{ + "type": "m.social.post", + "content": { + "msgtype": "m.text", + "body": "https://matrix.to/#/!originalroom:example.org/$original:example.org", + "m.social.repost_of": { + "event_id": "$original:example.org", + "room_id": "!originalroom:example.org", + "body": "This is the original post being reposted" + } + } +} +``` + +A permalink is used instead of an empty string so that a client with no support for this proposal at +all (one simply rendering the event as a plain `m.room.message`, since `body` is always a valid +message body) shows a normal, clickable link to the boosted post rather than a blank message bubble +that looks like a sending failure or a malformed event. Clients MAY additionally send a +`formatted_body` containing the same link as an HTML anchor, for nicer rendering in clients that +support formatted bodies but not this proposal. + +A boost and a quote-post are therefore still the same event shape; what distinguishes them is whether +the outer `body` is *only* a permalink to the reposted event, or contains anything else. A compliant +client detects a boost by checking, for any post with an `m.social.repost_of` block, whether `body` +(trimmed of surrounding whitespace) parses as a `matrix.to`/`matrix:` permalink whose room and event +match `m.social.repost_of`'s `room_id`/`event_id` (a body that doesn't parse, or points elsewhere, +means the sender meant it as actual commentary (a quote-post) even if that commentary happens to +look like a URI). On a detected boost, the client SHOULD NOT render the literal `body` as commentary; +instead it SHOULD render something like "Alice reposted Bob's post" above the embedded repost content, +naming both the user who reposted and the original post's author. No new +event type is needed to distinguish a boost from a quote-post; this deliberately avoids a combinatorial +explosion of near-identical event types for what is fundamentally one relationship (this post reshares +that post, with or without commentary). ### Liking @@ -232,7 +254,8 @@ an entire new event type whose only job is to disambiguate an edge case most use Using the standard `m.reaction`/`m.annotation` relation also means a like sent by a compliant client is still a completely ordinary, renderable reaction to any non-compliant Matrix client, and a 👍 reaction sent by a non-compliant client is automatically recognized as a like by compliant ones, keeping the -whole feature interoperable for free, the same rationale as reusing `m.room.message` for posts. +whole feature interoperable for free, the same rationale as +[reusing `m.room.message` for posts](#handling-mroommessage-in-social-rooms). ### Feeds @@ -296,6 +319,13 @@ still on a non-compliant or Phase-1 client, at the cost of a longer transition p MSC4133 stalls or changes shape significantly, this proposal's discoverability mechanism has no fallback (a profile-typed room a client happens to be a member of and was created by the target user remains usable as a weaker, best-effort substitute; see Profile/Group discoverability, above). +- **A quote-post whose entire commentary happens to be a permalink to the same event is + indistinguishable from a boost.** Because a boost is detected purely by `body` being nothing but a + matching permalink (see Proposal, above), a user who deliberately quote-posts with no text other than + a copy-pasted link to that same event will have their post rendered as a plain boost instead. This + proposal considers that an acceptable, rare edge case (the rendered outcome, "a repost with no + commentary", is arguably correct anyway, since a bare link duplicating `m.social.repost_of` carries no + information a boost doesn't already convey). - **Phase transition timing is deliberately unmeasured.** "Majority of active users"/"majority of posts" in the phased rollout are not tied to any concrete, verifiable metric, since Matrix does not currently have a reliable mechanism for measuring client adoption share across the network. This is @@ -322,9 +352,14 @@ still on a non-compliant or Phase-1 client, at the cost of a longer transition p doesn't carry that payload any more naturally than a plain content field would, so the relation form adds an extra concept without removing anything. - **Separate event types for boosts versus quote-reposts** (e.g. `m.social.boost` and - `m.social.quote_post`). Rejected in favor of a single mechanism (empty vs. non-empty outer `body`) - to minimize the number of new event types and avoid clients needing to special-case several - near-identical shapes. + `m.social.quote_post`). Rejected in favor of a single mechanism (a `body` that is only a permalink + to the reposted event, versus one that isn't) to minimize the number of new event types and avoid + clients needing to special-case several near-identical shapes. +- **Empty or absent outer `body` for boosts**, rather than an embedded permalink. This was the + original design, but was rejected: a blank `body` renders as an empty, confusing message bubble in + any client without support for this proposal, indistinguishable from a send failure or a malformed + event. A permalink-only `body` degrades gracefully instead, rendering as an ordinary clickable link + in a fully non-compliant client (see Proposal, above). - **A dedicated `m.social.like` event type.** Considered, but rejected for lack of value over reusing `m.reaction`: a like and a 👍 reaction are the same user intent for this proposal's purposes, and a new event type would only exist to disambiguate an edge case (someone reacting 👍 without "meaning" From 2b890323f3558298972eb4bd5890034c0639c850 Mon Sep 17 00:00:00 2001 From: q <41203525+q-TheDog@users.noreply.github.com> Date: Mon, 6 Jul 2026 17:53:21 -0400 Subject: [PATCH 6/7] Embed the original post's entire content in repost_of, not just body A repost previously only carried a copy of the original's body. Carry the whole content object instead (formatted_body, format, media fields like url/file/info, etc.), so a repost of a rich-formatted or media post renders the same as the original rather than degrading to a plain-text stand-in. --- proposals/4501-rooms-as-social-media-pages.md | 57 +++++++++++-------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/proposals/4501-rooms-as-social-media-pages.md b/proposals/4501-rooms-as-social-media-pages.md index ca79395a9f4..64f7e53b3a9 100644 --- a/proposals/4501-rooms-as-social-media-pages.md +++ b/proposals/4501-rooms-as-social-media-pages.md @@ -166,16 +166,19 @@ the outer `body` holding either the reposting user's own commentary (a quote-pos the reposted event (a boost); see below for which. `m.social.repost_of` contains the `event_id` and `room_id` of the original post, and a copy of its -`body` at the time of reposting. +entire `content` at the time of reposting, not just `body`: `formatted_body`, `format`, media fields +(`url`, `file`, `info`, etc.), and any other key present on the original event's `content` are all +copied in as-is, so a repost of, say, an image or a rich-formatted post renders identically to how the +original was authored, not just as a plain-text stand-in. -The body is embedded (rather than requiring viewers to fetch the original event) because the person +The content is embedded (rather than requiring viewers to fetch the original event) because the person viewing a repost in their feed may not share a room with the original poster at all, e.g., reposting a post from a public group into your own profile, seen by followers who aren't members of that group. -Embedding the body guarantees the repost is renderable without the viewer's client needing to join or -peek the original room. Clients SHOULD still attempt to resolve the live original event (via -`event_id` + `room_id`) where accessible, to support "view original", live reaction counts, or -detecting that the original has since been edited or redacted, falling back to the embedded body when -the original isn't reachable. +Embedding the content guarantees the repost is renderable, including any media it contains, without the +viewer's client needing to join or peek the original room. Clients SHOULD still attempt to resolve the +live original event (via `event_id` + `room_id`) where accessible, to support "view original", live +reaction counts, or detecting that the original has since been edited or redacted, falling back to the +embedded content when the original isn't reachable. **Quote-posting** is a repost with the reposting user's own added commentary in the *outer* post's `body`, with the quoted post held entirely inside `m.social.repost_of`: @@ -189,7 +192,12 @@ the original isn't reachable. "m.social.repost_of": { "event_id": "$original:example.org", "room_id": "!originalroom:example.org", - "body": "This is the original post being reposted" + "content": { + "msgtype": "m.text", + "body": "This is the original post being reposted", + "format": "org.matrix.custom.html", + "formatted_body": "This is the original post being reposted" + } } } } @@ -211,7 +219,10 @@ pointing at the same event referenced by `m.social.repost_of`: "m.social.repost_of": { "event_id": "$original:example.org", "room_id": "!originalroom:example.org", - "body": "This is the original post being reposted" + "content": { + "msgtype": "m.text", + "body": "This is the original post being reposted" + } } } } @@ -300,10 +311,10 @@ still on a non-compliant or Phase-1 client, at the cost of a longer transition p ## Potential issues -- **Repost body duplication.** Because `m.social.repost_of.body` is a copy taken at repost time, an - edit to the original post does not propagate to reposts that already copied the old body, the same - limitation any "forwarded message" feature already has. Clients can mitigate this by re-fetching the - live original where accessible (see Security considerations for the more serious version of this +- **Repost content duplication.** Because `m.social.repost_of.content` is a copy taken at repost time, + an edit to the original post does not propagate to reposts that already copied the old content, the + same limitation any "forwarded message" feature already has. Clients can mitigate this by re-fetching + the live original where accessible (see Security considerations for the more serious version of this concern). - **Feed construction doesn't distinguish "my posts" from "posts I follow" beyond room type/membership.** This proposal only specifies which *rooms* feed into a feed, not how a client @@ -348,7 +359,7 @@ still on a non-compliant or Phase-1 client, at the cost of a longer transition p kind of container classification, so reusing it is the smaller addition to the protocol. - **Model reposts as an `m.relates_to` relation** (e.g. `rel_type: "m.social.repost"`) referencing the original event, instead of an `m.social.repost_of` content block. Considered, but rejected: the - embedded original body is still needed regardless (see Proposal, above, for why), and a relation + embedded original content is still needed regardless (see Proposal, above, for why), and a relation doesn't carry that payload any more naturally than a plain content field would, so the relation form adds an extra concept without removing anything. - **Separate event types for boosts versus quote-reposts** (e.g. `m.social.boost` and @@ -374,15 +385,15 @@ still on a non-compliant or Phase-1 client, at the cost of a longer transition p proposal does not mandate any specific verification. This matches the existing trust model for `avatar_url`/`displayname`, which are equally self-asserted today. - **Reposts can misrepresent, or entirely fabricate, what the original said.** Because - `m.social.repost_of.body` is a snapshot taken at repost time, a malicious or careless repost could - keep an offensive or retracted statement circulating in others' feeds after the original author has - edited or redacted it. Nothing ties `body` to the actual content of the referenced `event_id`/ - `room_id` at all: a malicious user can point those fields at a real post while writing any `body` - they want, fabricating something the original author never said. Clients SHOULD indicate when a live - original event can no longer be found or has been redacted, distinct from a repost whose original is - unchanged, and SHOULD verify the embedded `body` against the live original's actual content where - the original is accessible, flagging a mismatch as a fabricated or altered quote rather than - silently trusting the embedded copy. + `m.social.repost_of.content` is a snapshot taken at repost time, a malicious or careless repost could + keep an offensive or retracted statement (or image, or other media) circulating in others' feeds + after the original author has edited or redacted it. Nothing ties `content` to the actual content of + the referenced `event_id`/`room_id` at all: a malicious user can point those fields at a real post + while embedding any `content` they want, fabricating something the original author never said or + posted. Clients SHOULD indicate when a live original event can no longer be found or has been + redacted, distinct from a repost whose original is unchanged, and SHOULD verify the embedded + `content` against the live original's actual content where the original is accessible, flagging a + mismatch as a fabricated or altered quote rather than silently trusting the embedded copy. - **Public, joinable profile/group rooms carry the same abuse surface as any public Matrix room today** (spam, unwanted joins, abusive content). This proposal introduces no new attack surface beyond what already exists for public `m.room.message`-based rooms, and defers entirely to existing From 9cc047b350f9efa8d5fe99edba519a6927e807d3 Mon Sep 17 00:00:00 2001 From: q <41203525+q-TheDog@users.noreply.github.com> Date: Mon, 6 Jul 2026 21:41:08 -0400 Subject: [PATCH 7/7] Add profile_user_id room state, embed repost sender/displayname m.social.profile_user_id is a new room state event (fixed empty state_key, guaranteeing a room can only claim one owner at a time) that lets a profile room assert which Matrix user it belongs to, independent of its m.room.create creator (which can be a bridge or appservice bot rather than the real owner). Since it isn't one of the state events a default power_levels overrides, it inherits state_default (50, Moderator) rather than Admin-level protection; documented that gap and recommended an explicit override so moderators can't reassign a profile out from under its owner. Also carry the original post's sender and, optionally, displayname in m.social.repost_of, alongside its content, so a repost can credit the original author without a fetch to the original room. --- proposals/4501-rooms-as-social-media-pages.md | 164 ++++++++++++++---- 1 file changed, 126 insertions(+), 38 deletions(-) diff --git a/proposals/4501-rooms-as-social-media-pages.md b/proposals/4501-rooms-as-social-media-pages.md index 64f7e53b3a9..89faab523c2 100644 --- a/proposals/4501-rooms-as-social-media-pages.md +++ b/proposals/4501-rooms-as-social-media-pages.md @@ -62,9 +62,62 @@ A profile room's display name, avatar, topic, and banner image are set with the events, same as any other room. There is no new state event for "profile bio" or similar; the room's existing metadata *is* the profile's metadata. -This proposal does not mandate how a client enforces "single ownership" of a profile room (e.g., via -power levels restricting who can post); that is left to the creating client's own conventions, the -same way it already is for any other room today. +A profile room additionally carries a new state event, `m.social.profile_user_id`, identifying the single +Matrix user the room is a profile *of*: + +```json +{ + "type": "m.social.profile_user_id", + "state_key": "", + "content": { + "user_id": "@alice:example.org" + } +} +``` + +The event always uses an empty `state_key`. Matrix's state resolution rules mean a room has exactly one +current event for a given `(type, state_key)` pair, so with a fixed, empty `state_key` a profile room +can hold exactly one `m.social.profile_user_id` at a time, whichever was sent last (subject to the room's +own power levels); it can never simultaneously claim to belong to two different users. + +Because `m.social.profile_user_id` is not one of the state events a default `m.room.power_levels` gives +its own override for (unlike `m.room.name` or `m.room.avatar`, which default to requiring power level +50), it falls back to that power levels event's `state_default`, which itself defaults to 50 ("Moderator" +in most clients) when a room is created with no explicit overrides, not 100 ("Admin"). Left at that +default, any user promoted to Moderator, not just the room's Admin/owner, would be able to send a new +`m.social.profile_user_id` and reassign the profile away from its actual owner. + +Clients creating a profile room SHOULD therefore explicitly set a power level requirement for +`m.social.profile_user_id` in the room's initial `m.room.power_levels` `events` override, at or above the +level of the intended owner (typically 100), separately from `state_default`: + +```json +{ + "type": "m.room.power_levels", + "state_key": "", + "content": { + "events": { + "m.social.profile_user_id": 100 + } + } +} +``` + +This is what lets a profile have moderators (e.g. at power level 50, able to redact posts or ban +abusive members) without those moderators being able to reassign or strip the profile's ownership out +from under its actual owner. + +`m.social.profile_user_id` is the inverse of `m.social.profile_room_id` (see Profile/Group +discoverability, below): that field lives on a user's own account and points at a room, while +`m.social.profile_user_id` lives on the room and points back at a user. This matters because a profile +room's `m.room.create` `creator` is not always the person the profile is *of*: a room created by a +bridge or appservice on a user's behalf (e.g., a Matrix/ActivityPub federation bridge creating a local +profile room for a fediverse account) will have the bridge's own bot as `creator`, not the person the +profile represents. `m.social.profile_user_id` lets that room assert its true intended owner regardless +of who technically created it, which `creator` alone cannot do. + +Like `m.social.profile_room_id`, `m.social.profile_user_id` is self-asserted; see Security considerations +for how a client can corroborate it. ### Profile/Group discoverability @@ -89,10 +142,12 @@ service, a room alias convention, or a shared space to find it. It also gives a otherwise disrupting it) by clearing the field. A room being of type `m.social.profile` does **not** by itself mean it is "the" official profile of -its creator. `m.social.profile_room_id` is the authoritative signal a client should use to decide -"this is user X's current profile." A profile-typed room's own type and creator are only a secondary, -best-effort fallback signal (useful for legacy clients, or while this field hasn't propagated yet), not -a substitute for it. +its creator. `m.social.profile_room_id` (set on the candidate user's own account) and +`m.social.profile_user_id` (set on the room itself, see Profile rooms, above) are the authoritative +signals a client should use to decide "this is user X's current profile," ideally checked together +(see Security considerations). A profile-typed room's `type` and `m.room.create` `creator` are only a +weaker, best-effort fallback signal (useful for legacy clients, or while neither of these fields has +propagated yet), not a substitute for either. `m.social.profile_room_id` only helps discover *a specific person's* profile once you already know who you're looking for. To discover new profiles and groups more generally, this proposal relies on the @@ -165,20 +220,25 @@ To mark a post as a repost of another post, its content includes an `m.social.re the outer `body` holding either the reposting user's own commentary (a quote-post) or a permalink to the reposted event (a boost); see below for which. -`m.social.repost_of` contains the `event_id` and `room_id` of the original post, and a copy of its -entire `content` at the time of reposting, not just `body`: `formatted_body`, `format`, media fields -(`url`, `file`, `info`, etc.), and any other key present on the original event's `content` are all -copied in as-is, so a repost of, say, an image or a rich-formatted post renders identically to how the -original was authored, not just as a plain-text stand-in. - -The content is embedded (rather than requiring viewers to fetch the original event) because the person -viewing a repost in their feed may not share a room with the original poster at all, e.g., reposting a -post from a public group into your own profile, seen by followers who aren't members of that group. -Embedding the content guarantees the repost is renderable, including any media it contains, without the -viewer's client needing to join or peek the original room. Clients SHOULD still attempt to resolve the -live original event (via `event_id` + `room_id`) where accessible, to support "view original", live -reaction counts, or detecting that the original has since been edited or redacted, falling back to the -embedded content when the original isn't reachable. +`m.social.repost_of` contains the `event_id` and `room_id` of the original post, the `sender` (Matrix +ID) of the original post's author, and a copy of its entire `content` at the time of reposting, not +just `body`: `formatted_body`, `format`, media fields (`url`, `file`, `info`, etc.), and any other key +present on the original event's `content` are all copied in as-is, so a repost of, say, an image or a +rich-formatted post renders identically to how the original was authored, not just as a plain-text +stand-in. `m.social.repost_of` MAY additionally include a `displayname`, snapshotting the original +author's display name at repost time for a nicer default rendering; clients MUST NOT assume it is +present and MUST fall back to rendering the bare `sender` Matrix ID when it is absent, the same as +Matrix already does anywhere else a display name is unset. + +The content and sender are embedded (rather than requiring viewers to fetch the original event) +because the person viewing a repost in their feed may not share a room with the original poster at +all, e.g., reposting a post from a public group into your own profile, seen by followers who aren't +members of that group. Embedding them guarantees the repost is renderable, including any media it +contains and proper attribution of who originally posted it, without the viewer's client needing to +join or peek the original room. Clients SHOULD still attempt to resolve the live original event (via +`event_id` + `room_id`) where accessible, to support "view original", live reaction counts, or +detecting that the original has since been edited or redacted, falling back to the embedded content +and sender/displayname when the original isn't reachable. **Quote-posting** is a repost with the reposting user's own added commentary in the *outer* post's `body`, with the quoted post held entirely inside `m.social.repost_of`: @@ -192,6 +252,8 @@ embedded content when the original isn't reachable. "m.social.repost_of": { "event_id": "$original:example.org", "room_id": "!originalroom:example.org", + "sender": "@bob:example.org", + "displayname": "Bob", "content": { "msgtype": "m.text", "body": "This is the original post being reposted", @@ -219,6 +281,7 @@ pointing at the same event referenced by `m.social.repost_of`: "m.social.repost_of": { "event_id": "$original:example.org", "room_id": "!originalroom:example.org", + "sender": "@bob:example.org", "content": { "msgtype": "m.text", "body": "This is the original post being reposted" @@ -243,7 +306,9 @@ match `m.social.repost_of`'s `room_id`/`event_id` (a body that doesn't parse, or means the sender meant it as actual commentary (a quote-post) even if that commentary happens to look like a URI). On a detected boost, the client SHOULD NOT render the literal `body` as commentary; instead it SHOULD render something like "Alice reposted Bob's post" above the embedded repost content, -naming both the user who reposted and the original post's author. No new +naming both the user who reposted (the outer event's own `sender`) and the original post's author +(`m.social.repost_of`'s `sender`/`displayname`), without either name requiring a fetch to the +original room. No new event type is needed to distinguish a boost from a quote-post; this deliberately avoids a combinatorial explosion of near-identical event types for what is fundamentally one relationship (this post reshares that post, with or without commentary). @@ -375,25 +440,47 @@ still on a non-compliant or Phase-1 client, at the cost of a longer transition p `m.reaction`: a like and a 👍 reaction are the same user intent for this proposal's purposes, and a new event type would only exist to disambiguate an edge case (someone reacting 👍 without "meaning" to like it) that most users will never notice or care about. See Liking, above. +- **Relying solely on a profile room's `m.room.create` `creator` to identify its owner**, rather than + adding `m.social.profile_user_id`. Rejected because `creator` is not always the person a profile room + is of: a room created by a bridge or appservice on a user's behalf will have the bridge's own bot as + `creator`. A dedicated state event lets the room assert its true intended owner independent of who + technically created it. See Profile rooms, above. ## Security considerations -- **`m.social.profile_room_id` is a self-asserted, unauthenticated claim.** Nothing stops a user from - pointing it at a room they don't own or aren't even a member of. Clients SHOULD treat the field as a - hint rather than proof of ownership, and MAY additionally check that the referenced room's own - `m.room.create` sender matches the profile's owner as a weak corroborating signal, though this - proposal does not mandate any specific verification. This matches the existing trust model for - `avatar_url`/`displayname`, which are equally self-asserted today. -- **Reposts can misrepresent, or entirely fabricate, what the original said.** Because - `m.social.repost_of.content` is a snapshot taken at repost time, a malicious or careless repost could - keep an offensive or retracted statement (or image, or other media) circulating in others' feeds - after the original author has edited or redacted it. Nothing ties `content` to the actual content of - the referenced `event_id`/`room_id` at all: a malicious user can point those fields at a real post - while embedding any `content` they want, fabricating something the original author never said or - posted. Clients SHOULD indicate when a live original event can no longer be found or has been - redacted, distinct from a repost whose original is unchanged, and SHOULD verify the embedded - `content` against the live original's actual content where the original is accessible, flagging a - mismatch as a fabricated or altered quote rather than silently trusting the embedded copy. +- **`m.social.profile_room_id` and `m.social.profile_user_id` are both self-asserted, unauthenticated + claims.** Nothing stops a user from pointing their `m.social.profile_room_id` at a room they don't + own or aren't even a member of, and nothing stops whoever holds sufficient power level in a room from + setting its `m.social.profile_user_id` to any user ID, including one who has never heard of the room. + Clients SHOULD treat either field alone as a hint rather than proof of ownership. The strongest + corroboration this proposal offers is checking that the two agree in both directions: the room's + `m.social.profile_user_id` names a user whose own `m.social.profile_room_id` points back at that same + room. Requiring agreement in both directions is harder to fake than either field alone, since it + needs control of both the claimed user's account and sufficient power level in the room, though it is + still not cryptographic proof. Clients MAY additionally fall back to checking the room's + `m.room.create` sender as a weaker signal still, though this proposal does not mandate any specific + verification. This matches the existing trust model for `avatar_url`/`displayname`, which are equally + self-asserted today. +- **`m.social.profile_user_id` defaults to Moderator-level, not Admin-level, protection.** A room's + `m.room.power_levels` only overrides specific well-known state events; anything else, including a + custom event type like `m.social.profile_user_id`, falls back to `state_default`, which itself defaults + to 50 ("Moderator") rather than 100 ("Admin") when a room is created with no explicit overrides. A + profile room created without an explicit power level override for `m.social.profile_user_id` (see + Profile rooms, above) can therefore have its ownership reassigned by any Moderator, not only the + room's Admin/owner. Clients creating profile rooms SHOULD set this override at room-creation time. +- **Reposts can misrepresent, or entirely fabricate, what the original said, or who said it.** + Because `m.social.repost_of.content`/`sender`/`displayname` are a snapshot taken at repost time, a + malicious or careless repost could keep an offensive or retracted statement (or image, or other + media) circulating in others' feeds after the original author has edited or redacted it, or + attribute real or fabricated content to a user who never posted it at all. Nothing ties `content`, + `sender`, or `displayname` to the actual content or author of the referenced `event_id`/`room_id`: + a malicious user can point those fields at a real post while embedding any `content`/`sender`/ + `displayname` they want, fabricating something the original author never said, or attributing real + content to the wrong person entirely. Clients SHOULD indicate when a live original event can no + longer be found or has been redacted, distinct from a repost whose original is unchanged, and + SHOULD verify the embedded `content`/`sender` against the live original's actual content/sender + where the original is accessible, flagging a mismatch as a fabricated or altered quote (or a + misattributed one) rather than silently trusting the embedded copy. - **Public, joinable profile/group rooms carry the same abuse surface as any public Matrix room today** (spam, unwanted joins, abusive content). This proposal introduces no new attack surface beyond what already exists for public `m.room.message`-based rooms, and defers entirely to existing @@ -416,6 +503,7 @@ all under the `org.matrix.msc4501.social.` namespace: | `m.social.post` | `org.matrix.msc4501.social.post` | | `m.social.repost_of` | `org.matrix.msc4501.social.repost_of` | | `m.social.profile_room_id` | `org.matrix.msc4501.social.profile_room_id` | +| `m.social.profile_user_id` | `org.matrix.msc4501.social.profile_user_id` | *(This mirrors how MSC3639 itself moved from `org.matrix.msc3639.*` unstable identifiers to `m.social.*` on acceptance; the same rename will happen here if/when this proposal is accepted.)*