Add support for deleting connections

This commit is contained in:
Half-Shot 2022-04-08 16:10:54 +01:00
parent 9140c4d1bc
commit 95e23473b2
5 changed files with 91 additions and 10 deletions

View File

@ -435,8 +435,8 @@ export class Bridge {
); );
connManager.push(discussionConnection); connManager.push(discussionConnection);
} catch (ex) { } catch (ex) {
log.error(ex); log.error("Failed to create discussion room", ex);
throw Error('Failed to create discussion room'); return;
} }
} }

View File

@ -351,11 +351,12 @@ export class ConnectionManager {
return this.connections.filter((c) => (c instanceof GitLabRepoConnection && c.path === pathWithNamespace)) as GitLabRepoConnection[]; return this.connections.filter((c) => (c instanceof GitLabRepoConnection && c.path === pathWithNamespace)) as GitLabRepoConnection[];
} }
public getConnectionsForJiraProject(project: JiraProject, eventName: string): JiraProjectConnection[] { public getConnectionsForJiraProject(project: {id: string, self: string, key: string}, eventName?: string): JiraProjectConnection[] {
return this.connections.filter((c) => return this.connections.filter((c) =>
(c instanceof JiraProjectConnection && (c instanceof JiraProjectConnection &&
c.interestedInProject(project) && c.interestedInProject(project) &&
c.isInterestedInHookEvent(eventName))) as JiraProjectConnection[]; (!eventName || c.isInterestedInHookEvent(eventName))
)) as JiraProjectConnection[];
} }
@ -388,12 +389,12 @@ export class ConnectionManager {
return this.connections.find((c) => c.connectionId === connectionId && c.roomId === roomId); return this.connections.find((c) => c.connectionId === connectionId && c.roomId === roomId);
} }
public async purgeConnection(roomId: string, connectionId: string, requireNoRemoveHandler = true) { public async purgeConnection(roomId: string, connectionId: string, requireRemoveHandler = true) {
const connection = this.connections.find((c) => c.connectionId === connectionId && c.roomId == roomId); const connection = this.connections.find((c) => c.connectionId === connectionId && c.roomId == roomId);
if (!connection) { if (!connection) {
throw Error("Connection not found"); throw Error("Connection not found");
} }
if (requireNoRemoveHandler && !connection.onRemove) { if (requireRemoveHandler && !connection.onRemove) {
throw Error("Connection doesn't support removal, and so cannot be safely removed"); throw Error("Connection doesn't support removal, and so cannot be safely removed");
} }
await connection.onRemove?.(); await connection.onRemove?.();

View File

@ -487,7 +487,13 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
} }
const orgRepoName = event.repository.full_name; const orgRepoName = event.repository.full_name;
let message = `**${event.issue.user.login}** created new issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${event.issue.title}"`; let actionText = "created new issue"
if (event.issue.comments > 0) {
// This was tranferred
actionText = "transferred issue"
}
let message = `**${event.issue.user.login}** ${actionText} [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${event.issue.title}"`;
message += (event.issue.assignee ? ` assigned to ${event.issue.assignee.login}` : ''); message += (event.issue.assignee ? ` assigned to ${event.issue.assignee.login}` : '');
if (this.showIssueRoomLink) { if (this.showIssueRoomLink) {
const appInstance = await this.githubInstance.getSafeOctokitForRepo(this.org, this.repo); const appInstance = await this.githubInstance.getSafeOctokitForRepo(this.org, this.repo);

View File

@ -109,7 +109,7 @@ export class JiraProjectConnection extends CommandConnection implements IConnect
return !this.state.events || this.state.events?.includes(eventName as JiraAllowedEventsNames); return !this.state.events || this.state.events?.includes(eventName as JiraAllowedEventsNames);
} }
public interestedInProject(project: JiraProject) { public interestedInProject(project: {id: string, self: string, key: string}) {
if (this.projectId === project.id) { if (this.projectId === project.id) {
return true; return true;
} }

View File

@ -13,8 +13,11 @@ import markdown from "markdown-it";
import { FigmaFileConnection } from "./FigmaFileConnection"; import { FigmaFileConnection } from "./FigmaFileConnection";
import { URL } from "url"; import { URL } from "url";
import { AdminRoom } from "../AdminRoom"; import { AdminRoom } from "../AdminRoom";
import { ConnectionManager } from "../ConnectionManager";
const md = new markdown(); const md = new markdown();
const GITHUB_REGEX = /^https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/;
const JIRA_REGEX = /.+\/projects\/(\w+)\/?(\w+\/?)*$/;
/** /**
* Handles setting up a room with connections. This connection is "virtual" in that it has * Handles setting up a room with connections. This connection is "virtual" in that it has
* no state, and is only invoked when messages from other clients fall through. * no state, and is only invoked when messages from other clients fall through.
@ -28,6 +31,7 @@ export class SetupConnection extends CommandConnection {
private readonly as: Appservice, private readonly as: Appservice,
private readonly tokenStore: UserTokenStore, private readonly tokenStore: UserTokenStore,
private readonly config: BridgeConfig, private readonly config: BridgeConfig,
private readonly connectionManager: ConnectionManager,
private readonly getOrCreateAdminRoom: (userId: string) => Promise<AdminRoom>, private readonly getOrCreateAdminRoom: (userId: string) => Promise<AdminRoom>,
private readonly githubInstance?: GithubInstance,) { private readonly githubInstance?: GithubInstance,) {
super( super(
@ -59,7 +63,7 @@ export class SetupConnection extends CommandConnection {
if (!octokit) { if (!octokit) {
throw new CommandError("User not logged in", "You are not logged into GitHub. Start a DM with this bot and use the command `github login`."); throw new CommandError("User not logged in", "You are not logged into GitHub. Start a DM with this bot and use the command `github login`.");
} }
const urlParts = /^https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(url.trim().toLowerCase()); const urlParts = GITHUB_REGEX.exec(url.trim().toLowerCase());
if (!urlParts) { if (!urlParts) {
throw new CommandError("Invalid GitHub url", "The GitHub url you entered was not valid"); throw new CommandError("Invalid GitHub url", "The GitHub url you entered was not valid");
} }
@ -69,6 +73,38 @@ export class SetupConnection extends CommandConnection {
await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge ${org}/${repo}`); await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge ${org}/${repo}`);
} }
@botCommand("remove github repo", "Remove a GitHub repository connection.", ["url"], [], true)
public async onRemoveGitHubRepo(userId: string, url: string) {
if (!this.githubInstance || !this.config.github) {
throw new CommandError("not-configured", "The bridge is not configured to support GitHub");
}
if (!this.config.checkPermission(userId, "github", BridgePermissionLevel.manageConnections)) {
throw new CommandError('You are not permitted to remove connections for GitHub');
}
if (!await this.as.botClient.userHasPowerLevelFor(userId, this.roomId, "", true)) {
throw new CommandError("not-configured", "You must be able to set state in a room ('Change settings') in order to remove integrations.");
}
if (!await this.as.botClient.userHasPowerLevelFor(this.as.botUserId, this.roomId, GitHubRepoConnection.CanonicalEventType, true)) {
throw new CommandError("Bot lacks power level to set room state", "I do not have permission to remove a bridge in this room. Please promote me to an Admin/Moderator");
}
const urlParts = GITHUB_REGEX.exec(url.trim().toLowerCase());
if (!urlParts) {
throw new CommandError("Invalid GitHub url", "The GitHub url you entered was not valid");
}
const [, org, repo] = urlParts;
const connections = await this.connectionManager.getConnectionsForGithubRepo(org, repo);
for (const connection of connections) {
await this.connectionManager.purgeConnection(connection.roomId, connection.connectionId, false);
}
if (connections.length === 0) {
throw new CommandError("no-connections-found", "No connections found matching that url");
} else if (connections.length === 1) {
await this.as.botClient.sendNotice(this.roomId, `Removed connection from this room`);
} else {
await this.as.botClient.sendNotice(this.roomId, `Removed ${connections.length} connections from this room`);
}
}
@botCommand("jira project", "Create a connection for a JIRA project. (You must be logged in with JIRA to do this)", ["url"], [], true) @botCommand("jira project", "Create a connection for a JIRA project. (You must be logged in with JIRA to do this)", ["url"], [], true)
public async onJiraProject(userId: string, urlStr: string) { public async onJiraProject(userId: string, urlStr: string) {
const url = new URL(urlStr); const url = new URL(urlStr);
@ -88,7 +124,7 @@ export class SetupConnection extends CommandConnection {
if (!jiraClient) { if (!jiraClient) {
throw new CommandError("User not logged in", "You are not logged into Jira. Start a DM with this bot and use the command `jira login`."); throw new CommandError("User not logged in", "You are not logged into Jira. Start a DM with this bot and use the command `jira login`.");
} }
const urlParts = /.+\/projects\/(\w+)\/?(\w+\/?)*$/.exec(url.pathname.toLowerCase()); const urlParts = JIRA_REGEX.exec(url.pathname.toLowerCase());
const projectKey = urlParts?.[1] || url.searchParams.get('projectKey'); const projectKey = urlParts?.[1] || url.searchParams.get('projectKey');
if (!projectKey) { if (!projectKey) {
throw new CommandError("Invalid Jira url", "The JIRA project url you entered was not valid. It should be in the format of `https://jira-instance/.../projects/PROJECTKEY/...` or `.../RapidBoard.jspa?projectKey=TEST`"); throw new CommandError("Invalid Jira url", "The JIRA project url you entered was not valid. It should be in the format of `https://jira-instance/.../projects/PROJECTKEY/...` or `.../RapidBoard.jspa?projectKey=TEST`");
@ -99,6 +135,44 @@ export class SetupConnection extends CommandConnection {
await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge Jira project ${res.connection.projectKey}`); await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge Jira project ${res.connection.projectKey}`);
} }
@botCommand("remove jira project", "Remove a GitHub repository connection.", ["url"], [], true)
public async onRemoveJiraProject(userId: string, urlStr: string) {
const url = new URL(urlStr);
if (!this.config.jira) {
throw new CommandError("not-configured", "The bridge is not configured to support Jira");
}
if (!this.config.checkPermission(userId, "jira", BridgePermissionLevel.manageConnections)) {
throw new CommandError('You are not permitted to remove connections for Jira');
}
if (!await this.as.botClient.userHasPowerLevelFor(userId, this.roomId, "", true)) {
throw new CommandError("not-configured", "You must be able to set state in a room ('Change settings') in order to remove connections.");
}
if (!await this.as.botClient.userHasPowerLevelFor(this.as.botUserId, this.roomId, GitHubRepoConnection.CanonicalEventType, true)) {
throw new CommandError("Bot lacks power level to set room state", "I do not have permission to remove a connection in this room. Please promote me to an Admin/Moderator");
}
const urlParts = JIRA_REGEX.exec(url.pathname.toLowerCase());
const projectKey = urlParts?.[1] || url.searchParams.get('projectKey');
if (!projectKey) {
throw new CommandError("Invalid Jira url", "The JIRA project url you entered was not valid. It should be in the format of `https://jira-instance/.../projects/PROJECTKEY/...` or `.../RapidBoard.jspa?projectKey=TEST`");
}
const safeUrl = `https://${url.host}/projects/${projectKey}`;
const connections = await this.connectionManager.getConnectionsForJiraProject({
id: "none",
self: safeUrl,
key: projectKey,
});
for (const connection of connections) {
await this.connectionManager.purgeConnection(connection.roomId, connection.connectionId, false);
}
if (connections.length === 0) {
throw new CommandError("no-connections-found", "No connections found matching that url");
} else if (connections.length === 1) {
await this.as.botClient.sendNotice(this.roomId, `Removed connection from this room`);
} else {
await this.as.botClient.sendNotice(this.roomId, `Removed ${connections.length} connections from this room`);
}
}
@botCommand("webhook", "Create an inbound webhook", ["name"], [], true) @botCommand("webhook", "Create an inbound webhook", ["name"], [], true)
public async onWebhook(userId: string, name: string) { public async onWebhook(userId: string, name: string) {
if (!this.config.generic?.enabled) { if (!this.config.generic?.enabled) {