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 cd7259389..f9d5bcec6 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?.secondsPortaled ?? 0 + : this.ircBridge.config.ircService.delayBridging?.secondsPlumbed ?? 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,6 +566,19 @@ export class MatrixHandler { } // get the virtual IRC user for this user promises.push((async () => { + 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; try { bridgedClient = await this.ircBridge.getBridgedClient( diff --git a/src/config/BridgeConfig.ts b/src/config/BridgeConfig.ts index eb02b5183..26ab2a88b 100644 --- a/src/config/BridgeConfig.ts +++ b/src/config/BridgeConfig.ts @@ -66,6 +66,10 @@ export interface BridgeConfig { inactiveAfterDays?: number; }; banLists?: MatrixBanSyncConfig; + delayBridging?: { + secondsPlumbed: number, + secondsPortaled: number, + }; }; sentry?: { enabled: boolean; diff --git a/src/datastore/DataStore.ts b/src/datastore/DataStore.ts index d54b5c077..77fbed079 100644 --- a/src/datastore/DataStore.ts +++ b/src/datastore/DataStore.ts @@ -192,5 +192,9 @@ export interface DataStore { getRoomCount(): Promise; + getFirstSeenTimeForUser(userId: string): Promise; + + setFirstSeenTimeForUser(userId: string): Promise; + destroy(): Promise; } diff --git a/src/datastore/NedbDataStore.ts b/src/datastore/NedbDataStore.ts index a9ab34dd7..b893b036f 100644 --- a/src/datastore/NedbDataStore.ts +++ b/src/datastore/NedbDataStore.ts @@ -771,6 +771,37 @@ export class NeDBDataStore implements DataStore { log.debug("Finished migrating rooms in database"); } + 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; + } + + return null; + } + + 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); + await this.userStore.setMatrixUser(user); + } + public async destroy() { // This will no-op } diff --git a/src/datastore/postgres/PgDataStore.ts b/src/datastore/postgres/PgDataStore.ts index a9b7f075e..2812d476d 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; @@ -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,6 +710,29 @@ export class PgDataStore implements DataStore { return res.rows[0]; } + 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; + } + + return null; + } + + 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 + ]); + } + public async destroy() { log.info("Destroy called"); if (this.hasEnded) { diff --git a/src/datastore/postgres/schema/v9.ts b/src/datastore/postgres/schema/v9.ts new file mode 100644 index 000000000..ee806d1e3 --- /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; + `); +}