diff --git a/config/config.sample.yaml b/config/config.sample.yaml index 10f6d2fb97e7f9ca24fff97f962683894245c11b..d000b2d3902c8bcc25326d68c1ac6426b613a4f1 100644 --- a/config/config.sample.yaml +++ b/config/config.sample.yaml @@ -31,6 +31,7 @@ bridge: disableJoinLeaveNotifications: false # Authentication configuration for the discord bot. auth: + # This MUST be a string (wrapped in quotes) clientID: "12345" botToken: "foobar" logging: @@ -85,10 +86,12 @@ channel: limits: # Delay in milliseconds between discord users joining a room. roomGhostJoinDelay: 6000 - # Delay in milliseconds before sending messages to discord to avoid echos. - # (Copies of a sent message may arrive from discord before we've + # Lock timeout in milliseconds before seinding messages to discord to avoid + # echos. Default is rather high as the lock will most likely time out + # before anyways. + # echos = (Copies of a sent message may arrive from discord before we've # fininished handling it, causing us to echo it back to the room) - discordSendDelay: 750 + discordSendDelay: 1500 ghosts: # Pattern for the ghosts nick, available is :nick, :username, :tag and :id nickPattern: ":nick" diff --git a/docs/howto.md b/docs/howto.md index cbdee791fc771018dbbf0481cb07da5d15c3357b..f43902f5b683228d426dabb541354ff0b4538d32 100644 --- a/docs/howto.md +++ b/docs/howto.md @@ -15,7 +15,7 @@ is formatted as https://discordapp.com/channels/``guildid``/``channelid`` * The ``adminme`` script is provided to set Admin/Moderator or any other custom power level to a specific user. * e.g. To set Alice to Admin on her ``example.com`` HS on default config. (``config.yaml``) - * ``npm run adminme -- -r '!AbcdefghijklmnopqR:example.com' -u '@Alice:example.com' -p '100'`` + * ``npm run adminme -- -m '!AbcdefghijklmnopqR:example.com' -u '@Alice:example.com' -p '100'`` * Run ``npm run adminme -- -h`` for usage. Please note that `!AbcdefghijklmnopqR:example.com` is the internal room id and will always begin with `!`. diff --git a/package-lock.json b/package-lock.json index 788bc7522b7a956aa2ede8ee4ff93ae294860217..dee36c57af602cdad92f50aeef6afe7b0b56e117 100644 --- a/package-lock.json +++ b/package-lock.json @@ -957,12 +957,11 @@ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" }, "discord-markdown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/discord-markdown/-/discord-markdown-2.0.0.tgz", - "integrity": "sha512-uiDNjFrMjAFK50Q+z7qI4siF6U3XBx6gZ6GywjrQnoiBmWK5d/wJDoIT051bOD4xXBHuSENXsReF9zBvbCjDGQ==", + "version": "git://github.com/Sorunome/discord-markdown.git#5086d4ccb10a90bcdc7656606f5b3f5430bffee9", + "from": "git://github.com/Sorunome/discord-markdown.git#5086d4ccb10a90bcdc7656606f5b3f5430bffee9", "requires": { "highlight.js": "^9.13.1", - "simple-markdown": "^0.4.2" + "simple-markdown": "^0.4.4" } }, "discord.js": { @@ -1757,9 +1756,9 @@ "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=" }, "highlight.js": { - "version": "9.13.1", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.13.1.tgz", - "integrity": "sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A==" + "version": "9.15.8", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.15.8.tgz", + "integrity": "sha512-RrapkKQWwE+wKdF73VsOa2RQdIoO3mxwJ4P8mhbI6KYJUraUHRKM5w5zQQKXNk0xNL4UVRdulV9SBJcmzJNzVA==" }, "hosted-git-info": { "version": "2.7.1", @@ -2792,9 +2791,9 @@ "dev": true }, "node-html-parser": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-1.1.11.tgz", - "integrity": "sha512-KOjvmbk0yWuy/cN8uqk6bVYS0Lue+jVWcLO/zmnCtz8FPXhj00apBN376FoM6QmFMMbJwXQdKf5ko6G1S6bnrw==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-1.1.16.tgz", + "integrity": "sha512-cfqTZIYDdp5cGh3NvCD5dcEDP7hfyni7WgyFacmDynLlIZaF3GVlRk8yMARhWp/PobWt1KaCV8VKdP5LKWiVbg==", "requires": { "he": "1.1.1" } diff --git a/package.json b/package.json index 4c8c220a6455a2767b8e2488d4a3906e80d0429c..0401f60b061b4eeb160d2504d67caad0be6afbd2 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "coverage": "tsc && nyc mocha", "build": "tsc", "start": "npm run-script build && node ./build/src/discordas.js -p 9005 -c config.yaml", + "debug": "npm run-script build && node --inspect ./build/src/discordas.js -p 9005 -c config.yaml", "addbot": "node ./build/tools/addbot.js", "adminme": "node ./build/tools/adminme.js", "usertool": "node ./build/tools/userClientTools.js", @@ -39,14 +40,14 @@ "better-sqlite3": "^5.0.1", "command-line-args": "^4.0.7", "command-line-usage": "^4.1.0", - "discord-markdown": "^2.0.0", + "discord-markdown": "git://github.com/Sorunome/discord-markdown#5086d4ccb10a90bcdc7656606f5b3f5430bffee9", "discord.js": "^11.5.1", "escape-html": "^1.0.3", "escape-string-regexp": "^1.0.5", "js-yaml": "^3.13.1", "matrix-bot-sdk": "0.4.0-beta.3", "mime": "^1.6.0", - "node-html-parser": "^1.1.11", + "node-html-parser": "^1.1.15", "pg-promise": "^8.5.1", "prom-client": "^11.3.0", "tslint": "^5.11.0", diff --git a/src/bot.ts b/src/bot.ts index 23457ab9dc0c631b03a607da97f3712505020ab5..9ed386093e736de6cd2d136ead790e002f1c5bac 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -38,6 +38,7 @@ import { IMatrixEvent, IMatrixMediaInfo } from "./matrixtypes"; import { Appservice, Intent } from "matrix-bot-sdk"; import { DiscordCommandHandler } from "./discordcommandhandler"; import { MetricPeg } from "./metrics"; +import { Lock } from "./structures/lock"; const log = new Log("DiscordBot"); @@ -49,6 +50,7 @@ const MATRIX_ICON_URL = "https://matrix.org/_matrix/media/r0/download/matrix.org class ChannelLookupResult { public channel: Discord.TextChannel; public botUser: boolean; + public canSendEmbeds: boolean; } interface IThirdPartyLookupField { @@ -82,8 +84,7 @@ export class DiscordBot { /* Handles messages queued up to be sent to matrix from discord. */ private discordMessageQueue: { [channelId: string]: Promise<void> }; - private channelLocks: Map<string, {i: NodeJS.Timeout|null, r: (() => void)|null}>; - private channelLockPromises: Map<string, Promise<{}>>; + private channelLock: Lock<string>; constructor( private config: DiscordBridgeConfig, private bridge: Appservice, @@ -106,8 +107,7 @@ export class DiscordBot { // init vars this.sentMessages = []; this.discordMessageQueue = {}; - this.channelLocks = new Map(); - this.channelLockPromises = new Map(); + this.channelLock = new Lock(this.config.limits.discordSendDelay); this.lastEventIds = {}; } @@ -139,43 +139,6 @@ export class DiscordBot { return this.provisioner; } - public lockChannel(channel: Discord.Channel) { - if (this.channelLocks.has(channel.id)) { - return; - } - - this.channelLocks[channel.id] = {i: null, r: null, p: null}; - const p = new Promise<{}>((resolve) => { - const i = setTimeout(() => { - log.warn(`Lock on channel ${channel.id} expired. Discord is lagging behind?`); - this.unlockChannel(channel); - }, this.config.limits.discordSendDelay); - const o = Object.assign({r: resolve, i, p: null}, this.channelLocks.get(channel.id) || {}); - this.channelLocks.set(channel.id, o); - }); - this.channelLockPromises.set(channel.id, p); - } - - public unlockChannel(channel: Discord.Channel) { - if (!this.channelLocks.has(channel.id)) { - return; - } - const lock = this.channelLocks.get(channel.id)!; - if (lock.i !== null) { - lock.r!(); - clearTimeout(lock.i); - } - this.channelLocks.delete(channel.id); - this.channelLockPromises.delete(channel.id); - } - - public async waitUnlock(channel: Discord.Channel) { - const promise = this.channelLockPromises.get(channel.id); - if (promise) { - await promise; - } - } - public GetIntentFromDiscordMember(member: Discord.GuildMember | Discord.User, webhookID?: string): Intent { if (webhookID) { // webhookID and user IDs are the same, they are unique, so no need to prefix _webhook_ @@ -239,7 +202,7 @@ export class DiscordBot { client.on("messageDelete", async (msg: Discord.Message) => { try { - await this.waitUnlock(msg.channel); + await this.channelLock.wait(msg.channel.id); this.clientFactory.bindMetricsToChannel(msg.channel as Discord.TextChannel); this.discordMessageQueue[msg.channel.id] = (async () => { await (this.discordMessageQueue[msg.channel.id] || Promise.resolve()); @@ -260,7 +223,7 @@ export class DiscordBot { msgs.forEach((msg) => { promiseArr.push(async () => { try { - await this.waitUnlock(msg.channel); + await this.channelLock.wait(msg.channel.id); this.clientFactory.bindMetricsToChannel(msg.channel as Discord.TextChannel); await this.DeleteDiscordMessage(msg); } catch (err) { @@ -275,7 +238,7 @@ export class DiscordBot { }); client.on("messageUpdate", async (oldMessage: Discord.Message, newMessage: Discord.Message) => { try { - await this.waitUnlock(newMessage.channel); + await this.channelLock.wait(newMessage.channel.id); this.clientFactory.bindMetricsToChannel(newMessage.channel as Discord.TextChannel); this.discordMessageQueue[newMessage.channel.id] = (async () => { await (this.discordMessageQueue[newMessage.channel.id] || Promise.resolve()); @@ -292,7 +255,7 @@ export class DiscordBot { client.on("message", async (msg: Discord.Message) => { try { MetricPeg.get.registerRequest(msg.id); - await this.waitUnlock(msg.channel); + await this.channelLock.wait(msg.channel.id); this.clientFactory.bindMetricsToChannel(msg.channel as Discord.TextChannel); this.discordMessageQueue[msg.channel.id] = (async () => { await (this.discordMessageQueue[msg.channel.id] || Promise.resolve()); @@ -399,6 +362,7 @@ export class DiscordBot { const lookupResult = new ChannelLookupResult(); lookupResult.channel = channel as Discord.TextChannel; lookupResult.botUser = this.bot.user.id === client.user.id; + lookupResult.canSendEmbeds = client.user.bot; // only bots can send embeds return lookupResult; } throw new Error(`Channel "${room}" not found`); @@ -416,10 +380,10 @@ export class DiscordBot { if (!msg) { return; } - this.lockChannel(channel); + this.channelLock.set(channel.id); const res = await channel.send(msg); await this.StoreMessagesSent(res, channel, event); - this.unlockChannel(channel); + this.channelLock.release(channel.id); } public async send( @@ -450,19 +414,56 @@ export class DiscordBot { } } try { - this.lockChannel(chan); - if (!botUser) { - // NOTE: Don't send replies to discord if we are a puppet. + this.channelLock.set(chan.id); + if (!roomLookup.canSendEmbeds) { + // NOTE: Don't send replies to discord if we are a puppet user. + let addText = ""; + if (embedSet.replyEmbed) { + for (const line of embedSet.replyEmbed.description!.split("\n")) { + addText += "\n> " + line; + } + } + msg = await chan.send(embed.description + addText, opts); + } else if (!botUser) { + if (embedSet.imageEmbed || embedSet.replyEmbed) { + let sendEmbed = new Discord.RichEmbed(); + if (embedSet.imageEmbed) { + if (!embedSet.replyEmbed) { + sendEmbed = embedSet.imageEmbed; + } else { + sendEmbed.setImage(embedSet.imageEmbed.image!.url); + } + } + if (embedSet.replyEmbed) { + if (!embedSet.imageEmbed) { + sendEmbed = embedSet.replyEmbed; + } else { + sendEmbed.addField("Replying to", embedSet.replyEmbed!.author!.name); + sendEmbed.addField("Reply text", embedSet.replyEmbed.description); + } + } + opts.embed = sendEmbed; + } msg = await chan.send(embed.description, opts); } else if (hook) { MetricPeg.get.remoteCall("hook.send"); + const embeds: Discord.RichEmbed[] = []; + if (embedSet.imageEmbed) { + embeds.push(embedSet.imageEmbed); + } + if (embedSet.replyEmbed) { + embeds.push(embedSet.replyEmbed); + } msg = await hook.send(embed.description, { avatarURL: embed!.author!.icon_url, - embeds: embedSet.replyEmbed ? [embedSet.replyEmbed] : undefined, + embeds, files: opts.file ? [opts.file] : undefined, username: embed!.author!.name, } as Discord.WebhookMessageOptions); } else { + if (embedSet.imageEmbed) { + embed.setImage(embedSet.imageEmbed.image!.url); + } if (embedSet.replyEmbed) { embed.addField("Replying to", embedSet.replyEmbed!.author!.name); embed.addField("Reply text", embedSet.replyEmbed.description); @@ -472,7 +473,7 @@ export class DiscordBot { } // Don't block on this. this.StoreMessagesSent(msg, chan, event).then(() => { - this.unlockChannel(chan); + this.channelLock.release(chan.id); }).catch(() => { log.warn("Failed to store sent message for ", event.event_id); }); @@ -503,9 +504,9 @@ export class DiscordBot { const msg = await chan.fetchMessage(storeEvent.DiscordId); try { - this.lockChannel(msg.channel); + this.channelLock.set(msg.channel.id); await msg.delete(); - this.unlockChannel(msg.channel); + this.channelLock.release(msg.channel.id); log.info(`Deleted message`); } catch (ex) { log.warn(`Failed to delete message`, ex); @@ -658,12 +659,12 @@ export class DiscordBot { /* tslint:disable-next-line no-any */ } as any, // XXX: Discord.js typings are wrong. `Unbanned.`); - this.lockChannel(botChannel); + this.channelLock.set(botChannel.id); res = await botChannel.send( `${kickee} was unbanned from this channel by ${kicker}.`, ) as Discord.Message; this.sentMessages.push(res.id); - this.unlockChannel(botChannel); + this.channelLock.release(botChannel.id); return; } const existingPerms = tchan.memberPermissions(kickee); @@ -672,13 +673,13 @@ export class DiscordBot { return; } const word = `${kickban === "ban" ? "banned" : "kicked"}`; - this.lockChannel(botChannel); + this.channelLock.set(botChannel.id); res = await botChannel.send( `${kickee} was ${word} from this channel by ${kicker}.` + (reason ? ` Reason: ${reason}` : ""), ) as Discord.Message; this.sentMessages.push(res.id); - this.unlockChannel(botChannel); + this.channelLock.release(botChannel.id); log.info(`${word} ${kickee}`); await tchan.overwritePermissions(kickee, diff --git a/src/config.ts b/src/config.ts index 94c5a91fc34037fe663c86226b58ab61f13e5b80..756595085475055c2cb06386506bf666c3889efa 100644 --- a/src/config.ts +++ b/src/config.ts @@ -135,7 +135,7 @@ export class DiscordBridgeConfigChannelDeleteOptions { class DiscordBridgeConfigLimits { public roomGhostJoinDelay: number = 6000; - public discordSendDelay: number = 750; + public discordSendDelay: number = 1500; } export class LoggingFile { diff --git a/src/discordmessageprocessor.ts b/src/discordmessageprocessor.ts index 29849fcac17724e4cc19f21c649dadd5ddc9f2bb..097b674bae061a9d9c8282b764fa23502b294fad 100644 --- a/src/discordmessageprocessor.ts +++ b/src/discordmessageprocessor.ts @@ -48,6 +48,10 @@ export class DiscordMessageProcessorResult { public msgtype: string; } +interface ISpoilerNode { + content: string; +} + interface IDiscordNode { id: string; } @@ -77,6 +81,7 @@ export class DiscordMessageProcessor { // as else it'll HTML escape the result of the discord syntax let contentPostmark = markdown.toHTML(content, { discordCallback: this.getDiscordParseCallbacksHTML(msg), + noExtraSpanTags: true, }); @@ -85,6 +90,7 @@ export class DiscordMessageProcessor { discordCallback: this.getDiscordParseCallbacks(msg), discordOnly: true, escapeHTML: false, + noExtraSpanTags: true, }); content = this.InsertEmbeds(content, msg); content = await this.InsertMxcImages(content, msg); @@ -144,6 +150,7 @@ export class DiscordMessageProcessor { discordCallback: this.getDiscordParseCallbacks(msg), discordOnly: true, escapeHTML: false, + noExtraSpanTags: true, }); } if (embed.fields) { @@ -153,6 +160,7 @@ export class DiscordMessageProcessor { discordCallback: this.getDiscordParseCallbacks(msg), discordOnly: true, escapeHTML: false, + noExtraSpanTags: true, }); } } @@ -164,6 +172,7 @@ export class DiscordMessageProcessor { discordCallback: this.getDiscordParseCallbacks(msg), discordOnly: true, escapeHTML: false, + noExtraSpanTags: true, }); } content += embedContent; @@ -191,6 +200,7 @@ export class DiscordMessageProcessor { embedContent += markdown.toHTML(embed.description, { discordCallback: this.getDiscordParseCallbacksHTML(msg), embed: true, + noExtraSpanTags: true, }) + "</p>"; } if (embed.fields) { @@ -199,6 +209,7 @@ export class DiscordMessageProcessor { embedContent += markdown.toHTML(field.value, { discordCallback: this.getDiscordParseCallbacks(msg), embed: true, + noExtraSpanTags: true, }) + "</p>"; } } @@ -211,6 +222,7 @@ export class DiscordMessageProcessor { embedContent += markdown.toHTML(embed.footer.text, { discordCallback: this.getDiscordParseCallbacksHTML(msg), embed: true, + noExtraSpanTags: true, }) + "</p>"; } content += embedContent; @@ -229,6 +241,15 @@ export class DiscordMessageProcessor { return `<a href="${MATRIX_TO_LINK}${escapeHtml(memberId)}">${escapeHtml(memberName)}</a>`; } + public InsertSpoiler(node: ISpoilerNode, html: boolean = false): string { + // matrix spoilers are still in MSC stage + // see https://github.com/matrix-org/matrix-doc/pull/2010 + if (!html) { + return `(Spoiler: ${node.content})`; + } + return `<span data-mx-spoiler>${node.content}</span>`; + } + public InsertChannel(node: IDiscordNode): string { // unfortunately these callbacks are sync, so we flag our channel with some special stuff // and later on grab the real channel pill async @@ -332,6 +353,7 @@ export class DiscordMessageProcessor { everyone: (_) => this.InsertRoom(msg, "@everyone"), here: (_) => this.InsertRoom(msg, "@here"), role: (node) => this.InsertRole(node, msg), + spoiler: (node) => this.InsertSpoiler(node), user: (node) => this.InsertUser(node, msg), }; } @@ -343,6 +365,7 @@ export class DiscordMessageProcessor { everyone: (_) => this.InsertRoom(msg, "@everyone"), here: (_) => this.InsertRoom(msg, "@here"), role: (node) => this.InsertRole(node, msg, true), + spoiler: (node) => this.InsertSpoiler(node, true), user: (node) => this.InsertUser(node, msg, true), }; } diff --git a/src/matrixeventprocessor.ts b/src/matrixeventprocessor.ts index c0305aeb713997c906ee1f4c4d52437efe91b502..cdc6b5a952f71f32e221638442defe00f76ce266 100644 --- a/src/matrixeventprocessor.ts +++ b/src/matrixeventprocessor.ts @@ -55,6 +55,7 @@ export class MatrixEventProcessorOpts { export interface IMatrixEventProcessorResult { messageEmbed: Discord.RichEmbed; replyEmbed?: Discord.RichEmbed; + imageEmbed?: Discord.RichEmbed; } export class MatrixEventProcessor { @@ -179,11 +180,13 @@ export class MatrixEventProcessor { const embedSet = await this.EventToEmbed(event, chan); const opts: Discord.MessageOptions = {}; - const file = await this.HandleAttachment(event, mxClient); + const file = await this.HandleAttachment(event, mxClient, roomLookup.canSendEmbeds); if (typeof(file) === "string") { embedSet.messageEmbed.description += " " + file; + } else if ((file as Discord.FileOptions).name && (file as Discord.FileOptions).attachment) { + opts.file = file as Discord.FileOptions; } else { - opts.file = file; + embedSet.imageEmbed = file as Discord.RichEmbed; } await this.discord.send(embedSet, opts, roomLookup, event); @@ -293,7 +296,11 @@ export class MatrixEventProcessor { }; } - public async HandleAttachment(event: IMatrixEvent, mxClient: MatrixClient): Promise<string|Discord.FileOptions> { + public async HandleAttachment( + event: IMatrixEvent, + mxClient: MatrixClient, + sendEmbeds: boolean = false, + ): Promise<string|Discord.FileOptions|Discord.RichEmbed> { if (!this.HasAttachment(event)) { return ""; } @@ -305,7 +312,7 @@ export class MatrixEventProcessor { if (!event.content.info) { // Fractal sends images without an info, which is technically allowed // but super unhelpful: https://gitlab.gnome.org/World/fractal/issues/206 - event.content.info = {size: 0}; + event.content.info = {mimetype: "", size: 0}; } if (!event.content.url) { @@ -326,6 +333,10 @@ export class MatrixEventProcessor { } as Discord.FileOptions; } } + if (sendEmbeds && event.content.info.mimetype.split("/")[0] === "image") { + return new Discord.RichEmbed() + .setImage(url); + } return `[${name}](${url})`; } diff --git a/src/matrixmessageprocessor.ts b/src/matrixmessageprocessor.ts index cdaf95f7dcf9a3a79327e716657e62d1276d97ba..3f32ba89243f214f15868b532a3416f276bbf559 100644 --- a/src/matrixmessageprocessor.ts +++ b/src/matrixmessageprocessor.ts @@ -246,6 +246,21 @@ export class MatrixMessageProcessor { return msg; } + private async parseSpanContent(node: Parser.HTMLElement): Promise<string> { + const content = await this.walkChildNodes(node); + const attrs = node.attributes; + // matrix spoilers are still in MSC stage + // see https://github.com/matrix-org/matrix-doc/pull/2010 + if (attrs["data-mx-spoiler"] !== undefined) { + const spoilerReason = attrs["data-mx-spoiler"]; + if (spoilerReason) { + return `(${spoilerReason})||${content}||`; + } + return `||${content}||`; + } + return content; + } + private async parseUlContent(node: Parser.HTMLElement): Promise<string> { this.listDepth++; const entries = await this.arrayChildNodes(node, ["li"]); @@ -353,6 +368,8 @@ export class MatrixMessageProcessor { case "h6": const level = parseInt(nodeHtml.tagName[1], 10); return `**${"#".repeat(level)} ${await this.walkChildNodes(nodeHtml)}**\n`; + case "span": + return await this.parseSpanContent(nodeHtml); default: return await this.walkChildNodes(nodeHtml); } diff --git a/src/store.ts b/src/store.ts index ddb35cd2d9cc2e87e3855671fbf4305330cb809b..69812a8dd3bd2402a4c60b3c14157912148d3552 100644 --- a/src/store.ts +++ b/src/store.ts @@ -160,23 +160,30 @@ export class DiscordStore { } } - public async deleteUserToken(discordId: string): Promise<void> { + public async deleteUserToken(mxid: string): Promise<void> { + const res = await this.db.Get("SELECT * from user_id_discord_id WHERE user_id = $id", { + id: mxid, + }); + if (!res) { + return; + } + const discordId = res.discord_id; log.silly("SQL", "deleteUserToken => ", discordId); try { await Promise.all([ this.db.Run( ` - DELETE FROM user_id_discord_id WHERE discord_id = $id; + DELETE FROM user_id_discord_id WHERE discord_id = $id ` , { - $id: discordId, + id: discordId, }), this.db.Run( ` - DELETE FROM discord_id_token WHERE discord_id = $id; + DELETE FROM discord_id_token WHERE discord_id = $id ` , { - $id: discordId, + id: discordId, }), ]); } catch (err) { diff --git a/src/structures/lock.ts b/src/structures/lock.ts new file mode 100644 index 0000000000000000000000000000000000000000..f400aab092a3a6f49381d9c47b43112d0500be9d --- /dev/null +++ b/src/structures/lock.ts @@ -0,0 +1,60 @@ +export class Lock<T> { + private locks: Map<T, {i: NodeJS.Timeout|null, r: (() => void)|null}>; + private lockPromises: Map<T, Promise<{}>>; + constructor( + private timeout: number, + ) { + this.locks = new Map(); + this.lockPromises = new Map(); + } + + public set(key: T) { + // if there is a lock set.....we don't set a second one ontop + if (this.locks.has(key)) { + return; + } + + // set a dummy lock so that if we re-set again before releasing it won't do anthing + this.locks.set(key, {i: null, r: null}); + + const p = new Promise<{}>((resolve) => { + // first we check if the lock has the key....if not, e.g. if it + // got released too quickly, we still want to resolve our promise + if (!this.locks.has(key)) { + resolve(); + return; + } + // create the interval that will release our promise after the timeout + const i = setTimeout(() => { + this.release(key); + }, this.timeout); + // aaand store to our lock + this.locks.set(key, {r: resolve, i}); + }); + this.lockPromises.set(key, p); + } + + public release(key: T) { + // if there is nothing to release then there is nothing to release + if (!this.locks.has(key)) { + return; + } + const lock = this.locks.get(key)!; + if (lock.r !== null) { + lock.r(); + } + if (lock.i !== null) { + clearTimeout(lock.i); + } + this.locks.delete(key); + this.lockPromises.delete(key); + } + + public async wait(key: T) { + // we wait for a lock release only if a promise is present + const promise = this.lockPromises.get(key); + if (promise) { + await promise; + } + } +} diff --git a/test/structures/test_lock.ts b/test/structures/test_lock.ts new file mode 100644 index 0000000000000000000000000000000000000000..114ddb9065f0f11a9acb288b43f71c58227ba827 --- /dev/null +++ b/test/structures/test_lock.ts @@ -0,0 +1,45 @@ +/* +Copyright 2019 matrix-appservice-discord + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { expect } from "chai"; +import { Lock } from "../../src/structures/lock"; +import { Util } from "../../src/util"; + +const LOCKTIMEOUT = 300; + +describe("Lock", () => { + it("should lock and unlock", async () => { + const lock = new Lock<string>(LOCKTIMEOUT); + const t = Date.now(); + lock.set("bunny"); + await lock.wait("bunny"); + const diff = Date.now() - t; + expect(diff).to.be.greaterThan(LOCKTIMEOUT - 1); + }); + it("should lock and unlock early, if unlocked", async () => { + const SHORTDELAY = 100; + const DELAY_ACCURACY = 5; + const lock = new Lock<string>(LOCKTIMEOUT); + setTimeout(() => lock.release("fox"), SHORTDELAY); + const t = Date.now(); + lock.set("fox"); + await lock.wait("fox"); + const diff = Date.now() - t; + // accuracy can be off by a few ms soemtimes + expect(diff).to.be.greaterThan(SHORTDELAY - DELAY_ACCURACY); + expect(diff).to.be.lessThan(SHORTDELAY + DELAY_ACCURACY); + }); +}); diff --git a/test/test_discordbot.ts b/test/test_discordbot.ts index 10df5fd801e4b366a2d6b818e33787af382daa7b..a3e80852d899dfc207da66a9a1678e9b6b21e8ba 100644 --- a/test/test_discordbot.ts +++ b/test/test_discordbot.ts @@ -403,46 +403,4 @@ describe("DiscordBot", () => { assert.equal(expected, ITERATIONS); }); }); - describe("locks", () => { - it("should lock and unlock a channel", async () => { - const bot = new modDiscordBot.DiscordBot( - "", - config, - mockBridge, - {}, - ) as DiscordBot; - const chan = new MockChannel("123") as any; - const t = Date.now(); - bot.lockChannel(chan); - await bot.waitUnlock(chan); - const diff = Date.now() - t; - expect(diff).to.be.greaterThan(config.limits.discordSendDelay - 1); - }); - it("should lock and unlock a channel early, if unlocked", async () => { - const discordSendDelay = 500; - const SHORTDELAY = 100; - const MINEXPECTEDDELAY = 95; - const bot = new modDiscordBot.DiscordBot( - "", - { - bridge: { - domain: "localhost", - }, - limits: { - discordSendDelay, - }, - }, - mockBridge, - {}, - ) as DiscordBot; - const chan = new MockChannel("123") as any; - setTimeout(() => bot.unlockChannel(chan), SHORTDELAY); - const t = Date.now(); - bot.lockChannel(chan); - await bot.waitUnlock(chan); - const diff = Date.now() - t; - // Date accuracy can be off by a few ms sometimes. - expect(diff).to.be.greaterThan(MINEXPECTEDDELAY); - }); - }); }); diff --git a/test/test_discordmessageprocessor.ts b/test/test_discordmessageprocessor.ts index d66dd949e2cbe76da2780a1853e5acd6775ee4b6..2938472011f76dedcae9df1de1a2c32f3962289e 100644 --- a/test/test_discordmessageprocessor.ts +++ b/test/test_discordmessageprocessor.ts @@ -391,6 +391,18 @@ describe("DiscordMessageProcessor", () => { Chai.assert.equal(reply, "<span data-mx-color=\"#dead88\"><strong>@role</strong></span>"); }); }); + describe("InsertSpoiler / HTML", () => { + it("parses spoilers", () => { + const processor = new DiscordMessageProcessor( + new DiscordMessageProcessorOpts("localhost"), bot as DiscordBot); + const content = { content: "foxies" }; + let reply = processor.InsertSpoiler(content); + Chai.assert.equal(reply, "(Spoiler: foxies)"); + + reply = processor.InsertSpoiler(content, true); + Chai.assert.equal(reply, "<span data-mx-spoiler>foxies</span>"); + }); + }); describe("InsertEmoji", () => { it("inserts static emojis to their post-parse flag", () => { const processor = new DiscordMessageProcessor( diff --git a/test/test_matrixeventprocessor.ts b/test/test_matrixeventprocessor.ts index 03ba47112152cd1d654e5b66e2d071893d10d81f..91f350af5394cf007ff60cee86f9ad470ea32a5a 100644 --- a/test/test_matrixeventprocessor.ts +++ b/test/test_matrixeventprocessor.ts @@ -597,6 +597,22 @@ describe("MatrixEventProcessor", () => { } as IMatrixEvent, mxClient); expect(ret).equals("[filename.webm](https://localhost/8000000)"); }); + it("Should reply embeds on large info.size images if set", async () => { + const LARGE_FILE = 8000000; + const processor = createMatrixEventProcessor(); + const ret = await processor.HandleAttachment({ + content: { + body: "filename.jpg", + info: { + mimetype: "image/jpeg", + size: LARGE_FILE, + }, + msgtype: "m.image", + url: "mxc://localhost/8000000", + }, + } as IMatrixEvent, mxClient, true); + expect((ret as Discord.RichEmbed).image!.url).equals("https://localhost/8000000"); + }); it("Should handle stickers.", async () => { const processor = createMatrixEventProcessor(); const attachment = (await processor.HandleAttachment({ diff --git a/test/test_matrixmessageprocessor.ts b/test/test_matrixmessageprocessor.ts index 3319248d0c5df278b9e81dcde44346c365a2e23a..d49c14bfe46e450331d0ff5793ad2e22bc682ea5 100644 --- a/test/test_matrixmessageprocessor.ts +++ b/test/test_matrixmessageprocessor.ts @@ -192,6 +192,13 @@ code **##### tail** **###### foxies**`); }); + it("strips simple span tags", async () => { + const mp = new MatrixMessageProcessor(bot); + const guild = new MockGuild("1234"); + const msg = getHtmlMessage("<span>bunny</span>"); + const result = await mp.FormatMessage(msg, guild as any); + expect(result).is.equal("bunny"); + }); }); describe("FormatMessage / formatted_body / complex", () => { it("html unescapes stuff inside of code", async () => { @@ -340,6 +347,20 @@ code const result = await mp.FormatMessage(msg, guild as any); expect(result).is.equal("[*yay?*](http://example.com)"); }); + it("Handles spoilers", async () => { + const mp = new MatrixMessageProcessor(bot); + const guild = new MockGuild("1234"); + const msg = getHtmlMessage("<span data-mx-spoiler>foxies</span>"); + const result = await mp.FormatMessage(msg, guild as any); + expect(result).is.equal("||foxies||"); + }); + it("Handles spoilers with reason", async () => { + const mp = new MatrixMessageProcessor(bot); + const guild = new MockGuild("1234"); + const msg = getHtmlMessage("<span data-mx-spoiler=\"floof\">foxies</span>"); + const result = await mp.FormatMessage(msg, guild as any); + expect(result).is.equal("(floof)||foxies||"); + }); }); describe("FormatMessage / formatted_body / emoji", () => { it("Inserts emoji by name", async () => { diff --git a/tools/addRoomsToDirectory.ts b/tools/addRoomsToDirectory.ts index 5b2f1840b0290a7c4c4e01ef7453e44a73077482..11c8a62b15e7b975eced6d57a5a1d907d7dc5473 100644 --- a/tools/addRoomsToDirectory.ts +++ b/tools/addRoomsToDirectory.ts @@ -42,10 +42,11 @@ const optionDefinitions = [ }, { alias: "r", - defaultValue: "room-store.db", - description: "The location of the registration file.", - name: "reg", + defaultValue: "discord-registration.yaml", + description: "The AS registration file.", + name: "registration", type: String, + typeLabel: "<discord-registration.yaml>", }, ]; @@ -66,7 +67,7 @@ if (options.help) { process.exit(0); } -const {store, appservice} = ToolsHelper.getToolDependencies(options.config, options.reg); +const {store, appservice} = ToolsHelper.getToolDependencies(options.config, options.registration); async function run() { try { diff --git a/tools/addbot.ts b/tools/addbot.ts index b8ef769b7ad9c5e5e9fe4e372f370531290d81cd..2dda777537df95d53e6cbb8bdaed47175e89b2f6 100644 --- a/tools/addbot.ts +++ b/tools/addbot.ts @@ -20,9 +20,45 @@ limitations under the License. */ import * as yaml from "js-yaml"; import * as fs from "fs"; +import * as args from "command-line-args"; +import * as usage from "command-line-usage"; import { Util } from "../src/util"; -const yamlConfig = yaml.safeLoad(fs.readFileSync("config.yaml", "utf8")); +const optionDefinitions = [ + { + alias: "h", + description: "Display this usage guide.", + name: "help", + type: Boolean, + }, + { + alias: "c", + defaultValue: "config.yaml", + description: "The AS config file.", + name: "config", + type: String, + typeLabel: "<config.yaml>", + }, +]; + +const options = args(optionDefinitions); + +if (options.help) { + /* tslint:disable:no-console */ + console.log(usage([ + { + content: "A tool to obtain the Discord bot invitation URL.", + header: "Add bot", + }, + { + header: "Options", + optionList: optionDefinitions, + }, + ])); + process.exit(0); +} + +const yamlConfig = yaml.safeLoad(fs.readFileSync(options.config, "utf8")); if (yamlConfig === null) { console.error("You have an error in your discord config."); } diff --git a/tools/adminme.ts b/tools/adminme.ts index eaf4f0d9a4cb658373774d72e0bea97fb7414fbc..f8b28180a87a01e7330e0cfec1b5044556400060 100644 --- a/tools/adminme.ts +++ b/tools/adminme.ts @@ -40,6 +40,14 @@ const optionDefinitions = [ }, { alias: "r", + defaultValue: "discord-registration.yaml", + description: "The AS registration file.", + name: "registration", + type: String, + typeLabel: "<discord-registration.yaml>", + }, + { + alias: "m", description: "The roomid to modify", name: "roomid", type: String, diff --git a/tools/chanfix.ts b/tools/chanfix.ts index a4fa45f79aa3387f6baf166191ab35a9efef1085..3e86a22ae5df4138be3de7ded4b348edc6fee313 100644 --- a/tools/chanfix.ts +++ b/tools/chanfix.ts @@ -38,6 +38,14 @@ const optionDefinitions = [ type: String, typeLabel: "<config.yaml>", }, + { + alias: "r", + defaultValue: "discord-registration.yaml", + description: "The AS registration file.", + name: "registration", + type: String, + typeLabel: "<discord-registration.yaml>", + }, ]; const options = args(optionDefinitions); diff --git a/tools/ghostfix.ts b/tools/ghostfix.ts index f8e9d6c2e9825b41c8b0690bb16e57686ef69e39..1d0310b2369ed2d2374326f861321421ba010d0f 100644 --- a/tools/ghostfix.ts +++ b/tools/ghostfix.ts @@ -49,6 +49,14 @@ const optionDefinitions = [ type: String, typeLabel: "<config.yaml>", }, + { + alias: "r", + defaultValue: "discord-registration.yaml", + description: "The AS registration file.", + name: "registration", + type: String, + typeLabel: "<discord-registration.yaml>", + }, ]; const options = args(optionDefinitions);