Compare commits
8 commits
232a9b7cae
...
ccdd2f0c9c
Author | SHA1 | Date | |
---|---|---|---|
ccdd2f0c9c | |||
a6f27a1144 | |||
8a20f98925 | |||
3baf007829 | |||
92d8f57875 | |||
a38e54dcd4 | |||
425a4d3110 | |||
c24eb3075c |
24 changed files with 560 additions and 94 deletions
|
@ -22,7 +22,7 @@ async function addReaction(data) {
|
||||||
assert.equal(typeof parentID, "string")
|
assert.equal(typeof parentID, "string")
|
||||||
const roomID = await createRoom.ensureRoom(data.channel_id)
|
const roomID = await createRoom.ensureRoom(data.channel_id)
|
||||||
const senderMxid = await registerUser.ensureSimJoined(user, roomID)
|
const senderMxid = await registerUser.ensureSimJoined(user, roomID)
|
||||||
const eventID = api.sendEvent(roomID, "m.reaction", {
|
const eventID = await api.sendEvent(roomID, "m.reaction", {
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
rel_type: "m.annotation",
|
rel_type: "m.annotation",
|
||||||
event_id: parentID,
|
event_id: parentID,
|
||||||
|
|
|
@ -21,8 +21,8 @@ async function roomToKState(roomID) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @params {string} roomID
|
* @param {string} roomID
|
||||||
* @params {any} kstate
|
* @param {any} kstate
|
||||||
*/
|
*/
|
||||||
function applyKStateDiffToRoom(roomID, kstate) {
|
function applyKStateDiffToRoom(roomID, kstate) {
|
||||||
const events = ks.kstateToState(kstate)
|
const events = ks.kstateToState(kstate)
|
||||||
|
@ -51,7 +51,7 @@ function convertNameAndTopic(channel, guild, customName) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {DiscordTypes.APIGuildTextChannel} channel
|
* @param {DiscordTypes.APIGuildTextChannel | DiscordTypes.APIThreadChannel} channel
|
||||||
* @param {DiscordTypes.APIGuild} guild
|
* @param {DiscordTypes.APIGuild} guild
|
||||||
*/
|
*/
|
||||||
async function channelToKState(channel, guild) {
|
async function channelToKState(channel, guild) {
|
||||||
|
@ -98,21 +98,27 @@ async function channelToKState(channel, guild) {
|
||||||
* @returns {Promise<string>} room ID
|
* @returns {Promise<string>} room ID
|
||||||
*/
|
*/
|
||||||
async function createRoom(channel, guild, spaceID, kstate) {
|
async function createRoom(channel, guild, spaceID, kstate) {
|
||||||
|
const [convertedName, convertedTopic] = convertNameAndTopic(channel, guild, null)
|
||||||
const roomID = await api.createRoom({
|
const roomID = await api.createRoom({
|
||||||
name: channel.name,
|
name: convertedName,
|
||||||
topic: channel.topic || undefined,
|
topic: convertedTopic,
|
||||||
preset: "private_chat",
|
preset: "private_chat",
|
||||||
visibility: "private",
|
visibility: "private",
|
||||||
invite: ["@cadence:cadence.moe"], // TODO
|
invite: ["@cadence:cadence.moe"], // TODO
|
||||||
initial_state: ks.kstateToState(kstate)
|
initial_state: ks.kstateToState(kstate)
|
||||||
})
|
})
|
||||||
|
|
||||||
db.prepare("INSERT INTO channel_room (channel_id, room_id) VALUES (?, ?)").run(channel.id, roomID)
|
let threadParent = null
|
||||||
|
if (channel.type === DiscordTypes.ChannelType.PublicThread) {
|
||||||
|
/** @type {DiscordTypes.APIThreadChannel} */ // @ts-ignore
|
||||||
|
const thread = channel
|
||||||
|
threadParent = thread.parent_id
|
||||||
|
}
|
||||||
|
|
||||||
|
db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent) VALUES (?, ?, ?, NULL, ?)").run(channel.id, roomID, channel.name, threadParent)
|
||||||
|
|
||||||
// Put the newly created child into the space
|
// Put the newly created child into the space
|
||||||
await api.sendState(spaceID, "m.space.child", roomID, { // TODO: should I deduplicate with the equivalent code from syncRoom?
|
_syncSpaceMember(channel, spaceID, roomID)
|
||||||
via: ["cadence.moe"] // TODO: use the proper server
|
|
||||||
})
|
|
||||||
|
|
||||||
return roomID
|
return roomID
|
||||||
}
|
}
|
||||||
|
@ -156,14 +162,15 @@ async function _syncRoom(channelID, shouldActuallySync) {
|
||||||
assert.ok(channel)
|
assert.ok(channel)
|
||||||
const guild = channelToGuild(channel)
|
const guild = channelToGuild(channel)
|
||||||
|
|
||||||
/** @type {string?} */
|
/** @type {{room_id: string, thread_parent: string?}} */
|
||||||
const existing = db.prepare("SELECT room_id from channel_room WHERE channel_id = ?").pluck().get(channel.id)
|
const existing = db.prepare("SELECT room_id, thread_parent from channel_room WHERE channel_id = ?").get(channelID)
|
||||||
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
const {spaceID, channelKState} = await channelToKState(channel, guild)
|
const {spaceID, channelKState} = await channelToKState(channel, guild)
|
||||||
return createRoom(channel, guild, spaceID, channelKState)
|
return createRoom(channel, guild, spaceID, channelKState)
|
||||||
} else {
|
} else {
|
||||||
if (!shouldActuallySync) {
|
if (!shouldActuallySync) {
|
||||||
return existing // only need to ensure room exists, and it does. return the room ID
|
return existing.room_id // only need to ensure room exists, and it does. return the room ID
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[room sync] to matrix: ${channel.name}`)
|
console.log(`[room sync] to matrix: ${channel.name}`)
|
||||||
|
@ -171,24 +178,68 @@ async function _syncRoom(channelID, shouldActuallySync) {
|
||||||
const {spaceID, channelKState} = await channelToKState(channel, guild)
|
const {spaceID, channelKState} = await channelToKState(channel, guild)
|
||||||
|
|
||||||
// sync channel state to room
|
// sync channel state to room
|
||||||
const roomKState = await roomToKState(existing)
|
const roomKState = await roomToKState(existing.room_id)
|
||||||
const roomDiff = ks.diffKState(roomKState, channelKState)
|
const roomDiff = ks.diffKState(roomKState, channelKState)
|
||||||
const roomApply = applyKStateDiffToRoom(existing, roomDiff)
|
const roomApply = applyKStateDiffToRoom(existing.room_id, roomDiff)
|
||||||
|
|
||||||
// sync room as space member
|
// sync room as space member
|
||||||
const spaceKState = await roomToKState(spaceID)
|
const spaceApply = _syncSpaceMember(channel, spaceID, existing.room_id)
|
||||||
const spaceDiff = ks.diffKState(spaceKState, {
|
|
||||||
[`m.space.child/${existing}`]: {
|
|
||||||
via: ["cadence.moe"] // TODO: use the proper server
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const spaceApply = applyKStateDiffToRoom(spaceID, spaceDiff)
|
|
||||||
await Promise.all([roomApply, spaceApply])
|
await Promise.all([roomApply, spaceApply])
|
||||||
|
|
||||||
return existing
|
return existing.room_id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function _unbridgeRoom(channelID) {
|
||||||
|
/** @ts-ignore @type {DiscordTypes.APIGuildChannel} */
|
||||||
|
const channel = discord.channels.get(channelID)
|
||||||
|
assert.ok(channel)
|
||||||
|
const roomID = db.prepare("SELECT room_id from channel_room WHERE channel_id = ?").pluck().get(channelID)
|
||||||
|
assert.ok(roomID)
|
||||||
|
const spaceID = db.prepare("SELECT space_id FROM guild_space WHERE guild_id = ?").pluck().get(channel.guild_id)
|
||||||
|
assert.ok(spaceID)
|
||||||
|
|
||||||
|
// remove room from being a space member
|
||||||
|
await api.sendState(spaceID, "m.space.child", roomID, {})
|
||||||
|
|
||||||
|
// send a notification in the room
|
||||||
|
await api.sendEvent(roomID, "m.room.message", {
|
||||||
|
msgtype: "m.notice",
|
||||||
|
body: "⚠️ This room was removed from the bridge."
|
||||||
|
})
|
||||||
|
|
||||||
|
// leave room
|
||||||
|
await api.leaveRoom(roomID)
|
||||||
|
|
||||||
|
// delete room from database
|
||||||
|
const {changes} = db.prepare("DELETE FROM channel_room WHERE room_id = ? AND channel_id = ?").run(roomID, channelID)
|
||||||
|
assert.equal(changes, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DiscordTypes.APIGuildTextChannel} channel
|
||||||
|
* @param {string} spaceID
|
||||||
|
* @param {string} roomID
|
||||||
|
* @returns {Promise<string[]>}
|
||||||
|
*/
|
||||||
|
async function _syncSpaceMember(channel, spaceID, roomID) {
|
||||||
|
const spaceKState = await roomToKState(spaceID)
|
||||||
|
let spaceEventContent = {}
|
||||||
|
if (
|
||||||
|
channel.type !== DiscordTypes.ChannelType.PrivateThread // private threads do not belong in the space (don't offer people something they can't join)
|
||||||
|
|| channel["thread_metadata"]?.archived // archived threads do not belong in the space (don't offer people conversations that are no longer relevant)
|
||||||
|
) {
|
||||||
|
spaceEventContent = {
|
||||||
|
via: ["cadence.moe"] // TODO: use the proper server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const spaceDiff = ks.diffKState(spaceKState, {
|
||||||
|
[`m.space.child/${roomID}`]: spaceEventContent
|
||||||
|
})
|
||||||
|
return applyKStateDiffToRoom(spaceID, spaceDiff)
|
||||||
|
}
|
||||||
|
|
||||||
function ensureRoom(channelID) {
|
function ensureRoom(channelID) {
|
||||||
return _syncRoom(channelID, false)
|
return _syncRoom(channelID, false)
|
||||||
}
|
}
|
||||||
|
@ -201,8 +252,11 @@ async function createAllForGuild(guildID) {
|
||||||
const channelIDs = discord.guildChannelMap.get(guildID)
|
const channelIDs = discord.guildChannelMap.get(guildID)
|
||||||
assert.ok(channelIDs)
|
assert.ok(channelIDs)
|
||||||
for (const channelID of channelIDs) {
|
for (const channelID of channelIDs) {
|
||||||
if (discord.channels.get(channelID)?.type === DiscordTypes.ChannelType.GuildText) { // TODO: guild sync thread channels and such. maybe make a helper function to check if a given channel is syncable?
|
const allowedTypes = [DiscordTypes.ChannelType.GuildText, DiscordTypes.ChannelType.PublicThread]
|
||||||
await syncRoom(channelID).then(r => console.log(`synced ${channelID}:`, r))
|
// @ts-ignore
|
||||||
|
if (allowedTypes.includes(discord.channels.get(channelID)?.type)) {
|
||||||
|
const roomID = await syncRoom(channelID)
|
||||||
|
console.log(`synced ${channelID} <-> ${roomID}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -213,3 +267,4 @@ module.exports.syncRoom = syncRoom
|
||||||
module.exports.createAllForGuild = createAllForGuild
|
module.exports.createAllForGuild = createAllForGuild
|
||||||
module.exports.channelToKState = channelToKState
|
module.exports.channelToKState = channelToKState
|
||||||
module.exports._convertNameAndTopic = convertNameAndTopic
|
module.exports._convertNameAndTopic = convertNameAndTopic
|
||||||
|
module.exports._unbridgeRoom = _unbridgeRoom
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
|
const assert = require("assert")
|
||||||
const passthrough = require("../../passthrough")
|
const passthrough = require("../../passthrough")
|
||||||
const { sync, db } = passthrough
|
const { sync, db } = passthrough
|
||||||
/** @type {import("../../matrix/api")} */
|
/** @type {import("../../matrix/api")} */
|
||||||
|
@ -9,13 +10,14 @@ const api = sync.require("../../matrix/api")
|
||||||
* @param {import("discord-api-types/v10").RESTGetAPIGuildResult} guild
|
* @param {import("discord-api-types/v10").RESTGetAPIGuildResult} guild
|
||||||
*/
|
*/
|
||||||
async function createSpace(guild) {
|
async function createSpace(guild) {
|
||||||
|
assert(guild.name)
|
||||||
const roomID = await api.createRoom({
|
const roomID = await api.createRoom({
|
||||||
name: guild.name,
|
name: guild.name,
|
||||||
preset: "private_chat",
|
preset: "private_chat", // cannot join space unless invited
|
||||||
visibility: "private",
|
visibility: "private",
|
||||||
power_level_content_override: {
|
power_level_content_override: {
|
||||||
events_default: 100,
|
events_default: 100, // space can only be managed by bridge
|
||||||
invite: 50
|
invite: 0 // any existing member can invite others
|
||||||
},
|
},
|
||||||
invite: ["@cadence:cadence.moe"], // TODO
|
invite: ["@cadence:cadence.moe"], // TODO
|
||||||
topic: guild.description || undefined,
|
topic: guild.description || undefined,
|
||||||
|
@ -27,13 +29,13 @@ async function createSpace(guild) {
|
||||||
type: "m.room.guest_access",
|
type: "m.room.guest_access",
|
||||||
state_key: "",
|
state_key: "",
|
||||||
content: {
|
content: {
|
||||||
guest_access: "can_join"
|
guest_access: "can_join" // guests can join space if other conditions are met
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "m.room.history_visibility",
|
type: "m.room.history_visibility",
|
||||||
content: {
|
content: {
|
||||||
history_visibility: "invited"
|
history_visibility: "invited" // any events sent after user was invited are visible
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -12,10 +12,7 @@ const api = sync.require("../../matrix/api")
|
||||||
* @param {import("discord-api-types/v10").APIGuild} guild
|
* @param {import("discord-api-types/v10").APIGuild} guild
|
||||||
*/
|
*/
|
||||||
async function editMessage(message, guild) {
|
async function editMessage(message, guild) {
|
||||||
console.log(`*** applying edit for message ${message.id} in channel ${message.channel_id}`)
|
|
||||||
const {roomID, eventsToRedact, eventsToReplace, eventsToSend, senderMxid} = await editToChanges.editToChanges(message, guild, api)
|
const {roomID, eventsToRedact, eventsToReplace, eventsToSend, senderMxid} = await editToChanges.editToChanges(message, guild, api)
|
||||||
console.log("making these changes:")
|
|
||||||
console.dir({eventsToRedact, eventsToReplace, eventsToSend}, {depth: null})
|
|
||||||
|
|
||||||
// 1. Replace all the things.
|
// 1. Replace all the things.
|
||||||
for (const {oldID, newContent} of eventsToReplace) {
|
for (const {oldID, newContent} of eventsToReplace) {
|
||||||
|
|
|
@ -85,7 +85,8 @@ async function ensureSimJoined(user, roomID) {
|
||||||
*/
|
*/
|
||||||
async function memberToStateContent(user, member, guildID) {
|
async function memberToStateContent(user, member, guildID) {
|
||||||
let displayname = user.username
|
let displayname = user.username
|
||||||
if (member.nick && member.nick !== displayname) displayname = member.nick + " | " + displayname // prepend nick if present
|
// if (member.nick && member.nick !== displayname) displayname = member.nick + " | " + displayname // prepend nick if present
|
||||||
|
if (member.nick) displayname = member.nick
|
||||||
|
|
||||||
const content = {
|
const content = {
|
||||||
displayname,
|
displayname,
|
||||||
|
|
|
@ -8,7 +8,7 @@ test("member2state: general", async t => {
|
||||||
await _memberToStateContent(testData.member.sheep.user, testData.member.sheep, testData.guild.general.id),
|
await _memberToStateContent(testData.member.sheep.user, testData.member.sheep, testData.guild.general.id),
|
||||||
{
|
{
|
||||||
avatar_url: "mxc://cadence.moe/rfemHmAtcprjLEiPiEuzPhpl",
|
avatar_url: "mxc://cadence.moe/rfemHmAtcprjLEiPiEuzPhpl",
|
||||||
displayname: "The Expert's Submarine | aprilsong",
|
displayname: "The Expert's Submarine",
|
||||||
membership: "join",
|
membership: "join",
|
||||||
"moe.cadence.ooye.member": {
|
"moe.cadence.ooye.member": {
|
||||||
avatar: "/guilds/112760669178241024/users/134826546694193153/avatars/38dd359aa12bcd52dd3164126c587f8c.png?size=1024"
|
avatar: "/guilds/112760669178241024/users/134826546694193153/avatars/38dd359aa12bcd52dd3164126c587f8c.png?size=1024"
|
||||||
|
|
|
@ -22,8 +22,11 @@ async function sendMessage(message, guild) {
|
||||||
|
|
||||||
let senderMxid = null
|
let senderMxid = null
|
||||||
if (!message.webhook_id) {
|
if (!message.webhook_id) {
|
||||||
assert(message.member)
|
if (message.member) { // available on a gateway message create event
|
||||||
senderMxid = await registerUser.syncUser(message.author, message.member, message.guild_id, roomID)
|
senderMxid = await registerUser.syncUser(message.author, message.member, message.guild_id, roomID)
|
||||||
|
} else { // well, good enough...
|
||||||
|
senderMxid = await registerUser.ensureSimJoined(message.author, roomID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const events = await messageToEvent.messageToEvent(message, guild, {}, {api})
|
const events = await messageToEvent.messageToEvent(message, guild, {}, {api})
|
||||||
|
@ -35,7 +38,7 @@ async function sendMessage(message, guild) {
|
||||||
const eventWithoutType = {...event}
|
const eventWithoutType = {...event}
|
||||||
delete eventWithoutType.$type
|
delete eventWithoutType.$type
|
||||||
|
|
||||||
const eventID = await api.sendEvent(roomID, eventType, event, senderMxid)
|
const eventID = await api.sendEvent(roomID, eventType, event, senderMxid, new Date(message.timestamp).getTime())
|
||||||
db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, channel_id, part, source) VALUES (?, ?, ?, ?, ?, ?, 1)").run(eventID, eventType, event.msgtype || null, message.id, message.channel_id, eventPart) // source 1 = discord
|
db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, channel_id, part, source) VALUES (?, ?, ?, ?, ?, ?, 1)").run(eventID, eventType, event.msgtype || null, message.id, message.channel_id, eventPart) // source 1 = discord
|
||||||
|
|
||||||
eventPart = 1 // TODO: use more intelligent algorithm to determine whether primary or supporting
|
eventPart = 1 // TODO: use more intelligent algorithm to determine whether primary or supporting
|
||||||
|
|
40
d2m/converters/message-to-event.embeds.test.js
Normal file
40
d2m/converters/message-to-event.embeds.test.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
const {test} = require("supertape")
|
||||||
|
const {messageToEvent} = require("./message-to-event")
|
||||||
|
const data = require("../../test/data")
|
||||||
|
const Ty = require("../../types")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} roomID
|
||||||
|
* @param {string} eventID
|
||||||
|
* @returns {(roomID: string, eventID: string) => Promise<Ty.Event.Outer<Ty.Event.M_Room_Message>>}
|
||||||
|
*/
|
||||||
|
function mockGetEvent(t, roomID_in, eventID_in, outer) {
|
||||||
|
return async function(roomID, eventID) {
|
||||||
|
t.equal(roomID, roomID_in)
|
||||||
|
t.equal(eventID, eventID_in)
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({
|
||||||
|
event_id: eventID_in,
|
||||||
|
room_id: roomID_in,
|
||||||
|
origin_server_ts: 1680000000000,
|
||||||
|
unsigned: {
|
||||||
|
age: 2245,
|
||||||
|
transaction_id: "$local.whatever"
|
||||||
|
},
|
||||||
|
...outer
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("message2event embeds: nothing but a field", async t => {
|
||||||
|
const events = await messageToEvent(data.message_with_embeds.nothing_but_a_field, data.guild.general, {})
|
||||||
|
t.deepEqual(events, [{
|
||||||
|
$type: "m.room.message",
|
||||||
|
"m.mentions": {},
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "Amanda"
|
||||||
|
}])
|
||||||
|
})
|
|
@ -27,11 +27,13 @@ function getDiscordParseCallbacks(message, useHTML) {
|
||||||
},
|
},
|
||||||
/** @param {{id: string, type: "discordChannel"}} node */
|
/** @param {{id: string, type: "discordChannel"}} node */
|
||||||
channel: node => {
|
channel: node => {
|
||||||
const {room_id, name, nick} = db.prepare("SELECT room_id, name, nick FROM channel_room WHERE channel_id = ?").get(node.id)
|
const row = db.prepare("SELECT room_id, name, nick FROM channel_room WHERE channel_id = ?").get(node.id)
|
||||||
if (room_id && useHTML) {
|
if (!row) {
|
||||||
return `<a href="https://matrix.to/#/${room_id}">#${nick || name}</a>`
|
return `<#${node.id}>` // fallback for when this channel is not bridged
|
||||||
|
} else if (useHTML) {
|
||||||
|
return `<a href="https://matrix.to/#/${row.room_id}">#${row.nick || row.name}</a>`
|
||||||
} else {
|
} else {
|
||||||
return `#${nick || name}`
|
return `#${row.nick || row.name}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/** @param {{animated: boolean, name: string, id: string, type: "discordEmoji"}} node */
|
/** @param {{animated: boolean, name: string, id: string, type: "discordEmoji"}} node */
|
||||||
|
@ -108,6 +110,13 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
addMention(repliedToEventSenderMxid)
|
addMention(repliedToEventSenderMxid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let msgtype = "m.text"
|
||||||
|
// Handle message type 4, channel name changed
|
||||||
|
if (message.type === DiscordTypes.MessageType.ChannelNameChange) {
|
||||||
|
msgtype = "m.emote"
|
||||||
|
message.content = "changed the channel name to **" + message.content + "**"
|
||||||
|
}
|
||||||
|
|
||||||
// Text content appears first
|
// Text content appears first
|
||||||
if (message.content) {
|
if (message.content) {
|
||||||
let content = message.content
|
let content = message.content
|
||||||
|
@ -188,7 +197,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
const newTextMessageEvent = {
|
const newTextMessageEvent = {
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
"m.mentions": mentions,
|
"m.mentions": mentions,
|
||||||
msgtype: "m.text",
|
msgtype,
|
||||||
body: body
|
body: body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,6 +248,22 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
size: attachment.size
|
size: attachment.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (attachment.content_type?.startsWith("video/") && attachment.width && attachment.height) {
|
||||||
|
return {
|
||||||
|
$type: "m.room.message",
|
||||||
|
"m.mentions": mentions,
|
||||||
|
msgtype: "m.video",
|
||||||
|
url: await file.uploadDiscordFileToMxc(attachment.url),
|
||||||
|
external_url: attachment.url,
|
||||||
|
body: attachment.description || attachment.filename,
|
||||||
|
filename: attachment.filename,
|
||||||
|
info: {
|
||||||
|
mimetype: attachment.content_type,
|
||||||
|
w: attachment.width,
|
||||||
|
h: attachment.height,
|
||||||
|
size: attachment.size
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
|
|
|
@ -330,4 +330,14 @@ test("message2event: very large attachment is linked instead of being uploaded",
|
||||||
}])
|
}])
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: read "edits of replies" in the spec
|
test("message2event: type 4 channel name change", async t => {
|
||||||
|
const events = await messageToEvent(data.special_message.thread_name_change, data.guild.general)
|
||||||
|
t.deepEqual(events, [{
|
||||||
|
$type: "m.room.message",
|
||||||
|
"m.mentions": {},
|
||||||
|
msgtype: "m.emote",
|
||||||
|
body: "changed the channel name to **worming**",
|
||||||
|
format: "org.matrix.custom.html",
|
||||||
|
formatted_body: "changed the channel name to <strong>worming</strong>"
|
||||||
|
}])
|
||||||
|
})
|
||||||
|
|
|
@ -16,6 +16,10 @@ test("user2name: works on emojis", t => {
|
||||||
t.equal(userToSimName({username: "🍪 Cookie Monster 🍪", discriminator: "0001"}), "cookie_monster")
|
t.equal(userToSimName({username: "🍪 Cookie Monster 🍪", discriminator: "0001"}), "cookie_monster")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("user2name: works on single emoji at the end", t => {
|
||||||
|
t.equal(userToSimName({username: "Amanda 🎵", discriminator: "2192"}), "amanda")
|
||||||
|
})
|
||||||
|
|
||||||
test("user2name: works on crazy name", t => {
|
test("user2name: works on crazy name", t => {
|
||||||
t.equal(userToSimName({username: "*** D3 &W (89) _7//-", discriminator: "0001"}), "d3_w_89__7//")
|
t.equal(userToSimName({username: "*** D3 &W (89) _7//-", discriminator: "0001"}), "d3_w_89__7//")
|
||||||
})
|
})
|
||||||
|
|
|
@ -35,6 +35,21 @@ const utils = {
|
||||||
arr.push(channel.id)
|
arr.push(channel.id)
|
||||||
client.channels.set(channel.id, channel)
|
client.channels.set(channel.id, channel)
|
||||||
}
|
}
|
||||||
|
for (const thread of message.d.threads || []) {
|
||||||
|
// @ts-ignore
|
||||||
|
thread.guild_id = message.d.id
|
||||||
|
arr.push(thread.id)
|
||||||
|
client.channels.set(thread.id, thread)
|
||||||
|
}
|
||||||
|
eventDispatcher.checkMissedMessages(client, message.d)
|
||||||
|
|
||||||
|
|
||||||
|
} else if (message.t === "THREAD_CREATE") {
|
||||||
|
client.channels.set(message.d.id, message.d)
|
||||||
|
|
||||||
|
|
||||||
|
} else if (message.t === "CHANNEL_UPDATE" || message.t === "THREAD_UPDATE") {
|
||||||
|
client.channels.set(message.d.id, message.d)
|
||||||
|
|
||||||
|
|
||||||
} else if (message.t === "GUILD_DELETE") {
|
} else if (message.t === "GUILD_DELETE") {
|
||||||
|
@ -67,10 +82,22 @@ const utils = {
|
||||||
|
|
||||||
// Event dispatcher for OOYE bridge operations
|
// Event dispatcher for OOYE bridge operations
|
||||||
try {
|
try {
|
||||||
if (message.t === "MESSAGE_CREATE") {
|
if (message.t === "CHANNEL_UPDATE") {
|
||||||
|
await eventDispatcher.onChannelOrThreadUpdate(client, message.d, false)
|
||||||
|
|
||||||
|
} else if (message.t === "THREAD_CREATE") {
|
||||||
|
console.log(message)
|
||||||
|
// await eventDispatcher.onThreadCreate(client, message.d)
|
||||||
|
|
||||||
|
} else if (message.t === "THREAD_UPDATE") {
|
||||||
|
await eventDispatcher.onChannelOrThreadUpdate(client, message.d, true)
|
||||||
|
|
||||||
|
} else if (message.t === "MESSAGE_CREATE") {
|
||||||
|
console.log(message)
|
||||||
await eventDispatcher.onMessageCreate(client, message.d)
|
await eventDispatcher.onMessageCreate(client, message.d)
|
||||||
|
|
||||||
} else if (message.t === "MESSAGE_UPDATE") {
|
} else if (message.t === "MESSAGE_UPDATE") {
|
||||||
|
console.log(message)
|
||||||
await eventDispatcher.onMessageUpdate(client, message.d)
|
await eventDispatcher.onMessageUpdate(client, message.d)
|
||||||
|
|
||||||
} else if (message.t === "MESSAGE_DELETE") {
|
} else if (message.t === "MESSAGE_DELETE") {
|
||||||
|
|
|
@ -10,11 +10,17 @@ const editMessage = sync.require("./actions/edit-message")
|
||||||
const deleteMessage = sync.require("./actions/delete-message")
|
const deleteMessage = sync.require("./actions/delete-message")
|
||||||
/** @type {import("./actions/add-reaction")}) */
|
/** @type {import("./actions/add-reaction")}) */
|
||||||
const addReaction = sync.require("./actions/add-reaction")
|
const addReaction = sync.require("./actions/add-reaction")
|
||||||
|
/** @type {import("./actions/create-room")}) */
|
||||||
|
const createRoom = sync.require("./actions/create-room")
|
||||||
/** @type {import("../matrix/api")}) */
|
/** @type {import("../matrix/api")}) */
|
||||||
const api = sync.require("../matrix/api")
|
const api = sync.require("../matrix/api")
|
||||||
|
|
||||||
let lastReportedEvent = 0
|
let lastReportedEvent = 0
|
||||||
|
|
||||||
|
function isGuildAllowed(guildID) {
|
||||||
|
return ["112760669178241024", "497159726455455754", "1100319549670301727"].includes(guildID)
|
||||||
|
}
|
||||||
|
|
||||||
// Grab Discord events we care about for the bridge, check them, and pass them on
|
// Grab Discord events we care about for the bridge, check them, and pass them on
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -29,11 +35,14 @@ module.exports = {
|
||||||
console.error(`while handling this ${gatewayMessage.t} gateway event:`)
|
console.error(`while handling this ${gatewayMessage.t} gateway event:`)
|
||||||
console.dir(gatewayMessage.d, {depth: null})
|
console.dir(gatewayMessage.d, {depth: null})
|
||||||
|
|
||||||
if (Date.now() - lastReportedEvent > 5000) {
|
if (Date.now() - lastReportedEvent < 5000) return
|
||||||
lastReportedEvent = Date.now()
|
lastReportedEvent = Date.now()
|
||||||
|
|
||||||
const channelID = gatewayMessage.d.channel_id
|
const channelID = gatewayMessage.d.channel_id
|
||||||
if (channelID) {
|
if (!channelID) return
|
||||||
const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(channelID)
|
const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(channelID)
|
||||||
|
if (!roomID) return
|
||||||
|
|
||||||
let stackLines = e.stack.split("\n")
|
let stackLines = e.stack.split("\n")
|
||||||
let cloudstormLine = stackLines.findIndex(l => l.includes("/node_modules/cloudstorm/"))
|
let cloudstormLine = stackLines.findIndex(l => l.includes("/node_modules/cloudstorm/"))
|
||||||
if (cloudstormLine !== -1) {
|
if (cloudstormLine !== -1) {
|
||||||
|
@ -45,17 +54,75 @@ module.exports = {
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
formatted_body: "\u26a0 <strong>Bridged event from Discord not delivered</strong>"
|
formatted_body: "\u26a0 <strong>Bridged event from Discord not delivered</strong>"
|
||||||
+ `<br>Gateway event: ${gatewayMessage.t}`
|
+ `<br>Gateway event: ${gatewayMessage.t}`
|
||||||
+ `<pre>${stackLines.join("\n")}</pre>`
|
+ `<br>${e.toString()}`
|
||||||
|
+ `<details><summary>Error trace</summary>`
|
||||||
|
+ `<pre>${stackLines.join("\n")}</pre></details>`
|
||||||
+ `<details><summary>Original payload</summary>`
|
+ `<details><summary>Original payload</summary>`
|
||||||
+ `<pre>${util.inspect(gatewayMessage.d, false, 4, false)}</pre></details>`,
|
+ `<pre>${util.inspect(gatewayMessage.d, false, 4, false)}</pre></details>`,
|
||||||
"m.mentions": {
|
"m.mentions": {
|
||||||
user_ids: ["@cadence:cadence.moe"]
|
user_ids: ["@cadence:cadence.moe"]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When logging back in, check if we missed any conversations in any channels. Bridge up to 49 missed messages per channel.
|
||||||
|
* If more messages were missed, only the latest missed message will be posted. TODO: Consider bridging more, or post a warning when skipping history?
|
||||||
|
* This can ONLY detect new messages, not any other kind of event. Any missed edits, deletes, reactions, etc will not be bridged.
|
||||||
|
* @param {import("./discord-client")} client
|
||||||
|
* @param {import("discord-api-types/v10").GatewayGuildCreateDispatchData} guild
|
||||||
|
*/
|
||||||
|
async checkMissedMessages(client, guild) {
|
||||||
|
if (guild.unavailable) return
|
||||||
|
const bridgedChannels = db.prepare("SELECT channel_id FROM channel_room").pluck().all()
|
||||||
|
const prepared = db.prepare("SELECT message_id FROM event_message WHERE channel_id = ? AND message_id = ?").pluck()
|
||||||
|
for (const channel of guild.channels.concat(guild.threads)) {
|
||||||
|
if (!bridgedChannels.includes(channel.id)) continue
|
||||||
|
if (!channel.last_message_id) continue
|
||||||
|
const latestWasBridged = prepared.get(channel.id, channel.last_message_id)
|
||||||
|
if (latestWasBridged) continue
|
||||||
|
|
||||||
|
/** More recent messages come first. */
|
||||||
|
console.log(`[check missed messages] in ${channel.id} (${guild.name} / ${channel.name}) because its last message ${channel.last_message_id} is not in the database`)
|
||||||
|
const messages = await client.snow.channel.getChannelMessages(channel.id, {limit: 50})
|
||||||
|
let latestBridgedMessageIndex = messages.findIndex(m => {
|
||||||
|
return prepared.get(channel.id, m.id)
|
||||||
|
})
|
||||||
|
console.log(`[check missed messages] got ${messages.length} messages; last message that IS bridged is at position ${latestBridgedMessageIndex} in the channel`)
|
||||||
|
if (latestBridgedMessageIndex === -1) latestBridgedMessageIndex = 1 // rather than crawling the ENTIRE channel history, let's just bridge the most recent 1 message to make it up to date.
|
||||||
|
for (let i = Math.min(messages.length, latestBridgedMessageIndex)-1; i >= 0; i--) {
|
||||||
|
const simulatedGatewayDispatchData = {
|
||||||
|
guild_id: guild.id,
|
||||||
|
mentions: [],
|
||||||
|
...messages[i]
|
||||||
|
}
|
||||||
|
await module.exports.onMessageCreate(client, simulatedGatewayDispatchData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("./discord-client")} client
|
||||||
|
* @param {import("discord-api-types/v10").APIChannel} thread
|
||||||
|
*/
|
||||||
|
async onThreadCreate(client, thread) {
|
||||||
|
console.log(thread)
|
||||||
|
const parentRoomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").get(thread.parent_id)
|
||||||
|
if (!parentRoomID) return // Not interested in a thread if we aren't interested in its wider channel
|
||||||
|
await createRoom.syncRoom(thread.id)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("./discord-client")} client
|
||||||
|
* @param {import("discord-api-types/v10").GatewayChannelUpdateDispatchData} channelOrThread
|
||||||
|
* @param {boolean} isThread
|
||||||
|
*/
|
||||||
|
async onChannelOrThreadUpdate(client, channelOrThread, isThread) {
|
||||||
|
const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").get(channelOrThread.id)
|
||||||
|
if (!roomID) return // No target room to update the data on
|
||||||
|
await createRoom.syncRoom(channelOrThread.id)
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import("./discord-client")} client
|
* @param {import("./discord-client")} client
|
||||||
* @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message
|
* @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message
|
||||||
|
@ -72,13 +139,13 @@ module.exports = {
|
||||||
const channel = client.channels.get(message.channel_id)
|
const channel = client.channels.get(message.channel_id)
|
||||||
if (!channel.guild_id) return // Nothing we can do in direct messages.
|
if (!channel.guild_id) return // Nothing we can do in direct messages.
|
||||||
const guild = client.guilds.get(channel.guild_id)
|
const guild = client.guilds.get(channel.guild_id)
|
||||||
if (message.guild_id !== "112760669178241024" && message.guild_id !== "497159726455455754") return // TODO: activate on other servers (requires the space creation flow to be done first)
|
if (!isGuildAllowed(guild.id)) return
|
||||||
await sendMessage.sendMessage(message, guild)
|
await sendMessage.sendMessage(message, guild)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import("./discord-client")} client
|
* @param {import("./discord-client")} client
|
||||||
* @param {import("discord-api-types/v10").GatewayMessageUpdateDispatchData} message
|
* @param {import("discord-api-types/v10").GatewayMessageUpdateDispatchData} data
|
||||||
*/
|
*/
|
||||||
async onMessageUpdate(client, data) {
|
async onMessageUpdate(client, data) {
|
||||||
if (data.webhook_id) {
|
if (data.webhook_id) {
|
||||||
|
@ -97,7 +164,7 @@ module.exports = {
|
||||||
const channel = client.channels.get(message.channel_id)
|
const channel = client.channels.get(message.channel_id)
|
||||||
if (!channel.guild_id) return // Nothing we can do in direct messages.
|
if (!channel.guild_id) return // Nothing we can do in direct messages.
|
||||||
const guild = client.guilds.get(channel.guild_id)
|
const guild = client.guilds.get(channel.guild_id)
|
||||||
if (message.guild_id !== "112760669178241024" && message.guild_id !== "497159726455455754") return // TODO: activate on other servers (requires the space creation flow to be done first)
|
if (!isGuildAllowed(guild.id)) return
|
||||||
await editMessage.editMessage(message, guild)
|
await editMessage.editMessage(message, guild)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -109,7 +176,6 @@ module.exports = {
|
||||||
async onReactionAdd(client, data) {
|
async onReactionAdd(client, data) {
|
||||||
if (data.user_id === client.user.id) return // m2d reactions are added by the discord bot user - do not reflect them back to matrix.
|
if (data.user_id === client.user.id) return // m2d reactions are added by the discord bot user - do not reflect them back to matrix.
|
||||||
if (data.emoji.id !== null) return // TODO: image emoji reactions
|
if (data.emoji.id !== null) return // TODO: image emoji reactions
|
||||||
console.log(data)
|
|
||||||
await addReaction.addReaction(data)
|
await addReaction.addReaction(data)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -118,7 +184,6 @@ module.exports = {
|
||||||
* @param {import("discord-api-types/v10").GatewayMessageDeleteDispatchData} data
|
* @param {import("discord-api-types/v10").GatewayMessageDeleteDispatchData} data
|
||||||
*/
|
*/
|
||||||
async onMessageDelete(client, data) {
|
async onMessageDelete(client, data) {
|
||||||
console.log(data)
|
|
||||||
await deleteMessage.deleteMessage(data)
|
await deleteMessage.deleteMessage(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ CREATE TABLE IF NOT EXISTS "channel_room" (
|
||||||
"room_id" TEXT NOT NULL UNIQUE,
|
"room_id" TEXT NOT NULL UNIQUE,
|
||||||
"name" TEXT,
|
"name" TEXT,
|
||||||
"nick" TEXT,
|
"nick" TEXT,
|
||||||
|
"thread_parent" TEXT,
|
||||||
PRIMARY KEY("channel_id")
|
PRIMARY KEY("channel_id")
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS "event_message" (
|
CREATE TABLE IF NOT EXISTS "event_message" (
|
||||||
|
@ -54,10 +55,10 @@ BEGIN TRANSACTION;
|
||||||
INSERT INTO guild_space (guild_id, space_id) VALUES
|
INSERT INTO guild_space (guild_id, space_id) VALUES
|
||||||
('112760669178241024', '!jjWAGMeQdNrVZSSfvz:cadence.moe');
|
('112760669178241024', '!jjWAGMeQdNrVZSSfvz:cadence.moe');
|
||||||
|
|
||||||
INSERT INTO channel_room (channel_id, room_id, name, nick) VALUES
|
INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent) VALUES
|
||||||
('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main'),
|
('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main', NULL),
|
||||||
('497161350934560778', '!edUxjVdzgUvXDUIQCK:cadence.moe', 'amanda-spam', NULL),
|
('497161350934560778', '!edUxjVdzgUvXDUIQCK:cadence.moe', 'amanda-spam', NULL, NULL),
|
||||||
('160197704226439168', '!uCtjHhfGlYbVnPVlkG:cadence.moe', 'the-stanley-parable-channel', 'bots');
|
('160197704226439168', '!uCtjHhfGlYbVnPVlkG:cadence.moe', 'the-stanley-parable-channel', 'bots', NULL);
|
||||||
|
|
||||||
INSERT INTO sim (discord_id, sim_name, localpart, mxid) VALUES
|
INSERT INTO sim (discord_id, sim_name, localpart, mxid) VALUES
|
||||||
('0', 'bot', '_ooye_bot', '@_ooye_bot:cadence.moe'),
|
('0', 'bot', '_ooye_bot', '@_ooye_bot:cadence.moe'),
|
||||||
|
|
|
@ -18,10 +18,12 @@ async function addReaction(event) {
|
||||||
// no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it
|
// no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it
|
||||||
|
|
||||||
let emoji = event.content["m.relates_to"].key // TODO: handle custom text or emoji reactions
|
let emoji = event.content["m.relates_to"].key // TODO: handle custom text or emoji reactions
|
||||||
emoji = encodeURIComponent(emoji)
|
let encoded = encodeURIComponent(emoji)
|
||||||
emoji = emoji.replace(/%EF%B8%8F/g, "")
|
let encodedTrimmed = encoded.replace(/%EF%B8%8F/g, "")
|
||||||
|
|
||||||
return discord.snow.channel.createReaction(channelID, messageID, emoji)
|
console.log("add reaction from matrix:", emoji, encoded, encodedTrimmed)
|
||||||
|
|
||||||
|
return discord.snow.channel.createReaction(channelID, messageID, encoded)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.addReaction = addReaction
|
module.exports.addReaction = addReaction
|
||||||
|
|
|
@ -41,19 +41,19 @@ async function ensureWebhook(channelID, forceCreate = false) {
|
||||||
async function withWebhook(channelID, callback) {
|
async function withWebhook(channelID, callback) {
|
||||||
const webhook = await ensureWebhook(channelID, false)
|
const webhook = await ensureWebhook(channelID, false)
|
||||||
return callback(webhook).catch(e => {
|
return callback(webhook).catch(e => {
|
||||||
console.error(e)
|
|
||||||
// TODO: check if the error was webhook-related and if webhook.created === false, then: const webhook = ensureWebhook(channelID, true); return callback(webhook)
|
// TODO: check if the error was webhook-related and if webhook.created === false, then: const webhook = ensureWebhook(channelID, true); return callback(webhook)
|
||||||
throw new Error(e)
|
throw e
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} channelID
|
* @param {string} channelID
|
||||||
* @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]}} data
|
* @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]}} data
|
||||||
|
* @param {string} [threadID]
|
||||||
*/
|
*/
|
||||||
async function sendMessageWithWebhook(channelID, data) {
|
async function sendMessageWithWebhook(channelID, data, threadID) {
|
||||||
const result = await withWebhook(channelID, async webhook => {
|
const result = await withWebhook(channelID, async webhook => {
|
||||||
return discord.snow.webhook.executeWebhook(webhook.id, webhook.token, data, {wait: true, disableEveryone: true})
|
return discord.snow.webhook.executeWebhook(webhook.id, webhook.token, data, {wait: true, thread_id: threadID, disableEveryone: true})
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,13 @@ const eventToMessage = sync.require("../converters/event-to-message")
|
||||||
/** @param {import("../../types").Event.Outer<any>} event */
|
/** @param {import("../../types").Event.Outer<any>} event */
|
||||||
async function sendEvent(event) {
|
async function sendEvent(event) {
|
||||||
// TODO: we just assume the bridge has already been created
|
// TODO: we just assume the bridge has already been created
|
||||||
const channelID = db.prepare("SELECT channel_id FROM channel_room WHERE room_id = ?").pluck().get(event.room_id)
|
const row = db.prepare("SELECT channel_id, thread_parent FROM channel_room WHERE room_id = ?").get(event.room_id)
|
||||||
|
let channelID = row.channel_id
|
||||||
|
let threadID = undefined
|
||||||
|
if (row.thread_parent) {
|
||||||
|
threadID = channelID
|
||||||
|
channelID = row.thread_parent // it's the thread's parent... get with the times...
|
||||||
|
}
|
||||||
|
|
||||||
// no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it
|
// no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it
|
||||||
|
|
||||||
|
@ -24,7 +30,7 @@ async function sendEvent(event) {
|
||||||
const messageResponses = []
|
const messageResponses = []
|
||||||
let eventPart = 0 // 0 is primary, 1 is supporting
|
let eventPart = 0 // 0 is primary, 1 is supporting
|
||||||
for (const message of messages) {
|
for (const message of messages) {
|
||||||
const messageResponse = await channelWebhook.sendMessageWithWebhook(channelID, message)
|
const messageResponse = await channelWebhook.sendMessageWithWebhook(channelID, message, threadID)
|
||||||
db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, channel_id, part, source) VALUES (?, ?, ?, ?, ?, ?, 0)").run(event.event_id, event.type, event.content.msgtype || null, messageResponse.id, channelID, eventPart) // source 0 = matrix
|
db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, channel_id, part, source) VALUES (?, ?, ?, ?, ?, ?, 0)").run(event.event_id, event.type, event.content.msgtype || null, messageResponse.id, channelID, eventPart) // source 0 = matrix
|
||||||
|
|
||||||
eventPart = 1 // TODO: use more intelligent algorithm to determine whether primary or supporting?
|
eventPart = 1 // TODO: use more intelligent algorithm to determine whether primary or supporting?
|
||||||
|
|
|
@ -16,11 +16,25 @@ function eventToMessage(event) {
|
||||||
/** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]})[]} */
|
/** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]})[]} */
|
||||||
const messages = []
|
const messages = []
|
||||||
|
|
||||||
|
let displayName = event.sender
|
||||||
|
let avatarURL = undefined
|
||||||
|
const match = event.sender.match(/^@(.*?):/)
|
||||||
|
if (match) {
|
||||||
|
displayName = match[1]
|
||||||
|
// TODO: get the media repo domain and the avatar url from the matrix member event
|
||||||
|
}
|
||||||
|
|
||||||
if (event.content.msgtype === "m.text") {
|
if (event.content.msgtype === "m.text") {
|
||||||
messages.push({
|
messages.push({
|
||||||
content: event.content.body,
|
content: event.content.body,
|
||||||
username: event.sender.replace(/^@/, ""),
|
username: displayName,
|
||||||
avatar_url: undefined, // TODO: provide the URL to the avatar from the homeserver's content repo
|
avatar_url: avatarURL
|
||||||
|
})
|
||||||
|
} else if (event.content.msgtype === "m.emote") {
|
||||||
|
messages.push({
|
||||||
|
content: `*${displayName} ${event.content.body}*`,
|
||||||
|
username: displayName,
|
||||||
|
avatar_url: avatarURL
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ test("event2message: janky test", t => {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
[{
|
[{
|
||||||
username: "cadence:cadence.moe",
|
username: "cadence",
|
||||||
content: "test",
|
content: "test",
|
||||||
avatar_url: undefined
|
avatar_url: undefined
|
||||||
}]
|
}]
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Grab Matrix events we care about, check them, and bridge them.
|
* Grab Matrix events we care about, check them, and bridge them.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const util = require("util")
|
||||||
const Ty = require("../types")
|
const Ty = require("../types")
|
||||||
const {sync, as} = require("../passthrough")
|
const {sync, as} = require("../passthrough")
|
||||||
|
|
||||||
|
@ -13,21 +14,58 @@ const sendEvent = sync.require("./actions/send-event")
|
||||||
const addReaction = sync.require("./actions/add-reaction")
|
const addReaction = sync.require("./actions/add-reaction")
|
||||||
/** @type {import("./converters/utils")} */
|
/** @type {import("./converters/utils")} */
|
||||||
const utils = sync.require("./converters/utils")
|
const utils = sync.require("./converters/utils")
|
||||||
|
/** @type {import("../matrix/api")}) */
|
||||||
|
const api = sync.require("../matrix/api")
|
||||||
|
|
||||||
sync.addTemporaryListener(as, "type:m.room.message",
|
let lastReportedEvent = 0
|
||||||
|
|
||||||
|
function guard(type, fn) {
|
||||||
|
return async function(event, ...args) {
|
||||||
|
try {
|
||||||
|
return await fn(event, ...args)
|
||||||
|
} catch (e) {
|
||||||
|
console.error("hit event-dispatcher's error handler with this exception:")
|
||||||
|
console.error(e) // TODO: also log errors into a file or into the database, maybe use a library for this? or just wing it?
|
||||||
|
console.error(`while handling this ${type} gateway event:`)
|
||||||
|
console.dir(event, {depth: null})
|
||||||
|
|
||||||
|
if (Date.now() - lastReportedEvent < 5000) return
|
||||||
|
lastReportedEvent = Date.now()
|
||||||
|
|
||||||
|
let stackLines = e.stack.split("\n")
|
||||||
|
api.sendEvent(event.room_id, "m.room.message", {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "\u26a0 Matrix event not delivered to Discord. See formatted content for full details.",
|
||||||
|
format: "org.matrix.custom.html",
|
||||||
|
formatted_body: "\u26a0 <strong>Matrix event not delivered to Discord</strong>"
|
||||||
|
+ `<br>Event type: ${type}`
|
||||||
|
+ `<br>${e.toString()}`
|
||||||
|
+ `<details><summary>Error trace</summary>`
|
||||||
|
+ `<pre>${stackLines.join("\n")}</pre></details>`
|
||||||
|
+ `<details><summary>Original payload</summary>`
|
||||||
|
+ `<pre>${util.inspect(event, false, 4, false)}</pre></details>`,
|
||||||
|
"m.mentions": {
|
||||||
|
user_ids: ["@cadence:cadence.moe"]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sync.addTemporaryListener(as, "type:m.room.message", guard("m.room.message",
|
||||||
/**
|
/**
|
||||||
* @param {Ty.Event.Outer<Ty.Event.M_Room_Message>} event it is a m.room.message because that's what this listener is filtering for
|
* @param {Ty.Event.Outer<Ty.Event.M_Room_Message>} event it is a m.room.message because that's what this listener is filtering for
|
||||||
*/
|
*/
|
||||||
async event => {
|
async event => {
|
||||||
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
||||||
const messageResponses = await sendEvent.sendEvent(event)
|
const messageResponses = await sendEvent.sendEvent(event)
|
||||||
})
|
}))
|
||||||
|
|
||||||
sync.addTemporaryListener(as, "type:m.reaction",
|
sync.addTemporaryListener(as, "type:m.reaction", guard("m.reaction",
|
||||||
/**
|
/**
|
||||||
* @param {Ty.Event.Outer<Ty.Event.M_Reaction>} event it is a m.reaction because that's what this listener is filtering for
|
* @param {Ty.Event.Outer<Ty.Event.M_Reaction>} event it is a m.reaction because that's what this listener is filtering for
|
||||||
*/
|
*/
|
||||||
async event => {
|
async event => {
|
||||||
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
||||||
await addReaction.addReaction(event)
|
await addReaction.addReaction(event)
|
||||||
})
|
}))
|
||||||
|
|
17
notes.md
17
notes.md
|
@ -9,6 +9,23 @@ A database will be used to store the discord id to matrix event id mapping. Tabl
|
||||||
|
|
||||||
There needs to be a way to easily manually trigger something later. For example, it should be easy to manually retry sending a message, or check all members for changes, etc.
|
There needs to be a way to easily manually trigger something later. For example, it should be easy to manually retry sending a message, or check all members for changes, etc.
|
||||||
|
|
||||||
|
## Discord's gateway when a new thread is created from an existing message:
|
||||||
|
|
||||||
|
1. Regular MESSAGE_CREATE of the message that it's going to branch off in the future. Example ID -6423
|
||||||
|
2. It MESSAGE_UPDATEd the ID -6423 with this whole data: {id:-6423,flags: 32,channel_id:-2084,guild_id:-1727} (ID is the message ID it's branching off, channel ID is the parent channel containing the message ID it's branching off)
|
||||||
|
3. It THREAD_CREATEd and gave us a channel object with type 11 (public thread) and parent ID -2084 and ID -6423.
|
||||||
|
4. It MESSAGE_CREATEd type 21 with blank content and a message reference pointing towards channel -2084 message -6423. (That's the message it branched from in the parent channel.) This MESSAGE_CREATE got ID -4631 (a new ID). Apart from that it's a regular message object.
|
||||||
|
5. Finally, as the first "real" message in that thread (which a user must send to create that thread!) it sent a regular message object with a new message ID and a channel ID of -6423.
|
||||||
|
|
||||||
|
When viewing this thread, it shows the message branched from at the top, and then the first "real" message right underneath, as separate groups.
|
||||||
|
|
||||||
|
## Current manual process for setting up a server
|
||||||
|
|
||||||
|
1. Call createSpace.createSpace(discord.guilds.get(GUILD_ID))
|
||||||
|
2. Call createRoom.createAllForGuild(GUILD_ID)
|
||||||
|
3. Edit source code of event-dispatcher.js isGuildAllowed() and add the guild ID to the list
|
||||||
|
4. If developing, make sure SSH port forward is activated, then wait for events to sync over!
|
||||||
|
|
||||||
## Transforming content
|
## Transforming content
|
||||||
|
|
||||||
1. Upload attachments to mxc if they are small enough.
|
1. Upload attachments to mxc if they are small enough.
|
||||||
|
|
1
stdin.js
1
stdin.js
|
@ -13,6 +13,7 @@ const registerUser = sync.require("./d2m/actions/register-user")
|
||||||
const mreq = sync.require("./matrix/mreq")
|
const mreq = sync.require("./matrix/mreq")
|
||||||
const api = sync.require("./matrix/api")
|
const api = sync.require("./matrix/api")
|
||||||
const sendEvent = sync.require("./m2d/actions/send-event")
|
const sendEvent = sync.require("./m2d/actions/send-event")
|
||||||
|
const eventDispatcher = sync.require("./d2m/event-dispatcher")
|
||||||
const guildID = "112760669178241024"
|
const guildID = "112760669178241024"
|
||||||
|
|
||||||
const extraContext = {}
|
const extraContext = {}
|
||||||
|
|
157
test/data.js
157
test/data.js
|
@ -816,6 +816,130 @@ module.exports = {
|
||||||
format_type: 1,
|
format_type: 1,
|
||||||
name: "pomu puff"
|
name: "pomu puff"
|
||||||
}]
|
}]
|
||||||
|
},
|
||||||
|
message_in_thread: {
|
||||||
|
type: 0,
|
||||||
|
tts: false,
|
||||||
|
timestamp: "2023-08-19T01:55:02.063000+00:00",
|
||||||
|
referenced_message: null,
|
||||||
|
position: 942,
|
||||||
|
pinned: false,
|
||||||
|
nonce: "1142275498206822400",
|
||||||
|
mentions: [],
|
||||||
|
mention_roles: [],
|
||||||
|
mention_everyone: false,
|
||||||
|
member: {
|
||||||
|
roles: [
|
||||||
|
"112767366235959296", "118924814567211009",
|
||||||
|
"204427286542417920", "199995902742626304",
|
||||||
|
"222168467627835392", "238028326281805825",
|
||||||
|
"259806643414499328", "265239342648131584",
|
||||||
|
"271173313575780353", "287733611912757249",
|
||||||
|
"225744901915148298", "305775031223320577",
|
||||||
|
"318243902521868288", "348651574924541953",
|
||||||
|
"349185088157777920", "378402925128712193",
|
||||||
|
"392141548932038658", "393912152173576203",
|
||||||
|
"482860581670486028", "495384759074160642",
|
||||||
|
"638988388740890635", "373336013109461013",
|
||||||
|
"530220455085473813", "454567553738473472",
|
||||||
|
"790724320824655873", "1123518980456452097",
|
||||||
|
"1040735082610167858", "695946570482450442",
|
||||||
|
"1123460940935991296", "849737964090556488"
|
||||||
|
],
|
||||||
|
premium_since: null,
|
||||||
|
pending: false,
|
||||||
|
nick: null,
|
||||||
|
mute: false,
|
||||||
|
joined_at: "2015-11-11T09:55:40.321000+00:00",
|
||||||
|
flags: 0,
|
||||||
|
deaf: false,
|
||||||
|
communication_disabled_until: null,
|
||||||
|
avatar: null
|
||||||
|
},
|
||||||
|
id: "1142275501721911467",
|
||||||
|
flags: 0,
|
||||||
|
embeds: [],
|
||||||
|
edited_timestamp: null,
|
||||||
|
content: "don't mind me, posting something for cadence",
|
||||||
|
components: [],
|
||||||
|
channel_id: "910283343378120754",
|
||||||
|
author: {
|
||||||
|
username: "kumaccino",
|
||||||
|
public_flags: 128,
|
||||||
|
id: "113340068197859328",
|
||||||
|
global_name: "kumaccino",
|
||||||
|
discriminator: "0",
|
||||||
|
avatar_decoration_data: null,
|
||||||
|
avatar: "b48302623a12bc7c59a71328f72ccb39"
|
||||||
|
},
|
||||||
|
attachments: [],
|
||||||
|
guild_id: "112760669178241024"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
message_with_embeds: {
|
||||||
|
nothing_but_a_field: {
|
||||||
|
guild_id: "497159726455455754",
|
||||||
|
mentions: [],
|
||||||
|
id: "1141934888862351440",
|
||||||
|
type: 20,
|
||||||
|
content: "",
|
||||||
|
channel_id: "497161350934560778",
|
||||||
|
author: {
|
||||||
|
id: "1109360903096369153",
|
||||||
|
username: "Amanda 🎵",
|
||||||
|
avatar: "d56cd1b26e043ae512edae2214962faa",
|
||||||
|
discriminator: "2192",
|
||||||
|
public_flags: 524288,
|
||||||
|
flags: 524288,
|
||||||
|
bot: true,
|
||||||
|
banner: null,
|
||||||
|
accent_color: null,
|
||||||
|
global_name: null,
|
||||||
|
avatar_decoration_data: null,
|
||||||
|
banner_color: null
|
||||||
|
},
|
||||||
|
attachments: [],
|
||||||
|
embeds: [
|
||||||
|
{
|
||||||
|
type: "rich",
|
||||||
|
color: 3092790,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "Amanda 🎵#2192 <:online:606664341298872324>\nwillow tree, branch 0",
|
||||||
|
value: "**❯ Uptime:**\n3m 55s\n**❯ Memory:**\n64.45MB",
|
||||||
|
inline: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
mention_roles: [],
|
||||||
|
pinned: false,
|
||||||
|
mention_everyone: false,
|
||||||
|
tts: false,
|
||||||
|
timestamp: "2023-08-18T03:21:33.629000+00:00",
|
||||||
|
edited_timestamp: null,
|
||||||
|
flags: 0,
|
||||||
|
components: [],
|
||||||
|
application_id: "1109360903096369153",
|
||||||
|
interaction: {
|
||||||
|
id: "1141934887608254475",
|
||||||
|
type: 2,
|
||||||
|
name: "stats",
|
||||||
|
user: {
|
||||||
|
id: "320067006521147393",
|
||||||
|
username: "papiophidian",
|
||||||
|
avatar: "47a19b0445069b826e136da4df4259bb",
|
||||||
|
discriminator: "0",
|
||||||
|
public_flags: 4194880,
|
||||||
|
flags: 4194880,
|
||||||
|
banner: null,
|
||||||
|
accent_color: null,
|
||||||
|
global_name: "PapiOphidian",
|
||||||
|
avatar_decoration_data: null,
|
||||||
|
banner_color: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
webhook_id: "1109360903096369153"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
message_update: {
|
message_update: {
|
||||||
|
@ -1219,5 +1343,38 @@ module.exports = {
|
||||||
],
|
],
|
||||||
guild_id: "112760669178241024"
|
guild_id: "112760669178241024"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
special_message: {
|
||||||
|
thread_name_change: {
|
||||||
|
id: "1142391602799710298",
|
||||||
|
type: 4,
|
||||||
|
content: "worming",
|
||||||
|
channel_id: "1142271000067706880",
|
||||||
|
author: {
|
||||||
|
id: "772659086046658620",
|
||||||
|
username: "cadence.worm",
|
||||||
|
avatar: "4b5c4b28051144e4c111f0113a0f1cf1",
|
||||||
|
discriminator: "0",
|
||||||
|
public_flags: 0,
|
||||||
|
flags: 0,
|
||||||
|
banner: null,
|
||||||
|
accent_color: null,
|
||||||
|
global_name: "cadence",
|
||||||
|
avatar_decoration_data: null,
|
||||||
|
banner_color: null
|
||||||
|
},
|
||||||
|
attachments: [],
|
||||||
|
embeds: [],
|
||||||
|
mentions: [],
|
||||||
|
mention_roles: [],
|
||||||
|
pinned: false,
|
||||||
|
mention_everyone: false,
|
||||||
|
tts: false,
|
||||||
|
timestamp: "2023-08-19T09:36:22.717000+00:00",
|
||||||
|
edited_timestamp: null,
|
||||||
|
flags: 0,
|
||||||
|
components: [],
|
||||||
|
position: 12
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ require("../matrix/kstate.test")
|
||||||
require("../matrix/api.test")
|
require("../matrix/api.test")
|
||||||
require("../matrix/read-registration.test")
|
require("../matrix/read-registration.test")
|
||||||
require("../d2m/converters/message-to-event.test")
|
require("../d2m/converters/message-to-event.test")
|
||||||
|
require("../d2m/converters/message-to-event.embeds.test")
|
||||||
require("../d2m/converters/edit-to-changes.test")
|
require("../d2m/converters/edit-to-changes.test")
|
||||||
require("../d2m/actions/create-room.test")
|
require("../d2m/actions/create-room.test")
|
||||||
require("../d2m/converters/user-to-mxid.test")
|
require("../d2m/converters/user-to-mxid.test")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue