Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

optionally make users wait to join bridged rooms #1645

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
9 changes: 7 additions & 2 deletions src/bridge/IrcBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd make this it's own function inside datastore personally, and have the logic for setting first seen time handled within. We might even be able to wrap the postgres stuff into one statement.

(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);
});
});
}
}
Expand Down
32 changes: 32 additions & 0 deletions src/bridge/MatrixHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,38 @@ 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);
jesopo marked this conversation as resolved.
Show resolved Hide resolved
// 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`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably need to let the user know why they are waiting :). Either a message to their admin room, or a link to a page.

this.ircBridge.appServiceUserId,
);
return;
}
}

let bridgedClient: BridgedClient|null = null;
try {
bridgedClient = await this.ircBridge.getBridgedClient(
Expand Down
4 changes: 4 additions & 0 deletions src/config/BridgeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ export interface BridgeConfig {
inactiveAfterDays?: number;
};
banLists?: MatrixBanSyncConfig;
delayBridging?: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these need more descriptions about what they do. It might even make sense to have this go under the membershipLists configuration where we configure Matrix joins.

We should certainly make it obvious that these are seconds.

plumbed: number,
portaled: number,
};
};
sentry?: {
enabled: boolean;
Expand Down
4 changes: 4 additions & 0 deletions src/datastore/DataStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,5 +192,9 @@ export interface DataStore {

getRoomCount(): Promise<number>;

getFirstSeenTimeForUser(userId: string): Promise<number|null>;

setFirstSeenTimeForUser(userId: string): Promise<void>;

destroy(): Promise<void>;
}
25 changes: 25 additions & 0 deletions src/datastore/NedbDataStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,31 @@ export class NeDBDataStore implements DataStore {
log.debug("Finished migrating rooms in database");
}

public async getFirstSeenTimeForUser(userId: string): Promise<number|null> {
const doc = await this.userStore.selectOne<unknown, {data: { first_seen_ts: number }}>({
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<void> {
let user = await this.userStore.getMatrixUser(userId);
jesopo marked this conversation as resolved.
Show resolved Hide resolved
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
}
Expand Down
28 changes: 21 additions & 7 deletions src/datastore/postgres/PgDataStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -714,6 +710,24 @@ export class PgDataStore implements DataStore {
return res.rows[0];
}

public async getFirstSeenTimeForUser(userId: string): Promise<number|null> {
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<void> {
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) {
Expand Down
10 changes: 10 additions & 0 deletions src/datastore/postgres/schema/v9.ts
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That might explode for all the existing items that don't have a value set.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see the line above it; we backfill first from last

`);
}