2024-01-11 02:56:58 +00:00
// @ts-check
2023-06-28 12:06:56 +00:00
const assert = require ( "assert" ) . strict
2023-10-10 01:03:53 +00:00
const DiscordTypes = require ( "discord-api-types/v10" )
2023-08-17 13:22:14 +00:00
const util = require ( "util" )
2023-09-18 10:51:59 +00:00
const { sync , db , select , from } = require ( "../passthrough" )
2023-05-04 20:25:00 +00:00
/** @type {import("./actions/send-message")}) */
const sendMessage = sync . require ( "./actions/send-message" )
2023-08-17 04:41:28 +00:00
/** @type {import("./actions/edit-message")}) */
const editMessage = sync . require ( "./actions/edit-message" )
2023-08-17 13:22:14 +00:00
/** @type {import("./actions/delete-message")}) */
const deleteMessage = sync . require ( "./actions/delete-message" )
2023-05-09 05:13:59 +00:00
/** @type {import("./actions/add-reaction")}) */
const addReaction = sync . require ( "./actions/add-reaction" )
2023-09-25 09:20:23 +00:00
/** @type {import("./actions/remove-reaction")}) */
const removeReaction = sync . require ( "./actions/remove-reaction" )
2023-08-21 11:31:40 +00:00
/** @type {import("./actions/announce-thread")}) */
const announceThread = sync . require ( "./actions/announce-thread" )
2023-08-19 11:12:36 +00:00
/** @type {import("./actions/create-room")}) */
const createRoom = sync . require ( "./actions/create-room" )
2023-08-23 00:37:25 +00:00
/** @type {import("./actions/create-space")}) */
const createSpace = sync . require ( "./actions/create-space" )
2023-10-10 04:41:53 +00:00
/** @type {import("./actions/update-pins")}) */
const updatePins = sync . require ( "./actions/update-pins" )
2023-08-17 13:22:14 +00:00
/** @type {import("../matrix/api")}) */
const api = sync . require ( "../matrix/api" )
2024-01-17 11:30:55 +00:00
/** @type {import("../discord/utils")} */
2024-01-18 23:39:41 +00:00
const dUtils = sync . require ( "../discord/utils" )
2023-09-17 08:15:29 +00:00
/** @type {import("../discord/discord-command-handler")}) */
const discordCommandHandler = sync . require ( "../discord/discord-command-handler" )
2024-01-18 23:39:41 +00:00
/** @type {import("../m2d/converters/utils")} */
const mxUtils = require ( "../m2d/converters/utils" )
2024-01-19 12:01:34 +00:00
/** @type {import("./actions/speedbump")} */
const speedbump = sync . require ( "./actions/speedbump" )
2023-08-17 13:22:14 +00:00
2024-01-17 11:30:55 +00:00
/** @type {any} */ // @ts-ignore bad types from semaphore
const Semaphore = require ( "@chriscdn/promise-semaphore" )
const checkMissedPinsSema = new Semaphore ( )
2023-08-17 13:22:14 +00:00
let lastReportedEvent = 0
2023-05-04 20:25:00 +00:00
// Grab Discord events we care about for the bridge, check them, and pass them on
2023-04-30 12:57:30 +00:00
2023-04-25 20:06:08 +00:00
module . exports = {
2023-08-17 13:22:14 +00:00
/ * *
* @ param { import ( "./discord-client" ) } client
* @ param { Error } e
* @ param { import ( "cloudstorm" ) . IGatewayMessage } gatewayMessage
* /
onError ( client , e , gatewayMessage ) {
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? definitely need to be able to store the formatted event body to load back in later
console . error ( ` while handling this ${ gatewayMessage . t } gateway event: ` )
2023-08-18 04:58:46 +00:00
console . dir ( gatewayMessage . d , { depth : null } )
2023-08-17 13:22:14 +00:00
2023-09-18 13:23:32 +00:00
if ( gatewayMessage . t === "TYPING_START" ) return
2023-08-19 06:39:23 +00:00
if ( Date . now ( ) - lastReportedEvent < 5000 ) return
lastReportedEvent = Date . now ( )
2024-01-11 02:56:58 +00:00
const channelID = gatewayMessage . d [ "channel_id" ]
2023-08-19 06:39:23 +00:00
if ( ! channelID ) return
2023-10-05 23:31:10 +00:00
const roomID = select ( "channel_room" , "room_id" , { channel _id : channelID } ) . pluck ( ) . get ( )
2023-08-19 06:39:23 +00:00
if ( ! roomID ) return
2024-01-11 02:56:58 +00:00
let stackLines = null
if ( e . stack ) {
stackLines = e . stack . split ( "\n" )
let cloudstormLine = stackLines . findIndex ( l => l . includes ( "/node_modules/cloudstorm/" ) )
if ( cloudstormLine !== - 1 ) {
stackLines = stackLines . slice ( 0 , cloudstormLine - 2 )
}
}
2024-01-18 23:39:41 +00:00
const builder = new mxUtils . MatrixStringBuilder ( )
builder . addLine ( "\u26a0 Bridged event from Discord not delivered" , "\u26a0 <strong>Bridged event from Discord not delivered</strong>" )
builder . addLine ( ` Gateway event: ${ gatewayMessage . t } ` )
builder . addLine ( e . toString ( ) )
2024-01-11 02:56:58 +00:00
if ( stackLines ) {
2024-01-18 23:39:41 +00:00
builder . addLine ( ` Error trace: \n ${ stackLines . join ( "\n" ) } ` , ` <details><summary>Error trace</summary><pre> ${ stackLines . join ( "\n" ) } </pre></details> ` )
2023-08-17 13:22:14 +00:00
}
2024-01-18 23:39:41 +00:00
builder . addLine ( "" , ` <details><summary>Original payload</summary><pre> ${ util . inspect ( gatewayMessage . d , false , 4 , false ) } </pre></details> ` )
2023-08-19 06:39:23 +00:00
api . sendEvent ( roomID , "m.room.message" , {
2024-01-18 23:39:41 +00:00
... builder . get ( ) ,
2023-09-03 04:03:37 +00:00
"moe.cadence.ooye.error" : {
source : "discord" ,
payload : gatewayMessage
} ,
2023-08-19 06:39:23 +00:00
"m.mentions" : {
user _ids : [ "@cadence:cadence.moe" ]
}
} )
2023-08-17 13:22:14 +00:00
} ,
2023-08-19 10:54:23 +00:00
/ * *
* 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
2023-10-10 01:03:53 +00:00
* @ param { DiscordTypes . GatewayGuildCreateDispatchData } guild
2023-08-19 10:54:23 +00:00
* /
async checkMissedMessages ( client , guild ) {
if ( guild . unavailable ) return
2023-09-18 10:51:59 +00:00
const bridgedChannels = select ( "channel_room" , "channel_id" ) . pluck ( ) . all ( )
2023-10-05 23:31:10 +00:00
const prepared = select ( "event_message" , "event_id" , { } , "WHERE message_id = ?" ) . pluck ( )
2023-08-19 10:54:23 +00:00
for ( const channel of guild . channels . concat ( guild . threads ) ) {
if ( ! bridgedChannels . includes ( channel . id ) ) continue
2024-01-11 02:56:58 +00:00
if ( ! ( "last_message_id" in channel ) || ! channel . last _message _id ) continue
2023-08-28 05:32:55 +00:00
const latestWasBridged = prepared . get ( channel . last _message _id )
2023-08-19 10:54:23 +00:00
if ( latestWasBridged ) continue
2024-01-17 11:30:55 +00:00
// Permissions check
const member = guild . members . find ( m => m . user ? . id === client . user . id )
if ( ! member ) return
if ( ! ( "permission_overwrites" in channel ) ) continue
2024-01-18 23:39:41 +00:00
const permissions = dUtils . getPermissions ( member . roles , guild . roles , client . user . id , channel . permission _overwrites )
2024-01-17 11:30:55 +00:00
const wants = BigInt ( 1 << 10 ) | BigInt ( 1 << 16 ) // VIEW_CHANNEL + READ_MESSAGE_HISTORY
if ( ( permissions & wants ) !== wants ) continue // We don't have permission to look back in this channel
2023-08-19 10:54:23 +00:00
/** More recent messages come first. */
2023-09-03 13:38:30 +00:00
// 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`)
let messages
try {
messages = await client . snow . channel . getChannelMessages ( channel . id , { limit : 50 } )
} catch ( e ) {
if ( e . message === ` {"message": "Missing Access", "code": 50001} ` ) { // pathetic error handling from SnowTransfer
console . log ( ` [check missed messages] no permissions to look back in channel ${ channel . name } ( ${ channel . id } ) ` )
continue // Sucks.
} else {
throw e // Sucks more.
}
}
2023-08-19 10:54:23 +00:00
let latestBridgedMessageIndex = messages . findIndex ( m => {
2023-08-28 05:32:55 +00:00
return prepared . get ( m . id )
2023-08-19 10:54:23 +00:00
} )
2023-09-03 13:38:30 +00:00
// console.log(`[check missed messages] got ${messages.length} messages; last message that IS bridged is at position ${latestBridgedMessageIndex} in the channel`)
2023-08-19 10:54:23 +00:00
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 ,
2023-08-28 04:20:16 +00:00
backfill : true ,
2023-08-19 10:54:23 +00:00
... messages [ i ]
}
await module . exports . onMessageCreate ( client , simulatedGatewayDispatchData )
}
}
} ,
2024-01-17 11:30:55 +00:00
/ * *
* When logging back in , check if the pins on Matrix - side are up to date . If they aren ' t , update all pins .
* Rather than query every room on Matrix - side , we cache the latest pinned message in the database and compare against that .
* @ param { import ( "./discord-client" ) } client
* @ param { DiscordTypes . GatewayGuildCreateDispatchData } guild
* /
async checkMissedPins ( client , guild ) {
if ( guild . unavailable ) return
const member = guild . members . find ( m => m . user ? . id === client . user . id )
if ( ! member ) return
for ( const channel of guild . channels ) {
if ( ! ( "last_pin_timestamp" in channel ) || ! channel . last _pin _timestamp ) continue // Only care about channels that have pins
if ( ! ( "permission_overwrites" in channel ) ) continue
const lastPin = updatePins . convertTimestamp ( channel . last _pin _timestamp )
// Permissions check
2024-01-18 23:39:41 +00:00
const permissions = dUtils . getPermissions ( member . roles , guild . roles , client . user . id , channel . permission _overwrites )
2024-01-17 11:30:55 +00:00
const wants = BigInt ( 1 << 10 ) | BigInt ( 1 << 16 ) // VIEW_CHANNEL + READ_MESSAGE_HISTORY
if ( ( permissions & wants ) !== wants ) continue // We don't have permission to look up the pins in this channel
const row = select ( "channel_room" , [ "room_id" , "last_bridged_pin_timestamp" ] , { channel _id : channel . id } ) . get ( )
if ( ! row ) continue // Only care about already bridged channels
if ( row . last _bridged _pin _timestamp == null || lastPin > row . last _bridged _pin _timestamp ) {
checkMissedPinsSema . request ( ( ) => updatePins . updatePins ( channel . id , row . room _id , lastPin ) )
}
}
} ,
2024-01-10 10:56:10 +00:00
/ * *
* When logging back in , check if we missed any changes to emojis or stickers . Apply the changes if so .
* @ param { DiscordTypes . GatewayGuildCreateDispatchData } guild
* /
async checkMissedExpressions ( guild ) {
const data = { guild _id : guild . id , ... guild }
createSpace . syncSpaceExpressions ( data , true )
} ,
2023-08-20 20:07:05 +00:00
/ * *
2023-08-21 05:25:51 +00:00
* Announces to the parent room that the thread room has been created .
* See notes . md , "Ignore MESSAGE_UPDATE and bridge THREAD_CREATE as the announcement"
2023-08-20 20:07:05 +00:00
* @ param { import ( "./discord-client" ) } client
2023-10-10 01:03:53 +00:00
* @ param { DiscordTypes . APIThreadChannel } thread
2023-08-20 20:07:05 +00:00
* /
async onThreadCreate ( client , thread ) {
2024-01-11 02:56:58 +00:00
const channelID = thread . parent _id || undefined
const parentRoomID = select ( "channel_room" , "room_id" , { channel _id : channelID } ) . pluck ( ) . get ( )
2023-08-20 20:07:05 +00:00
if ( ! parentRoomID ) return // Not interested in a thread if we aren't interested in its wider channel
2023-08-21 05:25:51 +00:00
const threadRoomID = await createRoom . syncRoom ( thread . id ) // Create room (will share the same inflight as the initial message to the thread)
await announceThread . announceThread ( parentRoomID , threadRoomID , thread )
2023-08-20 20:07:05 +00:00
} ,
2023-08-23 00:37:25 +00:00
/ * *
* @ param { import ( "./discord-client" ) } client
2023-10-10 01:03:53 +00:00
* @ param { DiscordTypes . GatewayGuildUpdateDispatchData } guild
2023-08-23 00:37:25 +00:00
* /
async onGuildUpdate ( client , guild ) {
2023-10-05 23:31:10 +00:00
const spaceID = select ( "guild_space" , "space_id" , { guild _id : guild . id } ) . pluck ( ) . get ( )
2023-08-23 00:37:25 +00:00
if ( ! spaceID ) return
2023-09-20 04:37:24 +00:00
await createSpace . syncSpace ( guild )
2023-08-23 00:37:25 +00:00
} ,
2023-08-20 20:07:05 +00:00
/ * *
2023-08-19 11:12:36 +00:00
* @ param { import ( "./discord-client" ) } client
2023-10-10 01:03:53 +00:00
* @ param { DiscordTypes . GatewayChannelUpdateDispatchData } channelOrThread
2023-08-19 11:12:36 +00:00
* @ param { boolean } isThread
* /
async onChannelOrThreadUpdate ( client , channelOrThread , isThread ) {
2023-10-05 23:31:10 +00:00
const roomID = select ( "channel_room" , "room_id" , { channel _id : channelOrThread . id } ) . pluck ( ) . get ( )
2023-08-19 11:12:36 +00:00
if ( ! roomID ) return // No target room to update the data on
await createRoom . syncRoom ( channelOrThread . id )
} ,
2023-10-10 04:41:53 +00:00
/ * *
* @ param { import ( "./discord-client" ) } client
* @ param { DiscordTypes . GatewayChannelPinsUpdateDispatchData } data
* /
async onChannelPinsUpdate ( client , data ) {
const roomID = select ( "channel_room" , "room_id" , { channel _id : data . channel _id } ) . pluck ( ) . get ( )
if ( ! roomID ) return // No target room to update pins in
2024-01-17 11:30:55 +00:00
const convertedTimestamp = updatePins . convertTimestamp ( data . last _pin _timestamp )
await updatePins . updatePins ( data . channel _id , roomID , convertedTimestamp )
2023-10-10 04:41:53 +00:00
} ,
2023-04-25 20:06:08 +00:00
/ * *
* @ param { import ( "./discord-client" ) } client
2023-10-10 01:03:53 +00:00
* @ param { DiscordTypes . GatewayMessageCreateDispatchData } message
2023-04-25 20:06:08 +00:00
* /
2023-08-18 04:58:46 +00:00
async onMessageCreate ( client , message ) {
2023-10-07 09:53:02 +00:00
if ( message . author . username === "Deleted User" ) return // Nothing we can do for deleted users.
2023-07-03 12:39:42 +00:00
if ( message . webhook _id ) {
2023-10-05 23:31:10 +00:00
const row = select ( "webhook" , "webhook_id" , { webhook _id : message . webhook _id } ) . pluck ( ) . get ( )
2024-01-19 12:01:34 +00:00
if ( row ) return // The message was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it.
} else {
const speedbumpID = select ( "channel_room" , "speedbump_id" , { channel _id : message . channel _id } ) . pluck ( ) . get ( )
if ( speedbumpID ) {
const affected = await speedbump . doSpeedbump ( message . id )
if ( affected ) return
2023-07-03 12:39:42 +00:00
}
}
2023-07-04 20:41:15 +00:00
const channel = client . channels . get ( message . channel _id )
2024-01-11 02:56:58 +00:00
if ( ! channel || ! ( "guild_id" in channel ) || ! channel . guild _id ) return // Nothing we can do in direct messages.
2023-07-04 20:41:15 +00:00
const guild = client . guilds . get ( channel . guild _id )
2024-01-11 02:56:58 +00:00
assert ( guild )
2023-08-24 05:09:25 +00:00
2023-08-25 04:03:43 +00:00
await sendMessage . sendMessage ( message , guild ) ,
await discordCommandHandler . execute ( message , channel , guild )
2023-04-25 20:06:08 +00:00
} ,
2023-08-17 04:41:28 +00:00
/ * *
* @ param { import ( "./discord-client" ) } client
2023-10-10 01:03:53 +00:00
* @ param { DiscordTypes . GatewayMessageUpdateDispatchData } data
2023-08-17 04:41:28 +00:00
* /
2023-08-18 04:58:46 +00:00
async onMessageUpdate ( client , data ) {
2023-08-17 07:03:09 +00:00
if ( data . webhook _id ) {
2023-10-05 23:31:10 +00:00
const row = select ( "webhook" , "webhook_id" , { webhook _id : data . webhook _id } ) . pluck ( ) . get ( )
2023-08-17 07:03:09 +00:00
if ( row ) {
// The update was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it.
return
}
}
2023-08-17 04:41:28 +00:00
// Based on looking at data they've sent me over the gateway, this is the best way to check for meaningful changes.
// If the message content is a string then it includes all interesting fields and is meaningful.
if ( typeof data . content === "string" ) {
2023-10-10 01:03:53 +00:00
/** @type {DiscordTypes.GatewayMessageCreateDispatchData} */
2024-01-11 02:56:58 +00:00
// @ts-ignore
2023-08-17 04:41:28 +00:00
const message = data
const channel = client . channels . get ( message . channel _id )
2024-01-11 02:56:58 +00:00
if ( ! channel || ! ( "guild_id" in channel ) || ! channel . guild _id ) return // Nothing we can do in direct messages.
2023-08-17 04:41:28 +00:00
const guild = client . guilds . get ( channel . guild _id )
2024-01-11 02:56:58 +00:00
assert ( guild )
2023-08-18 04:58:46 +00:00
await editMessage . editMessage ( message , guild )
2023-08-17 04:41:28 +00:00
}
} ,
2023-04-25 20:06:08 +00:00
/ * *
* @ param { import ( "./discord-client" ) } client
2023-10-10 01:03:53 +00:00
* @ param { DiscordTypes . GatewayMessageReactionAddDispatchData } data
2023-04-25 20:06:08 +00:00
* /
2023-08-18 04:58:46 +00:00
async onReactionAdd ( client , data ) {
2023-07-04 20:41:15 +00:00
if ( data . user _id === client . user . id ) return // m2d reactions are added by the discord bot user - do not reflect them back to matrix.
2023-08-25 04:01:19 +00:00
discordCommandHandler . onReactionAdd ( data )
2023-08-18 04:58:46 +00:00
await addReaction . addReaction ( data )
2023-08-17 13:22:14 +00:00
} ,
2023-09-25 09:20:23 +00:00
/ * *
* @ param { import ( "./discord-client" ) } client
2023-10-10 01:03:53 +00:00
* @ param { DiscordTypes . GatewayMessageReactionRemoveDispatchData | DiscordTypes . GatewayMessageReactionRemoveEmojiDispatchData | DiscordTypes . GatewayMessageReactionRemoveAllDispatchData } data
2023-09-25 09:20:23 +00:00
* /
2023-10-10 01:03:53 +00:00
async onSomeReactionsRemoved ( client , data ) {
await removeReaction . removeSomeReactions ( data )
2023-09-25 09:20:23 +00:00
} ,
2023-09-25 10:15:36 +00:00
/ * *
* @ param { import ( "./discord-client" ) } client
2023-10-10 01:03:53 +00:00
* @ param { DiscordTypes . GatewayMessageDeleteDispatchData } data
2023-08-17 13:22:14 +00:00
* /
2023-08-18 04:58:46 +00:00
async onMessageDelete ( client , data ) {
2024-01-19 12:01:34 +00:00
speedbump . onMessageDelete ( data . id )
2023-08-18 04:58:46 +00:00
await deleteMessage . deleteMessage ( data )
2023-09-17 11:07:33 +00:00
} ,
2024-01-11 02:56:42 +00:00
/ * *
* @ param { import ( "./discord-client" ) } client
* @ param { DiscordTypes . GatewayMessageDeleteBulkDispatchData } data
* /
async onMessageDeleteBulk ( client , data ) {
await deleteMessage . deleteMessageBulk ( data )
} ,
2023-09-17 11:07:33 +00:00
/ * *
* @ param { import ( "./discord-client" ) } client
2023-10-10 01:03:53 +00:00
* @ param { DiscordTypes . GatewayTypingStartDispatchData } data
2023-09-17 11:07:33 +00:00
* /
async onTypingStart ( client , data ) {
2023-10-05 23:31:10 +00:00
const roomID = select ( "channel_room" , "room_id" , { channel _id : data . channel _id } ) . pluck ( ) . get ( )
2023-09-17 11:07:33 +00:00
if ( ! roomID ) return
2023-10-05 23:31:10 +00:00
const mxid = from ( "sim" ) . join ( "sim_member" , "mxid" ) . where ( { user _id : data . user _id , room _id : roomID } ) . pluck ( "mxid" ) . get ( )
2023-09-17 11:07:33 +00:00
if ( ! mxid ) return
// Each Discord user triggers the notification every 8 seconds as long as they remain typing.
// Discord does not send typing stopped events, so typing only stops if the timeout is reached or if the user sends their message.
// (We have to manually stop typing on Matrix-side when the message is sent. This is part of the send action.)
await api . sendTyping ( roomID , true , mxid , 10000 )
2023-09-18 13:23:32 +00:00
} ,
/ * *
* @ param { import ( "./discord-client" ) } client
2023-10-10 01:03:53 +00:00
* @ param { DiscordTypes . GatewayGuildEmojisUpdateDispatchData | DiscordTypes . GatewayGuildStickersUpdateDispatchData } data
2023-09-18 13:23:32 +00:00
* /
async onExpressionsUpdate ( client , data ) {
2024-01-10 10:56:10 +00:00
await createSpace . syncSpaceExpressions ( data , false )
2023-04-25 20:06:08 +00:00
}
}