Add commands to remove/list Jira connections (#503)

* Add commands to remove/list Jira connections

* Add docs page for Jira connections

* Clarify "instance" and webhook reqs in Jira docs

- Jira has "instances" instead of "organizations", so use the former term
- Don't suggest that webhook support can work for multiple instances
- Mention that webhooks require special access
- Make some minor grammar changes
This commit is contained in:
Andrew Ferrazzutti 2022-10-03 09:54:14 -04:00 committed by GitHub
parent 80a26283e9
commit fa85dc070b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 121 additions and 16 deletions

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

@ -0,0 +1 @@
Add bot commands to list and remove Jira connections.

View File

@ -15,6 +15,7 @@
- [Room Configuration](./usage/room_configuration.md)
- [GitHub Repo](./usage/room_configuration/github_repo.md)
- [GitLab Project](./usage/room_configuration/gitlab_project.md)
- [JIRA Project](./usage/room_configuration/jira_project.md)
- [📊 Metrics](./metrics.md)
# 🧑‍💻 Development

View File

@ -1,21 +1,22 @@
# JIRA
## Adding a webhook to a JIRA Organisation
## Adding a webhook to a JIRA Instance
This should be done for all JIRA organisations you wish to bridge. The setup steps are the same for both On-Prem and Cloud.
This should be done for the JIRA instance you wish to bridge. The setup steps are the same for both On-Prem and Cloud.
You need to go to the `WebHooks` configuration page under Settings > System.
Note that this may require administrative access to the JIRA instance.
Next, add a webhook that points to `/` on the public webhooks address for hookshot. You should also include a
secret value by appending `?secret=your-webhook-secret`. The secret value can be anything, but should
be reasonably secure and should also be stored in the `config.yml` file.
Ensure that you enable all the events that you wish to be bridge.
Ensure that you enable all the events that you wish to be bridged.
## Configuration
You can now set some configuration in the bridge `config.yml`
You can now set some configuration in the bridge `config.yml`:
```yaml
jira:

View File

@ -0,0 +1,38 @@
JIRA Project
=================
This connection type connects a JIRA project to a room.
You can run commands to create and assign issues, and receive notifications when issues are created.
## Setting up
To set up a connection to a JIRA project in a new room:
(NB you must have permission to bridge JIRA projects before you can use this command, see [auth](../auth.html#jira).)
1. Create a new, unencrypted room. It can be public or private.
1. Invite the bridge bot (e.g. `@hookshot:example.com`).
1. Give the bridge bot moderator permissions or higher (power level 50) (or otherwise configure the room so the bot can edit room state).
1. Send the command `!hookshot jira project https://jira-instance/.../projects/PROJECTKEY/...`.
1. If you have permission to bridge this repo, the bridge will respond with a confirmation message.
## Managing connections
Send the command `!hookshot jira list project` to list all of a room's connections to JIRA projects.
Send the command `!hookshot jira remove project <url>` to remove a room's connection to a JIRA project at a given URL.
## Configuration
This connection supports a few options which can be defined in the room state:
| Option | Description | Allowed values | Default |
|--------|-------------|----------------|---------|
|events|Choose to include notifications for some event types|Array of: [Supported event types](#supported-event-types) |*empty*|
|commandPrefix|Choose the prefix to use when sending commands to the bot|A string, ideally starts with "!"|`!jira`|
### Supported event types
This connection currently supports sending messages only when a `issue.created` action happens on the project.

View File

@ -1,7 +1,7 @@
// We need to instantiate some functions which are not directly called, which confuses typescript.
import { BotCommands, botCommand, compileBotCommands, HelpFunction } from "../BotCommands";
import { CommandConnection } from "./CommandConnection";
import { GenericHookConnection, GitHubRepoConnection, JiraProjectConnection } from ".";
import { GenericHookConnection, GitHubRepoConnection, JiraProjectConnection, JiraProjectConnectionState } from ".";
import { CommandError } from "../errors";
import { v4 as uuid } from "uuid";
import { BridgePermissionLevel } from "../Config/Config";
@ -112,29 +112,93 @@ export class SetupConnection extends CommandConnection {
await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge ${connection.path}`);
}
private async checkJiraLogin(userId: string, urlStr: string) {
const jiraClient = await this.provisionOpts.tokenStore.getJiraForUser(userId, urlStr);
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`.");
}
}
private async getJiraProjectSafeUrl(userId: string, urlStr: string) {
const url = new URL(urlStr);
const urlParts = /\/projects\/(\w+)\/?(\w+\/?)*$/.exec(url.pathname);
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`.");
}
return `https://${url.host}/projects/${projectKey}`;
}
@botCommand("jira project", { help: "Create a connection for a JIRA project. (You must be logged in with JIRA to do this.)", requiredArgs: ["url"], includeUserId: true, category: "jira"})
public async onJiraProject(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.");
}
await this.checkUserPermissions(userId, "jira", JiraProjectConnection.CanonicalEventType);
await this.checkJiraLogin(userId, urlStr);
const safeUrl = await this.getJiraProjectSafeUrl(userId, urlStr);
const jiraClient = await this.provisionOpts.tokenStore.getJiraForUser(userId, urlStr);
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 urlParts = /.+\/projects\/(\w+)\/?(\w+\/?)*$/.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 res = await JiraProjectConnection.provisionConnection(this.roomId, userId, { url: safeUrl }, this.provisionOpts);
await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge Jira project ${res.connection.projectKey}.`);
}
@botCommand("jira list project", { help: "Show JIRA projects currently connected to.", category: "jira"})
public async onJiraListProject() {
const projects: JiraProjectConnectionState[] = await this.as.botClient.getRoomState(this.roomId).catch((err: any) => {
if (err.body.errcode === 'M_NOT_FOUND') {
return []; // not an error to us
}
throw err;
}).then(events =>
events.filter(
(ev: any) => (
ev.type === JiraProjectConnection.CanonicalEventType ||
ev.type === JiraProjectConnection.LegacyCanonicalEventType
) && ev.content.url
).map(ev => ev.content)
);
if (projects.length === 0) {
return this.as.botClient.sendHtmlNotice(this.roomId, md.renderInline('Not connected to any JIRA projects'));
} else {
return this.as.botClient.sendHtmlNotice(this.roomId, md.render(
'Currently connected to these JIRA projects:\n\n' +
projects.map(project => ` - ${project.url}`).join('\n')
));
}
}
@botCommand("jira remove project", { help: "Remove a connection for a JIRA project.", requiredArgs: ["url"], includeUserId: true, category: "jira"})
public async onJiraRemoveProject(userId: string, urlStr: string) {
await this.checkUserPermissions(userId, "jira", JiraProjectConnection.CanonicalEventType);
await this.checkJiraLogin(userId, urlStr);
const safeUrl = await this.getJiraProjectSafeUrl(userId, urlStr);
const eventTypes = [
JiraProjectConnection.CanonicalEventType,
JiraProjectConnection.LegacyCanonicalEventType,
];
let event = null;
let eventType = "";
for (eventType of eventTypes) {
try {
event = await this.as.botClient.getRoomStateEvent(this.roomId, eventType, safeUrl);
break;
} catch (err: any) {
if (err.body.errcode !== 'M_NOT_FOUND') {
throw err;
}
}
}
if (!event || Object.keys(event).length === 0) {
throw new CommandError("Invalid Jira project URL", `Feed "${urlStr}" is not currently bridged to this room`);
}
await this.as.botClient.sendStateEvent(this.roomId, eventType, safeUrl, {});
return this.as.botClient.sendHtmlNotice(this.roomId, md.renderInline(`Room no longer bridged to Jira project \`${safeUrl}\`.`));
}
@botCommand("webhook", { help: "Create an inbound webhook.", requiredArgs: ["name"], includeUserId: true, category: "webhook"})
public async onWebhook(userId: string, name: string) {
if (!this.config.generic?.enabled) {