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 {