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 { export interface BridgeConfigGoNebMigrator {
apiUrl: string; apiUrl: string;
serviceIds?: string[]; serviceIds?: string[];
goNebBotPrefix?: string;
} }
export interface BridgeConfigRoot { export interface BridgeConfigRoot {

View File

@ -70,6 +70,7 @@ export class BridgeWidgetApi extends ProvisioningApi {
this.goNebMigrator = new GoNebMigrator( this.goNebMigrator = new GoNebMigrator(
this.config.goNebMigrator.apiUrl, this.config.goNebMigrator.apiUrl,
this.config.goNebMigrator.serviceIds, this.config.goNebMigrator.serviceIds,
this.config.goNebMigrator.goNebBotPrefix,
); );
} }
@ -90,7 +91,12 @@ export class BridgeWidgetApi extends ProvisioningApi {
const botUser = await this.getBotUserInRoom(roomId); const botUser = await this.getBotUserInRoom(roomId);
await assertUserPermissionsInRoom(req.userId, roomId, "read", botUser.intent); 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); res.send(connections);
} }

View File

@ -46,10 +46,14 @@ interface GoNebGithubWebhookService extends GoNebService {
} }
export class GoNebMigrator { export class GoNebMigrator {
private goNebBotPrefix: string;
constructor( constructor(
private apiUrl: string, private apiUrl: string,
private serviceIds?: string[], private serviceIds?: string[],
) {} goNebBotPrefix?: string,
) {
this.goNebBotPrefix = goNebBotPrefix ?? '@_neb_';
}
static convertFeeds(goNebFeeds: GoNebFeedsConfig): Map<string, FeedConnectionState[]> { static convertFeeds(goNebFeeds: GoNebFeedsConfig): Map<string, FeedConnectionState[]> {
const feedsPerRoom = new 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 feeds: MigratedFeed[] = [];
const github: MigratedGithub[] = []; const github: MigratedGithub[] = [];
const serviceIds = [ const serviceIds = new Set([
...(this.serviceIds ?? []), ...(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) { for (const id of serviceIds) {
const endpoint = this.apiUrl + (this.apiUrl.endsWith('/') ? '' : '/') + 'admin/getService'; const endpoint = this.apiUrl + (this.apiUrl.endsWith('/') ? '' : '/') + 'admin/getService';
@ -116,7 +120,7 @@ export class GoNebMigrator {
} }
case 'github-webhook': { case 'github-webhook': {
const service = obj as GoNebGithubWebhookService; const service = obj as GoNebGithubWebhookService;
if (service.Config.ClientUserID === userId) { if (userIds.has(service.Config.ClientUserID)) {
const roomRepos = service.Config.Rooms[roomId]?.Repos; const roomRepos = service.Config.Rooms[roomId]?.Repos;
if (roomRepos) { if (roomRepos) {
const githubConnections = GoNebMigrator.convertGithub(roomRepos); const githubConnections = GoNebMigrator.convertGithub(roomRepos);
@ -137,6 +141,43 @@ export class GoNebMigrator {
github, 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 // from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#encoding_for_rfc3986