2023-08-24 05:09:25 +00:00
// @ts-check
const assert = require ( "assert" ) . strict
const util = require ( "util" )
const DiscordTypes = require ( "discord-api-types/v10" )
2023-09-12 08:43:56 +00:00
const reg = require ( "../matrix/read-registration" )
2023-10-02 08:48:06 +00:00
const { addbot } = require ( "../addbot" )
2023-09-12 08:43:56 +00:00
2023-09-18 10:51:59 +00:00
const { discord , sync , db , select } = require ( "../passthrough" )
2023-08-24 05:09:25 +00:00
/** @type {import("../matrix/api")}) */
const api = sync . require ( "../matrix/api" )
2023-08-25 04:01:19 +00:00
/** @type {import("../matrix/file")} */
const file = sync . require ( "../matrix/file" )
2023-10-12 11:37:18 +00:00
/** @type {import("../d2m/actions/create-space")} */
const createSpace = sync . require ( "../d2m/actions/create-space" )
2023-09-17 08:15:29 +00:00
/** @type {import("./utils")} */
const utils = sync . require ( "./utils" )
2023-08-24 05:09:25 +00:00
2023-08-25 04:01:19 +00:00
const PREFIX = "//"
let buttons = [ ]
/ * *
* @ param { string } channelID where to add the button
* @ param { string } messageID where to add the button
* @ param { string } emoji emoji to add as a button
* @ param { string } userID only listen for responses from this user
* @ returns { Promise < import ( "discord-api-types/v10" ) . GatewayMessageReactionAddDispatchData > }
* /
async function addButton ( channelID , messageID , emoji , userID ) {
await discord . snow . channel . createReaction ( channelID , messageID , emoji )
return new Promise ( resolve => {
buttons . push ( { channelID , messageID , userID , resolve , created : Date . now ( ) } )
} )
}
// Clear out old buttons every so often to free memory
setInterval ( ( ) => {
const now = Date . now ( )
buttons = buttons . filter ( b => now - b . created < 2 * 60 * 60 * 1000 )
} , 10 * 60 * 1000 )
/** @param {import("discord-api-types/v10").GatewayMessageReactionAddDispatchData} data */
function onReactionAdd ( data ) {
const button = buttons . find ( b => b . channelID === data . channel _id && b . messageID === data . message _id && b . userID === data . user _id )
if ( button ) {
buttons = buttons . filter ( b => b !== button ) // remove button data so it can't be clicked again
button . resolve ( data )
}
}
2023-08-24 05:09:25 +00:00
/ * *
* @ callback CommandExecute
* @ param { DiscordTypes . GatewayMessageCreateDispatchData } message
* @ param { DiscordTypes . APIGuildTextChannel } channel
* @ param { DiscordTypes . APIGuild } guild
2023-08-25 04:01:19 +00:00
* @ param { Partial < DiscordTypes . RESTPostAPIChannelMessageJSONBody > } [ ctx ]
2023-08-24 05:09:25 +00:00
* /
/ * *
* @ typedef Command
* @ property { string [ ] } aliases
* @ property { ( message : DiscordTypes . GatewayMessageCreateDispatchData , channel : DiscordTypes . APIGuildTextChannel , guild : DiscordTypes . APIGuild ) => Promise < any > } execute
* /
/** @param {CommandExecute} execute */
function replyctx ( execute ) {
/** @type {CommandExecute} */
return function ( message , channel , guild , ctx = { } ) {
ctx . message _reference = {
message _id : message . id ,
channel _id : channel . id ,
guild _id : guild . id ,
fail _if _not _exists : false
}
return execute ( message , channel , guild , ctx )
}
}
/** @type {Command[]} */
const commands = [ {
aliases : [ "icon" , "avatar" , "roomicon" , "roomavatar" , "channelicon" , "channelavatar" ] ,
execute : replyctx (
async ( message , channel , guild , ctx ) => {
2023-08-25 04:01:19 +00:00
// Guard
2023-10-05 23:31:10 +00:00
const roomID = select ( "channel_room" , "room_id" , { channel _id : channel . id } ) . pluck ( ) . get ( )
2023-08-24 05:09:25 +00:00
if ( ! roomID ) return discord . snow . channel . createMessage ( channel . id , {
... ctx ,
content : "This channel isn't bridged to the other side."
} )
2023-08-25 04:01:19 +00:00
// Current avatar
2023-08-24 05:09:25 +00:00
const avatarEvent = await api . getStateEvent ( roomID , "m.room.avatar" , "" )
2023-08-25 04:01:19 +00:00
const avatarURLParts = avatarEvent ? . url . match ( /^mxc:\/\/([^/]+)\/(\w+)$/ )
let currentAvatarMessage =
2023-09-12 08:43:56 +00:00
( avatarURLParts ? ` Current room-specific avatar: ${ reg . ooye . server _origin } /_matrix/media/r0/download/ ${ avatarURLParts [ 1 ] } / ${ avatarURLParts [ 2 ] } `
2023-08-25 04:01:19 +00:00
: "No avatar. Now's your time to strike. Use `//icon` again with a link or upload to set the room-specific avatar." )
// Next potential avatar
const nextAvatarURL = message . attachments . find ( a => a . content _type ? . startsWith ( "image/" ) ) ? . url || message . content . match ( /https?:\/\/[^ ]+\.[^ ]+\.(?:png|jpg|jpeg|webp)\b/ ) ? . [ 0 ]
let nextAvatarMessage =
( nextAvatarURL ? ` \n You want to set it to: ${ nextAvatarURL } \n Hit ✅ to make it happen. `
: "" )
const sent = await discord . snow . channel . createMessage ( channel . id , {
2023-08-24 05:09:25 +00:00
... ctx ,
2023-08-25 04:01:19 +00:00
content : currentAvatarMessage + nextAvatarMessage
2023-08-24 05:09:25 +00:00
} )
2023-08-25 04:01:19 +00:00
if ( nextAvatarURL ) {
addButton ( channel . id , sent . id , "✅" , message . author . id ) . then ( async data => {
const mxcUrl = await file . uploadDiscordFileToMxc ( nextAvatarURL )
await api . sendState ( roomID , "m.room.avatar" , "" , {
url : mxcUrl
} )
db . prepare ( "UPDATE channel_room SET custom_avatar = ? WHERE channel_id = ?" ) . run ( mxcUrl , channel . id )
await discord . snow . channel . createMessage ( channel . id , {
... ctx ,
content : "Your creation is unleashed. Any complaints will be redirected to Grelbo."
} )
} )
}
2023-08-24 05:09:25 +00:00
}
)
} , {
aliases : [ "invite" ] ,
execute : replyctx (
async ( message , channel , guild , ctx ) => {
2023-09-17 08:15:29 +00:00
// Check guild is bridged
2023-10-05 23:31:10 +00:00
const spaceID = select ( "guild_space" , "space_id" , { guild _id : guild . id } ) . pluck ( ) . get ( )
const roomID = select ( "channel_room" , "room_id" , { channel _id : channel . id } ) . pluck ( ) . get ( )
2023-09-17 08:15:29 +00:00
if ( ! spaceID || ! roomID ) return discord . snow . channel . createMessage ( channel . id , {
... ctx ,
content : "This server isn't bridged to Matrix, so you can't invite Matrix users."
} )
// Check CREATE_INSTANT_INVITE permission
assert ( message . member )
const guildPermissions = utils . getPermissions ( message . member . roles , guild . roles )
2024-03-06 04:37:16 +00:00
if ( ! ( guildPermissions & DiscordTypes . PermissionFlagsBits . CreateInstantInvite ) ) {
2023-09-17 08:15:29 +00:00
return discord . snow . channel . createMessage ( channel . id , {
... ctx ,
content : "You don't have permission to invite people to this Discord server."
} )
}
2023-09-17 08:33:37 +00:00
// Guard against accidental mentions instead of the MXID
if ( message . content . match ( /<[@#:].*>/ ) ) return discord . snow . channel . createMessage ( channel . id , {
... ctx ,
content : "You have to say the Matrix ID of the person you want to invite, but you mentioned a Discord user in your message.\nOne way to fix this is by writing `` ` `` backticks `` ` `` around the Matrix ID."
} )
2023-09-17 08:15:29 +00:00
// Get named MXID
const mxid = message . content . match ( /@([^:]+):([a-z0-9:-]+\.[a-z0-9.:-]+)/ ) ? . [ 0 ]
if ( ! mxid ) return discord . snow . channel . createMessage ( channel . id , {
... ctx ,
content : "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`"
} )
// Check for existing invite to the space
let spaceMember
try {
spaceMember = await api . getStateEvent ( spaceID , "m.room.member" , mxid )
} catch ( e ) { }
if ( spaceMember && spaceMember . membership === "invite" ) {
return discord . snow . channel . createMessage ( channel . id , {
... ctx ,
content : ` \` ${ mxid } \` already has an invite, which they haven't accepted yet. `
} )
}
// Invite Matrix user if not in space
if ( ! spaceMember || spaceMember . membership !== "join" ) {
await api . inviteToRoom ( spaceID , mxid )
return discord . snow . channel . createMessage ( channel . id , {
... ctx ,
content : ` You invited \` ${ mxid } \` to the server. `
} )
}
// The Matrix user *is* in the space, maybe we want to invite them to this channel?
let roomMember
try {
roomMember = await api . getStateEvent ( roomID , "m.room.member" , mxid )
} catch ( e ) { }
if ( ! roomMember || ( roomMember . membership !== "join" && roomMember . membership !== "invite" ) ) {
const sent = await discord . snow . channel . createMessage ( channel . id , {
... ctx ,
content : ` \` ${ mxid } \` is already in this server. Would you like to additionally invite them to this specific channel? \n Hit ✅ to make it happen. `
} )
return addButton ( channel . id , sent . id , "✅" , message . author . id ) . then ( async data => {
await api . inviteToRoom ( roomID , mxid )
await discord . snow . channel . createMessage ( channel . id , {
... ctx ,
content : ` You invited \` ${ mxid } \` to the channel. `
} )
} )
}
// The Matrix user *is* in the space and in the channel.
await discord . snow . channel . createMessage ( channel . id , {
2023-08-24 05:09:25 +00:00
... ctx ,
2023-09-17 08:15:29 +00:00
content : ` \` ${ mxid } \` is already in this server and this channel. `
2023-08-24 05:09:25 +00:00
} )
}
)
2023-10-02 08:48:06 +00:00
} , {
aliases : [ "addbot" ] ,
execute : replyctx (
async ( message , channel , guild , ctx ) => {
return discord . snow . channel . createMessage ( channel . id , {
... ctx ,
content : addbot ( )
} )
}
)
2023-10-12 11:37:18 +00:00
} , {
aliases : [ "privacy" , "discoverable" , "publish" , "published" ] ,
execute : replyctx (
async ( message , channel , guild , ctx ) => {
const current = select ( "guild_space" , "privacy_level" , { guild _id : guild . id } ) . pluck ( ) . get ( )
if ( current == null ) {
return discord . snow . channel . createMessage ( channel . id , {
... ctx ,
content : "This server isn't bridged to the other side."
} )
}
const levels = [ "invite" , "link" , "directory" ]
const level = levels . findIndex ( x => message . content . includes ( x ) )
if ( level === - 1 ) {
return discord . snow . channel . createMessage ( channel . id , {
... ctx ,
content : "**Usage: `//privacy <level>`**. This will set who can join the space on Matrix-side. There are three levels:"
+ "\n`invite`: Can only join with a direct in-app invite from another Matrix user, or the //invite command."
+ "\n`link`: Matrix links can be created and shared like Discord's invite links. `invite` features also work."
+ "\n`directory`: Publishes to the Matrix in-app directory, like Server Discovery. Preview enabled. `invite` and `link` also work."
+ ` \n **Current privacy level: \` ${ levels [ current ] } \` ** `
} )
}
2023-10-12 12:07:52 +00:00
assert ( message . member )
const guildPermissions = utils . getPermissions ( message . member . roles , guild . roles )
if ( guild . owner _id !== message . author . id && ! ( guildPermissions & BigInt ( 0x28 ) ) ) { // MANAGE_GUILD | ADMINISTRATOR
return discord . snow . channel . createMessage ( channel . id , {
... ctx ,
content : "You don't have permission to change the privacy level. You need Manage Server or Administrator."
} )
}
2023-10-12 11:37:18 +00:00
db . prepare ( "UPDATE guild_space SET privacy_level = ? WHERE guild_id = ?" ) . run ( level , guild . id )
discord . snow . channel . createMessage ( channel . id , {
... ctx ,
content : ` Privacy level updated to \` ${ levels [ level ] } \` . Changes will apply shortly. `
} )
await createSpace . syncSpaceFully ( guild . id )
}
)
2023-08-24 05:09:25 +00:00
} ]
/** @type {CommandExecute} */
async function execute ( message , channel , guild ) {
2023-08-25 04:01:19 +00:00
if ( ! message . content . startsWith ( PREFIX ) ) return
const words = message . content . slice ( PREFIX . length ) . split ( " " )
2023-08-24 05:09:25 +00:00
const commandName = words [ 0 ]
const command = commands . find ( c => c . aliases . includes ( commandName ) )
if ( ! command ) return
await command . execute ( message , channel , guild )
}
module . exports . execute = execute
2023-08-25 04:01:19 +00:00
module . exports . onReactionAdd = onReactionAdd