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] 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 {