Compare commits

..

2 commits

Author SHA1 Message Date
Bea
950cb03123
feat(m2d): strip per-message profile fallbacks from message content
Remove data-mx-profile-fallback elements from formatted_body and
displayname prefix from plain body when per-message profile is used.
2026-03-20 14:19:54 +00:00
Bea
5a5a9d9f0c
feat(m2d): support MSC4144 per-message profiles
Override webhook username and avatar_url from m.per_message_profile
(and unstable com.beeper.per_message_profile) when present.
The stable key takes priority over the unstable prefix.
2026-03-20 13:54:19 +00:00
3 changed files with 51 additions and 36 deletions

View file

@ -62,20 +62,7 @@ async function _interact({guild_id, data}, {api}) {
.sort((a, b) => webGuild._getPosition(a, discord.channels) - webGuild._getPosition(b, discord.channels)) .sort((a, b) => webGuild._getPosition(a, discord.channels) - webGuild._getPosition(b, discord.channels))
.filter(channel => from("channel_room").join("member_cache", "room_id").select("mxid").where({channel_id: channel.id, mxid: event.sender}).get()) .filter(channel => from("channel_room").join("member_cache", "room_id").select("mxid").where({channel_id: channel.id, mxid: event.sender}).get())
const matrixMember = select("member_cache", ["displayname", "avatar_url"], {room_id: message.room_id, mxid: event.sender}).get() const matrixMember = select("member_cache", ["displayname", "avatar_url"], {room_id: message.room_id, mxid: event.sender}).get()
// Check for per-message profile const name = matrixMember?.displayname || event.sender
const perMessageProfile = event.content?.["com.beeper.per_message_profile"]
let name = matrixMember?.displayname || event.sender
let avatar = utils.getPublicUrlForMxc(matrixMember?.avatar_url)
let profileNote = ""
if (perMessageProfile) {
if (perMessageProfile.displayname) {
name = perMessageProfile.displayname
}
if ("avatar_url" in perMessageProfile) {
avatar = perMessageProfile.avatar_url ? utils.getPublicUrlForMxc(perMessageProfile.avatar_url) : undefined
}
profileNote = " (sent with a per-message profile)"
}
return { return {
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
data: { data: {
@ -83,9 +70,9 @@ async function _interact({guild_id, data}, {api}) {
author: { author: {
name, name,
url: `https://matrix.to/#/${event.sender}`, url: `https://matrix.to/#/${event.sender}`,
icon_url: avatar icon_url: utils.getPublicUrlForMxc(matrixMember?.avatar_url)
}, },
description: `This Matrix message was delivered to Discord by **Out Of Your Element**${profileNote}.\n[View on Matrix →](<https://matrix.to/#/${message.room_id}/${message.event_id}?${via}>)\n\n**User ID**: [${event.sender}](<https://matrix.to/#/${event.sender}>)`, description: `This Matrix message was delivered to Discord by **Out Of Your Element**.\n[View on Matrix →](<https://matrix.to/#/${message.room_id}/${message.event_id}?${via}>)\n\n**User ID**: [${event.sender}](<https://matrix.to/#/${event.sender}>)`,
color: 0x0dbd8b, color: 0x0dbd8b,
fields: [{ fields: [{
name: "In Channels", name: "In Channels",

View file

@ -557,18 +557,10 @@ async function eventToMessage(event, guild, channel, di) {
const member = await getMemberFromCacheOrHomeserver(event.room_id, event.sender, di?.api) const member = await getMemberFromCacheOrHomeserver(event.room_id, event.sender, di?.api)
if (member.displayname) displayName = member.displayname if (member.displayname) displayName = member.displayname
if (member.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(member.avatar_url) if (member.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(member.avatar_url)
// MSC4144: Override display name and avatar from per-message profile if present // Override display name and avatar from MSC4144 per-message profile if present
const perMessageProfile = event.content["com.beeper.per_message_profile"] const perMessageProfile = event.content["m.per_message_profile"] || event.content["com.beeper.per_message_profile"]
if (perMessageProfile?.displayname) displayName = perMessageProfile.displayname if (perMessageProfile?.displayname) displayName = perMessageProfile.displayname
if (perMessageProfile && "avatar_url" in perMessageProfile) { if (perMessageProfile?.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(perMessageProfile.avatar_url)
if (perMessageProfile.avatar_url === "") {
// empty string avatar_url clears the avatar (use default)
avatarURL = undefined
} else if (perMessageProfile.avatar_url) {
// omitted/null falls back to member avatar
avatarURL = mxUtils.getPublicUrlForMxc(perMessageProfile.avatar_url)
}
}
// If the display name is too long to be put into the webhook (80 characters is the maximum), // If the display name is too long to be put into the webhook (80 characters is the maximum),
// put the excess characters into displayNameRunoff, later to be put at the top of the message // put the excess characters into displayNameRunoff, later to be put at the top of the message
let [displayNameShortened, displayNameRunoff] = splitDisplayName(displayName) let [displayNameShortened, displayNameRunoff] = splitDisplayName(displayName)
@ -813,7 +805,7 @@ async function eventToMessage(event, guild, channel, di) {
let input = event.content.formatted_body let input = event.content.formatted_body
if (perMessageProfile?.has_fallback) { if (perMessageProfile?.has_fallback) {
// Strip fallback elements added for clients that don't support per-message profiles // Strip fallback elements added for clients that don't support per-message profiles
input = input.replace(/<(\w+)[^>]*\bdata-mx-profile-fallback\b[^>]*>[^<]*<\/\1>/g, "") input = input.replace(/<(\w+)[^>]*\bdata-mx-profile-fallback\b[^>]*>[\s\S]*?<\/\1>/g, "")
} }
if (event.content.msgtype === "m.emote") { if (event.content.msgtype === "m.emote") {
input = `* ${displayName} ${input}` input = `* ${displayName} ${input}`

View file

@ -5526,7 +5526,40 @@ test("event2message: known and unknown emojis in the end are used for sprite she
) )
}) })
test("event2message: com.beeper.per_message_profile overrides displayname and avatar_url", async t => { test("event2message: m.per_message_profile overrides displayname and avatar_url", async t => {
t.deepEqual(
await eventToMessage({
type: "m.room.message",
sender: "@cadence:cadence.moe",
content: {
msgtype: "m.text",
body: "hello from a custom profile",
"m.per_message_profile": {
id: "custom-id",
displayname: "Custom Name",
avatar_url: "mxc://maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo"
}
},
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
}),
{
ensureJoined: [],
messagesToDelete: [],
messagesToEdit: [],
messagesToSend: [{
username: "Custom Name",
content: "hello from a custom profile",
avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo",
allowed_mentions: {
parse: ["users", "roles"]
}
}]
}
)
})
test("event2message: com.beeper.per_message_profile (unstable prefix) overrides displayname and avatar_url", async t => {
t.deepEqual( t.deepEqual(
await eventToMessage({ await eventToMessage({
type: "m.room.message", type: "m.room.message",
@ -5559,18 +5592,21 @@ test("event2message: com.beeper.per_message_profile overrides displayname and av
) )
}) })
test("event2message: com.beeper.per_message_profile empty avatar_url clears avatar", async t => { test("event2message: m.per_message_profile takes priority over com.beeper.per_message_profile", async t => {
t.deepEqual( t.deepEqual(
await eventToMessage({ await eventToMessage({
type: "m.room.message", type: "m.room.message",
sender: "@cadence:cadence.moe", sender: "@cadence:cadence.moe",
content: { content: {
msgtype: "m.text", msgtype: "m.text",
body: "hello with cleared avatar", body: "stable wins",
"m.per_message_profile": {
id: "stable-id",
displayname: "Stable Name"
},
"com.beeper.per_message_profile": { "com.beeper.per_message_profile": {
id: "no-avatar", id: "unstable-id",
displayname: "No Avatar User", displayname: "Unstable Name"
avatar_url: ""
} }
}, },
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
@ -5581,8 +5617,8 @@ test("event2message: com.beeper.per_message_profile empty avatar_url clears avat
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
username: "No Avatar User", username: "Stable Name",
content: "hello with cleared avatar", content: "stable wins",
avatar_url: undefined, avatar_url: undefined,
allowed_mentions: { allowed_mentions: {
parse: ["users", "roles"] parse: ["users", "roles"]