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

Add bridge info state event syncer #312

Merged
merged 3 commits into from
Mar 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/312.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add the `BridgeInfoStateSyncer` helper component to sync MSC2346 format state events to rooms.
141 changes: 141 additions & 0 deletions src/components/bridge-info-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { Bridge } from "../bridge";
import logger from "./logging";
import PQueue from "p-queue";

const log = logger.get("BridgeStateSyncer");
export interface MappingInfo {
creator?: string;
protocol: {
id: string;
displayname?: string;
// eslint-disable-next-line camelcase
avatar_url?: `mxc://${string}`;
// eslint-disable-next-line camelcase
external_url?: string;
};
network?: {
id: string;
displayname?: string;
// eslint-disable-next-line camelcase
avatar_url?: `mxc://${string}`;
// eslint-disable-next-line camelcase
external_url?: string;
};
channel: {
id: string;
displayname?: string;
// eslint-disable-next-line camelcase
avatar_url?: `mxc://${string}`;
// eslint-disable-next-line camelcase
external_url?: string;
};
}

export interface MSC2364Content extends MappingInfo {
bridgebot: string;
}

interface Opts<BridgeMappingInfo> {
/**
* The name of the bridge implementation, ideally in Java package naming format:
* @example org.matrix.matrix-appservice-irc
*/
bridgeName: string;
/**
* This should return some standard information about a given
* mapping.
*/
getMapping: (roomId: string, info: BridgeMappingInfo) => Promise<MappingInfo>;
}

/**
* This class ensures that rooms contain a valid bridge info
* event ([MSC2346](https://github.com/matrix-org/matrix-doc/pull/2346))
* which displays the connected protocol, network and room.
*/
export class BridgeInfoStateSyncer<BridgeMappingInfo> {
public static readonly EventType = "uk.half-shot.bridge";
constructor(private bridge: Bridge, private opts: Opts<BridgeMappingInfo>) {
}

/**
* Check all rooms and ensure they have correct state.
* @param allMappings All bridged room mappings
* @param concurrency How many rooms to handle at a time, defaults to 3.
*/
public async initialSync(allMappings: Record<string, BridgeMappingInfo[]>, concurrency = 3) {
log.info("Beginning sync of bridge state events");
const syncQueue = new PQueue({ concurrency });
Object.entries(allMappings).forEach(([roomId, mappings]) => {
syncQueue.add(() => this.syncRoom(roomId, mappings));
});
return syncQueue.onIdle();
}

private async syncRoom(roomId: string, mappings: BridgeMappingInfo[]) {
log.info(`Syncing ${roomId}`);
const intent = this.bridge.getIntent();
for (const mappingInfo of mappings) {
const realMapping = await this.opts.getMapping(roomId, mappingInfo);
const key = this.createStateKey(realMapping);
const content = this.createBridgeInfoContent(realMapping);
try {
const eventData: MSC2364Content|null = await intent.getStateEvent(
roomId, BridgeInfoStateSyncer.EventType, key, true
);
if (eventData !== null) { // If found, validate.
if (JSON.stringify(eventData) === JSON.stringify(content)) {
continue;
}
log.debug(`${key} for ${roomId} is invalid, updating`);
}
}
catch (ex) {
log.warn(`Encountered error when trying to sync ${roomId}`);
break; // To be on the safe side, do not retry this room.
}

// Event wasn't found or was invalid, let's try setting one.
const eventContent = this.createBridgeInfoContent(realMapping);
try {
await intent.sendStateEvent(
roomId, BridgeInfoStateSyncer.EventType, key, eventContent as unknown as Record<string, unknown>
);
}
catch (ex) {
log.error(`Failed to update room with new state content: ${ex.message}`);
}
}
}

public async createInitialState(roomId: string, bridgeMappingInfo: BridgeMappingInfo) {
const mapping = await this.opts.getMapping(roomId, bridgeMappingInfo);
return {
type: BridgeInfoStateSyncer.EventType,
content: this.createBridgeInfoContent(mapping),
state_key: this.createStateKey(mapping),
};
}

public createStateKey(mapping: MappingInfo) {
const networkId = mapping.network ? mapping.network?.id.replace(/\//g, "%2F") + "/" : "";
const channel = mapping.channel.id.replace(/\//g, "%2F");
return `${this.opts.bridgeName}:/${networkId}${channel}`;
}

public createBridgeInfoContent(mapping: MappingInfo)
: MSC2364Content {
const content: MSC2364Content = {
bridgebot: this.bridge.botUserId,
protocol: mapping.protocol,
channel: mapping.channel,
};
if (mapping.creator) {
content.creator = mapping.creator;
}
if (mapping.network) {
content.network = mapping.network;
}
return content;
}
}
15 changes: 12 additions & 3 deletions src/components/intent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -720,11 +720,20 @@ export class Intent {
* state if they are not already joined.
* @param roomId The room to get the state from.
* @param eventType The event type to fetch.
* @param [stateKey=""] The state key of the event to fetch.
* @param stateKey The state key of the event to fetch.
* @param returnNull Return null on not found, rather than throwing
*/
public async getStateEvent(roomId: string, eventType: string, stateKey = "") {
public async getStateEvent(roomId: string, eventType: string, stateKey = "", returnNull = false) {
await this._ensureJoined(roomId);
return this.client.getStateEvent(roomId, eventType, stateKey);
try {
return await this.client.getStateEvent(roomId, eventType, stateKey);
}
catch (ex) {
if (ex.errcode !== "M_NOT_FOUND" || !returnNull) {
throw ex;
}
}
return null;
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export * from "./components/membership-queue";
export * as Logging from "./components/logging";
export { unstable } from "./errors";
export * from "./components/event-types";
export * from "./components/bridge-info-state";


/* eslint-disable @typescript-eslint/no-var-requires */
Expand Down