From bf6d09c4900e8bbeec4c8f6127580fd01f24bc62 Mon Sep 17 00:00:00 2001 From: jesopo Date: Wed, 2 Nov 2022 16:57:59 +0000 Subject: [PATCH 01/10] optionally make users wait to join bridged rooms --- src/bridge/MatrixHandler.ts | 29 +++++++++++++++++++++++++++ src/datastore/DataStore.ts | 4 ++++ src/datastore/NedbDataStore.ts | 9 +++++++++ src/datastore/postgres/PgDataStore.ts | 9 +++++++++ 4 files changed, 51 insertions(+) diff --git a/src/bridge/MatrixHandler.ts b/src/bridge/MatrixHandler.ts index cd7259389..4dfb2a006 100644 --- a/src/bridge/MatrixHandler.ts +++ b/src/bridge/MatrixHandler.ts @@ -538,6 +538,35 @@ export class MatrixHandler { } // get the virtual IRC user for this user promises.push((async () => { + const entry = await this.ircBridge.getStore().getRoom(event.room_id, room.server.domain, room.channel); + // is this a portal room? + const delayTime = ["alias", "join"].includes( + (entry?.data?.origin as string|null) ?? "unknown" + //TODO: pull these two numbers from the config file + ) ? 3600 : 0; + + if (delayTime > 0) { + let remaining = delayTime; + const firstSeen = await this.ircBridge.getStore().getAccountFirstSeen(user.getId()); + if (firstSeen === null) { + await this.ircBridge.getStore().setAccountFirstSeen(user.getId(), new Date()); + } else { + remaining = Math.max(0, ((firstSeen.getTime() / 1000) + delayTime) - (new Date().getTime() / 1000)); + } + + if (remaining > 0) { + await this.membershipQueue.leave( + event.room_id, + user.getId(), + req, + true, + `Please wait ${remaining} seconds`, + this.ircBridge.appServiceUserId, + ); + return; + } + } + let bridgedClient: BridgedClient|null = null; try { bridgedClient = await this.ircBridge.getBridgedClient( diff --git a/src/datastore/DataStore.ts b/src/datastore/DataStore.ts index d54b5c077..349686dd5 100644 --- a/src/datastore/DataStore.ts +++ b/src/datastore/DataStore.ts @@ -192,5 +192,9 @@ export interface DataStore { getRoomCount(): Promise; + getAccountFirstSeen(userId: string): Promise; + + setAccountFirstSeen(userId: string, when: Date): Promise; + destroy(): Promise; } diff --git a/src/datastore/NedbDataStore.ts b/src/datastore/NedbDataStore.ts index a9ab34dd7..99c4dbda8 100644 --- a/src/datastore/NedbDataStore.ts +++ b/src/datastore/NedbDataStore.ts @@ -771,6 +771,15 @@ export class NeDBDataStore implements DataStore { log.debug("Finished migrating rooms in database"); } + //TODO + public async getAccountFirstSeen(userId: string): Promise { + return null; + } + + //TODO + public async setAccountFirstSeen(userId: string, when: Date): Promise { + } + public async destroy() { // This will no-op } diff --git a/src/datastore/postgres/PgDataStore.ts b/src/datastore/postgres/PgDataStore.ts index a9b7f075e..9205401ae 100644 --- a/src/datastore/postgres/PgDataStore.ts +++ b/src/datastore/postgres/PgDataStore.ts @@ -714,6 +714,15 @@ export class PgDataStore implements DataStore { return res.rows[0]; } + //TODO + public async getAccountFirstSeen(userId: string): Promise { + return null; + } + + //TODO + public async setAccountFirstSeen(userId: string, when: Date): Promise { + } + public async destroy() { log.info("Destroy called"); if (this.hasEnded) { From c14c7c943d981f61882528ec61775970ce45be37 Mon Sep 17 00:00:00 2001 From: jesopo Date: Wed, 9 Nov 2022 23:12:17 +0000 Subject: [PATCH 02/10] store first seen time in last_seen, move setFirstSeen --- src/bridge/IrcBridge.ts | 9 +++++++-- src/bridge/MatrixHandler.ts | 6 +++--- src/datastore/DataStore.ts | 4 ++-- src/datastore/NedbDataStore.ts | 26 +++++++++++++++++++++----- src/datastore/postgres/PgDataStore.ts | 27 ++++++++++++++++----------- 5 files changed, 49 insertions(+), 23 deletions(-) diff --git a/src/bridge/IrcBridge.ts b/src/bridge/IrcBridge.ts index e4c26e8b7..25d4bda7e 100644 --- a/src/bridge/IrcBridge.ts +++ b/src/bridge/IrcBridge.ts @@ -1072,8 +1072,13 @@ export class IrcBridge { if (userIds) { for (const userId of userIds) { this.activityTracker.setLastActiveTime(userId); - this.dataStore.updateLastSeenTimeForUser(userId).catch((ex) => { - log.warn(`Failed to bump last active time for ${userId} in database`, ex); + this.dataStore.getFirstSeenTimeForUser(userId).then((when) => { + (when === null + ? this.dataStore.setFirstSeenTimeForUser + : this.dataStore.updateLastSeenTimeForUser + )(userId).catch((ex) => { + log.warn(`Failed to bump first/last active time for ${userId} in database`, ex); + }); }); } } diff --git a/src/bridge/MatrixHandler.ts b/src/bridge/MatrixHandler.ts index 4dfb2a006..24b470263 100644 --- a/src/bridge/MatrixHandler.ts +++ b/src/bridge/MatrixHandler.ts @@ -547,11 +547,11 @@ export class MatrixHandler { if (delayTime > 0) { let remaining = delayTime; - const firstSeen = await this.ircBridge.getStore().getAccountFirstSeen(user.getId()); + const firstSeen = await this.ircBridge.getStore().getFirstSeenTimeForUser(user.getId()); if (firstSeen === null) { - await this.ircBridge.getStore().setAccountFirstSeen(user.getId(), new Date()); + // jeepers. this shouldn't happen! } else { - remaining = Math.max(0, ((firstSeen.getTime() / 1000) + delayTime) - (new Date().getTime() / 1000)); + remaining = Math.max(0, (firstSeen + (delayTime * 1000)) - Date.now()); } if (remaining > 0) { diff --git a/src/datastore/DataStore.ts b/src/datastore/DataStore.ts index 349686dd5..77fbed079 100644 --- a/src/datastore/DataStore.ts +++ b/src/datastore/DataStore.ts @@ -192,9 +192,9 @@ export interface DataStore { getRoomCount(): Promise; - getAccountFirstSeen(userId: string): Promise; + getFirstSeenTimeForUser(userId: string): Promise; - setAccountFirstSeen(userId: string, when: Date): Promise; + setFirstSeenTimeForUser(userId: string): Promise; destroy(): Promise; } diff --git a/src/datastore/NedbDataStore.ts b/src/datastore/NedbDataStore.ts index 99c4dbda8..3e2be83e9 100644 --- a/src/datastore/NedbDataStore.ts +++ b/src/datastore/NedbDataStore.ts @@ -771,13 +771,29 @@ export class NeDBDataStore implements DataStore { log.debug("Finished migrating rooms in database"); } - //TODO - public async getAccountFirstSeen(userId: string): Promise { - return null; + public async getFirstSeenTimeForUser(userId: string): Promise { + const doc = await this.userStore.selectOne({ + type: "matrix", + "id": userId, + "data.first_seen_ts": {$exists: true}, + }); + + if (doc !== null) { + return doc.data.first_seen_ts; + } else { + return null; + } } - //TODO - public async setAccountFirstSeen(userId: string, when: Date): Promise { + public async setFirstSeenTimeForUser(userId: string): Promise { + let user = await this.userStore.getMatrixUser(userId); + if (!user) { + user = new MatrixUser(userId); + } + const now = Date.now(); + user.set("first_seen_ts", now); + user.set("last_seen_ts", now); + await this.userStore.setMatrixUser(user); } public async destroy() { diff --git a/src/datastore/postgres/PgDataStore.ts b/src/datastore/postgres/PgDataStore.ts index 9205401ae..9b9090d6d 100644 --- a/src/datastore/postgres/PgDataStore.ts +++ b/src/datastore/postgres/PgDataStore.ts @@ -643,15 +643,11 @@ export class PgDataStore implements DataStore { } public async updateLastSeenTimeForUser(userId: string) { - const statement = PgDataStore.BuildUpsertStatement("last_seen", "(user_id)", [ - "user_id", - "ts", - ]); - await this.pgPool.query(statement, [userId, Date.now()]); + await this.pgPool.query("UPDATE last_seen SET last = $2 WHERE user_id = $1", [userId, Date.now()]); } public async getLastSeenTimeForUsers(): Promise<{ user_id: string; ts: number }[]> { - const res = await this.pgPool.query(`SELECT * FROM last_seen`); + const res = await this.pgPool.query(`SELECT user_id, last FROM last_seen`); return res.rows; } @@ -714,13 +710,22 @@ export class PgDataStore implements DataStore { return res.rows[0]; } - //TODO - public async getAccountFirstSeen(userId: string): Promise { - return null; + public async getFirstSeenTimeForUser(userId: string): Promise { + const res = await this.pgPool.query( + "SELECT first FROM last_seen WHERE user_id = $1;", [userId] + ); + if (res.rows) { + return res.rows[0].first; + } else { + return null; + } } - //TODO - public async setAccountFirstSeen(userId: string, when: Date): Promise { + public async setFirstSeenTimeForUser(userId: string): Promise { + const now = Date.now(); + await this.pgPool.query("INSERT INTO last_seen (user_id, first, last) VALUES ($1, $2, $3)", [ + userId, now, now + ]); } public async destroy() { From a6a430877db7a30cf34fd12784e8d9f16b77d409 Mon Sep 17 00:00:00 2001 From: jesopo Date: Wed, 9 Nov 2022 23:20:07 +0000 Subject: [PATCH 03/10] add schema migration script --- src/datastore/postgres/PgDataStore.ts | 2 +- src/datastore/postgres/schema/v9.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 src/datastore/postgres/schema/v9.ts diff --git a/src/datastore/postgres/PgDataStore.ts b/src/datastore/postgres/PgDataStore.ts index 9b9090d6d..8544d5cd0 100644 --- a/src/datastore/postgres/PgDataStore.ts +++ b/src/datastore/postgres/PgDataStore.ts @@ -55,7 +55,7 @@ interface RoomRecord { export class PgDataStore implements DataStore { private serverMappings: {[domain: string]: IrcServer} = {}; - public static readonly LATEST_SCHEMA = 8; + public static readonly LATEST_SCHEMA = 9; private pgPool: Pool; private hasEnded = false; private cryptoStore?: StringCrypto; diff --git a/src/datastore/postgres/schema/v9.ts b/src/datastore/postgres/schema/v9.ts new file mode 100644 index 000000000..1ed1517b2 --- /dev/null +++ b/src/datastore/postgres/schema/v9.ts @@ -0,0 +1,10 @@ +import { PoolClient } from "pg"; + +export async function runSchema(connection: PoolClient) { + await connection.query(` + ALTER TABLE last_seen ADD COLUMN first BIGINT; + ALTER TABLE last_seen RENAME COLUMN ts TO last; + UPDATE last_seen SET first = last"; + ALTER TABLE last_seen ALTER COLUMN first SET NOT NULL; + `); +} From 23043338114805daf61631371783c64685fb26d5 Mon Sep 17 00:00:00 2001 From: jesopo Date: Thu, 10 Nov 2022 13:37:05 +0000 Subject: [PATCH 04/10] else block lints --- src/bridge/MatrixHandler.ts | 3 ++- src/datastore/NedbDataStore.ts | 3 ++- src/datastore/postgres/PgDataStore.ts | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/bridge/MatrixHandler.ts b/src/bridge/MatrixHandler.ts index 24b470263..a989fa459 100644 --- a/src/bridge/MatrixHandler.ts +++ b/src/bridge/MatrixHandler.ts @@ -550,7 +550,8 @@ export class MatrixHandler { const firstSeen = await this.ircBridge.getStore().getFirstSeenTimeForUser(user.getId()); if (firstSeen === null) { // jeepers. this shouldn't happen! - } else { + } + else { remaining = Math.max(0, (firstSeen + (delayTime * 1000)) - Date.now()); } diff --git a/src/datastore/NedbDataStore.ts b/src/datastore/NedbDataStore.ts index 3e2be83e9..05f51091b 100644 --- a/src/datastore/NedbDataStore.ts +++ b/src/datastore/NedbDataStore.ts @@ -780,7 +780,8 @@ export class NeDBDataStore implements DataStore { if (doc !== null) { return doc.data.first_seen_ts; - } else { + } + else { return null; } } diff --git a/src/datastore/postgres/PgDataStore.ts b/src/datastore/postgres/PgDataStore.ts index 8544d5cd0..de25efb5c 100644 --- a/src/datastore/postgres/PgDataStore.ts +++ b/src/datastore/postgres/PgDataStore.ts @@ -716,7 +716,8 @@ export class PgDataStore implements DataStore { ); if (res.rows) { return res.rows[0].first; - } else { + } + else { return null; } } From 3cc0a91b5a397d85ee8633faf83bd96644992eed Mon Sep 17 00:00:00 2001 From: jesopo Date: Thu, 10 Nov 2022 13:48:04 +0000 Subject: [PATCH 05/10] add delayBridging to config --- src/bridge/MatrixHandler.ts | 4 +++- src/config/BridgeConfig.ts | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/bridge/MatrixHandler.ts b/src/bridge/MatrixHandler.ts index a989fa459..ffd5c2eab 100644 --- a/src/bridge/MatrixHandler.ts +++ b/src/bridge/MatrixHandler.ts @@ -543,7 +543,9 @@ export class MatrixHandler { const delayTime = ["alias", "join"].includes( (entry?.data?.origin as string|null) ?? "unknown" //TODO: pull these two numbers from the config file - ) ? 3600 : 0; + ) + ? this.ircBridge.config.ircService.delayBridging?.portaled ?? 0 + : this.ircBridge.config.ircService.delayBridging?.plumbed ?? 0; if (delayTime > 0) { let remaining = delayTime; diff --git a/src/config/BridgeConfig.ts b/src/config/BridgeConfig.ts index eb02b5183..9752ecb33 100644 --- a/src/config/BridgeConfig.ts +++ b/src/config/BridgeConfig.ts @@ -66,6 +66,10 @@ export interface BridgeConfig { inactiveAfterDays?: number; }; banLists?: MatrixBanSyncConfig; + delayBridging?: { + plumbed: number, + portaled: number, + }; }; sentry?: { enabled: boolean; From bb9030b64ac1857a46b2e437b97808808214fa62 Mon Sep 17 00:00:00 2001 From: jesopo Date: Thu, 10 Nov 2022 13:48:46 +0000 Subject: [PATCH 06/10] else/return lints --- src/datastore/NedbDataStore.ts | 5 ++--- src/datastore/postgres/PgDataStore.ts | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/datastore/NedbDataStore.ts b/src/datastore/NedbDataStore.ts index 05f51091b..5d97076a6 100644 --- a/src/datastore/NedbDataStore.ts +++ b/src/datastore/NedbDataStore.ts @@ -781,9 +781,8 @@ export class NeDBDataStore implements DataStore { if (doc !== null) { return doc.data.first_seen_ts; } - else { - return null; - } + + return null; } public async setFirstSeenTimeForUser(userId: string): Promise { diff --git a/src/datastore/postgres/PgDataStore.ts b/src/datastore/postgres/PgDataStore.ts index de25efb5c..045a385aa 100644 --- a/src/datastore/postgres/PgDataStore.ts +++ b/src/datastore/postgres/PgDataStore.ts @@ -717,9 +717,8 @@ export class PgDataStore implements DataStore { if (res.rows) { return res.rows[0].first; } - else { - return null; - } + + return null; } public async setFirstSeenTimeForUser(userId: string): Promise { From eec46e2cb09388e2160ed688e2d609eb23050b05 Mon Sep 17 00:00:00 2001 From: jesopo Date: Thu, 10 Nov 2022 13:53:33 +0000 Subject: [PATCH 07/10] errant speech mark in schema/v9.ts --- src/datastore/postgres/schema/v9.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/datastore/postgres/schema/v9.ts b/src/datastore/postgres/schema/v9.ts index 1ed1517b2..ee806d1e3 100644 --- a/src/datastore/postgres/schema/v9.ts +++ b/src/datastore/postgres/schema/v9.ts @@ -4,7 +4,7 @@ export async function runSchema(connection: PoolClient) { await connection.query(` ALTER TABLE last_seen ADD COLUMN first BIGINT; ALTER TABLE last_seen RENAME COLUMN ts TO last; - UPDATE last_seen SET first = last"; + UPDATE last_seen SET first = last; ALTER TABLE last_seen ALTER COLUMN first SET NOT NULL; `); } From 07760d35a29c95c21772b20d030cdcdb21b3df70 Mon Sep 17 00:00:00 2001 From: jesopo Date: Fri, 11 Nov 2022 11:41:26 +0000 Subject: [PATCH 08/10] protect from overwiring first_seen in setFirstSeenTimeForUser --- src/datastore/NedbDataStore.ts | 6 ++++++ src/datastore/postgres/PgDataStore.ts | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/datastore/NedbDataStore.ts b/src/datastore/NedbDataStore.ts index 5d97076a6..b893b036f 100644 --- a/src/datastore/NedbDataStore.ts +++ b/src/datastore/NedbDataStore.ts @@ -786,10 +786,16 @@ export class NeDBDataStore implements DataStore { } public async setFirstSeenTimeForUser(userId: string): Promise { + if (await this.getFirstSeenTimeForUser(userId) !== null) { + // we already have a first seen time, don't overwrite it + return; + } + let user = await this.userStore.getMatrixUser(userId); if (!user) { user = new MatrixUser(userId); } + const now = Date.now(); user.set("first_seen_ts", now); user.set("last_seen_ts", now); diff --git a/src/datastore/postgres/PgDataStore.ts b/src/datastore/postgres/PgDataStore.ts index 045a385aa..2812d476d 100644 --- a/src/datastore/postgres/PgDataStore.ts +++ b/src/datastore/postgres/PgDataStore.ts @@ -722,6 +722,11 @@ export class PgDataStore implements DataStore { } public async setFirstSeenTimeForUser(userId: string): Promise { + if (await this.getFirstSeenTimeForUser(userId) !== null) { + // we already have a first seen time, don't overwrite it + return; + } + const now = Date.now(); await this.pgPool.query("INSERT INTO last_seen (user_id, first, last) VALUES ($1, $2, $3)", [ userId, now, now From 7fde7522f23548dfb64df7912d93b1bd38e8985e Mon Sep 17 00:00:00 2001 From: jesopo Date: Fri, 11 Nov 2022 16:32:10 +0000 Subject: [PATCH 09/10] refactor delayed join lookup in to its own function --- src/bridge/MatrixHandler.ts | 69 +++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/src/bridge/MatrixHandler.ts b/src/bridge/MatrixHandler.ts index ffd5c2eab..f8464f7d4 100644 --- a/src/bridge/MatrixHandler.ts +++ b/src/bridge/MatrixHandler.ts @@ -489,6 +489,34 @@ export class MatrixHandler { return null; } + private async _checkForBlock(event: OnMemberEventData, room: IrcRoom, user: MatrixUser): Promise { + const entry = await this.ircBridge.getStore().getRoom(event.room_id, room.server.domain, room.channel); + // is this a portal room? + const delayTime = ["alias", "join"].includes( + (entry?.data?.origin as string|null) ?? "unknown" + //TODO: pull these two numbers from the config file + ) + ? this.ircBridge.config.ircService.delayBridging?.portaled ?? 0 + : this.ircBridge.config.ircService.delayBridging?.plumbed ?? 0; + + if (delayTime > 0) { + let remaining = delayTime; + const firstSeen = await this.ircBridge.getStore().getFirstSeenTimeForUser(user.getId()); + if (firstSeen === null) { + // jeepers. this shouldn't happen! + } + else { + remaining = Math.max(0, (firstSeen + (delayTime * 1000)) - Date.now()); + } + + if (remaining > 0) { + return `You must wait ${remaining} seconds before attempting to join a bridged channel`; + } + } + + return null; + } + private async _onJoin(req: BridgeRequest, event: OnMemberEventData, user: MatrixUser): Promise { req.log.info("onJoin: usr=%s rm=%s id=%s", event.state_key, event.room_id, event.event_id); @@ -538,36 +566,17 @@ export class MatrixHandler { } // get the virtual IRC user for this user promises.push((async () => { - const entry = await this.ircBridge.getStore().getRoom(event.room_id, room.server.domain, room.channel); - // is this a portal room? - const delayTime = ["alias", "join"].includes( - (entry?.data?.origin as string|null) ?? "unknown" - //TODO: pull these two numbers from the config file - ) - ? this.ircBridge.config.ircService.delayBridging?.portaled ?? 0 - : this.ircBridge.config.ircService.delayBridging?.plumbed ?? 0; - - if (delayTime > 0) { - let remaining = delayTime; - const firstSeen = await this.ircBridge.getStore().getFirstSeenTimeForUser(user.getId()); - if (firstSeen === null) { - // jeepers. this shouldn't happen! - } - else { - remaining = Math.max(0, (firstSeen + (delayTime * 1000)) - Date.now()); - } - - if (remaining > 0) { - await this.membershipQueue.leave( - event.room_id, - user.getId(), - req, - true, - `Please wait ${remaining} seconds`, - this.ircBridge.appServiceUserId, - ); - return; - } + const blockReason = await this._checkForBlock(event, room, user); + if (blockReason !== null) { + await this.membershipQueue.leave( + event.room_id, + user.getId(), + req, + true, + blockReason, + this.ircBridge.appServiceUserId, + ); + return; } let bridgedClient: BridgedClient|null = null; From 4fae8fdd22f72bdfab49d0e9658fef13b02a1ad4 Mon Sep 17 00:00:00 2001 From: jesopo Date: Fri, 11 Nov 2022 16:34:47 +0000 Subject: [PATCH 10/10] make it clear that block times are seconds --- src/bridge/MatrixHandler.ts | 4 ++-- src/config/BridgeConfig.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/bridge/MatrixHandler.ts b/src/bridge/MatrixHandler.ts index f8464f7d4..f9d5bcec6 100644 --- a/src/bridge/MatrixHandler.ts +++ b/src/bridge/MatrixHandler.ts @@ -496,8 +496,8 @@ export class MatrixHandler { (entry?.data?.origin as string|null) ?? "unknown" //TODO: pull these two numbers from the config file ) - ? this.ircBridge.config.ircService.delayBridging?.portaled ?? 0 - : this.ircBridge.config.ircService.delayBridging?.plumbed ?? 0; + ? this.ircBridge.config.ircService.delayBridging?.secondsPortaled ?? 0 + : this.ircBridge.config.ircService.delayBridging?.secondsPlumbed ?? 0; if (delayTime > 0) { let remaining = delayTime; diff --git a/src/config/BridgeConfig.ts b/src/config/BridgeConfig.ts index 9752ecb33..26ab2a88b 100644 --- a/src/config/BridgeConfig.ts +++ b/src/config/BridgeConfig.ts @@ -67,8 +67,8 @@ export interface BridgeConfig { }; banLists?: MatrixBanSyncConfig; delayBridging?: { - plumbed: number, - portaled: number, + secondsPlumbed: number, + secondsPortaled: number, }; }; sentry?: {