Allow users to import other people's go-neb services (#695)

* Allow room admins to import other people's go-neb services

This requires us to guess what these other people's MXIDs were,
so we scroll through the list of room members and make educated guesses
about which of them are Scalar+go-neb bots, and which users they were set up by.

* Relax our requirements for scraping others' go-neb connections

* Changelog

* Linting

---------

Co-authored-by: Tadeusz Sośnierz <tadeusz@sosnierz.com>
This commit is contained in:
Tadeusz Sośnierz 2023-04-03 16:13:43 +02:00 committed by GitHub
parent 76e2b53cf0
commit 0ce06c4ea7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 56 additions and 7 deletions

1
changelog.d/695.feature Normal file
View File

@ -0,0 +1 @@
Allow users to import other people's go-neb services.

View File

@ -434,6 +434,7 @@ export interface BridgeConfigMetrics {
export interface BridgeConfigGoNebMigrator {
apiUrl: string;
serviceIds?: string[];
goNebBotPrefix?: string;
}
export interface BridgeConfigRoot {

View File

@ -70,6 +70,7 @@ export class BridgeWidgetApi extends ProvisioningApi {
this.goNebMigrator = new GoNebMigrator(
this.config.goNebMigrator.apiUrl,
this.config.goNebMigrator.serviceIds,
this.config.goNebMigrator.goNebBotPrefix,
);
}
@ -90,7 +91,12 @@ export class BridgeWidgetApi extends ProvisioningApi {
const botUser = await this.getBotUserInRoom(roomId);
await assertUserPermissionsInRoom(req.userId, roomId, "read", botUser.intent);
const connections = await this.goNebMigrator.getConnectionsForRoom(roomId, req.userId);
const userIds = this.goNebMigrator.getGoNebUsersFromRoomMembers(
await botUser.intent.underlyingClient.getJoinedRoomMembers(roomId)
);
const connections = await this.goNebMigrator.getConnectionsForRoom(roomId, new Set(userIds));
res.send(connections);
}

View File

@ -46,10 +46,14 @@ interface GoNebGithubWebhookService extends GoNebService {
}
export class GoNebMigrator {
private goNebBotPrefix: string;
constructor(
private apiUrl: string,
private serviceIds?: string[],
) {}
goNebBotPrefix?: string,
) {
this.goNebBotPrefix = goNebBotPrefix ?? '@_neb_';
}
static convertFeeds(goNebFeeds: GoNebFeedsConfig): Map<string, FeedConnectionState[]> {
const feedsPerRoom = new Map<string, FeedConnectionState[]>();
@ -83,14 +87,14 @@ export class GoNebMigrator {
});
}
public async getConnectionsForRoom(roomId: string, userId: string): Promise<MigratedConnections> {
public async getConnectionsForRoom(roomId: string, userIds: Set<string>): Promise<MigratedConnections> {
const feeds: MigratedFeed[] = [];
const github: MigratedGithub[] = [];
const serviceIds = [
const serviceIds = new Set([
...(this.serviceIds ?? []),
...['rssbot', 'github'].map(type => `${type}/${strictEncodeURIComponent(userId)}/${strictEncodeURIComponent(roomId)}`),
];
...['rssbot', 'github'].flatMap(type => Array.from(userIds).map(userId => `${type}/${strictEncodeURIComponent(userId)}/${strictEncodeURIComponent(roomId)}`)),
]);
for (const id of serviceIds) {
const endpoint = this.apiUrl + (this.apiUrl.endsWith('/') ? '' : '/') + 'admin/getService';
@ -116,7 +120,7 @@ export class GoNebMigrator {
}
case 'github-webhook': {
const service = obj as GoNebGithubWebhookService;
if (service.Config.ClientUserID === userId) {
if (userIds.has(service.Config.ClientUserID)) {
const roomRepos = service.Config.Rooms[roomId]?.Repos;
if (roomRepos) {
const githubConnections = GoNebMigrator.convertGithub(roomRepos);
@ -137,6 +141,43 @@ export class GoNebMigrator {
github,
};
}
public getGoNebUsersFromRoomMembers(members: string[]): string[] {
const goNebUsers = [];
for (const member of members) {
if (member.startsWith(this.goNebBotPrefix)) {
try {
const mxid = this.getUserMxid(member);
goNebUsers.push(mxid);
} catch (err: unknown) {
log.error(`${member} looks like a go-neb mxid, but we failed to extract the owner mxid from it (${err})`);
}
}
}
return goNebUsers;
}
private getUserMxid(botMxid: string): string {
let userPart = botMxid.substring(this.goNebBotPrefix.length);
// strip the service type (before first '_') and server name (after ':')
try {
[, userPart] = userPart.match(/[^_]+_([^:]+):.*/)!;
} catch (err: unknown) {
throw new Error(`${botMxid} does not look like a Scalar-produced go-neb mxid`);
}
// decode according to https://spec.matrix.org/v1.2/appendices/#mapping-from-other-character-sets,
return userPart.replace(/=\w\w/g, (match) => {
// first the lowercased string...
const code = parseInt(match.substring(1), 16);
return String.fromCharCode(code);
}).replace(/_\w/g, (match) => {
// and then reapply the uppercase where applicable
return match.substring(1).toUpperCase();
});
}
}
// from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#encoding_for_rfc3986