Refactor setup commands (#141)

* Ensure setup connection uses provisionConnection function

* Drop unused parameters from JiraProjectConnection

* Fix typo

* Convert ApiErrors into CommandError when possible

* Fix a stray bug

* changelog
This commit is contained in:
Will Hunt 2022-01-10 21:10:14 +00:00 committed by GitHub
parent 05852d9de8
commit 632791be63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 32 additions and 65 deletions

2
Cargo.lock generated
View File

@ -147,7 +147,7 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "matrix-hookshot"
version = "1.0.0"
version = "1.1.0"
dependencies = [
"contrast",
"hex",

1
changelog.d/141.misc Normal file
View File

@ -0,0 +1 @@
Refactor setup commands code to use the same checks as the provisioning code.

View File

@ -4,18 +4,17 @@ import { AdminAccountData, AdminRoomCommandHandler } from "./AdminRoomCommandHan
import { botCommand, compileBotCommands, handleCommand, BotCommands } from "./BotCommands";
import { BridgeConfig } from "./Config/Config";
import { BridgeRoomState, BridgeRoomStateGitHub } from "./Widgets/BridgeWidgetInterface";
import { CommandError } from "./errors";
import { Endpoints } from "@octokit/types";
import { FormatUtil } from "./FormatUtil";
import { GetUserResponse } from "./Gitlab/Types";
import { GitHubBotCommands } from "./Github/AdminCommands";
import { GithubGraphQLClient, GithubInstance } from "./Github/GithubInstance";
import { GithubGraphQLClient } from "./Github/GithubInstance";
import { GitLabClient } from "./Gitlab/Client";
import { Intent } from "matrix-bot-sdk";
import { JiraBotCommands } from "./Jira/AdminCommands";
import { MatrixMessageContent } from "./MatrixEvent";
import { NotifFilter, NotificationFilterStateContent } from "./NotificationFilters";
import { GitHubOAuthToken, ProjectsListResponseData } from "./Github/Types";
import { ProjectsListResponseData } from "./Github/Types";
import { UserTokenStore } from "./UserTokenStore";
import {v4 as uuid} from "uuid";
import LogWrapper from "./LogWrapper";
@ -200,7 +199,7 @@ export class AdminRoom extends AdminRoomCommandHandler {
if (!username) {
const me = await octokit.users.getAuthenticated();
// TODO: Fix
username = me.data.name!;
username = me.data.login;
}
let res: ProjectsListResponseData;

View File

@ -1,6 +1,7 @@
import markdown from "markdown-it";
import stringArgv from "string-argv";
import { CommandError } from "./errors";
import { ApiError } from "./provisioning/api";
import { MatrixMessageContent } from "./MatrixEvent";
const md = new markdown();
@ -15,7 +16,7 @@ export function botCommand(prefix: string, help: string, requiredArgs: string[]
includeUserId,
});
}
type BotCommandResult = {status: boolean, reaction?: string};
type BotCommandResult = {status?: boolean, reaction?: string}|undefined;
type BotCommandFunction = (...args: string[]) => Promise<BotCommandResult>;
export type BotCommands = {[prefix: string]: {
@ -82,6 +83,9 @@ export async function handleCommand(userId: string, command: string, botCommands
return {handled: true, result};
} catch (ex) {
const commandError = ex as CommandError;
if (ex instanceof ApiError) {
return {handled: true, error: ex.error, humanError: ex.error};
}
return {handled: true, error: commandError.message, humanError: commandError.humanError};
}
}

View File

@ -72,7 +72,7 @@ export class ConnectionManager {
if (!this.config.jira) {
throw Error('JIRA is not configured');
}
const res = await JiraProjectConnection.provisionConnection(roomId, userId, data, this.as, this.commentProcessor, this.messageClient, this.tokenStore);
const res = await JiraProjectConnection.provisionConnection(roomId, userId, data, this.as, this.tokenStore);
await this.as.botIntent.underlyingClient.sendStateEvent(roomId, JiraProjectConnection.CanonicalEventType, res.connection.stateKey, res.stateEventContent);
this.push(res.connection);
return res.connection;
@ -191,7 +191,7 @@ export class ConnectionManager {
if (!this.config.jira) {
throw Error('JIRA is not configured');
}
return new JiraProjectConnection(roomId, this.as, state.content, state.stateKey, this.commentProcessor, this.messageClient, this.tokenStore);
return new JiraProjectConnection(roomId, this.as, state.content, state.stateKey, this.tokenStore);
}
if (FigmaFileConnection.EventTypes.includes(state.type)) {

View File

@ -48,7 +48,7 @@ export abstract class CommandConnection extends BaseConnection {
log.warn(`Failed to handle command:`, error);
return true;
} else {
const reaction = commandResult.result.reaction || '✅';
const reaction = commandResult.result?.reaction || '✅';
await this.botClient.sendEvent(this.roomId, "m.reaction", {
"m.relates_to": {
rel_type: "m.annotation",

View File

@ -141,7 +141,7 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
const validData = validateState(data);
const octokit = await tokenStore.getOctokitForUser(userId);
if (!octokit) {
throw new ApiError("User is not authenticated with JIRA", ErrCode.ForbiddenUser);
throw new ApiError("User is not authenticated with GitHub", ErrCode.ForbiddenUser);
}
const me = await octokit.users.getAuthenticated();
let permissionLevel;
@ -479,7 +479,7 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
}
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 message = `**${event.issue.user.login}** created new issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${event.issue.title}"`;
message += (event.issue.assignee ? ` assigned to ${event.issue.assignee.login}` : '');
if (this.showIssueRoomLink) {
const appInstance = await this.githubInstance.getSafeOctokitForRepo(this.org, this.repo);

View File

@ -1,8 +1,6 @@
import { IConnection } from "./IConnection";
import { Appservice } from "matrix-bot-sdk";
import LogWrapper from "../LogWrapper";
import { CommentProcessor } from "../CommentProcessor";
import { MessageSenderClient } from "../MatrixSender"
import { JiraIssueEvent, JiraIssueUpdatedEvent } from "../Jira/WebhookTypes";
import { FormatUtil } from "../FormatUtil";
import markdownit from "markdown-it";
@ -64,8 +62,7 @@ export class JiraProjectConnection extends CommandConnection implements IConnect
static botCommands: BotCommands;
static helpMessage: (cmdPrefix?: string) => MatrixMessageContent;
static async provisionConnection(roomId: string, userId: string, data: Record<string, unknown>, as: Appservice,
commentProcessor: CommentProcessor, messageClient: MessageSenderClient, tokenStore: UserTokenStore) {
static async provisionConnection(roomId: string, userId: string, data: Record<string, unknown>, as: Appservice, tokenStore: UserTokenStore) {
const validData = validateJiraConnectionState(data);
const jiraClient = await tokenStore.getJiraForUser(userId);
if (!jiraClient) {
@ -75,7 +72,7 @@ export class JiraProjectConnection extends CommandConnection implements IConnect
if (!jiraResourceClient) {
throw new ApiError("User is not authenticated with this JIRA instance", ErrCode.ForbiddenUser);
}
const connection = new JiraProjectConnection(roomId, as, data, validData.url, commentProcessor, messageClient, tokenStore);
const connection = new JiraProjectConnection(roomId, as, data, validData.url, tokenStore);
if (!connection.projectKey) {
throw Error('Expected projectKey to be defined');
}
@ -83,7 +80,7 @@ export class JiraProjectConnection extends CommandConnection implements IConnect
// Just need to check that the user can access this.
await jiraResourceClient.getProject(connection.projectKey);
} catch (ex) {
throw new ApiError("User cannot open this project", ErrCode.ForbiddenUser);
throw new ApiError("Requested project was not found", ErrCode.ForbiddenUser);
}
log.info(`Created connection via provisionConnection ${connection.toString()}`);
return {stateEventContent: validData, connection};
@ -131,8 +128,6 @@ export class JiraProjectConnection extends CommandConnection implements IConnect
private readonly as: Appservice,
private state: JiraProjectConnectionState,
stateKey: string,
private readonly commentProcessor: CommentProcessor,
private readonly messageClient: MessageSenderClient,
private readonly tokenStore: UserTokenStore,) {
super(
roomId,

View File

@ -2,21 +2,17 @@
import { Appservice } from "matrix-bot-sdk";
import { BotCommands, botCommand, compileBotCommands } from "../BotCommands";
import { MatrixMessageContent } from "../MatrixEvent";
import LogWrapper from "../LogWrapper";
import { CommandConnection } from "./CommandConnection";
import { GenericHookConnection, GitHubRepoConnection, GitHubRepoConnectionState, JiraProjectConnection, JiraProjectConnectionState } from ".";
import { GenericHookConnection, GitHubRepoConnection, JiraProjectConnection } from ".";
import { CommandError } from "../errors";
import { UserTokenStore } from "../UserTokenStore";
import { GithubInstance } from "../Github/GithubInstance";
import { JiraProject } from "../Jira/Types";
import { v4 as uuid } from "uuid";
import { BridgeConfig } from "../Config/Config";
import markdown from "markdown-it";
import { FigmaFileConnection } from "./FigmaFileConnection";
const md = new markdown();
const log = new LogWrapper("SetupConnection");
/**
* 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.
@ -44,7 +40,7 @@ export class SetupConnection extends CommandConnection {
@botCommand("github repo", "Create a connection for a GitHub repository. (You must be logged in with GitHub to do this)", ["url"], [], true)
public async onGitHubRepo(userId: string, url: string) {
if (!this.githubInstance) {
if (!this.githubInstance || !this.config.github) {
throw new CommandError("not-configured", "The bridge is not configured to support GitHub");
}
if (!await this.as.botClient.userHasPowerLevelFor(userId, this.roomId, "", true)) {
@ -57,30 +53,14 @@ export class SetupConnection extends CommandConnection {
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`.");
}
const res = /^https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(url.trim().toLowerCase());
if (!res) {
const urlParts = /^https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(url.trim().toLowerCase());
if (!urlParts) {
throw new CommandError("Invalid GitHub url", "The GitHub url you entered was not valid");
}
const [, org, repo] = res;
let resultRepo
try {
resultRepo = await octokit.repos.get({owner: org, repo});
} catch (ex) {
throw new CommandError("Invalid GitHub repo", "Could not find the requested GitHub repo. Do you have permission to view it?");
}
// Check if we have a webhook for this repo
try {
await this.githubInstance.getOctokitForRepo(org, repo);
} catch (ex) {
log.warn(`No app instance for new git connection:`, ex);
// We might be able to do it via a personal access token
await this.as.botClient.sendNotice(this.roomId, `Note: There doesn't appear to be a GitHub App install that covers this repository so webhooks won't work.`)
}
await this.as.botClient.sendStateEvent(this.roomId, GitHubRepoConnection.CanonicalEventType, url, {
org,
repo,
} as GitHubRepoConnectionState);
await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge ${resultRepo.data.full_name}`);
const [, org, repo] = urlParts;
const res = await GitHubRepoConnection.provisionConnection(this.roomId, userId, {org, repo}, this.as, this.tokenStore, this.githubInstance, this.config.github);
await this.as.botClient.sendStateEvent(this.roomId, GitHubRepoConnection.CanonicalEventType, url, res.stateEventContent);
await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge ${org}/${repo}`);
}
@botCommand("jira project", "Create a connection for a JIRA project. (You must be logged in with JIRA to do this)", ["url"], [], true)
@ -98,27 +78,15 @@ export class SetupConnection extends CommandConnection {
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`.");
}
const res = /^https:\/\/([A-z.\-_]+)\/.+\/projects\/(\w+)\/?(\w+\/?)*$/.exec(url.trim().toLowerCase());
if (!res) {
const urlParts = /^https:\/\/([A-z.\-_]+)\/.+\/projects\/(\w+)\/?(\w+\/?)*$/.exec(url.trim().toLowerCase());
if (!urlParts) {
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/...`");
}
const [, origin, projectKey] = res;
const [, origin, projectKey] = urlParts;
const safeUrl = `https://${origin}/projects/${projectKey}`;
const jiraOriginClient = await jiraClient.getClientForUrl(new URL(safeUrl));
if (!jiraOriginClient) {
throw new CommandError("User does not have permission to access this JIRA instance", "You do not have access to this JIRA instance. You may need to log into Jira again to provide access");
}
let jiraProject: JiraProject;
try {
jiraProject = await jiraOriginClient.getProject(projectKey.toUpperCase());
} catch (ex) {
log.warn(`Failed to get jira project:`, ex);
throw new CommandError("Missing or invalid JIRA project", "Could not find the requested JIRA project. Do you have permission to view it?");
}
await this.as.botClient.sendStateEvent(this.roomId, JiraProjectConnection.CanonicalEventType, safeUrl, {
url: safeUrl,
} as JiraProjectConnectionState);
await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge Jira project '${jiraProject.name}' (${jiraProject.key})`);
const res = await JiraProjectConnection.provisionConnection(this.roomId, userId, { url: safeUrl }, this.as, this.tokenStore);
await this.as.botClient.sendStateEvent(this.roomId, JiraProjectConnection.CanonicalEventType, safeUrl, res.stateEventContent);
await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge Jira project ${projectKey}`);
}
@botCommand("webhook", "Create a inbound webhook", ["name"], [], true)