diff --git a/config/config.sample.yaml b/config/config.sample.yaml index 98e8c4d5..0d8b8330 100644 --- a/config/config.sample.yaml +++ b/config/config.sample.yaml @@ -40,7 +40,7 @@ bridge: disableRoomTopicNotifications: false # Enable reactions from Matrix to Discord. This will only respond to # the first reaction of each emoji and is intended for cases where - # the bridge is used by a single matrix used + # the bridge is used by a single matrix user enableMatrixReactions: false # Auto-determine the language of code blocks (this can be CPU-intensive) determineCodeLanguage: false diff --git a/src/bot.ts b/src/bot.ts index 0aba6d73..174a9732 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -19,6 +19,7 @@ import { DiscordClientFactory } from "./clientfactory"; import { DiscordStore } from "./store"; import { DbEmoji } from "./db/dbdataemoji"; import { DbEvent } from "./db/dbdataevent"; +import { DbReaction } from "./db/dbdatareaction"; import { DiscordMessageProcessor } from "./discordmessageprocessor"; import { IDiscordMessageParserResult } from "@mx-puppet/matrix-discord-parser"; import { MatrixEventProcessor, MatrixEventProcessorOpts, IMatrixEventProcessorResult } from "./matrixeventprocessor"; @@ -656,12 +657,20 @@ export class DiscordBot { log.info(`Got redact request for ${event.redacts}`); log.verbose(`Event:`, event); - const storeEvent = await this.store.Get(DbEvent, {matrix_id: `${event.redacts};${event.room_id}`}); + const storeEvent = await this.store.Get(DbEvent, { matrix_id: `${event.redacts};${event.room_id}` }); + const storeReaction = await this.store.Get(DbReaction, { matrix_id: `${event.redacts};${event.room_id}` }); - if (!storeEvent || !storeEvent.Result) { - log.warn(`Could not redact because the event was not in the store.`); - return; + if (storeEvent && storeEvent.Result) { + return this.ProcessMatrixRedactEvent(event, storeEvent); + } else if (storeReaction && storeReaction.Result) { + return this.ProcessMatrixRedactReaction(event, storeReaction); } + + log.warn(`Could not redact because the event was not in the store.`); + return; + } + + public async ProcessMatrixRedactEvent(event: IMatrixEvent, storeEvent: DbEvent) { log.info(`Redact event matched ${storeEvent.ResultCount} entries`); while (storeEvent.Next()) { log.info(`Deleting discord msg ${storeEvent.DiscordId}`); @@ -680,6 +689,31 @@ export class DiscordBot { } } + public async ProcessMatrixRedactReaction(event: IMatrixEvent, storeReaction: DbReaction) { + log.info(`Redact reaction matched ${storeReaction.ResultCount} entries`); + while (storeReaction.Next()) { + log.info(`Deleting discord reaction ${storeReaction.DiscordId}`); + const result = await this.LookupRoom(storeReaction.GuildId, storeReaction.ChannelId, event.sender); + const chan = result.channel; + + const msg = await chan.messages.fetch(storeReaction.DiscordId); + try { + this.channelLock.set(msg.channel.id); + + const reaction = msg.reactions.resolve(storeReaction.Emoji) + if (!reaction) { + log.warn(`Could not find reaction for emoji ${storeReaction.Emoji}`) + continue + } + await reaction.users.remove() + this.channelLock.release(msg.channel.id); + log.info(`Deleted reaction`); + } catch (ex) { + log.warn(`Failed to delete message`, ex); + } + } + } + public async ProcessMatrixReaction(event: IMatrixEvent) { if (!this.config.bridge.enableMatrixReactions) { return; @@ -692,7 +726,13 @@ export class DiscordBot { const relatesTo = event.content['m.relates_to']; - const storeEvent = await this.store.Get(DbEvent, { matrix_id: `${relatesTo.event_id};${event.room_id}` }); + const matrixID = `${relatesTo.event_id};${event.room_id}` + const storeEvent = await this.store.Get(DbEvent, { matrix_id: matrixID }); + const storeReaction = await this.store.Get(DbReaction, { matrix_id: matrixID, emoji: relatesTo.key }); + + if (storeReaction && storeReaction.Result) { + log.verbose(`ignoring duplicate reaction`) + } if (!storeEvent || !storeEvent.Result) { log.warn(`Could not react because the event was not in the store.`); @@ -713,6 +753,19 @@ export class DiscordBot { } catch (ex) { log.warn(`Failed to react to message`, ex); } + + try { + const dbReaction = new DbReaction() + dbReaction.MatrixId = `${event.event_id};${event.room_id}`; + dbReaction.DiscordId = storeEvent.DiscordId; + dbReaction.ChannelId = storeEvent.ChannelId; + dbReaction.GuildId = storeEvent.GuildId; + dbReaction.Emoji = relatesTo.key; + await this.store.Insert(dbReaction); + } catch (ex) { + log.warn(`Failed to store reaction event`) + log.verbose(ex) + } } } diff --git a/src/db/dbdatareaction.ts b/src/db/dbdatareaction.ts new file mode 100644 index 00000000..f803f787 --- /dev/null +++ b/src/db/dbdatareaction.ts @@ -0,0 +1,159 @@ +/* +Copyright 2017, 2018 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 { DiscordStore } from "../store"; +import { ISqlCommandParameters } from "./connector"; +import { IDbDataMany } from "./dbdatainterface"; + +export class DbReaction implements IDbDataMany { + public MatrixId: string; + public DiscordId: string; + public GuildId: string; + public ChannelId: string; + public Result: boolean; + public Emoji: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private rows: any[]; + + get ResultCount(): number { + return this.rows.length; + } + + public async RunQuery(store: DiscordStore, params: ISqlCommandParameters): Promise { + this.rows = []; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let rowsM: any[] | null = null; + if (params.matrix_id && params.emoji) { + rowsM = await store.db.All(` + SELECT * + FROM reaction_store + WHERE matrix_id = $id AND emoji = $emoji`, { + id: params.matrix_id, + emoji: params.emoji, + }); + } else if (params.matrix_id) { + rowsM = await store.db.All(` + SELECT * + FROM reaction_store + WHERE matrix_id = $id`, { + id: params.matrix_id + }); + } else if (params.discord_id) { + rowsM = await store.db.All(` + SELECT * + FROM reaction_store + WHERE discord_id = $id`, { + id: params.discord_id, + }); + } else { + throw new Error("Unknown/incorrect id given as a param"); + } + + for (const rowM of rowsM) { + const row = { + /* eslint-disable @typescript-eslint/naming-convention */ + discord_id: rowM.discord_id, + matrix_id: rowM.matrix_id, + emoji: rowM.emoji, + /* eslint-enable @typescript-eslint/naming-convention */ + }; + for (const rowD of await store.db.All(` + SELECT * + FROM discord_msg_store + WHERE msg_id = $id`, { + id: rowM.discord_id, + })) { + this.rows.push({ + /* eslint-disable @typescript-eslint/naming-convention */ + ...row, + guild_id: rowD.guild_id, + channel_id: rowD.channel_id, + /* eslint-enable @typescript-eslint/naming-convention */ + }); + } + } + this.Result = this.rows.length !== 0; + } + + public Next(): boolean { + if (!this.Result || this.ResultCount === 0) { + return false; + } + const item = this.rows.shift(); + this.MatrixId = item.matrix_id; + this.DiscordId = item.discord_id; + this.Emoji = item.emoji; + this.GuildId = item.guild_id; + this.ChannelId = item.channel_id; + return true; + } + + public async Insert(store: DiscordStore): Promise { + await store.db.Run(` + INSERT INTO reaction_store + (matrix_id,discord_id,emoji) + VALUES ($matrix_id,$discord_id,$emoji);`, { + /* eslint-disable @typescript-eslint/naming-convention */ + discord_id: this.DiscordId, + matrix_id: this.MatrixId, + emoji: this.Emoji, + /* eslint-enable @typescript-eslint/naming-convention */ + }); + // Check if the discord item exists? + const msgExists = await store.db.Get(` + SELECT * + FROM discord_msg_store + WHERE msg_id = $id`, { + id: this.DiscordId, + }) != null; + if (msgExists) { + return; + } + return store.db.Run(` + INSERT INTO discord_msg_store + (msg_id, guild_id, channel_id) + VALUES ($msg_id, $guild_id, $channel_id);`, { + /* eslint-disable @typescript-eslint/naming-convention */ + channel_id: this.ChannelId, + guild_id: this.GuildId, + msg_id: this.DiscordId, + /* eslint-enable @typescript-eslint/naming-convention */ + }); + } + + public async Update(store: DiscordStore): Promise { + throw new Error("Update is not implemented"); + } + + public async Delete(store: DiscordStore): Promise { + await store.db.Run(` + DELETE FROM reaction_store + WHERE matrix_id = $matrix_id + AND discord_id = $discord_id;`, { + /* eslint-disable @typescript-eslint/naming-convention */ + discord_id: this.DiscordId, + matrix_id: this.MatrixId, + /* eslint-enable @typescript-eslint/naming-convention */ + }); + return store.db.Run(` + DELETE FROM discord_msg_store + WHERE msg_id = $discord_id;`, { + /* eslint-disable @typescript-eslint/naming-convention */ + discord_id: this.DiscordId, + /* eslint-enable @typescript-eslint/naming-convention */ + }); + } +} diff --git a/src/db/schema/v13.ts b/src/db/schema/v13.ts new file mode 100644 index 00000000..492beb86 --- /dev/null +++ b/src/db/schema/v13.ts @@ -0,0 +1,37 @@ +/* +Copyright 2017, 2018 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 { DiscordStore } from "../../store"; +import { IDbSchema } from "./dbschema"; + +export class Schema implements IDbSchema { + public description = "create event_store table"; + public async run(store: DiscordStore): Promise { + await store.createTable(` + CREATE TABLE reaction_store ( + matrix_id TEXT NOT NULL, + discord_id TEXT NOT NULL, + emoji TEXT NOT NULL, + PRIMARY KEY(matrix_id, discord_id) + );`, "event_store"); + } + + public async rollBack(store: DiscordStore): Promise { + await store.db.Run( + `DROP TABLE IF EXISTS reaction_store;`, + ); + } +} diff --git a/src/store.ts b/src/store.ts index 9cbed771..9877c6f6 100644 --- a/src/store.ts +++ b/src/store.ts @@ -27,7 +27,7 @@ import { DbUserStore } from "./db/userstore"; import { IAppserviceStorageProvider } from "matrix-bot-sdk"; import { UserActivitySet, UserActivity } from "matrix-appservice-bridge"; const log = new Log("DiscordStore"); -export const CURRENT_SCHEMA = 12; +export const CURRENT_SCHEMA = 13; /** * Stores data for specific users and data not specific to rooms. */