From d3a719d42e356481f908c34b0aba9e585959881e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadeusz=20=E2=80=9Etadzik=E2=80=9D=20So=C5=9Bnierz?= Date: Fri, 29 Mar 2024 11:34:12 +0100 Subject: [PATCH 1/8] Use correct Slack ghosts to redact messages sent from Slack This makes us the original sender's intent to redact messages, so that we do not require an elevated PL to handle message deletion. --- src/BaseSlackHandler.ts | 1 + src/SlackEventHandler.ts | 13 +++++++++++-- src/SlackGhost.ts | 7 +++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/BaseSlackHandler.ts b/src/BaseSlackHandler.ts index c9f29c0c..ee7bf875 100644 --- a/src/BaseSlackHandler.ts +++ b/src/BaseSlackHandler.ts @@ -50,6 +50,7 @@ export interface ISlackEventMessageAttachment { } export interface ISlackMessageEvent extends ISlackEvent { + team?: string; team_domain?: string; team_id?: string; user?: string; diff --git a/src/SlackEventHandler.ts b/src/SlackEventHandler.ts index eeaa527f..aeefa4d9 100644 --- a/src/SlackEventHandler.ts +++ b/src/SlackEventHandler.ts @@ -291,8 +291,17 @@ export class SlackEventHandler extends BaseSlackHandler { } else if (msg.subtype === "message_deleted" && msg.deleted_ts) { const originalEvent = await this.main.datastore.getEventBySlackId(msg.channel, msg.deleted_ts); if (originalEvent) { - const botClient = this.main.botIntent.matrixClient; - await botClient.redactEvent(originalEvent.roomId, originalEvent.eventId); + const previousMessage = msg.previous_message; + if (!previousMessage) { + log.error("Cannot delete message with no previous_message:", msg); + return; + } + if (!previousMessage.user) { + log.error("Cannot redact Slack message if we don't know the original sender:", previousMessage); + return; + } + const ghost = await this.main.ghostStore.get(previousMessage.user, previousMessage.team_domain, previousMessage.team); + await ghost.redactEvent(originalEvent.roomId, originalEvent.eventId); return; } // If we don't have the event diff --git a/src/SlackGhost.ts b/src/SlackGhost.ts index 9fd574e2..b43a5edc 100644 --- a/src/SlackGhost.ts +++ b/src/SlackGhost.ts @@ -321,6 +321,13 @@ export class SlackGhost { return Slackdown.parse(body); } + public async redactEvent(roomId: string, eventId: string) { + if (!this._intent) { + throw Error('No intent associated with ghost'); + } + await this._intent.matrixClient.redactEvent(roomId, eventId); + } + public async sendInThread(roomId: string, text: string, slackRoomId: string, slackEventTs: string, replyEvent: IMatrixReplyEvent): Promise { const content = { From 2443e46c9490455db4175e9d4a08aed88c75299c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadeusz=20=E2=80=9Etadzik=E2=80=9D=20So=C5=9Bnierz?= Date: Fri, 29 Mar 2024 11:41:08 +0100 Subject: [PATCH 2/8] Use our Matrix bot to redact Matrix messages deleted on Slack --- src/SlackEventHandler.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/SlackEventHandler.ts b/src/SlackEventHandler.ts index aeefa4d9..1566c46b 100644 --- a/src/SlackEventHandler.ts +++ b/src/SlackEventHandler.ts @@ -296,12 +296,24 @@ export class SlackEventHandler extends BaseSlackHandler { log.error("Cannot delete message with no previous_message:", msg); return; } - if (!previousMessage.user) { + if (previousMessage.subtype === 'bot_message') { + // Sent from Matrix, try to remove it with our bot account + try { + const botClient = this.main.botIntent.matrixClient; + await botClient.redactEvent(originalEvent.roomId, originalEvent.eventId, "Deleted on Slack"); + } catch (err) { + log.error("Failed to remove message", previousMessage, "with our Matrix bot: insufficient power level? Error:", err); + return; + } + } + else if (!previousMessage.user) { log.error("Cannot redact Slack message if we don't know the original sender:", previousMessage); return; } - const ghost = await this.main.ghostStore.get(previousMessage.user, previousMessage.team_domain, previousMessage.team); - await ghost.redactEvent(originalEvent.roomId, originalEvent.eventId); + else { + const ghost = await this.main.ghostStore.get(previousMessage.user, previousMessage.team_domain, previousMessage.team); + await ghost.redactEvent(originalEvent.roomId, originalEvent.eventId); + } return; } // If we don't have the event From dd11c2a88abf727bea1a20759af3bf5c8eaf8c13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadeusz=20=E2=80=9Etadzik=E2=80=9D=20So=C5=9Bnierz?= Date: Thu, 4 Apr 2024 13:13:15 +0200 Subject: [PATCH 3/8] Don't delete all bot message with our main account --- src/SlackEventHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SlackEventHandler.ts b/src/SlackEventHandler.ts index 1566c46b..4b446213 100644 --- a/src/SlackEventHandler.ts +++ b/src/SlackEventHandler.ts @@ -296,7 +296,7 @@ export class SlackEventHandler extends BaseSlackHandler { log.error("Cannot delete message with no previous_message:", msg); return; } - if (previousMessage.subtype === 'bot_message') { + if (previousMessage.subtype === 'bot_message' && (previousMessage.bot_id === team.bot_id)) { // Sent from Matrix, try to remove it with our bot account try { const botClient = this.main.botIntent.matrixClient; From b4f2911c5a102b72825db225b3129887523bcdf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadeusz=20=E2=80=9Etadzik=E2=80=9D=20So=C5=9Bnierz?= Date: Fri, 5 Apr 2024 11:47:09 +0200 Subject: [PATCH 4/8] Refactor message deletion; fallback to using our bot if we can't determine a more fitting user --- src/SlackEventHandler.ts | 67 ++++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/src/SlackEventHandler.ts b/src/SlackEventHandler.ts index 4b446213..dee007a0 100644 --- a/src/SlackEventHandler.ts +++ b/src/SlackEventHandler.ts @@ -18,6 +18,7 @@ import { BaseSlackHandler, ISlackEvent, ISlackMessageEvent, ISlackUser } from ". import { BridgedRoom } from "./BridgedRoom"; import { Main, METRIC_RECEIVED_MESSAGE } from "./Main"; import { Logger } from "matrix-appservice-bridge"; +import { TeamEntry } from "./datastore/Models"; const log = new Logger("SlackEventHandler"); /** @@ -289,32 +290,10 @@ export class SlackEventHandler extends BaseSlackHandler { } } } else if (msg.subtype === "message_deleted" && msg.deleted_ts) { - const originalEvent = await this.main.datastore.getEventBySlackId(msg.channel, msg.deleted_ts); - if (originalEvent) { - const previousMessage = msg.previous_message; - if (!previousMessage) { - log.error("Cannot delete message with no previous_message:", msg); - return; - } - if (previousMessage.subtype === 'bot_message' && (previousMessage.bot_id === team.bot_id)) { - // Sent from Matrix, try to remove it with our bot account - try { - const botClient = this.main.botIntent.matrixClient; - await botClient.redactEvent(originalEvent.roomId, originalEvent.eventId, "Deleted on Slack"); - } catch (err) { - log.error("Failed to remove message", previousMessage, "with our Matrix bot: insufficient power level? Error:", err); - return; - } - } - else if (!previousMessage.user) { - log.error("Cannot redact Slack message if we don't know the original sender:", previousMessage); - return; - } - else { - const ghost = await this.main.ghostStore.get(previousMessage.user, previousMessage.team_domain, previousMessage.team); - await ghost.redactEvent(originalEvent.roomId, originalEvent.eventId); - } - return; + try { + await this.deleteMessage(msg, team); + } catch (err) { + log.error(err); } // If we don't have the event throw Error("unknown_message"); @@ -336,6 +315,42 @@ export class SlackEventHandler extends BaseSlackHandler { return room.onSlackMessage(msg); } + private async deleteMessage(msg: ISlackMessageEvent, team: TeamEntry): Promise { + const originalEvent = await this.main.datastore.getEventBySlackId(msg.channel, msg.deleted_ts!); + if (originalEvent) { + const previousMessage = msg.previous_message; + if (!previousMessage) { + throw new Error(`Cannot delete message with no previous_message: ${JSON.stringify(msg)}`); + } + + // Try to determine the Matrix user responsible for deleting the message, fallback to our main bot if all else fails + if (!previousMessage.user) { + log.warn("We don't know the original sender of", previousMessage, "will try to remove with our bot"); + } + + const isOurMessage = previousMessage.subtype === 'bot_message' && (previousMessage.bot_id === team.bot_id); + + if (previousMessage.user && !isOurMessage) { + try { + const ghost = await this.main.ghostStore.get(previousMessage.user, previousMessage.team_domain, previousMessage.team); + await ghost.redactEvent(originalEvent.roomId, originalEvent.eventId); + return; + } catch (err) { + log.warn(`Failed to remove message on behalf of ${previousMessage.user}, falling back to our bot`); + } + } + + try { + const botClient = this.main.botIntent.matrixClient; + await botClient.redactEvent(originalEvent.roomId, originalEvent.eventId, "Deleted on Slack"); + } catch (err) { + throw new Error( + `Failed to remove message ${JSON.stringify(previousMessage)} with our Matrix bot. insufficient power level? Error: ${err}` + ); + } + } + } + private async handleReaction(event: ISlackEventReaction, teamId: string) { // Reactions store the channel in the item const channel = event.item.channel; From 0fe144d0ba1b327c5291244c88f5562af7b92c6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadeusz=20=E2=80=9Etadzik=E2=80=9D=20So=C5=9Bnierz?= Date: Fri, 5 Apr 2024 15:18:54 +0200 Subject: [PATCH 5/8] Formalize our one-to-many relationship between Slack and Matrix events We may produce multiple Matrix events per Slack events, if the Slack events contains text with attachments. This should have been forbidden with a DB constraint, but for some reason it happened regardless. This updates the constraint to match reality, differentiating Matrix events by the type of incoming Slack event. For backwards compatibility reasons getEventBySlackId() returns the "main" Matrix event. --- src/BridgedRoom.ts | 5 +++-- src/SlackGhost.ts | 11 +++++++++-- src/datastore/Models.ts | 1 + src/datastore/postgres/PgDatastore.ts | 23 +++++++++++++---------- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/BridgedRoom.ts b/src/BridgedRoom.ts index 126d6815..f5632164 100644 --- a/src/BridgedRoom.ts +++ b/src/BridgedRoom.ts @@ -878,7 +878,7 @@ export class BridgedRoom { formatted_body: `${file.name}`, msgtype: "m.text", }; - await ghost.sendMessage(this.matrixRoomId, messageContent, channelId, slackEventId); + await ghost.sendMessage(this.matrixRoomId, messageContent, channelId, slackEventId, { type: "attachment" }); return; } @@ -917,7 +917,7 @@ export class BridgedRoom { formatted_body: htmlCode, msgtype: "m.text", }; - await ghost.sendMessage(this.matrixRoomId, messageContent, channelId, slackEventId); + await ghost.sendMessage(this.matrixRoomId, messageContent, channelId, slackEventId, { type: "attachment" }); return; } @@ -954,6 +954,7 @@ export class BridgedRoom { slackFileToMatrixMessage(file, fileContentUri, thumbnailContentUri), channelId, slackEventId, + { type: "attachment" }, ); } diff --git a/src/SlackGhost.ts b/src/SlackGhost.ts index 9fd574e2..e20e9dd8 100644 --- a/src/SlackGhost.ts +++ b/src/SlackGhost.ts @@ -19,7 +19,7 @@ import * as Slackdown from "Slackdown"; import { ISlackUser } from "./BaseSlackHandler"; import { WebClient } from "@slack/web-api"; import { BotsInfoResponse, UsersInfoResponse } from "./SlackResponses"; -import { UserEntry, Datastore } from "./datastore/Models"; +import { UserEntry, Datastore, EventEntryExtra } from "./datastore/Models"; import axios from "axios"; const log = new Logger("SlackGhost"); @@ -368,7 +368,13 @@ export class SlackGhost { await this.sendMessage(roomId, content, slackRoomID, slackEventTS); } - public async sendMessage(roomId: string, msg: Record, slackRoomId: string, slackEventTs: string): Promise<{event_id: string}> { + public async sendMessage( + roomId: string, + msg: Record, + slackRoomId: string, + slackEventTs: string, + eventExtras?: EventEntryExtra, + ): Promise<{event_id: string}> { if (!this._intent) { throw Error('No intent associated with ghost'); } @@ -383,6 +389,7 @@ export class SlackGhost { matrixEvent.event_id, slackRoomId, slackEventTs, + eventExtras, ); return { diff --git a/src/datastore/Models.ts b/src/datastore/Models.ts index 3d0f15b2..6b97ce17 100644 --- a/src/datastore/Models.ts +++ b/src/datastore/Models.ts @@ -50,6 +50,7 @@ export interface EventEntry { } export interface EventEntryExtra { + type?: 'attachment'; slackThreadMessages?: string[]; } diff --git a/src/datastore/postgres/PgDatastore.ts b/src/datastore/postgres/PgDatastore.ts index 87663c89..1416cb3e 100644 --- a/src/datastore/postgres/PgDatastore.ts +++ b/src/datastore/postgres/PgDatastore.ts @@ -64,7 +64,7 @@ export interface SchemaRunUserMessage { type SchemaRunFn = (db: IDatabase) => Promise; export class PgDatastore implements Datastore, ClientEncryptionStore, ProvisioningStore { - public static readonly LATEST_SCHEMA = 16; + public static readonly LATEST_SCHEMA = 17; public readonly postgresDb: IDatabase; constructor(connectionString: string) { @@ -180,15 +180,18 @@ export class PgDatastore implements Datastore, ClientEncryptionStore, Provisioni public async getEventBySlackId(slackChannel: string, slackTs: string): Promise { log.debug(`getEventBySlackId: ${slackChannel} ${slackTs}`); - return this.postgresDb.oneOrNone( - "SELECT * FROM events WHERE slackChannel = ${slackChannel} AND slackTs = ${slackTs} LIMIT 1", - { slackChannel, slackTs }, e => e && { - roomId: e.roomid, - eventId: e.eventid, - slackChannelId: slackChannel, - slackTs, - _extras: JSON.parse(e.extras), - }); + const events = await this.postgresDb.manyOrNone( + "SELECT * FROM events WHERE slackChannel = ${slackChannel} AND slackTs = ${slackTs}", + { slackChannel, slackTs } + ).then(entries => entries.map(e => ({ + roomId: e.roomid, + eventId: e.eventid, + slackChannelId: slackChannel, + slackTs, + _extras: JSON.parse(e.extras) as EventEntryExtra, + }))); + + return events.find(e => e._extras.type !== 'attachment') ?? null; } public async deleteEventByMatrixId(roomId: string, eventId: string): Promise { From 0f2f98944ca9844334240f73c76e6638a99cd008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadeusz=20=E2=80=9Etadzik=E2=80=9D=20So=C5=9Bnierz?= Date: Fri, 5 Apr 2024 16:06:34 +0200 Subject: [PATCH 6/8] Add forgotten DB migration --- src/datastore/postgres/schema/v17.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/datastore/postgres/schema/v17.ts diff --git a/src/datastore/postgres/schema/v17.ts b/src/datastore/postgres/schema/v17.ts new file mode 100644 index 00000000..d9b09b39 --- /dev/null +++ b/src/datastore/postgres/schema/v17.ts @@ -0,0 +1,11 @@ +import { IDatabase } from "pg-promise"; + +export const runSchema = async(db: IDatabase) => { + await db.none(` + ALTER TABLE events DROP CONSTRAINT cons_events_unique; + `); + + await db.none(` + ALTER TABLE events ADD CONSTRAINT cons_events_unique UNIQUE(eventid, roomid, slackchannel, slackts, extras); + `); +}; From a4fe71c628e6a4ace317aba328029863430e6b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadeusz=20=E2=80=9Etadzik=E2=80=9D=20So=C5=9Bnierz?= Date: Fri, 5 Apr 2024 16:55:32 +0200 Subject: [PATCH 7/8] Be more careful when dropping incoming messages Messages with a message_changed will have the same `ts` as the original message, but a different `event_ts`. We now check both when considering whether or not to drop a message, and log our reasons regardless. --- src/BridgedRoom.ts | 7 ++++--- src/SlackEventHandler.ts | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/BridgedRoom.ts b/src/BridgedRoom.ts index f5632164..f7c25764 100644 --- a/src/BridgedRoom.ts +++ b/src/BridgedRoom.ts @@ -699,8 +699,9 @@ export class BridgedRoom { await new Promise((r) => setTimeout(r, PUPPET_INCOMING_DELAY_MS)); } } - if (this.recentSlackMessages.includes(message.ts)) { + if (this.recentSlackMessages.includes(message.event_ts ?? message.ts)) { // We sent this, ignore. + log.debug('Ignoring message recently sent by us'); return; } try { @@ -712,8 +713,8 @@ export class BridgedRoom { } this.slackSendLock = this.slackSendLock.then(() => { // Check again - if (this.recentSlackMessages.includes(message.ts)) { - // We sent this, ignore + if (this.recentSlackMessages.includes(message.event_ts ?? message.ts)) { + log.debug('Ignoring message recently sent by us'); return; } return this.handleSlackMessage(message, ghost).catch((ex) => { diff --git a/src/SlackEventHandler.ts b/src/SlackEventHandler.ts index dee007a0..508bd085 100644 --- a/src/SlackEventHandler.ts +++ b/src/SlackEventHandler.ts @@ -284,6 +284,7 @@ export class SlackEventHandler extends BaseSlackHandler { if (msg.message.bot_id !== undefined) { // Check the edit wasn't sent by us if (msg.message.bot_id === team.bot_id) { + log.debug('Ignoring a message_changed since it was sent by us'); return; } else { msg.user_id = msg.message.bot_id; From ae509b0ea8296a28c4cc99f9335ade40602f0183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadeusz=20=E2=80=9Etadzik=E2=80=9D=20So=C5=9Bnierz?= Date: Fri, 5 Apr 2024 16:57:02 +0200 Subject: [PATCH 8/8] Support removing attachments from Slack messages We now track which attachments was linked to which Matrix message, and support both removing them individually and dropping the whole event "tree". --- src/BridgedRoom.ts | 63 +++++++++++++++++++++++++-- src/SlackEventHandler.ts | 41 ++--------------- src/datastore/Models.ts | 3 +- src/datastore/NedbDatastore.ts | 4 ++ src/datastore/postgres/PgDatastore.ts | 13 ++++-- 5 files changed, 78 insertions(+), 46 deletions(-) diff --git a/src/BridgedRoom.ts b/src/BridgedRoom.ts index f7c25764..0ccf525e 100644 --- a/src/BridgedRoom.ts +++ b/src/BridgedRoom.ts @@ -879,7 +879,7 @@ export class BridgedRoom { formatted_body: `${file.name}`, msgtype: "m.text", }; - await ghost.sendMessage(this.matrixRoomId, messageContent, channelId, slackEventId, { type: "attachment" }); + await ghost.sendMessage(this.matrixRoomId, messageContent, channelId, slackEventId, { attachment_id: file.id }); return; } @@ -918,7 +918,7 @@ export class BridgedRoom { formatted_body: htmlCode, msgtype: "m.text", }; - await ghost.sendMessage(this.matrixRoomId, messageContent, channelId, slackEventId, { type: "attachment" }); + await ghost.sendMessage(this.matrixRoomId, messageContent, channelId, slackEventId, { attachment_id: file.id }); return; } @@ -955,7 +955,7 @@ export class BridgedRoom { slackFileToMatrixMessage(file, fileContentUri, thumbnailContentUri), channelId, slackEventId, - { type: "attachment" }, + { attachment_id: file.id }, ); } @@ -1020,6 +1020,30 @@ export class BridgedRoom { const newMessageRich = substitutions.slackToMatrix(message.text!); const newMessage = ghost.prepareBody(newMessageRich); + // Check if any of the attachments have been deleted. + // Slack unfortunately puts a "tombstone" in both message versions in this event, + // so let's try to remove every single one even if we may have deleted it before. + for (const file of message.message?.files ?? []) { + if (file.mode === 'tombstone') { + const events = await this.main.datastore.getEventsBySlackId(channelId, message.previous_message!.ts); + const event = events.find(e => e._extras.attachment_id === file.id); + if (event) { + const team = message.team_id ? await this.main.datastore.getTeam(message.team_id) : null; + if (!team) { + log.warn("Cannot determine team for message", message, "so we cannot delete attachment", file.id); + continue; + } + try { + await this.deleteMessage(message, event, team); + } catch (err) { + log.warn(`Failed to delete attachment ${file.id}:`, err); + } + } else { + log.warn(`Tried to remove tombstoned attachmend ${file.id} but we didn't find a Matrix event for it`); + } + } + } + // The substitutions might make the messages the same if (previousMessage === newMessage) { log.debug("Ignoring edit message because messages are the same post-substitutions."); @@ -1240,6 +1264,39 @@ export class BridgedRoom { this.recentSlackMessages.shift(); } } + + public async deleteMessage(msg: ISlackMessageEvent, event: EventEntry, team: TeamEntry): Promise { + const previousMessage = msg.previous_message; + if (!previousMessage) { + throw new Error(`Cannot delete message with no previous_message: ${JSON.stringify(msg)}`); + } + + // Try to determine the Matrix user responsible for deleting the message, fallback to our main bot if all else fails + if (!previousMessage.user) { + log.warn("We don't know the original sender of", previousMessage, "will try to remove with our bot"); + } + + const isOurMessage = previousMessage.subtype === 'bot_message' && (previousMessage.bot_id === team.bot_id); + + if (previousMessage.user && !isOurMessage) { + try { + const ghost = await this.main.ghostStore.get(previousMessage.user, previousMessage.team_domain, previousMessage.team); + await ghost.redactEvent(event.roomId, event.eventId); + return; + } catch (err) { + log.warn(`Failed to remove message on behalf of ${previousMessage.user}, falling back to our bot`); + } + } + + try { + const botClient = this.main.botIntent.matrixClient; + await botClient.redactEvent(event.roomId, event.eventId, "Deleted on Slack"); + } catch (err) { + throw new Error( + `Failed to remove message ${JSON.stringify(previousMessage)} with our Matrix bot. insufficient power level? Error: ${err}` + ); + } + } } /** diff --git a/src/SlackEventHandler.ts b/src/SlackEventHandler.ts index 508bd085..e00c53eb 100644 --- a/src/SlackEventHandler.ts +++ b/src/SlackEventHandler.ts @@ -18,7 +18,7 @@ import { BaseSlackHandler, ISlackEvent, ISlackMessageEvent, ISlackUser } from ". import { BridgedRoom } from "./BridgedRoom"; import { Main, METRIC_RECEIVED_MESSAGE } from "./Main"; import { Logger } from "matrix-appservice-bridge"; -import { TeamEntry } from "./datastore/Models"; +import { EventEntry, TeamEntry } from "./datastore/Models"; const log = new Logger("SlackEventHandler"); /** @@ -292,7 +292,8 @@ export class SlackEventHandler extends BaseSlackHandler { } } else if (msg.subtype === "message_deleted" && msg.deleted_ts) { try { - await this.deleteMessage(msg, team); + const events = await this.main.datastore.getEventsBySlackId(msg.channel, msg.deleted_ts!); + await Promise.all(events.map(event => room.deleteMessage(msg, event, team))); } catch (err) { log.error(err); } @@ -316,42 +317,6 @@ export class SlackEventHandler extends BaseSlackHandler { return room.onSlackMessage(msg); } - private async deleteMessage(msg: ISlackMessageEvent, team: TeamEntry): Promise { - const originalEvent = await this.main.datastore.getEventBySlackId(msg.channel, msg.deleted_ts!); - if (originalEvent) { - const previousMessage = msg.previous_message; - if (!previousMessage) { - throw new Error(`Cannot delete message with no previous_message: ${JSON.stringify(msg)}`); - } - - // Try to determine the Matrix user responsible for deleting the message, fallback to our main bot if all else fails - if (!previousMessage.user) { - log.warn("We don't know the original sender of", previousMessage, "will try to remove with our bot"); - } - - const isOurMessage = previousMessage.subtype === 'bot_message' && (previousMessage.bot_id === team.bot_id); - - if (previousMessage.user && !isOurMessage) { - try { - const ghost = await this.main.ghostStore.get(previousMessage.user, previousMessage.team_domain, previousMessage.team); - await ghost.redactEvent(originalEvent.roomId, originalEvent.eventId); - return; - } catch (err) { - log.warn(`Failed to remove message on behalf of ${previousMessage.user}, falling back to our bot`); - } - } - - try { - const botClient = this.main.botIntent.matrixClient; - await botClient.redactEvent(originalEvent.roomId, originalEvent.eventId, "Deleted on Slack"); - } catch (err) { - throw new Error( - `Failed to remove message ${JSON.stringify(previousMessage)} with our Matrix bot. insufficient power level? Error: ${err}` - ); - } - } - } - private async handleReaction(event: ISlackEventReaction, teamId: string) { // Reactions store the channel in the item const channel = event.item.channel; diff --git a/src/datastore/Models.ts b/src/datastore/Models.ts index 6b97ce17..241a2429 100644 --- a/src/datastore/Models.ts +++ b/src/datastore/Models.ts @@ -50,7 +50,7 @@ export interface EventEntry { } export interface EventEntryExtra { - type?: 'attachment'; + attachment_id?: string; slackThreadMessages?: string[]; } @@ -114,6 +114,7 @@ export interface Datastore extends ProvisioningStore { upsertEvent(roomId: string, eventId: string, channelId: string, ts: string, extras?: EventEntryExtra): Promise; upsertEvent(roomIdOrEntry: EventEntry): Promise; getEventByMatrixId(roomId: string, eventId: string): Promise; + getEventsBySlackId(channelId: string, ts: string): Promise; getEventBySlackId(channelId: string, ts: string): Promise; deleteEventByMatrixId(roomId: string, eventId: string): Promise; diff --git a/src/datastore/NedbDatastore.ts b/src/datastore/NedbDatastore.ts index bc2ebce0..3d0d9282 100644 --- a/src/datastore/NedbDatastore.ts +++ b/src/datastore/NedbDatastore.ts @@ -241,6 +241,10 @@ export class NedbDatastore implements Datastore { return this.storedEventToEventEntry(storedEvent); } + public async getEventsBySlackId(channelId: string, ts: string): Promise { + return this.getEventBySlackId(channelId, ts).then(e => e ? [e] : []); + } + public async getEventBySlackId(channelId: string, ts: string): Promise { const storedEvent = await this.eventStore.getEntryByRemoteId(channelId, ts); if (!storedEvent) { diff --git a/src/datastore/postgres/PgDatastore.ts b/src/datastore/postgres/PgDatastore.ts index 1416cb3e..bbe0ced9 100644 --- a/src/datastore/postgres/PgDatastore.ts +++ b/src/datastore/postgres/PgDatastore.ts @@ -178,9 +178,8 @@ export class PgDatastore implements Datastore, ClientEncryptionStore, Provisioni }); } - public async getEventBySlackId(slackChannel: string, slackTs: string): Promise { - log.debug(`getEventBySlackId: ${slackChannel} ${slackTs}`); - const events = await this.postgresDb.manyOrNone( + public async getEventsBySlackId(slackChannel: string, slackTs: string): Promise { + return this.postgresDb.manyOrNone( "SELECT * FROM events WHERE slackChannel = ${slackChannel} AND slackTs = ${slackTs}", { slackChannel, slackTs } ).then(entries => entries.map(e => ({ @@ -190,8 +189,14 @@ export class PgDatastore implements Datastore, ClientEncryptionStore, Provisioni slackTs, _extras: JSON.parse(e.extras) as EventEntryExtra, }))); + } - return events.find(e => e._extras.type !== 'attachment') ?? null; + /** + * @deprecated One Slack event may map to many Matrix events -- use getEventsBySlackId() + */ + public async getEventBySlackId(slackChannel: string, slackTs: string): Promise { + const events = await this.getEventsBySlackId(slackChannel, slackTs); + return events.find(e => !e._extras.attachment_id) ?? null; } public async deleteEventByMatrixId(roomId: string, eventId: string): Promise {