Compare commits

..

2 commits

Author SHA1 Message Date
Bea
a96d0dd1f2
chore: add global prettier ignore file
As prettier is not configured for this project, and the style doesn't
meet prettier's opinionated defaults, it's worth dropping this blanket
ignore until such a time (if ever) a prettier config is produced that
matches the styleguide of this project.
2026-03-24 19:11:40 +00:00
Bea
088aa32b0e
chore: editorconfig matching existing code format styles
An editor config has been written that matches existing indentation and
formatting used by this project, which should extend the courtesy of
intended tab use to other editors like NeoVim which are often conigured
to adhere to `.editorconfig` by default.

As part of this change, the official EditorConfig extension has been
added to VSCode recommendations, which should additionally enable
Code-like editors to additionally adhere to the rules of other files
like json and markdown which are the only types that deviate from
Cadence's global default.
2026-03-24 19:11:39 +00:00
21 changed files with 61 additions and 992 deletions

16
.editorconfig Normal file
View file

@ -0,0 +1,16 @@
root = true
[*]
indent_style = tab
tab_width = 3
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.json]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false

1
.prettierignore Normal file
View file

@ -0,0 +1 @@
*

5
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"recommendations": [
"editorconfig.editorconfig"
]
}

48
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "out-of-your-element",
"version": "3.5.1",
"version": "3.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "out-of-your-element",
"version": "3.5.1",
"version": "3.4.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@chriscdn/promise-semaphore": "^3.0.1",
@ -30,7 +30,7 @@
"enquirer": "^2.4.1",
"entities": "^5.0.0",
"get-relative-path": "^1.0.2",
"h3": "^1.15.10",
"h3": "^1.15.1",
"heatsync": "^2.7.2",
"htmx.org": "^2.0.4",
"lru-cache": "^11.0.2",
@ -276,9 +276,9 @@
}
},
"node_modules/@emnapi/runtime": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
"integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
"license": "MIT",
"optional": true,
"dependencies": {
@ -1163,9 +1163,9 @@
}
},
"node_modules/brace-expansion": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1488,9 +1488,9 @@
"license": "MIT"
},
"node_modules/domino": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/domino/-/domino-2.1.7.tgz",
"integrity": "sha512-3rcXhx0ixJV2nj8J0tljzejTF73A35LVVdnTQu79UAqTBFEgYPMgGtykMuu/BDqaOZphATku1ddRUn/RtqUHYQ==",
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz",
"integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==",
"license": "BSD-2-Clause"
},
"node_modules/emoji-regex": {
@ -1587,9 +1587,9 @@
}
},
"node_modules/flatted": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz",
"integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==",
"dev": true,
"license": "ISC"
},
@ -1617,9 +1617,9 @@
"license": "MIT"
},
"node_modules/fullstore": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/fullstore/-/fullstore-4.0.2.tgz",
"integrity": "sha512-syOev4kA0lZy4VkfBJZ99ZL4cIiSgiKt0G8SpP0kla1tpM1c+V/jBOVY/OqqGtR2XLVcM83SjFPFC3R2YIwqjQ==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/fullstore/-/fullstore-4.0.0.tgz",
"integrity": "sha512-Y9hN79Q1CFU8akjGnTZoBnTzlA/o8wmtBijJOI8dKCmdC7GLX7OekpLxmbaeRetTOi4OdFGjfsg4c5dxP3jgPw==",
"dev": true,
"license": "MIT",
"engines": {
@ -1688,9 +1688,9 @@
}
},
"node_modules/h3": {
"version": "1.15.10",
"resolved": "https://registry.npmjs.org/h3/-/h3-1.15.10.tgz",
"integrity": "sha512-YzJeWSkDZxAhvmp8dexjRK5hxziRO7I9m0N53WhvYL5NiWfkUkzssVzY9jvGu0HBoLFW6+duYmNSn6MaZBCCtg==",
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/h3/-/h3-1.15.6.tgz",
"integrity": "sha512-oi15ESLW5LRthZ+qPCi5GNasY/gvynSKUQxgiovrY63bPAtG59wtM+LSrlcwvOHAXzGrXVLnI97brbkdPF9WoQ==",
"license": "MIT",
"dependencies": {
"cookie-es": "^1.2.2",
@ -1937,9 +1937,9 @@
"license": "MIT"
},
"node_modules/json-with-bigint": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.8.tgz",
"integrity": "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==",
"version": "3.5.7",
"resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.7.tgz",
"integrity": "sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw==",
"dev": true,
"license": "MIT"
},

View file

@ -1,6 +1,6 @@
{
"name": "out-of-your-element",
"version": "3.5.1",
"version": "3.4.0",
"description": "A bridge between Matrix and Discord",
"main": "index.js",
"repository": {
@ -39,7 +39,7 @@
"enquirer": "^2.4.1",
"entities": "^5.0.0",
"get-relative-path": "^1.0.2",
"h3": "^1.15.10",
"h3": "^1.15.1",
"heatsync": "^2.7.2",
"htmx.org": "^2.0.4",
"lru-cache": "^11.0.2",

View file

@ -13,5 +13,5 @@ const {prompt} = require("enquirer")
reg.ooye.web_password = passwordResponse.web_password
writeRegistration(reg)
console.log("Saved. This change should be applied instantly.")
console.log("Saved. Restart Out Of Your Element to apply this change.")
})()

View file

@ -193,16 +193,6 @@ async function channelToKState(channel, guild, di) {
// Don't overwrite room topic if the topic has been customised
if (hasCustomTopic) delete channelKState["m.room.topic/"]
// Make voice channels be a Matrix voice room (MSC3417)
if (channel.type === DiscordTypes.ChannelType.GuildVoice) {
creationContent.type = "org.matrix.msc3417.call"
channelKState["org.matrix.msc3401.call/"] = {
"m.intent": "m.room",
"m.type": "m.voice",
"m.name": customName || channel.name
}
}
// Don't add a space parent if it's self service
// (The person setting up self-service has already put it in their preferred space to be able to get this far.)
const autocreate = select("guild_active", "autocreate", {guild_id: guild.id}).pluck().get()

View file

@ -190,17 +190,6 @@ test("channel2room: read-only discord channel", async t => {
t.equal(api.getCalled(), 2)
})
test("channel2room: voice channel", async t => {
const api = mockAPI(t)
const state = kstateStripConditionals(await channelToKState(testData.channel.voice, testData.guild.general, {api}).then(x => x.channelKState))
t.equal(state["m.room.create/"].type, "org.matrix.msc3417.call")
t.deepEqual(state["org.matrix.msc3401.call/"], {
"m.intent": "m.room",
"m.name": "🍞丨[8user] Piece",
"m.type": "m.voice"
})
})
test("convertNameAndTopic: custom name and topic", t => {
t.deepEqual(
_convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:", type: 0}, {id: "456"}, "hauntings"),

View file

@ -34,10 +34,7 @@ async function emojisToState(emojis, guild) {
if (e.data?.errcode === "M_TOO_LARGE") { // Very unlikely to happen. Only possible for 3x-series emojis uploaded shortly after animated emojis were introduced, when there was no 256 KB size limit.
return
}
e["emoji"] = {
name: emoji.name,
id: emoji.id
}
console.error(`Trying to handle emoji ${emoji.name} (${emoji.id}), but...`)
throw e
})
))

View file

@ -357,17 +357,6 @@ async function messageToEvent(message, guild, options = {}, di) {
}]
}
if (message.type === DiscordTypes.MessageType.ChannelFollowAdd) {
return [{
$type: "m.room.message",
msgtype: "m.emote",
body: `set this room to receive announcements from ${message.content}`,
format: "org.matrix.custom.html",
formatted_body: tag`set this room to receive announcements from <strong>${message.content}</strong>`,
"m.mentions": {}
}]
}
let isInteraction = (message.type === DiscordTypes.MessageType.ChatInputCommand || message.type === DiscordTypes.MessageType.ContextMenuCommand) && message.interaction && "name" in message.interaction
let isThinkingInteraction = isInteraction && !!((message.flags || 0) & DiscordTypes.MessageFlags.Loading)
@ -669,7 +658,7 @@ async function messageToEvent(message, guild, options = {}, di) {
const match = repliedToEventSenderMxid.match(/^@([^:]*)/)
assert(match)
repliedToDisplayName = referenced.author.username || match[1] || "a Matrix user" // grab the localpart as the display name, whatever
repliedToUserHtml = tag`<a href="https://matrix.to/#/${repliedToEventSenderMxid}">${repliedToDisplayName}</a>`
repliedToUserHtml = `<a href="https://matrix.to/#/${repliedToEventSenderMxid}">${repliedToDisplayName}</a>`
} else {
repliedToDisplayName = referenced.author.global_name || referenced.author.username || "a Discord user"
repliedToUserHtml = repliedToDisplayName
@ -694,12 +683,6 @@ async function messageToEvent(message, guild, options = {}, di) {
+ html
body = `${repliedToDisplayName}: ${repliedToBody}`.split("\n").map(line => "> " + line).join("\n") // scenario 1 part B for mentions
+ "\n\n" + body
} else if (referenced.type === DiscordTypes.MessageType.UserJoin) {
// Discord user join messages are bridged as joins, not text events. Generate substitute text for reply.
const joinerMxid = select("sim", "mxid", {user_id: referenced.author.id}).pluck().get()
const joinerHtml = joinerMxid ? tag`<a href="https://matrix.to/#/${joinerMxid}">${repliedToDisplayName}</a>` : tag`<strong>${repliedToDisplayName}</strong>`
html = `<blockquote>${joinerHtml} joined the room</blockquote>` + html
body = `> ${repliedToDisplayName} joined the room\n\n` + body
} else { // repliedToUnknownEvent
const dateDisplay = dUtils.howOldUnbridgedMessage(referenced.timestamp, message.timestamp)
html = `<blockquote>In reply to ${dateDisplay} from ${repliedToDisplayName}:`

View file

@ -4,7 +4,6 @@ const {MatrixServerError} = require("../../matrix/mreq")
const data = require("../../../test/data")
const {mockGetEffectivePower} = require("../../matrix/utils.test")
const Ty = require("../../types")
const {db} = require("../../passthrough")
/**
* @param {string} roomID
@ -734,31 +733,6 @@ test("message2event: reply to a Discord message that wasn't bridged", async t =>
}])
})
test("message2event: reply to a Discord member join (who didn't join on Matrix)", async t => {
const events = await messageToEvent(data.message.reply_to_member_join, data.guild.general)
t.deepEqual(events, [{
$type: "m.room.message",
msgtype: "m.text",
body: "> PEASANT!! joined the room\n\nwhen the broke friend who we pay to bring food shows up at the medieval lord party",
format: "org.matrix.custom.html",
formatted_body: "<blockquote><strong>PEASANT!!</strong> joined the room</blockquote>when the broke friend who we pay to bring food shows up at the medieval lord party",
"m.mentions": {}
}])
})
test("message2event: reply to a Discord member join (who did join on Matrix)", async t => {
db.prepare("INSERT INTO sim (user_id, username, sim_name, mxid) VALUES ('1461677775554478161', 'peasant321_76775', 'peasant321_76775', '@_ooye_peasant321_76775:cadence.moe')").run()
const events = await messageToEvent(data.message.reply_to_member_join, data.guild.general)
t.deepEqual(events, [{
$type: "m.room.message",
msgtype: "m.text",
body: "> PEASANT!! joined the room\n\nwhen the broke friend who we pay to bring food shows up at the medieval lord party",
format: "org.matrix.custom.html",
formatted_body: `<blockquote><a href="https://matrix.to/#/@_ooye_peasant321_76775:cadence.moe">PEASANT!!</a> joined the room</blockquote>when the broke friend who we pay to bring food shows up at the medieval lord party`,
"m.mentions": {}
}])
})
test("message2event: simple written @mention for matrix user", async t => {
const events = await messageToEvent(data.message.simple_written_at_mention_for_matrix, data.guild.general, {}, {
api: {
@ -1168,19 +1142,6 @@ test("message2event: type 4 channel name change", async t => {
}])
})
test("message2event: type 12 channel follow add", async t => {
const events = await messageToEvent(data.special_message.channel_follow_add, data.guild.general)
t.deepEqual(events, [{
$type: "m.room.message",
"m.mentions": {},
msgtype: "m.emote",
body: "set this room to receive announcements from PluralKit #downtime",
format: "org.matrix.custom.html",
formatted_body: "set this room to receive announcements from <strong>PluralKit #downtime</strong>",
"m.mentions": {}
}])
})
test("message2event: thread start message reference", async t => {
const events = await messageToEvent(data.special_message.thread_start_context, data.guild.general, {}, {
api: {

View file

@ -22,7 +22,7 @@ function pinsToList(pins, kstate) {
/** @type {string[]} */
const result = []
for (const pin of pins.items) {
const eventID = select("event_message", "event_id", {message_id: pin.message.id}, "ORDER BY part ASC").pluck().get()
const eventID = select("event_message", "event_id", {message_id: pin.message.id, part: 0}).pluck().get()
if (eventID && !alreadyPinned.includes(eventID)) result.push(eventID)
}
result.reverse()

View file

@ -54,7 +54,6 @@ async function _interact({guild_id, data}, {api}) {
// from Matrix
const event = await api.getEvent(message.room_id, message.event_id)
const via = await utils.getViaServersQuery(message.room_id, api)
const channelsInGuild = discord.guildChannelMap.get(guild_id)
assert(channelsInGuild)
const inChannels = channelsInGuild
@ -62,35 +61,8 @@ async function _interact({guild_id, data}, {api}) {
.map(/** @returns {DiscordTypes.APIGuildChannel} */ cid => discord.channels.get(cid))
.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())
let inChannelsText = inChannels.map(c => `<#${c.id}>`).join(" • ")
if (inChannelsText.length > 1024) {
inChannelsText = `In ${inChannels.length} channels`
}
const matrixMember = select("member_cache", ["displayname", "avatar_url"], {room_id: message.room_id, mxid: event.sender}).get()
let name = matrixMember?.displayname || event.sender
let avatar = utils.getPublicUrlForMxc(matrixMember?.avatar_url)
// Check for per-message profile
const perMessageProfile = event.content?.["com.beeper.per_message_profile"]
let profileNote = ""
if (perMessageProfile) {
if (perMessageProfile.displayname) {
name = perMessageProfile.displayname
}
if ("avatar_url" in perMessageProfile) {
if (perMessageProfile.avatar_url) {
// use provided avatar_url
avatar = utils.getPublicUrlForMxc(perMessageProfile.avatar_url)
} else if (perMessageProfile.avatar_url === "") {
// empty string avatar_url clears the avatar
avatar = undefined
}
// else, omitted/null falls back to member avatar
}
profileNote = "Sent with a per-message profile.\n"
}
const name = matrixMember?.displayname || event.sender
return {
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
data: {
@ -98,13 +70,13 @@ async function _interact({guild_id, data}, {api}) {
author: {
name,
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**.\n[View on Matrix →](<https://matrix.to/#/${message.room_id}/${message.event_id}?${via}>)\n\n${profileNote}**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,
fields: [{
name: "In Channels",
value: inChannelsText
value: inChannels.map(c => `<#${c.id}>`).join(" • ")
}, {
name: "\u200b",
value: idInfo

View file

@ -85,118 +85,3 @@ test("matrix info: shows info for matrix source message", async t => {
)
t.equal(called, 1)
})
test("matrix info: shows username for per-message profile", async t => {
let called = 0
const msg = await _interact({
data: {
target_id: "1128118177155526666",
resolved: {
messages: {
"1141501302736695316": data.message.simple_reply_to_matrix_user
}
}
},
guild_id: "112760669178241024"
}, {
api: {
async getEvent(roomID, eventID) {
called++
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
t.equal(eventID, "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4")
return {
event_id: eventID,
room_id: roomID,
type: "m.room.message",
content: {
msgtype: "m.text",
body: "master chief: i like the halo",
format: "org.matrix.custom.html",
formatted_body: "<strong>master chief: </strong>i like the halo",
"com.beeper.per_message_profile": {
has_fallback: true,
displayname: "master chief",
avatar_url: ""
}
},
sender: "@cadence:cadence.moe"
}
},
async getJoinedMembers(roomID) {
return {
joined: {}
}
},
async getStateEventOuter(roomID, type, key) {
return {
content: {
room_version: "11"
}
}
},
async getStateEvent(roomID, type, key) {
return {}
}
}
})
t.equal(msg.data.embeds[0].author.name, "master chief")
t.match(msg.data.embeds[0].description, "Sent with a per-message profile")
t.equal(called, 1)
})
test("matrix info: shows avatar for per-message profile", async t => {
let called = 0
const msg = await _interact({
data: {
target_id: "1128118177155526666",
resolved: {
messages: {
"1141501302736695316": data.message.simple_reply_to_matrix_user
}
}
},
guild_id: "112760669178241024"
}, {
api: {
async getEvent(roomID, eventID) {
called++
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
t.equal(eventID, "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4")
return {
event_id: eventID,
room_id: roomID,
type: "m.room.message",
content: {
msgtype: "m.text",
body: "?",
format: "org.matrix.custom.html",
formatted_body: "?",
"com.beeper.per_message_profile": {
avatar_url: "mxc://cadence.moe/HXfFuougamkURPPMflTJRxGc"
}
},
sender: "@mystery:cadence.moe"
}
},
async getJoinedMembers(roomID) {
return {
joined: {}
}
},
async getStateEventOuter(roomID, type, key) {
return {
content: {
room_version: "11"
}
}
},
async getStateEvent(roomID, type, key) {
return {}
}
}
})
t.equal(msg.data.embeds[0].author.name, "@mystery:cadence.moe")
t.equal(msg.data.embeds[0].author.icon_url, "https://bridge.example.org/download/matrix/cadence.moe/HXfFuougamkURPPMflTJRxGc")
t.match(msg.data.embeds[0].description, "Sent with a per-message profile")
t.equal(called, 1)
})

View file

@ -182,394 +182,6 @@ function filterTo(xs, fn) {
return filtered
}
const supportedPlaintextPreviewExtensions = new Set([
"4d",
"abnf",
"accesslog",
"actionscript",
"ada",
"adoc",
"alan",
"angelscript",
"ansi",
"apache",
"apacheconf",
"applescript",
"arcade",
"arduino",
"arm",
"armasm",
"as",
"asc",
"asciidoc",
"aspectj",
"ass",
"atom",
"autohotkey",
"autoit",
"avrasm",
"awk",
"axapta",
"bash",
"basic",
"bat",
"bbcode",
"bf",
"bind",
"blade",
"bnf",
"brainfuck",
"c",
"c++",
"cal",
"capnp",
"capnproto",
"cc",
"chaos",
"chapel",
"chpl",
"cisco",
"clj",
"clojure",
"cls",
"cmake.in",
"cmake",
"cmd",
"coffee",
"coffeescript",
"console",
"coq",
"cos",
"cpc",
"cpp",
"cr",
"craftcms",
"crm",
"crmsh",
"crystal",
"cs",
"csharp",
"cshtml",
"cson",
"csp",
"css",
"csv",
"cxx",
"cypher",
"d",
"dart",
"delphi",
"dfm",
"diff",
"django",
"dns",
"docker",
"dockerfile",
"dos",
"dpr",
"dsconfig",
"dst",
"dts",
"dust",
"dylan",
"ebnf",
"elixir",
"elm",
"erl",
"erlang",
"ex",
"extempore",
"f90",
"f95",
"fix",
"fortran",
"freepascal",
"fs",
"fsharp",
"gams",
"gauss",
"gawk",
"gcode",
"gdscript",
"gemspec",
"gf",
"gherkin",
"glsl",
"gms",
"gn",
"gni",
"go",
"godot",
"golang",
"golo",
"gololang",
"gradle",
"graph",
"groovy",
"gss",
"gyp",
"h",
"h++",
"haml",
"handlebars",
"haskell",
"haxe",
"hbs",
"hcl",
"hh",
"hpp",
"hs",
"html.handlebars",
"html.hbs",
"html",
"http",
"https",
"hx",
"hxx",
"hy",
"hylang",
"i",
"i7",
"iced",
"iecst",
"inform7",
"ini",
"ino",
"instances",
"iol",
"irb",
"irpf90",
"java",
"javascript",
"jinja",
"jolie",
"js",
"json",
"jsp",
"jsx",
"julia-repl",
"julia",
"k",
"kaos",
"kdb",
"kotlin",
"kt",
"lasso",
"lassoscript",
"lazarus",
"ldif",
"leaf",
"lean",
"less",
"lfm",
"lisp",
"livecodeserver",
"livescript",
"ln",
"lock",
"log",
"lpr",
"ls",
"ls",
"lua",
"mak",
"make",
"makefile",
"markdown",
"mathematica",
"matlab",
"mawk",
"maxima",
"md",
"mel",
"mercury",
"mirc",
"mizar",
"mk",
"mkd",
"mkdown",
"ml",
"ml",
"mm",
"mma",
"mojolicious",
"monkey",
"moon",
"moonscript",
"mrc",
"n1ql",
"nawk",
"nc",
"never",
"nginx",
"nginxconf",
"nim",
"nimrod",
"nix",
"nsis",
"obj-c",
"obj-c++",
"objc",
"objective-c++",
"objectivec",
"ocaml",
"ocl",
"ol",
"openscad",
"osascript",
"oxygene",
"p21",
"parser3",
"pas",
"pascal",
"patch",
"pcmk",
"perl",
"pf.conf",
"pf",
"pgsql",
"php",
"php3",
"php4",
"php5",
"php6",
"php7",
"pl",
"plaintext",
"plist",
"pm",
"podspec",
"pony",
"postgres",
"postgresql",
"powershell",
"pp",
"processing",
"profile",
"prolog",
"properties",
"proto",
"protobuf",
"ps",
"ps1",
"puppet",
"py",
"pycon",
"python-repl",
"python",
"qml",
"r",
"razor-cshtml",
"razor",
"rb",
"re",
"reasonml",
"rebol",
"red-system",
"red",
"redbol",
"rf",
"rib",
"robot",
"rpm-spec",
"rpm-specfile",
"rpm",
"rs",
"rsl",
"rss",
"ruby",
"ruleslanguage",
"rust",
"sas",
"SAS",
"sc",
"scad",
"scala",
"scheme",
"sci",
"scilab",
"scl",
"scss",
"sh",
"shell",
"shexc",
"smali",
"smalltalk",
"sml",
"sol",
"solidity",
"spec",
"specfile",
"sql",
"srt",
"ssa",
"st",
"stan",
"stanfuncs",
"stata",
"step",
"stp",
"structured-text",
"styl",
"stylus",
"subunit",
"supercollider",
"svelte",
"svg",
"swift",
"tao",
"tap",
"tcl",
"terraform",
"tex",
"text",
"tf",
"thor",
"thrift",
"tk",
"toml",
"tp",
"ts",
"tsql",
"tsx",
"ttml",
"twig",
"txt",
"typescript",
"unicorn-rails-log",
"v",
"vala",
"vb",
"vba",
"vbnet",
"vbs",
"vbscript",
"verilog",
"vhdl",
"vim",
"vtt",
"wl",
"x++",
"x86asm",
"xhtml",
"xjb",
"xl",
"xml",
"xpath",
"xq",
"xquery",
"xsd",
"xsl",
"xtlang",
"xtm",
"yaml",
"yml",
"zep",
"zephir",
"zone",
"zsh"
])
module.exports.getPermissions = getPermissions
module.exports.getDefaultPermissions = getDefaultPermissions
module.exports.hasPermission = hasPermission
@ -582,4 +194,3 @@ module.exports.timestampToSnowflakeInexact = timestampToSnowflakeInexact
module.exports.getPublicUrlForCdn = getPublicUrlForCdn
module.exports.howOldUnbridgedMessage = howOldUnbridgedMessage
module.exports.filterTo = filterTo
module.exports.supportedPlaintextPreviewExtensions = supportedPlaintextPreviewExtensions

View file

@ -13,7 +13,7 @@ async function updatePins(pins, prev) {
const diff = diffPins.diffPins(pins, prev)
for (const [event_id, added] of diff) {
const row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
.select("reference_channel_id", "message_id").where({event_id}).and("ORDER BY part ASC").get()
.select("reference_channel_id", "message_id").get()
if (!row) continue
if (added) {
discord.snow.channel.addChannelPinnedMessage(row.reference_channel_id, row.message_id, "Message pinned on Matrix")

View file

@ -550,30 +550,13 @@ async function eventToMessage(event, guild, channel, di) {
/** @type {string[]} */
let messageIDsToEdit = []
let replyLine = ""
// Extract a basic display name from the sender
const match = event.sender.match(/^@(.*?):/)
if (match) displayName = match[1]
// Try to extract an accurate display name and avatar URL from the member event
const member = await getMemberFromCacheOrHomeserver(event.room_id, event.sender, di?.api)
if (member.displayname) displayName = member.displayname
if (member.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(member.avatar_url)
// MSC4144: Override display name and avatar from per-message profile if present
const perMessageProfile = event.content["com.beeper.per_message_profile"]
if (perMessageProfile?.displayname) displayName = perMessageProfile.displayname
if (perMessageProfile && "avatar_url" in perMessageProfile) {
if (perMessageProfile.avatar_url) {
// use provided avatar_url
avatarURL = mxUtils.getPublicUrlForMxc(perMessageProfile.avatar_url)
} else if (perMessageProfile.avatar_url === "") {
// empty string avatar_url clears the avatar
avatarURL = undefined
}
// else, omitted/null falls back to member avatar
}
// 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
let [displayNameShortened, displayNameRunoff] = splitDisplayName(displayName)
@ -876,9 +859,8 @@ async function eventToMessage(event, guild, channel, di) {
const doc = domino.createDocument(
// DOM parsers arrange elements in the <head> and <body>. Wrapping in a custom element ensures elements are reliably arranged in a single element.
'<x-turndown id="turndown-root">' + input + '</x-turndown>'
)
const root = doc.getElementById("turndown-root")
assert(root)
);
const root = doc.getElementById("turndown-root");
async function forEachNode(event, node) {
for (; node; node = node.nextSibling) {
// Check written mentions
@ -894,8 +876,7 @@ async function eventToMessage(event, guild, channel, di) {
let preNode
if (node.nodeType === 3 && node.nodeValue.includes("```") && (preNode = nodeIsChildOf(node, ["PRE"]))) {
if (preNode.firstChild?.nodeName === "CODE") {
let ext = preNode.firstChild.className.match(/language-(\S+)/)?.[1]
if (!dUtils.supportedPlaintextPreviewExtensions.has(ext)) ext = "txt"
const ext = preNode.firstChild.className.match(/language-(\S+)/)?.[1] || "txt"
const filename = `inline_code.${ext}`
// Build the replacement <code> node
const replacementCode = doc.createElement("code")
@ -932,7 +913,6 @@ async function eventToMessage(event, guild, channel, di) {
}
}
await forEachNode(event, root)
if (perMessageProfile?.has_fallback) root.querySelectorAll("[data-mx-profile-fallback]").forEach(x => x.remove())
// SPRITE SHEET EMOJIS FEATURE: Emojis at the end of the message that we don't know about will be reuploaded as a sprite sheet.
// First we need to determine which emojis are at the end.
@ -964,10 +944,6 @@ async function eventToMessage(event, guild, channel, di) {
} else {
// Looks like we're using the plaintext body!
content = event.content.body
if (perMessageProfile?.has_fallback && perMessageProfile.displayname && content.startsWith(perMessageProfile.displayname + ": ")) {
// Strip the display name prefix fallback added for clients that don't support per-message profiles
content = content.slice(perMessageProfile.displayname.length + 2)
}
if (event.content.msgtype === "m.emote") {
content = `* ${displayName} ${content}`

View file

@ -1155,38 +1155,6 @@ test("event2message: code blocks are uploaded as attachments instead if they con
)
})
test("event2message: code blocks are uploaded as attachments instead if they contain incompatible backticks (use txt extension if discord does not recognise the language)", async t => {
t.deepEqual(
await eventToMessage({
type: "m.room.message",
sender: "@cadence:cadence.moe",
content: {
msgtype: "m.text",
body: "wrong body",
format: "org.matrix.custom.html",
formatted_body: 'So if you run code like this<pre><code class="language-if">System.out.println("```");</code></pre>it should print a markdown formatted code block'
},
event_id: "$pGkWQuGVmrPNByrFELxhzI6MCBgJecr5I2J3z88Gc2s",
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
}),
{
ensureJoined: [],
messagesToDelete: [],
messagesToEdit: [],
messagesToSend: [{
username: "cadence [they]",
content: "So if you run code like this `[inline_code.txt]` it should print a markdown formatted code block",
attachments: [{id: "0", filename: "inline_code.txt"}],
pendingFiles: [{name: "inline_code.txt", buffer: Buffer.from('System.out.println("```");')}],
avatar_url: undefined,
allowed_mentions: {
parse: ["users", "roles"]
}
}]
}
)
})
test("event2message: code blocks are uploaded as attachments instead if they contain incompatible backticks (default to txt file extension)", async t => {
t.deepEqual(
await eventToMessage({
@ -5558,141 +5526,6 @@ 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 => {
t.deepEqual(
await eventToMessage({
type: "m.room.message",
sender: "@cadence:cadence.moe",
content: {
msgtype: "m.text",
body: "hello from unstable profile",
"com.beeper.per_message_profile": {
id: "custom-id",
displayname: "Unstable Name",
avatar_url: "mxc://maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo"
}
},
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
}),
{
ensureJoined: [],
messagesToDelete: [],
messagesToEdit: [],
messagesToSend: [{
username: "Unstable Name",
content: "hello from unstable profile",
avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo",
allowed_mentions: {
parse: ["users", "roles"]
}
}]
}
)
})
test("event2message: com.beeper.per_message_profile empty avatar_url clears avatar", async t => {
t.deepEqual(
await eventToMessage({
type: "m.room.message",
sender: "@cadence:cadence.moe",
content: {
msgtype: "m.text",
body: "hello with cleared avatar",
"com.beeper.per_message_profile": {
id: "no-avatar",
displayname: "No Avatar User",
avatar_url: ""
}
},
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
}),
{
ensureJoined: [],
messagesToDelete: [],
messagesToEdit: [],
messagesToSend: [{
username: "No Avatar User",
content: "hello with cleared avatar",
avatar_url: undefined,
allowed_mentions: {
parse: ["users", "roles"]
}
}]
}
)
})
test("event2message: data-mx-profile-fallback element is stripped from formatted_body when per-message profile is present", async t => {
t.deepEqual(
await eventToMessage({
type: "m.room.message",
sender: "@cadence:cadence.moe",
content: {
msgtype: "m.text",
body: "Tidus Herboren: one more test",
format: "org.matrix.custom.html",
formatted_body: "<strong data-mx-profile-fallback>Tidus Herboren: </strong>one more test",
"com.beeper.per_message_profile": {
id: "tidus",
displayname: "Tidus Herboren",
avatar_url: "mxc://maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo",
has_fallback: true
}
},
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
}),
{
ensureJoined: [],
messagesToDelete: [],
messagesToEdit: [],
messagesToSend: [{
username: "Tidus Herboren",
content: "one more test",
avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo",
allowed_mentions: {
parse: ["users", "roles"]
}
}]
}
)
})
test("event2message: displayname prefix is stripped from plain body when per-message profile has_fallback", async t => {
t.deepEqual(
await eventToMessage({
type: "m.room.message",
sender: "@cadence:cadence.moe",
content: {
msgtype: "m.text",
body: "Tidus Herboren: one more test",
"com.beeper.per_message_profile": {
id: "tidus",
displayname: "Tidus Herboren",
has_fallback: true
}
},
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
}),
{
ensureJoined: [],
messagesToDelete: [],
messagesToEdit: [],
messagesToSend: [{
username: "Tidus Herboren",
content: "one more test",
avatar_url: undefined,
allowed_mentions: {
parse: ["users", "roles"]
}
}]
}
)
})
test("event2message: all unknown chess emojis are used for sprite sheet", async t => {
t.deepEqual(
await eventToMessage({

View file

@ -78,15 +78,6 @@ function readRegistration() {
/** @type {import("../types").AppServiceRegistrationConfig} */ // @ts-ignore
let reg = readRegistration()
if (reg) {
fs.watch(registrationFilePath, {persistent: false}, () => {
let newReg = readRegistration()
if (newReg) {
Object.assign(reg, newReg)
}
})
}
module.exports.registrationFilePath = registrationFilePath
module.exports.readRegistration = readRegistration
module.exports.getTemplateRegistration = getTemplateRegistration

View file

@ -23,26 +23,10 @@ const setPresence = sync.require("./d2m/actions/set-presence")
const channelWebhook = sync.require("./m2d/actions/channel-webhook")
const guildID = "112760669178241024"
async function ping() {
const result = await api.ping().catch(e => ({ok: false, status: "net", root: e.message}))
if (result.ok) {
return "Ping OK. The homeserver and OOYE are talking to each other fine."
} else {
if (typeof result.root === "string") {
var msg = `Cannot reach homeserver: ${result.root}`
} else if (result.root.error) {
var msg = `Homeserver said: [${result.status}] ${result.root.error}`
} else {
var msg = `Homeserver said: [${result.status}] ${JSON.stringify(result.root)}`
}
return msg + "\nMatrix->Discord won't work until you fix this.\nIf your installation has recently changed, consider `npm run setup` again."
}
}
if (process.stdin.isTTY) {
setImmediate(() => {
if (!passthrough.repl) {
const cli = repl.start({prompt: "", eval: customEval, writer: s => s})
const cli = repl.start({ prompt: "", eval: customEval, writer: s => s })
Object.assign(cli.context, passthrough)
passthrough.repl = cli
}

View file

@ -19,26 +19,6 @@ module.exports = {
default_thread_rate_limit_per_user: 0,
guild_id: "112760669178241024"
},
voice: {
voice_background_display: null,
version: 1774469910848,
user_limit: 0,
type: 2,
theme_color: null,
status: null,
rtc_region: null,
rate_limit_per_user: 0,
position: 0,
permission_overwrites: [],
parent_id: "805261291908104252",
nsfw: false,
name: "🍞丨[8user] Piece",
last_message_id: "1459912691098325137",
id: "1036840786093953084",
flags: 0,
bitrate: 256000,
guild_id: "112760669178241024"
},
updates: {
type: 0,
topic: "Updates and release announcements for Out Of Your Element.",
@ -2035,80 +2015,6 @@ module.exports = {
tts: false
}
},
reply_to_member_join: {
type: 19,
content: "when the broke friend who we pay to bring food shows up at the medieval lord party",
mentions: [],
mention_roles: [],
attachments: [],
embeds: [],
timestamp: "2026-03-30T12:11:04.443000+00:00",
edited_timestamp: null,
flags: 0,
components: [],
id: "1488148556962332692",
channel_id: "475599038536744962",
author: {
id: "576945009408999426",
username: "randomllama121",
avatar: "08510a70f957106dad1580323c40cd7a",
discriminator: "0",
public_flags: 128,
flags: 128,
banner: null,
accent_color: null,
global_name: "random :3",
avatar_decoration_data: null,
collectibles: null,
display_name_styles: null,
banner_color: null,
clan: null,
primary_guild: null
},
pinned: false,
mention_everyone: false,
tts: false,
message_reference: {
type: 0,
channel_id: "475599038536744962",
message_id: "1488146734352826478",
guild_id: "475599038536744960"
},
referenced_message: {
type: 7,
content: "",
mentions: [],
mention_roles: [],
attachments: [],
embeds: [],
timestamp: "2026-03-30T12:03:49.899000+00:00",
edited_timestamp: null,
flags: 0,
components: [],
id: "1488146734352826478",
channel_id: "475599038536744962",
author: {
id: "1461677775554478161",
username: "peasant321_76775",
avatar: null,
discriminator: "0",
public_flags: 0,
flags: 0,
banner: null,
accent_color: null,
global_name: "PEASANT!!",
avatar_decoration_data: null,
collectibles: null,
display_name_styles: null,
banner_color: null,
clan: null,
primary_guild: null
},
pinned: false,
mention_everyone: false,
tts: false
}
},
attachment_no_content: {
id: "1124628646670389348",
type: 0,
@ -6264,37 +6170,6 @@ module.exports = {
components: [],
position: 12
},
channel_follow_add: {
type: 12,
content: "PluralKit #downtime",
attachments: [],
embeds: [],
timestamp: "2026-03-24T23:16:04.097Z",
edited_timestamp: null,
flags: 0,
components: [],
id: "1486141581047369888",
channel_id: "1451125453082591314",
author: {
id: "154058479798059009",
username: "exaptations",
discriminator: "0",
avatar: "57b5cfe09a48a5902f2eb8fa65bb1b80",
bot: false,
flags: 0,
globalName: "Exa",
},
pinned: false,
mentions: [],
mention_roles: [],
mention_everyone: false,
tts: false,
message_reference: {
type: 0,
channel_id: "1015204661701124206",
guild_id: "466707357099884544"
}
},
updated_to_start_thread_from_here: {
t: "MESSAGE_UPDATE",
s: 19,