Add Matrix command handler + emoji command
This commit is contained in:
		
							parent
							
								
									c1cbdfee82
								
							
						
					
					
						commit
						d1e3640078
					
				
					 5 changed files with 285 additions and 12 deletions
				
			
		| 
						 | 
					@ -63,7 +63,7 @@ async function stickersToState(stickers) {
 | 
				
			||||||
			if (sticker && sticker.description) body += ` - ${sticker.description}`
 | 
								if (sticker && sticker.description) body += ` - ${sticker.description}`
 | 
				
			||||||
			if (!body) body = undefined
 | 
								if (!body) body = undefined
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			let shortcode = sticker.name.toLowerCase().replace(/[^a-zA-Z0-9-_]/g, "-").replace(/^-|-$/g, "").replace(/--+/g, "-")
 | 
								let shortcode = sticker.name.toLowerCase().replace(/[^a-zA-Z0-9_-]/g, "-").replace(/^-|-$/g, "").replace(/--+/g, "-")
 | 
				
			||||||
			while (shortcodes.includes(shortcode)) shortcode = shortcode + "~"
 | 
								while (shortcodes.includes(shortcode)) shortcode = shortcode + "~"
 | 
				
			||||||
			shortcodes.push(shortcode)
 | 
								shortcodes.push(shortcode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,15 +15,6 @@ const utils = sync.require("../converters/utils")
 | 
				
			||||||
/** @type {import("./emoji-sheet")} */
 | 
					/** @type {import("./emoji-sheet")} */
 | 
				
			||||||
const emojiSheet = sync.require("./emoji-sheet")
 | 
					const emojiSheet = sync.require("./emoji-sheet")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const BLOCK_ELEMENTS = [
 | 
					 | 
				
			||||||
	"ADDRESS", "ARTICLE", "ASIDE", "AUDIO", "BLOCKQUOTE", "BODY", "CANVAS",
 | 
					 | 
				
			||||||
	"CENTER", "DD", "DETAILS", "DIR", "DIV", "DL", "DT", "FIELDSET", "FIGCAPTION", "FIGURE",
 | 
					 | 
				
			||||||
	"FOOTER", "FORM", "FRAMESET", "H1", "H2", "H3", "H4", "H5", "H6", "HEADER",
 | 
					 | 
				
			||||||
	"HGROUP", "HR", "HTML", "ISINDEX", "LI", "MAIN", "MENU", "NAV", "NOFRAMES",
 | 
					 | 
				
			||||||
	"NOSCRIPT", "OL", "OUTPUT", "P", "PRE", "SECTION", "SUMMARY", "TABLE", "TBODY", "TD",
 | 
					 | 
				
			||||||
	"TFOOT", "TH", "THEAD", "TR", "UL"
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/** @type {[RegExp, string][]} */
 | 
					/** @type {[RegExp, string][]} */
 | 
				
			||||||
const markdownEscapes = [
 | 
					const markdownEscapes = [
 | 
				
			||||||
	[/\\/g, '\\\\'],
 | 
						[/\\/g, '\\\\'],
 | 
				
			||||||
| 
						 | 
					@ -235,7 +226,7 @@ function splitDisplayName(displayName) {
 | 
				
			||||||
async function uploadEndOfMessageSpriteSheet(content, attachments, pendingFiles) {
 | 
					async function uploadEndOfMessageSpriteSheet(content, attachments, pendingFiles) {
 | 
				
			||||||
	if (!content.includes("<::>")) return content // No unknown emojis, nothing to do
 | 
						if (!content.includes("<::>")) return content // No unknown emojis, nothing to do
 | 
				
			||||||
	// Remove known and unknown emojis from the end of the message
 | 
						// Remove known and unknown emojis from the end of the message
 | 
				
			||||||
	const r = /<a?:[a-zA-Z0-9_-]*:[0-9]*>\s*$/
 | 
						const r = /<a?:[a-zA-Z0-9_]*:[0-9]*>\s*$/
 | 
				
			||||||
	while (content.match(r)) {
 | 
						while (content.match(r)) {
 | 
				
			||||||
		content = content.replace(r, "")
 | 
							content = content.replace(r, "")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -403,7 +394,7 @@ async function eventToMessage(event, guild, di) {
 | 
				
			||||||
				beforeTag = beforeTag || ""
 | 
									beforeTag = beforeTag || ""
 | 
				
			||||||
				afterContext = afterContext || ""
 | 
									afterContext = afterContext || ""
 | 
				
			||||||
				afterTag = afterTag || ""
 | 
									afterTag = afterTag || ""
 | 
				
			||||||
				if (!BLOCK_ELEMENTS.includes(beforeTag.toUpperCase()) && !BLOCK_ELEMENTS.includes(afterTag.toUpperCase())) {
 | 
									if (!utils.BLOCK_ELEMENTS.includes(beforeTag.toUpperCase()) && !utils.BLOCK_ELEMENTS.includes(afterTag.toUpperCase())) {
 | 
				
			||||||
					return beforeContext + "<br>" + afterContext
 | 
										return beforeContext + "<br>" + afterContext
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
					return whole
 | 
										return whole
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,6 +8,15 @@ let hasher = null
 | 
				
			||||||
// @ts-ignore
 | 
					// @ts-ignore
 | 
				
			||||||
require("xxhash-wasm")().then(h => hasher = h)
 | 
					require("xxhash-wasm")().then(h => hasher = h)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const BLOCK_ELEMENTS = [
 | 
				
			||||||
 | 
						"ADDRESS", "ARTICLE", "ASIDE", "AUDIO", "BLOCKQUOTE", "BODY", "CANVAS",
 | 
				
			||||||
 | 
						"CENTER", "DD", "DETAILS", "DIR", "DIV", "DL", "DT", "FIELDSET", "FIGCAPTION", "FIGURE",
 | 
				
			||||||
 | 
						"FOOTER", "FORM", "FRAMESET", "H1", "H2", "H3", "H4", "H5", "H6", "HEADER",
 | 
				
			||||||
 | 
						"HGROUP", "HR", "HTML", "ISINDEX", "LI", "MAIN", "MENU", "NAV", "NOFRAMES",
 | 
				
			||||||
 | 
						"NOSCRIPT", "OL", "OUTPUT", "P", "PRE", "SECTION", "SUMMARY", "TABLE", "TBODY", "TD",
 | 
				
			||||||
 | 
						"TFOOT", "TH", "THEAD", "TR", "UL"
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Determine whether an event is the bridged representation of a discord message.
 | 
					 * Determine whether an event is the bridged representation of a discord message.
 | 
				
			||||||
 * Such messages shouldn't be bridged again.
 | 
					 * Such messages shouldn't be bridged again.
 | 
				
			||||||
| 
						 | 
					@ -54,6 +63,7 @@ function getEventIDHash(eventID) {
 | 
				
			||||||
	return signedHash
 | 
						return signedHash
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports.BLOCK_ELEMENTS = BLOCK_ELEMENTS
 | 
				
			||||||
module.exports.eventSenderIsFromDiscord = eventSenderIsFromDiscord
 | 
					module.exports.eventSenderIsFromDiscord = eventSenderIsFromDiscord
 | 
				
			||||||
module.exports.getPublicUrlForMxc = getPublicUrlForMxc
 | 
					module.exports.getPublicUrlForMxc = getPublicUrlForMxc
 | 
				
			||||||
module.exports.getEventIDHash = getEventIDHash
 | 
					module.exports.getEventIDHash = getEventIDHash
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +14,8 @@ const sendEvent = sync.require("./actions/send-event")
 | 
				
			||||||
const addReaction = sync.require("./actions/add-reaction")
 | 
					const addReaction = sync.require("./actions/add-reaction")
 | 
				
			||||||
/** @type {import("./actions/redact")} */
 | 
					/** @type {import("./actions/redact")} */
 | 
				
			||||||
const redact = sync.require("./actions/redact")
 | 
					const redact = sync.require("./actions/redact")
 | 
				
			||||||
 | 
					/** @type {import("../matrix/matrix-command-handler")} */
 | 
				
			||||||
 | 
					const matrixCommandHandler = sync.require("../matrix/matrix-command-handler")
 | 
				
			||||||
/** @type {import("./converters/utils")} */
 | 
					/** @type {import("./converters/utils")} */
 | 
				
			||||||
const utils = sync.require("./converters/utils")
 | 
					const utils = sync.require("./converters/utils")
 | 
				
			||||||
/** @type {import("../matrix/api")}) */
 | 
					/** @type {import("../matrix/api")}) */
 | 
				
			||||||
| 
						 | 
					@ -78,6 +80,10 @@ sync.addTemporaryListener(as, "type:m.room.message", guard("m.room.message",
 | 
				
			||||||
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)
 | 
				
			||||||
 | 
						if (event.type === "m.room.message" && event.content.msgtype === "m.text") {
 | 
				
			||||||
 | 
							// @ts-ignore
 | 
				
			||||||
 | 
							await matrixCommandHandler.execute(event)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}))
 | 
					}))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
sync.addTemporaryListener(as, "type:m.sticker", guard("m.sticker",
 | 
					sync.addTemporaryListener(as, "type:m.sticker", guard("m.sticker",
 | 
				
			||||||
| 
						 | 
					@ -99,6 +105,7 @@ async event => {
 | 
				
			||||||
		// Try to bridge a failed event again?
 | 
							// Try to bridge a failed event again?
 | 
				
			||||||
		await retry(event.room_id, event.content["m.relates_to"].event_id)
 | 
							await retry(event.room_id, event.content["m.relates_to"].event_id)
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
 | 
							matrixCommandHandler.onReactionAdd(event)
 | 
				
			||||||
		await addReaction.addReaction(event)
 | 
							await addReaction.addReaction(event)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}))
 | 
					}))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										265
									
								
								matrix/matrix-command-handler.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										265
									
								
								matrix/matrix-command-handler.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,265 @@
 | 
				
			||||||
 | 
					// @ts-check
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const assert = require("assert").strict
 | 
				
			||||||
 | 
					const Ty = require("../types")
 | 
				
			||||||
 | 
					const {pipeline} = require("stream").promises
 | 
				
			||||||
 | 
					const sharp = require("sharp")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const {discord, sync, db, select} = require("../passthrough")
 | 
				
			||||||
 | 
					/** @type {import("./api")}) */
 | 
				
			||||||
 | 
					const api = sync.require("./api")
 | 
				
			||||||
 | 
					/** @type {import("../m2d/converters/utils")} */
 | 
				
			||||||
 | 
					const mxUtils = sync.require("../m2d/converters/utils")
 | 
				
			||||||
 | 
					/** @type {import("../discord/utils")} */
 | 
				
			||||||
 | 
					const dUtils = sync.require("../discord/utils")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const PREFIXES = ["//", "/"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const EMOJI_SIZE = 128
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** This many normal emojis + this many animated emojis. The total number is doubled. */
 | 
				
			||||||
 | 
					const TIER_EMOJI_SLOTS = new Map([
 | 
				
			||||||
 | 
						[1, 100],
 | 
				
			||||||
 | 
						[2, 150],
 | 
				
			||||||
 | 
						[3, 250]
 | 
				
			||||||
 | 
					])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** @param {number} tier */
 | 
				
			||||||
 | 
					function getSlotCount(tier) {
 | 
				
			||||||
 | 
						return TIER_EMOJI_SLOTS.get(tier) || 50
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let buttons = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @param {string} roomID where to add the button
 | 
				
			||||||
 | 
					 * @param {string} eventID where to add the button
 | 
				
			||||||
 | 
					 * @param {string} key emoji to add as a button
 | 
				
			||||||
 | 
					 * @param {string} mxid only listen for responses from this user
 | 
				
			||||||
 | 
					 * @returns {Promise<import("discord-api-types/v10").GatewayMessageReactionAddDispatchData>}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					async function addButton(roomID, eventID, key, mxid) {
 | 
				
			||||||
 | 
						await api.sendEvent(roomID, "m.reaction", {
 | 
				
			||||||
 | 
							"m.relates_to": {
 | 
				
			||||||
 | 
								rel_type: "m.annotation",
 | 
				
			||||||
 | 
								event_id: eventID,
 | 
				
			||||||
 | 
								key
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						return new Promise(resolve => {
 | 
				
			||||||
 | 
							buttons.push({roomID, eventID, mxid, key, 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 {Ty.Event.Outer<Ty.Event.M_Reaction>} event */
 | 
				
			||||||
 | 
					function onReactionAdd(event) {
 | 
				
			||||||
 | 
						const button = buttons.find(b => b.roomID === event.room_id && b.mxid === event.sender && b.eventID === event.content["m.relates_to"]?.event_id && b.key === event.content["m.relates_to"]?.key)
 | 
				
			||||||
 | 
						if (button) {
 | 
				
			||||||
 | 
							buttons = buttons.filter(b => b !== button) // remove button data so it can't be clicked again
 | 
				
			||||||
 | 
							button.resolve(event)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @callback CommandExecute
 | 
				
			||||||
 | 
					 * @param {Ty.Event.Outer_M_Room_Message} event
 | 
				
			||||||
 | 
					 * @param {any} [ctx]
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @typedef Command
 | 
				
			||||||
 | 
					 * @property {string[]} aliases
 | 
				
			||||||
 | 
					 * @property {CommandExecute} execute
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** @param {CommandExecute} execute */
 | 
				
			||||||
 | 
					function replyctx(execute) {
 | 
				
			||||||
 | 
						/** @type {CommandExecute} */
 | 
				
			||||||
 | 
						return function(event, ctx = {}) {
 | 
				
			||||||
 | 
							ctx["m.relates_to"] = {
 | 
				
			||||||
 | 
								"m.in_reply_to": {
 | 
				
			||||||
 | 
									event_id: event.event_id
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return execute(event, ctx)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const NEWLINE_ELEMENTS = mxUtils.BLOCK_ELEMENTS.concat(["BR"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MatrixStringBuilder {
 | 
				
			||||||
 | 
						constructor() {
 | 
				
			||||||
 | 
							this.body = ""
 | 
				
			||||||
 | 
							this.formattedBody = ""
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @param {string} body
 | 
				
			||||||
 | 
						 * @param {string} formattedBody
 | 
				
			||||||
 | 
						 * @param {any} [condition]
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						add(body, formattedBody, condition = true) {
 | 
				
			||||||
 | 
							if (condition) {
 | 
				
			||||||
 | 
								if (!formattedBody) formattedBody = body
 | 
				
			||||||
 | 
								this.body += body
 | 
				
			||||||
 | 
								this.formattedBody += formattedBody
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return this
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @param {string} body
 | 
				
			||||||
 | 
						 * @param {string} [formattedBody]
 | 
				
			||||||
 | 
						 * @param {any} [condition]
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						addLine(body, formattedBody, condition = true) {
 | 
				
			||||||
 | 
							if (condition) {
 | 
				
			||||||
 | 
								if (!formattedBody) formattedBody = body
 | 
				
			||||||
 | 
								if (this.body.length && this.body.slice(-1) !== "\n") this.body += "\n"
 | 
				
			||||||
 | 
								this.body += body
 | 
				
			||||||
 | 
								const match = this.formattedBody.match(/<\/?([a-zA-Z]+[a-zA-Z0-9]*)[^>]*>\s*$/)
 | 
				
			||||||
 | 
								if (this.formattedBody.length && (!match || !NEWLINE_ELEMENTS.includes(match[1].toUpperCase()))) this.formattedBody += "<br>"
 | 
				
			||||||
 | 
								this.formattedBody += formattedBody
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return this
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						get() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								msgtype: "m.text",
 | 
				
			||||||
 | 
								body: this.body,
 | 
				
			||||||
 | 
								format: "org.matrix.custom.html",
 | 
				
			||||||
 | 
								formatted_body: this.formattedBody
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** @type {Command[]} */
 | 
				
			||||||
 | 
					const commands = [{
 | 
				
			||||||
 | 
						aliases: ["emoji"],
 | 
				
			||||||
 | 
						execute: replyctx(
 | 
				
			||||||
 | 
							async (event, ctx) => {
 | 
				
			||||||
 | 
								// Guard
 | 
				
			||||||
 | 
								/** @type {string} */ // @ts-ignore
 | 
				
			||||||
 | 
								const channelID = select("channel_room", "channel_id", "WHERE room_id = ?").pluck().get(event.room_id)
 | 
				
			||||||
 | 
								const guildID = discord.channels.get(channelID)?.["guild_id"]
 | 
				
			||||||
 | 
								let matrixOnlyReason = null
 | 
				
			||||||
 | 
								const matrixOnlyConclusion = "So the emoji will be uploaded on Matrix-side only. It will still be usable over the bridge, but may have degraded functionality."
 | 
				
			||||||
 | 
								if (!guildID) {
 | 
				
			||||||
 | 
									matrixOnlyReason = "NOT_BRIDGED"
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									const guild = discord.guilds.get(guildID)
 | 
				
			||||||
 | 
									assert(guild)
 | 
				
			||||||
 | 
									const slots = getSlotCount(guild.premium_tier)
 | 
				
			||||||
 | 
									const permissions = dUtils.getPermissions([], guild.roles)
 | 
				
			||||||
 | 
									if (guild.emojis.length >= slots) {
 | 
				
			||||||
 | 
										matrixOnlyReason = "CAPACITY"
 | 
				
			||||||
 | 
									} else if (!(permissions | 0x40000000n)) { // MANAGE_GUILD_EXPRESSIONS (apparently CREATE_GUILD_EXPRESSIONS isn't good enough...)
 | 
				
			||||||
 | 
										matrixOnlyReason = "USER_PERMISSIONS"
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const nameMatch = event.content.body.match(/:([a-zA-Z0-9_]{2,}):/)
 | 
				
			||||||
 | 
								if (!nameMatch) {
 | 
				
			||||||
 | 
									return api.sendEvent(event.room_id, "m.room.message", {
 | 
				
			||||||
 | 
										...ctx,
 | 
				
			||||||
 | 
										msgtype: "m.text",
 | 
				
			||||||
 | 
										body: "Not sure what you want to call this emoji. Try writing a new :name: in colons. The name can have letters, numbers, and underscores."
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								const name = nameMatch[1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								let mxc
 | 
				
			||||||
 | 
								const mxcMatch = event.content.body.match(/(mxc:\/\/.*?)\b/)
 | 
				
			||||||
 | 
								if (mxcMatch) {
 | 
				
			||||||
 | 
									mxc = mxcMatch[1]
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (!mxc && event.content["m.relates_to"]?.["m.in_reply_to"]?.event_id) {
 | 
				
			||||||
 | 
									const repliedToEventID = event.content["m.relates_to"]["m.in_reply_to"].event_id
 | 
				
			||||||
 | 
									const repliedToEvent = await api.getEvent(event.room_id, repliedToEventID)
 | 
				
			||||||
 | 
									if (repliedToEvent.type === "m.room.message" && repliedToEvent.content.msgtype === "m.image" && repliedToEvent.content.url) {
 | 
				
			||||||
 | 
										mxc = repliedToEvent.content.url
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (!mxc) {
 | 
				
			||||||
 | 
									return api.sendEvent(event.room_id, "m.room.message", {
 | 
				
			||||||
 | 
										...ctx,
 | 
				
			||||||
 | 
										msgtype: "m.text",
 | 
				
			||||||
 | 
										body: "Not sure what image you wanted to add. Try replying to an uploaded image when you use the command, or write an mxc:// URL in your message."
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const sent = await api.sendEvent(event.room_id, "m.room.message", {
 | 
				
			||||||
 | 
									...ctx,
 | 
				
			||||||
 | 
									...new MatrixStringBuilder()
 | 
				
			||||||
 | 
										.addLine("## Emoji preview", "<h2>Emoji preview</h2>")
 | 
				
			||||||
 | 
										.addLine(`Ⓜ️ This room isn't bridged to Discord. ${matrixOnlyConclusion}`, `Ⓜ️ <em>This room isn't bridged to Discord. ${matrixOnlyConclusion}</em>`, matrixOnlyReason === "NOT_BRIDGED")
 | 
				
			||||||
 | 
										.addLine(`Ⓜ️ *Discord ran out of space for emojis. ${matrixOnlyConclusion}`, `Ⓜ️ <em>Discord ran out of space for emojis. ${matrixOnlyConclusion}</em>`, matrixOnlyReason === "CAPACITY")
 | 
				
			||||||
 | 
										.addLine(`Ⓜ️ *If you were a Discord user, you wouldn't have permission to create emojis. ${matrixOnlyConclusion}`, `Ⓜ️ <em>If you were a Discord user, you wouldn't have permission to create emojis. ${matrixOnlyConclusion}</em>`, matrixOnlyReason === "CAPACITY")
 | 
				
			||||||
 | 
										.addLine("[Preview not available in plain text.]", `Preview: <img data-mx-emoticon height="48" src="${mxc}">`)
 | 
				
			||||||
 | 
										.addLine("Hit ✅ to add it.")
 | 
				
			||||||
 | 
										.get()
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								addButton(event.room_id, sent, "✅", event.sender).then(async () => {
 | 
				
			||||||
 | 
									const publicUrl = mxUtils.getPublicUrlForMxc(mxc)
 | 
				
			||||||
 | 
									// @ts-ignore
 | 
				
			||||||
 | 
									const resizeInput = await fetch(publicUrl, {agent: false}).then(res => res.arrayBuffer())
 | 
				
			||||||
 | 
									const resizeOutput = await sharp(resizeInput)
 | 
				
			||||||
 | 
										.resize(EMOJI_SIZE, EMOJI_SIZE, {fit: "inside", withoutEnlargement: true, background: {r: 0, g: 0, b: 0, alpha: 0}})
 | 
				
			||||||
 | 
										.png()
 | 
				
			||||||
 | 
										.toBuffer({resolveWithObject: true})
 | 
				
			||||||
 | 
									if (matrixOnlyReason) {
 | 
				
			||||||
 | 
										// Edit some state keys
 | 
				
			||||||
 | 
										api.sendEvent(event.room_id, "m.room.message", {
 | 
				
			||||||
 | 
											...ctx,
 | 
				
			||||||
 | 
											msgtype: "m.text",
 | 
				
			||||||
 | 
											body: "Sorry, adding Matrix-only emojis not supported yet!!"
 | 
				
			||||||
 | 
										})
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										// Upload it to Discord and have the bridge sync it back to Matrix again
 | 
				
			||||||
 | 
										console.log(`uploading emoji ${resizeOutput.data.length} bytes to :${name}:`)
 | 
				
			||||||
 | 
										const emoji = await discord.snow.guildAssets.createEmoji(guildID, {name, image: "data:image/png;base64," + resizeOutput.data.toString("base64")})
 | 
				
			||||||
 | 
										api.sendEvent(event.room_id, "m.room.message", {
 | 
				
			||||||
 | 
											...ctx,
 | 
				
			||||||
 | 
											msgtype: "m.text",
 | 
				
			||||||
 | 
											body: `Created :${name}:`
 | 
				
			||||||
 | 
										})
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** @type {CommandExecute} */
 | 
				
			||||||
 | 
					async function execute(event) {
 | 
				
			||||||
 | 
						let realBody = event.content.body
 | 
				
			||||||
 | 
						while (realBody.startsWith("> ")) {
 | 
				
			||||||
 | 
							const i = realBody.indexOf("\n")
 | 
				
			||||||
 | 
							if (i === -1) return
 | 
				
			||||||
 | 
							realBody = realBody.slice(i + 1)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						realBody = realBody.replace(/^\s*/, "")
 | 
				
			||||||
 | 
						let words
 | 
				
			||||||
 | 
						for (const prefix of PREFIXES) {
 | 
				
			||||||
 | 
							if (realBody.startsWith(prefix)) {
 | 
				
			||||||
 | 
								words = realBody.slice(prefix.length).split(" ")
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (!words) return
 | 
				
			||||||
 | 
						const commandName = words[0]
 | 
				
			||||||
 | 
						const command = commands.find(c => c.aliases.includes(commandName))
 | 
				
			||||||
 | 
						if (!command) return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						await command.execute(event)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports.execute = execute
 | 
				
			||||||
 | 
					module.exports.onReactionAdd = onReactionAdd
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue