Skip to content

Commit 4ae9a35

Browse files
Half-Shotheftig
andauthored
Add tests to ensure publicity syncing works for +s modes (#1698)
* PgDataStore: Fix syntax of array parameters Arrays passed as query parameters become PostgreSQL arrays. However, `foo IN $1` is not valid syntax, as `IN` can only be used with subqueries (`foo IN (SELECT ...)`) and lists (`foo IN ($1, $2)`), which require parentheses. `foo IN ($1)` is interpreted as a list and the expression is compared with the whole array, not checked against its members. The correct syntax for checking if a value matches any member of an array parameter is `foo = ANY($1)`, described in https://www.postgresql.org/docs/15/functions-comparisons.html#id-1.5.8.30.16. Fix `getIrcChannelsForRoomIds` and `getRoomsVisibility`. While we're at it, replace `getMappingsForChannelByOrigin`'s workable but awkward building of a parameter list. Signed-off-by: Jan Alexander Steffens (heftig) <[email protected]> * Add tests for publicity-syncing * Await publicity * Refactor to fix publicity sync * Refactor * changelog * Ensure PgDataStore returns private for missing entries --------- Signed-off-by: Jan Alexander Steffens (heftig) <[email protected]> Co-authored-by: Jan Alexander Steffens (heftig) <[email protected]>
1 parent 97a4c97 commit 4ae9a35

File tree

8 files changed

+119
-59
lines changed

8 files changed

+119
-59
lines changed

changelog.d/1660.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix SQL syntax errors in the PostgreSQL data store.

changelog.d/1698.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add tests that cover publicity syncing behaviour.

spec/integ/publicity.spec.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
2+
import { Request } from "matrix-appservice-bridge";
3+
import { IrcBridge } from "../../lib/bridge/IrcBridge";
4+
import { BridgeRequest } from "../../lib/models/BridgeRequest";
5+
import envBundle from "../util/env-bundle";
6+
7+
describe("Publicity Syncing", function() {
8+
const {env, roomMapping, test} = envBundle();
9+
10+
beforeEach(async () => {
11+
await test.beforeEach(env);
12+
13+
env.ircMock._autoConnectNetworks(
14+
roomMapping.server, roomMapping.botNick, roomMapping.server
15+
);
16+
env.ircMock._autoJoinChannels(
17+
roomMapping.server, roomMapping.botNick, roomMapping.channel
18+
);
19+
20+
await test.initEnv(env);
21+
});
22+
23+
afterEach(async () => test.afterEach(env));
24+
25+
it("should ensure rooms with no visibility state are private", async () => {
26+
const ircBridge: IrcBridge = env.ircBridge as IrcBridge;
27+
const store = ircBridge.getStore();
28+
const roomVis = await store.getRoomsVisibility([roomMapping.roomId]);
29+
expect(roomVis.get(roomMapping.roomId)).toBe('private');
30+
});
31+
32+
it("should ensure rooms with +s channels are set to private visibility", async () => {
33+
const ircBridge: IrcBridge = env.ircBridge as IrcBridge;
34+
const store = ircBridge.getStore();
35+
// Ensure opposite state
36+
await store.setRoomVisibility(roomMapping.roomId, "public");
37+
const req = new BridgeRequest(new Request({
38+
data: {
39+
isFromIrc: true,
40+
}
41+
}));
42+
const server = ircBridge.getServer(roomMapping.server)!;
43+
await ircBridge.ircHandler.roomAccessSyncer.onMode(
44+
req, server, roomMapping.channel, "", "s", true, null
45+
);
46+
const roomVis = await store.getRoomsVisibility([roomMapping.roomId]);
47+
expect(roomVis.get(roomMapping.roomId)).toBe('private');
48+
});
49+
50+
it("should ensure rooms with -s channels are set to public visibility", async () => {
51+
const ircBridge: IrcBridge = env.ircBridge as IrcBridge;
52+
const store = ircBridge.getStore();
53+
const req = new BridgeRequest(new Request({
54+
data: {
55+
isFromIrc: true,
56+
}
57+
}));
58+
const server = ircBridge.getServer(roomMapping.server)!;
59+
await ircBridge.ircHandler.roomAccessSyncer.onMode(
60+
req, server, roomMapping.channel, "", "s", false, null
61+
);
62+
const roomVis = await store.getRoomsVisibility([roomMapping.roomId]);
63+
expect(roomVis.get(roomMapping.roomId)).toBe('public');
64+
});
65+
});

src/bridge/IrcBridge.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -802,7 +802,7 @@ export class IrcBridge {
802802
log.info("Connecting to IRC networks...");
803803
await this.connectToIrcNetworks();
804804

805-
promiseutil.allSettled(this.ircServers.map((server) => {
805+
await promiseutil.allSettled(this.ircServers.map((server) => {
806806
// Call MODE on all known channels to get modes of all channels
807807
return Bluebird.cast(this.publicitySyncer.initModes(server));
808808
})).catch((err) => {

src/bridge/PublicitySyncer.ts

Lines changed: 25 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ export class PublicitySyncer {
4848

4949
public async initModes (server: IrcServer) {
5050
//Get all channels and call modes for each one
51-
5251
const channels = await this.ircBridge.getStore().getTrackedChannelsForServer(server.domain);
52+
log.info(`Syncing modes for ${channels.length} on ${server.domain}`);
5353
await Promise.all(channels.map((channel) =>
5454
this.initModeQueue.enqueue(`${channel}@${server.domain}`, {
5555
channel,
@@ -61,47 +61,40 @@ export class PublicitySyncer {
6161
/**
6262
* Returns the key used when calling `updateVisibilityMap` for updating an IRC channel
6363
* visibility mode (+s or -s).
64-
* ```
65-
* // Set channel on server to be +s
66-
* const key = publicitySyncer.getIRCVisMapKey(server.getNetworkId(), channel);
67-
* publicitySyncer.updateVisibilityMap(true, key, true);
68-
* ```
6964
* @param {string} networkId
7065
* @param {string} channel
7166
* @returns {string}
7267
*/
73-
public getIRCVisMapKey(networkId: string, channel: string) {
68+
private getIRCVisMapKey(networkId: string, channel: string) {
7469
return `${networkId} ${channel}`;
7570
}
7671

77-
public updateVisibilityMap(isMode: boolean, key: string, value: boolean, channel: string, server: IrcServer) {
78-
log.debug(`updateVisibilityMap: isMode:${isMode} k:${key} v:${value} chan:${channel} srv:${server.domain}`);
72+
/**
73+
* Update the visibility of a given channel
74+
*
75+
* @param isSecret Is the channel secret.
76+
* @param channel Channel name
77+
* @param server Server the channel is part of.
78+
* @returns If the channel publicity was synced.
79+
*/
80+
public async updateVisibilityMap(channel: string, server: IrcServer, isSecret: boolean): Promise<boolean> {
81+
const key = this.getIRCVisMapKey(server.getNetworkId(), channel);
82+
log.debug(`updateVisibilityMap ${key} isSecret:${isSecret}`);
7983
let hasChanged = false;
80-
if (isMode) {
81-
if (typeof value !== 'boolean') {
82-
throw new Error('+s state must be indicated with a boolean');
83-
}
84-
if (this.visibilityMap.channelIsSecret.get(key) !== value) {
85-
this.visibilityMap.channelIsSecret.set(key, value);
86-
hasChanged = true;
87-
}
88-
}
89-
else {
90-
if (typeof value !== 'string' || (value !== "private" && value !== "public")) {
91-
throw new Error('Room visibility must = "private" | "public"');
92-
}
93-
94-
if (this.visibilityMap.roomVisibilities.get(key) !== value) {
95-
this.visibilityMap.roomVisibilities.set(key, value);
96-
hasChanged = true;
97-
}
84+
if (this.visibilityMap.channelIsSecret.get(key) !== isSecret) {
85+
this.visibilityMap.channelIsSecret.set(key, isSecret);
86+
hasChanged = true;
9887
}
9988

10089
if (hasChanged) {
101-
this.solveVisibility(channel, server).catch((err: Error) => {
102-
log.error(`Failed to sync publicity for ${channel}: ` + err.message);
103-
});
90+
try {
91+
await this.solveVisibility(channel, server)
92+
}
93+
catch (err) {
94+
throw Error(`Failed to sync publicity for ${channel}: ${err.message}`);
95+
}
10496
}
97+
return hasChanged;
10598
}
10699

107100
/* Solve any inconsistencies between the currently known state of channels '+s' modes
@@ -126,17 +119,8 @@ export class PublicitySyncer {
126119
this.visibilityMap.mappings = new Map();
127120

128121
// Update rooms to correct visibilities
129-
let currentStates: Map<string, MatrixDirectoryVisibility> = new Map();
130-
131-
// Assume private by default
132-
for (const roomId of roomIds) {
133-
currentStates.set(roomId, "private");
134-
}
135-
136-
currentStates = {
137-
...currentStates,
138-
...await this.ircBridge.getStore().getRoomsVisibility(roomIds),
139-
};
122+
const currentStates: Map<string, MatrixDirectoryVisibility>
123+
= await this.ircBridge.getStore().getRoomsVisibility(roomIds);
140124

141125
const correctState = this.visibilityMap.channelIsSecret.get(visKey) ? 'private' : 'public';
142126

src/bridge/RoomAccessSyncer.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -423,17 +423,23 @@ export class RoomAccessSyncer {
423423
req.log.info("Not syncing publicity: shouldPublishRooms is false");
424424
return;
425425
}
426-
const key = this.ircBridge.publicitySyncer.getIRCVisMapKey(
427-
server.getNetworkId(), channel
428-
);
429426

427+
try {
428+
// Update the visibility for all rooms connected to this channel
429+
await this.ircBridge.publicitySyncer.updateVisibilityMap(
430+
channel, server, enabled,
431+
);
432+
}
433+
catch (ex) {
434+
log.error(
435+
`Failed to update visibility map for ${channel} ${server.getNetworkId()}: ${ex}`
436+
);
437+
}
438+
439+
// Only set this after we've applied the changes.
430440
matrixRooms.map((room) => {
431441
this.ircBridge.getStore().setModeForRoom(room.getId(), "s", enabled);
432442
});
433-
// Update the visibility for all rooms connected to this channel
434-
this.ircBridge.publicitySyncer.updateVisibilityMap(
435-
true, key, enabled, channel, server,
436-
);
437443
}
438444
// "k" and "i"
439445
await Promise.all(matrixRooms.map((room) =>

src/datastore/NedbDataStore.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,8 +305,8 @@ export class NeDBDataStore implements DataStore {
305305
*/
306306
public async getMatrixRoomsForChannel(server: IrcServer, channel: string): Promise<MatrixRoom[]> {
307307
const ircRoom = new IrcRoom(server, channel);
308-
return await this.roomStore.getLinkedMatrixRooms(
309-
IrcRoom.createId(ircRoom.getServer(), ircRoom.getChannel())
308+
return this.roomStore.getLinkedMatrixRooms(
309+
ircRoom.getId()
310310
);
311311
}
312312

src/datastore/postgres/PgDataStore.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ export class PgDataStore implements DataStore, ProvisioningStore {
255255

256256
public async getIrcChannelsForRoomIds(roomIds: string[]): Promise<{ [roomId: string]: IrcRoom[] }> {
257257
const entries = await this.pgPool.query(
258-
"SELECT room_id, irc_domain, irc_channel FROM rooms WHERE room_id IN $1",
258+
"SELECT room_id, irc_domain, irc_channel FROM rooms WHERE room_id = ANY($1)",
259259
[roomIds]
260260
);
261261
const mapping: { [roomId: string]: IrcRoom[] } = {};
@@ -292,14 +292,14 @@ export class PgDataStore implements DataStore, ProvisioningStore {
292292
if (!Array.isArray(origin)) {
293293
origin = [origin];
294294
}
295-
const inStatement = origin.map((_, i) => `\$${i + 3}`).join(", ");
296295
const entries = await this.pgPool.query<RoomRecord>(
297-
`SELECT * FROM rooms WHERE irc_domain = $1 AND irc_channel = $2 AND origin IN (${inStatement})`,
296+
"SELECT * FROM rooms WHERE irc_domain = $1 AND irc_channel = $2 AND origin = ANY($3)",
298297
[
299298
server.domain,
300299
// Channels must be lowercase
301300
toIrcLowerCase(channel),
302-
].concat(origin));
301+
origin,
302+
]);
303303
return entries.rows.map((e) => PgDataStore.pgToRoomEntry(e));
304304
}
305305

@@ -663,10 +663,13 @@ export class PgDataStore implements DataStore, ProvisioningStore {
663663
}
664664

665665
public async getRoomsVisibility(roomIds: string[]): Promise<Map<string, MatrixDirectoryVisibility>> {
666-
const map: Map<string, MatrixDirectoryVisibility> = new Map();
667-
const res = await this.pgPool.query("SELECT room_id, visibility FROM room_visibility WHERE room_id IN $1", [
668-
roomIds,
669-
]);
666+
const map: Map<string, MatrixDirectoryVisibility> = new Map(
667+
roomIds.map(r => [r, 'private'])
668+
);
669+
const res = await this.pgPool.query(
670+
"SELECT room_id, visibility FROM room_visibility WHERE room_id = ANY($1)",
671+
[roomIds]
672+
);
670673
for (const row of res.rows) {
671674
map.set(row.room_id, row.visibility ? "public" : "private");
672675
}

0 commit comments

Comments
 (0)