Minor grammar corrections in code and documentation (#267)

* setupWidget -> setUpWidget

* setup -> (to) set up; add full stops

* setupAdminRoom -> setUpAdminRoom

* it's -> its

* Create 267.feature

* log in to; Currently, ; full stops

* Improve copy of error messages

* Update 267.feature

* Rename 267.feature to 267.misc

Co-authored-by: Will Hunt <will@half-shot.uk>
This commit is contained in:
Christian Paul 2022-04-07 17:14:35 +02:00 committed by GitHub
parent 14abb011b6
commit 4060ded7f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 125 additions and 124 deletions

View File

@ -17,7 +17,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Login to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@v1 uses: docker/login-action@v1
with: with:
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}

View File

@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Get release tag - name: Get release tag
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Login to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@v1 uses: docker/login-action@v1
with: with:
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}

View File

@ -65,7 +65,7 @@ Bugfixes
- Fix a bug which caused GitHub "ready for review" events to be unhandled. ([\#149](https://github.com/matrix-org/matrix-hookshot/issues/149)) - Fix a bug which caused GitHub "ready for review" events to be unhandled. ([\#149](https://github.com/matrix-org/matrix-hookshot/issues/149))
- Fix a bug preventing `!hookshot jira project` from working ([\#166](https://github.com/matrix-org/matrix-hookshot/issues/166)) - Fix a bug preventing `!hookshot jira project` from working ([\#166](https://github.com/matrix-org/matrix-hookshot/issues/166))
- Fix a few issues preventing GitHub notifications from working ([\#173](https://github.com/matrix-org/matrix-hookshot/issues/173)) - Fix a few issues preventing GitHub notifications from working ([\#173](https://github.com/matrix-org/matrix-hookshot/issues/173))
- Fixed an issue where the bridge bot would change it's displayname if a webhook event is handled while `generic.userIdPrefix` is not set in the config. ([\#215](https://github.com/matrix-org/matrix-hookshot/issues/215)) - Fixed an issue where the bridge bot would change its displayname if a webhook event is handled while `generic.userIdPrefix` is not set in the config. ([\#215](https://github.com/matrix-org/matrix-hookshot/issues/215))
- Remove nonfunctional `gitlab notifications toggle` command. ([\#226](https://github.com/matrix-org/matrix-hookshot/issues/226)) - Remove nonfunctional `gitlab notifications toggle` command. ([\#226](https://github.com/matrix-org/matrix-hookshot/issues/226))

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

@ -0,0 +1 @@
Make some grammar corrections in code, chat notices and documentation.

View File

@ -108,11 +108,11 @@ Each permission set can have a services. The `service` field can be:
- `*`, for any service. - `*`, for any service.
The `level` can be: The `level` can be:
- `commands` Can run commands within connected rooms, but NOT log into the bridge. - `commands` Can run commands within connected rooms, but NOT log in to the bridge.
- `login` All the above, and can also log into the bridge. - `login` All the above, and can also log in to the bridge.
- `notifications` All the above, and can also bridge their notifications. - `notifications` All the above, and can also bridge their notifications.
- `manageConnections` All the above, and can create and delete connections (either via the provisioner, setup commands, or state events). - `manageConnections` All the above, and can create and delete connections (either via the provisioner, setup commands, or state events).
- `admin` All permissions. Currently there are no admin features so this exists as a placeholder. - `admin` All permissions. Currently, there are no admin features so this exists as a placeholder.
When permissions are checked, if a user matches any of the permission set and one When permissions are checked, if a user matches any of the permission set and one
of those grants the right level for a service, they are allowed access. If none of the of those grants the right level for a service, they are allowed access. If none of the
@ -134,7 +134,7 @@ permissions:
services: services:
- service: github - service: github
level: manageConnections level: manageConnections
# Allow users on this domain to login to jira and github. # Allow users on this domain to log in to jira and github.
- actor: support.example.com - actor: support.example.com
services: services:
- service: jira - service: jira

View File

@ -29,7 +29,7 @@ The `accessToken` should be the personal access token for your account.
The `passcode` should be a randomly generated code which is used to authenticate requests from Figma. The `passcode` should be a randomly generated code which is used to authenticate requests from Figma.
The bridge will automatically setup a webhook on Figma for you upon startup, and will automatically reconfigure that webhook if the `publicUrl` or `passcode` changes. The bridge will automatically set up a webhook on Figma for you upon startup, and will automatically reconfigure that webhook if the `publicUrl` or `passcode` changes.
## Next steps ## Next steps

View File

@ -25,7 +25,7 @@ jira:
... # See below ... # See below
``` ```
You can omit the `oauth` section if you are not planning to allow users to login and use interactive features (i.e. webhook only mode). You can omit the `oauth` section if you are not planning to allow users to log in and use interactive features (i.e. webhook only mode).
## Connecting Matrix users to JIRA ## Connecting Matrix users to JIRA
@ -85,7 +85,7 @@ jira:
# The path to your webhooks listener on the "/jira/oauth" path. # The path to your webhooks listener on the "/jira/oauth" path.
redirect_uri: http://localhost:5065/jira/oauth redirect_uri: http://localhost:5065/jira/oauth
``` ```
To start with, setup your JIRA instance to support OAuth. To start with, set up your JIRA instance to support OAuth.
1. Open the **Administration** page for your JIRA instance. 1. Open the **Administration** page for your JIRA instance.
@ -104,7 +104,7 @@ To start with, setup your JIRA instance to support OAuth.
1. Enter your `consumerKey` from the config file above. 1. Enter your `consumerKey` from the config file above.
2. The `consumerName` can be anything, but will be visible to users of your app. You could use something like `Matrix`, `Hookshot` or anything else. 2. The `consumerName` can be anything, but will be visible to users of your app. You could use something like `Matrix`, `Hookshot` or anything else.
3. The `publicKey` can be generated by running `openssl rsa -in jira_privatekey.pem -pubout` on the key you created earlier. 3. The `publicKey` can be generated by running `openssl rsa -in jira_privatekey.pem -pubout` on the key you created earlier.
7. Congratulations, you now have OAuth setup. 7. Congratulations, you now have OAuth set up.
## Next steps ## Next steps

View File

@ -38,7 +38,7 @@ To authenticate with a personal access token:
You can authenticate with GitLab by supplying a Personal Access Token. OAuth-style authentication isn't supported You can authenticate with GitLab by supplying a Personal Access Token. OAuth-style authentication isn't supported
yet. yet.
- You will need to have configured a GitLab instance in your config.yml for the instance you want to login to. - You will need to have configured a GitLab instance in your config.yml for the instance you want to log in to.
- Open https://%instance%/-/profile/personal_access_tokens (GitLab > User Settings > Access Tokens), where instance is your GitLab instance address. - Open https://%instance%/-/profile/personal_access_tokens (GitLab > User Settings > Access Tokens), where instance is your GitLab instance address.
- For the public GitLab server, this would be "gitlab.com" - For the public GitLab server, this would be "gitlab.com"
- Give it a good name, and a sensible expiration date. For scopes you will need to tick `api`. - Give it a good name, and a sensible expiration date. For scopes you will need to tick `api`.
@ -49,7 +49,7 @@ yet.
## JIRA ## JIRA
You can login to JIRA via OAuth. This means you will need to have configured OAuth support in your `config.yml`, and You can log in to JIRA via OAuth. This means you will need to have configured OAuth support in your `config.yml`, and
have the endpoints required accessible from the internet. Authentication is required when trying to bridge JIRA resources into rooms. have the endpoints required accessible from the internet. Authentication is required when trying to bridge JIRA resources into rooms.
- Say `jira login` to get the URL to authenticate via. - Say `jira login` to get the URL to authenticate via.

View File

@ -160,7 +160,7 @@ export class AdminRoom extends AdminRoomCommandHandler {
private async setGitHubNotificationsStateParticipating() { private async setGitHubNotificationsStateParticipating() {
const newData = await this.saveAccountData((data) => { const newData = await this.saveAccountData((data) => {
if (!data.github?.notifications?.enabled) { if (!data.github?.notifications?.enabled) {
throw Error('Notifications are not enabled') throw Error('Notifications are not enabled.')
} }
const oldState = data.github?.notifications?.participating ?? false; const oldState = data.github?.notifications?.participating ?? false;
return { return {
@ -174,17 +174,17 @@ export class AdminRoom extends AdminRoomCommandHandler {
}; };
}); });
if (newData.github?.notifications?.participating) { if (newData.github?.notifications?.participating) {
return this.sendNotice(`Filtering for events you are participating in`); return this.sendNotice(`Filtering for events you are participating in.`);
} }
return this.sendNotice(`Showing all events`); return this.sendNotice(`Showing all events.`);
} }
@botCommand("github notifications", {help: "Show the current notification settings", category: "github"}) @botCommand("github notifications", {help: "Show the current notification settings", category: "github"})
public async getGitHubNotificationsState() { public async getGitHubNotificationsState() {
if (!this.notificationsEnabled("github")) { if (!this.notificationsEnabled("github")) {
return this.sendNotice(`Notifications are disabled`); return this.sendNotice(`Notifications are disabled.`);
} }
return this.sendNotice(`Notifications are enabled, ${this.notificationsParticipating("github") ? "Showing only events you are particiapting in" : "Showing all events"}`); return this.sendNotice(`Notifications are enabled, ${this.notificationsParticipating("github") ? "Showing only events you are particiapting in." : "Showing all events."}`);
} }
@ -192,7 +192,7 @@ export class AdminRoom extends AdminRoomCommandHandler {
// @ts-ignore - property is used // @ts-ignore - property is used
private async listProjects(username?: string, repo?: string) { private async listProjects(username?: string, repo?: string) {
if (!this.config.github) { if (!this.config.github) {
return this.sendNotice("The bridge is not configured with GitHub support"); return this.sendNotice("The bridge is not configured with GitHub support.");
} }
const octokit = await this.tokenStore.getOctokitForUser(this.userId); const octokit = await this.tokenStore.getOctokitForUser(this.userId);
if (!octokit) { if (!octokit) {
@ -218,7 +218,7 @@ export class AdminRoom extends AdminRoomCommandHandler {
})).data; })).data;
} catch (ex) { } catch (ex) {
log.warn(`Failed to fetch projects:`, ex); log.warn(`Failed to fetch projects:`, ex);
return this.sendNotice(`Failed to fetch projects due to an error. See logs for details`); return this.sendNotice(`Failed to fetch projects due to an error. See logs for details.`);
} }
const content = `Projects for ${username}:\n${FormatUtil.projectListing(res)}\n`; const content = `Projects for ${username}:\n${FormatUtil.projectListing(res)}\n`;
@ -234,7 +234,7 @@ export class AdminRoom extends AdminRoomCommandHandler {
// @ts-ignore - property is used // @ts-ignore - property is used
private async listProjects(org: string, repo?: string) { private async listProjects(org: string, repo?: string) {
if (!this.config.github) { if (!this.config.github) {
return this.sendNotice("The bridge is not configured with GitHub support"); return this.sendNotice("The bridge is not configured with GitHub support.");
} }
const octokit = await this.tokenStore.getOctokitForUser(this.userId); const octokit = await this.tokenStore.getOctokitForUser(this.userId);
if (!octokit) { if (!octokit) {
@ -254,10 +254,10 @@ export class AdminRoom extends AdminRoomCommandHandler {
})); }));
} catch (ex) { } catch (ex) {
if (ex.status === 404) { if (ex.status === 404) {
return this.sendNotice('Not found'); return this.sendNotice(`${repo ? "Repository" : "Org"} does not exist.`);
} }
log.warn(`Failed to fetch projects:`, ex); log.warn(`Failed to fetch projects:`, ex);
return this.sendNotice(`Failed to fetch projects due to an error. See logs for details`); return this.sendNotice(`Failed to fetch projects due to an error. See logs for details.`);
} }
const content = `Projects for ${org}:\n` + res.data.map(r => ` - ${FormatUtil.projectListing([r])}\n`).join("\n"); const content = `Projects for ${org}:\n` + res.data.map(r => ` - ${FormatUtil.projectListing([r])}\n`).join("\n");
@ -273,7 +273,7 @@ export class AdminRoom extends AdminRoomCommandHandler {
// @ts-ignore - property is used // @ts-ignore - property is used
private async openProject(projectId: string) { private async openProject(projectId: string) {
if (!this.config.github) { if (!this.config.github) {
return this.sendNotice("The bridge is not configured with GitHub support"); return this.sendNotice("The bridge is not configured with GitHub support.");
} }
const octokit = await this.tokenStore.getOctokitForUser(this.userId); const octokit = await this.tokenStore.getOctokitForUser(this.userId);
if (!octokit) { if (!octokit) {
@ -287,10 +287,10 @@ export class AdminRoom extends AdminRoomCommandHandler {
this.emit('open.project', project.data); this.emit('open.project', project.data);
} catch (ex) { } catch (ex) {
if (ex.status === 404) { if (ex.status === 404) {
return this.sendNotice('Not found'); return this.sendNotice('Project does not exist.');
} }
log.warn(`Failed to fetch project:`, ex); log.warn(`Failed to fetch project:`, ex);
return this.sendNotice(`Failed to fetch project due to an error. See logs for details`); return this.sendNotice(`Failed to fetch project due to an error. See logs for details.`);
} }
} }
@ -299,7 +299,7 @@ export class AdminRoom extends AdminRoomCommandHandler {
private async listDiscussions(owner: string, repo: string, numberStr: string) { private async listDiscussions(owner: string, repo: string, numberStr: string) {
const number = parseInt(numberStr); const number = parseInt(numberStr);
if (!this.config.github) { if (!this.config.github) {
return this.sendNotice("The bridge is not configured with GitHub support"); return this.sendNotice("The bridge is not configured with GitHub support.");
} }
const octokit = await this.tokenStore.getOctokitForUser(this.userId); const octokit = await this.tokenStore.getOctokitForUser(this.userId);
if (!octokit) { if (!octokit) {
@ -311,10 +311,10 @@ export class AdminRoom extends AdminRoomCommandHandler {
this.emit('open.discussion', owner, repo, discussions); this.emit('open.discussion', owner, repo, discussions);
} catch (ex) { } catch (ex) {
if (ex.status === 404) { if (ex.status === 404) {
return this.sendNotice('Not found'); return this.sendNotice('Discussion does not exist.');
} }
log.warn(`Failed to fetch discussions:`, ex); log.warn(`Failed to fetch discussions:`, ex);
return this.sendNotice(`Failed to fetch discussions due to an error. See logs for details`); return this.sendNotice(`Failed to fetch discussions due to an error. See logs for details.`);
} }
} }
@ -325,7 +325,7 @@ export class AdminRoom extends AdminRoomCommandHandler {
// @ts-ignore - property is used // @ts-ignore - property is used
private async gitLabOpenIssue(url: string) { private async gitLabOpenIssue(url: string) {
if (!this.config.gitlab) { if (!this.config.gitlab) {
return this.sendNotice("The bridge is not configured with GitLab support"); return this.sendNotice("The bridge is not configured with GitLab support.");
} }
const urlResult = GitLabClient.splitUrlIntoParts(this.config.gitlab.instances, url); const urlResult = GitLabClient.splitUrlIntoParts(this.config.gitlab.instances, url);
@ -336,7 +336,7 @@ export class AdminRoom extends AdminRoomCommandHandler {
const instance = this.config.gitlab.instances[instanceName]; const instance = this.config.gitlab.instances[instanceName];
const client = await this.tokenStore.getGitLabForUser(this.userId, instance.url); const client = await this.tokenStore.getGitLabForUser(this.userId, instance.url);
if (!client) { if (!client) {
return this.sendNotice("You have not added a personal access token for GitLab"); return this.sendNotice("You have not added a personal access token for GitLab.");
} }
const getIssueOpts = { const getIssueOpts = {
issue: parseInt(parts[parts.length-1]), issue: parseInt(parts[parts.length-1]),
@ -351,11 +351,11 @@ export class AdminRoom extends AdminRoomCommandHandler {
public async setGitLabPersonalAccessToken(instanceName: string, accessToken: string) { public async setGitLabPersonalAccessToken(instanceName: string, accessToken: string) {
let me: GetUserResponse; let me: GetUserResponse;
if (!this.config.gitlab) { if (!this.config.gitlab) {
return this.sendNotice("The bridge is not configured with GitLab support"); return this.sendNotice("The bridge is not configured with GitLab support.");
} }
const instance = this.config.gitlab.instances[instanceName]; const instance = this.config.gitlab.instances[instanceName];
if (!instance) { if (!instance) {
return this.sendNotice("The bridge is not configured for this GitLab instance"); return this.sendNotice("The bridge is not configured for this GitLab instance.");
} }
try { try {
const client = new GitLabClient(instance.url, accessToken); const client = new GitLabClient(instance.url, accessToken);
@ -365,22 +365,22 @@ export class AdminRoom extends AdminRoomCommandHandler {
log.error("Gitlab auth error:", ex); log.error("Gitlab auth error:", ex);
return this.sendNotice("Could not authenticate with GitLab. Is your token correct?"); return this.sendNotice("Could not authenticate with GitLab. Is your token correct?");
} }
await this.sendNotice(`Connected as ${me.username}. Token stored`); await this.sendNotice(`Connected as ${me.username}. Token stored.`);
return this.tokenStore.storeUserToken("gitlab", this.userId, accessToken, instance.url); return this.tokenStore.storeUserToken("gitlab", this.userId, accessToken, instance.url);
} }
@botCommand("gitlab hastoken", {help: "Check if you have a token stored for GitLab", requiredArgs: ["instanceName"], category: "gitlab"}) @botCommand("gitlab hastoken", {help: "Check if you have a token stored for GitLab", requiredArgs: ["instanceName"], category: "gitlab"})
public async gitlabHasPersonalToken(instanceName: string) { public async gitlabHasPersonalToken(instanceName: string) {
if (!this.config.gitlab) { if (!this.config.gitlab) {
return this.sendNotice("The bridge is not configured with GitLab support"); return this.sendNotice("The bridge is not configured with GitLab support.");
} }
const instance = this.config.gitlab.instances[instanceName]; const instance = this.config.gitlab.instances[instanceName];
if (!instance) { if (!instance) {
return this.sendNotice("The bridge is not configured for this GitLab instance"); return this.sendNotice("The bridge is not configured for this GitLab instance.");
} }
const result = await this.tokenStore.getUserToken("gitlab", this.userId, instance.url); const result = await this.tokenStore.getUserToken("gitlab", this.userId, instance.url);
if (result === null) { if (result === null) {
return this.sendNotice("You do not currently have a token stored"); return this.sendNotice("You do not currently have a token stored.");
} }
return this.sendNotice("A token is stored for your GitLab account."); return this.sendNotice("A token is stored for your GitLab account.");
} }
@ -388,7 +388,7 @@ export class AdminRoom extends AdminRoomCommandHandler {
@botCommand("filters list", "List your saved filters") @botCommand("filters list", "List your saved filters")
public async getFilters() { public async getFilters() {
if (this.notifFilter.empty) { if (this.notifFilter.empty) {
return this.sendNotice("You do not currently have any filters"); return this.sendNotice("You do not currently have any filters.");
} }
const filterText = Object.entries(this.notifFilter.filters).map(([name, value]) => { const filterText = Object.entries(this.notifFilter.filters).map(([name, value]) => {
const userText = value.users.length ? `users: ${value.users.join("|")}` : ''; const userText = value.users.length ? `users: ${value.users.join("|")}` : '';
@ -407,7 +407,7 @@ export class AdminRoom extends AdminRoomCommandHandler {
const users = parameters.filter(param => param.toLowerCase().startsWith("users:")).map(param => param.toLowerCase().substring("users:".length).split(",")).flat(); const users = parameters.filter(param => param.toLowerCase().startsWith("users:")).map(param => param.toLowerCase().substring("users:".length).split(",")).flat();
const repos = parameters.filter(param => param.toLowerCase().startsWith("repos:")).map(param => param.toLowerCase().substring("repos:".length).split(",")).flat(); const repos = parameters.filter(param => param.toLowerCase().startsWith("repos:")).map(param => param.toLowerCase().substring("repos:".length).split(",")).flat();
if (orgs.length + users.length + repos.length === 0) { if (orgs.length + users.length + repos.length === 0) {
return this.sendNotice("You must specify some filter options like 'orgs:matrix-org,half-shot', 'users:Half-Shot' or 'repos:matrix-hookshot'"); return this.sendNotice("You must specify some filter options like 'orgs:matrix-org,half-shot', 'users:Half-Shot' or 'repos:matrix-hookshot'.");
} }
this.notifFilter.setFilter(name, { this.notifFilter.setFilter(name, {
orgs, orgs,
@ -415,20 +415,20 @@ export class AdminRoom extends AdminRoomCommandHandler {
repos, repos,
}); });
await this.botIntent.underlyingClient.sendStateEvent(this.roomId, NotifFilter.StateType, "", this.notifFilter.getStateContent()); await this.botIntent.underlyingClient.sendStateEvent(this.roomId, NotifFilter.StateType, "", this.notifFilter.getStateContent());
return this.sendNotice(`Stored new filter "${name}". You can now apply the filter by saying 'filters notifications toggle $name'`); return this.sendNotice(`Stored new filter "${name}". You can now apply the filter by saying 'filters notifications toggle $name'.`);
} }
@botCommand("filters notifications toggle", "Apply a filter as a whitelist to your notifications", ["name"]) @botCommand("filters notifications toggle", "Apply a filter as a whitelist to your notifications", ["name"])
public async setFiltersNotificationsToggle(name: string) { public async setFiltersNotificationsToggle(name: string) {
if (!this.notifFilter.filters[name]) { if (!this.notifFilter.filters[name]) {
return this.sendNotice(`Filter "${name}" doesn't exist'`); return this.sendNotice(`Filter "${name}" doesn't exist.`);
} }
if (this.notifFilter.forNotifications.has(name)) { if (this.notifFilter.forNotifications.has(name)) {
this.notifFilter.forNotifications.delete(name); this.notifFilter.forNotifications.delete(name);
await this.sendNotice(`Filter "${name}" disabled for notifications`); await this.sendNotice(`Filter "${name}" disabled for notifications.`);
} else { } else {
this.notifFilter.forNotifications.add(name); this.notifFilter.forNotifications.add(name);
await this.sendNotice(`Filter "${name}" enabled for notifications`); await this.sendNotice(`Filter "${name}" enabled for notifications.`);
} }
return this.botIntent.underlyingClient.sendStateEvent(this.roomId, NotifFilter.StateType, "", this.notifFilter.getStateContent()); return this.botIntent.underlyingClient.sendStateEvent(this.roomId, NotifFilter.StateType, "", this.notifFilter.getStateContent());
} }
@ -449,7 +449,7 @@ export class AdminRoom extends AdminRoomCommandHandler {
const checkPermission = (service: string, level: BridgePermissionLevel) => this.config.checkPermission(this.userId, service, level); const checkPermission = (service: string, level: BridgePermissionLevel) => this.config.checkPermission(this.userId, service, level);
const result = await handleCommand(this.userId, command, AdminRoom.botCommands, this, checkPermission); const result = await handleCommand(this.userId, command, AdminRoom.botCommands, this, checkPermission);
if (!result.handled) { if (!result.handled) {
return this.sendNotice("Command not understood"); return this.sendNotice("Command not understood.");
} }
if ("error" in result) { if ("error" in result) {
@ -488,7 +488,7 @@ export class AdminRoom extends AdminRoomCommandHandler {
} }
} }
public async setupWidget() { public async setUpWidget() {
try { try {
const res = await this.botIntent.underlyingClient.getRoomStateEvent(this.roomId, "im.vector.modular.widgets", "bridge_control"); const res = await this.botIntent.underlyingClient.getRoomStateEvent(this.roomId, "im.vector.modular.widgets", "bridge_control");
if (res) { if (res) {

View File

@ -106,10 +106,10 @@ export async function handleCommand(
if (command) { if (command) {
const permissionService = command.permissionService || defaultPermissionService; const permissionService = command.permissionService || defaultPermissionService;
if (permissionService && !permissionCheckFn(permissionService, command.permissionLevel || BridgePermissionLevel.commands)) { if (permissionService && !permissionCheckFn(permissionService, command.permissionLevel || BridgePermissionLevel.commands)) {
return {handled: true, error: "You do not have permission to use this command"}; return {handled: true, error: "You do not have permission to use this command."};
} }
if (command.requiredArgs && command.requiredArgs.length > parts.length - i) { if (command.requiredArgs && command.requiredArgs.length > parts.length - i) {
return {handled: true, error: "Missing args"}; return {handled: true, error: "Missing at least one required parameter."};
} }
const args = parts.slice(i); const args = parts.slice(i);
if (command.includeUserId) { if (command.includeUserId) {

View File

@ -109,7 +109,7 @@ export class Bridge {
} }
if (this.config.figma) { if (this.config.figma) {
// Ensure webhooks are setup // Ensure webhooks are set up
await ensureFigmaWebhooks(this.config.figma, this.as.botClient); await ensureFigmaWebhooks(this.config.figma, this.as.botClient);
} }
@ -621,12 +621,12 @@ export class Bridge {
// No state yet // No state yet
} }
} }
const adminRoom = await this.setupAdminRoom(roomId, accountData, notifContent || NotifFilter.getDefaultContent()); const adminRoom = await this.setUpAdminRoom(roomId, accountData, notifContent || NotifFilter.getDefaultContent());
// Call this on startup to set the state // Call this on startup to set the state
await this.onAdminRoomSettingsChanged(adminRoom, accountData, { admin_user: accountData.admin_user }); await this.onAdminRoomSettingsChanged(adminRoom, accountData, { admin_user: accountData.admin_user });
log.debug(`Room ${roomId} is connected to: ${adminRoom.toString()}`); log.debug(`Room ${roomId} is connected to: ${adminRoom.toString()}`);
} catch (ex) { } catch (ex) {
log.error(`Failed to setup admin room ${roomId}:`, ex); log.error(`Failed to set up admin room ${roomId}:`, ex);
} }
})); }));
@ -678,7 +678,7 @@ export class Bridge {
} }
await retry(() => this.as.botIntent.joinRoom(roomId), 5); await retry(() => this.as.botIntent.joinRoom(roomId), 5);
if (event.content.is_direct) { if (event.content.is_direct) {
const room = await this.setupAdminRoom(roomId, {admin_user: event.sender}, NotifFilter.getDefaultContent()); const room = await this.setUpAdminRoom(roomId, {admin_user: event.sender}, NotifFilter.getDefaultContent());
await this.as.botIntent.underlyingClient.setRoomAccountData( await this.as.botIntent.underlyingClient.setRoomAccountData(
BRIDGE_ROOM_TYPE, roomId, room.accountData, BRIDGE_ROOM_TYPE, roomId, room.accountData,
); );
@ -1016,10 +1016,10 @@ export class Bridge {
is_direct: true, is_direct: true,
preset: "trusted_private_chat", preset: "trusted_private_chat",
}); });
return this.setupAdminRoom(roomId, {admin_user: userId}, NotifFilter.getDefaultContent()); return this.setUpAdminRoom(roomId, {admin_user: userId}, NotifFilter.getDefaultContent());
} }
private async setupAdminRoom(roomId: string, accountData: AdminAccountData, notifContent: NotificationFilterStateContent) { private async setUpAdminRoom(roomId: string, accountData: AdminAccountData, notifContent: NotificationFilterStateContent) {
const adminRoom = new AdminRoom( const adminRoom = new AdminRoom(
roomId, accountData, notifContent, this.as.botIntent, this.tokenStore, this.config, roomId, accountData, notifContent, this.as.botIntent, this.tokenStore, this.config,
); );
@ -1053,9 +1053,9 @@ export class Bridge {
}); });
this.adminRooms.set(roomId, adminRoom); this.adminRooms.set(roomId, adminRoom);
if (this.config.widgets?.addToAdminRooms && this.config.widgets.publicUrl) { if (this.config.widgets?.addToAdminRooms && this.config.widgets.publicUrl) {
await adminRoom.setupWidget(); await adminRoom.setUpWidget();
} }
log.debug(`Setup ${roomId} as an admin room for ${adminRoom.userId}`); log.debug(`Set up ${roomId} as an admin room for ${adminRoom.userId}`);
return adminRoom; return adminRoom;
} }
} }

View File

@ -48,7 +48,7 @@ export abstract class CommandConnection extends BaseConnection {
}); });
await this.botClient.sendEvent(this.roomId, 'm.room.message', { await this.botClient.sendEvent(this.roomId, 'm.room.message', {
msgtype: "m.notice", msgtype: "m.notice",
body: humanError ? `Failed to handle command: ${humanError}` : "Failed to handle command", body: humanError ? `Failed to handle command: ${humanError}` : "Failed to handle command.",
}); });
log.warn(`Failed to handle command:`, error); log.warn(`Failed to handle command:`, error);
return true; return true;

View File

@ -514,7 +514,7 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
const workflow = workflows.data.workflows.find(w => w.name.toLowerCase().trim() === name.toLowerCase().trim()); const workflow = workflows.data.workflows.find(w => w.name.toLowerCase().trim() === name.toLowerCase().trim());
if (!workflow) { if (!workflow) {
const workflowNames = workflows.data.workflows.map(w => w.name).join(', '); const workflowNames = workflows.data.workflows.map(w => w.name).join(', ');
await this.as.botIntent.sendText(this.roomId, `Could not find a workflow by the name of "${name}". The workflows on this repository are ${workflowNames}`, "m.notice"); await this.as.botIntent.sendText(this.roomId, `Could not find a workflow by the name of "${name}". The workflows on this repository are ${workflowNames}.`, "m.notice");
return; return;
} }
try { try {
@ -544,7 +544,7 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
throw ex; throw ex;
} }
await this.as.botIntent.sendText(this.roomId, `Workflow started`, "m.notice"); await this.as.botIntent.sendText(this.roomId, `Workflow started.`, "m.notice");
} }
public async onIssueCreated(event: IssuesOpenedEvent) { public async onIssueCreated(event: IssuesOpenedEvent) {

View File

@ -76,7 +76,7 @@ export class GitLabRepoConnection extends CommandConnection {
public async onCreateIssue(userId: string, title: string, description?: string, labels?: string) { public async onCreateIssue(userId: string, title: string, description?: string, labels?: string) {
const client = await this.tokenStore.getGitLabForUser(userId, this.instance.url); const client = await this.tokenStore.getGitLabForUser(userId, this.instance.url);
if (!client) { if (!client) {
await this.as.botIntent.sendText(this.roomId, "You must login to create an issue", "m.notice"); await this.as.botIntent.sendText(this.roomId, "You must be logged in to create an issue.", "m.notice");
throw Error('Not logged in'); throw Error('Not logged in');
} }
const res = await client.issues.create({ const res = await client.issues.create({
@ -99,7 +99,7 @@ export class GitLabRepoConnection extends CommandConnection {
public async onClose(userId: string, number: string) { public async onClose(userId: string, number: string) {
const client = await this.tokenStore.getGitLabForUser(userId, this.instance.url); const client = await this.tokenStore.getGitLabForUser(userId, this.instance.url);
if (!client) { if (!client) {
await this.as.botIntent.sendText(this.roomId, "You must login to create an issue", "m.notice"); await this.as.botIntent.sendText(this.roomId, "You must be logged in to create an issue.", "m.notice");
throw Error('Not logged in'); throw Error('Not logged in');
} }

View File

@ -16,7 +16,7 @@ export interface IConnection {
*/ */
get connectionId(): string; get connectionId(): string;
/** /**
* When a room gets an update to it's state. * When a room gets an update to its state.
*/ */
onStateUpdate?: (ev: MatrixEvent<unknown>) => Promise<void>; onStateUpdate?: (ev: MatrixEvent<unknown>) => Promise<void>;
/** /**

View File

@ -41,19 +41,19 @@ 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) @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) { public async onGitHubRepo(userId: string, url: string) {
if (!this.githubInstance || !this.config.github) { if (!this.githubInstance || !this.config.github) {
throw new CommandError("not-configured", "The bridge is not configured to support GitHub"); throw new CommandError("not-configured", "The bridge is not configured to support GitHub.");
} }
if (!this.config.checkPermission(userId, "github", BridgePermissionLevel.manageConnections)) { if (!this.config.checkPermission(userId, "github", BridgePermissionLevel.manageConnections)) {
throw new CommandError('You are not permitted to provision connections for GitHub'); throw new CommandError('You are not permitted to provision connections for GitHub.');
} }
if (!await this.as.botClient.userHasPowerLevelFor(userId, this.roomId, "", true)) { 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 setup new integrations."); throw new CommandError("not-configured", "You must be able to set state in a room ('Change settings') in order to set up new integrations.");
} }
if (!await this.as.botClient.userHasPowerLevelFor(this.as.botUserId, this.roomId, GitHubRepoConnection.CanonicalEventType, true)) { 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 setup a bridge in this room. Please promote me to an Admin/Moderator"); throw new CommandError("Bot lacks power level to set room state", "I do not have permission to set up a bridge in this room. Please promote me to an Admin/Moderator.");
} }
const octokit = await this.tokenStore.getOctokitForUser(userId); const octokit = await this.tokenStore.getOctokitForUser(userId);
if (!octokit) { if (!octokit) {
@ -61,7 +61,7 @@ export class SetupConnection extends CommandConnection {
} }
const urlParts = /^https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(url.trim().toLowerCase()); const urlParts = /^https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.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.");
} }
const [, org, repo] = urlParts; const [, org, repo] = urlParts;
const res = await GitHubRepoConnection.provisionConnection(this.roomId, userId, {org, repo}, this.as, this.tokenStore, this.githubInstance, this.config.github); const res = await GitHubRepoConnection.provisionConnection(this.roomId, userId, {org, repo}, this.as, this.tokenStore, this.githubInstance, this.config.github);
@ -69,20 +69,20 @@ 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("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);
if (!this.config.jira) { if (!this.config.jira) {
throw new CommandError("not-configured", "The bridge is not configured to support Jira"); throw new CommandError("not-configured", "The bridge is not configured to support Jira.");
} }
if (!this.config.checkPermission(userId, "jira", BridgePermissionLevel.manageConnections)) { if (!this.config.checkPermission(userId, "jira", BridgePermissionLevel.manageConnections)) {
throw new CommandError('You are not permitted to provision connections for Jira'); throw new CommandError('You are not permitted to provision connections for Jira.');
} }
if (!await this.as.botClient.userHasPowerLevelFor(userId, this.roomId, "", true)) { 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 setup new integrations."); throw new CommandError("not-configured", "You must be able to set state in a room ('Change settings') in order to set up new integrations.");
} }
if (!await this.as.botClient.userHasPowerLevelFor(this.as.botUserId, this.roomId, GitHubRepoConnection.CanonicalEventType, true)) { 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 setup a bridge in this room. Please promote me to an Admin/Moderator"); throw new CommandError("Bot lacks power level to set room state", "I do not have permission to set up a bridge in this room. Please promote me to an Admin/Moderator.");
} }
const jiraClient = await this.tokenStore.getJiraForUser(userId, urlStr); const jiraClient = await this.tokenStore.getJiraForUser(userId, urlStr);
if (!jiraClient) { if (!jiraClient) {
@ -91,57 +91,57 @@ export class SetupConnection extends CommandConnection {
const urlParts = /.+\/projects\/(\w+)\/?(\w+\/?)*$/.exec(url.pathname.toLowerCase()); const urlParts = /.+\/projects\/(\w+)\/?(\w+\/?)*$/.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`.");
} }
const safeUrl = `https://${url.host}/projects/${projectKey}`; const safeUrl = `https://${url.host}/projects/${projectKey}`;
const res = await JiraProjectConnection.provisionConnection(this.roomId, userId, { url: safeUrl }, this.as, this.tokenStore); 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.sendStateEvent(this.roomId, JiraProjectConnection.CanonicalEventType, safeUrl, res.stateEventContent);
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("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) {
throw new CommandError("not-configured", "The bridge is not configured to support webhooks"); throw new CommandError("not-configured", "The bridge is not configured to support webhooks.");
} }
if (!this.config.checkPermission(userId, "webhooks", BridgePermissionLevel.manageConnections)) { if (!this.config.checkPermission(userId, "webhooks", BridgePermissionLevel.manageConnections)) {
throw new CommandError('You are not permitted to provision connections for generic webhooks'); throw new CommandError('You are not permitted to provision connections for generic webhooks.');
} }
if (!await this.as.botClient.userHasPowerLevelFor(userId, this.roomId, "", true)) { 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 setup new integrations."); throw new CommandError("not-configured", "You must be able to set state in a room ('Change settings') in order to set up new integrations.");
} }
if (!await this.as.botClient.userHasPowerLevelFor(this.as.botUserId, this.roomId, GitHubRepoConnection.CanonicalEventType, true)) { 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 setup a bridge in this room. Please promote me to an Admin/Moderator"); throw new CommandError("Bot lacks power level to set room state", "I do not have permission to set up a bridge in this room. Please promote me to an Admin/Moderator.");
} }
if (!name || name.length < 3 || name.length > 64) { if (!name || name.length < 3 || name.length > 64) {
throw new CommandError("Bad webhook name", "A webhook name must be between 3-64 characters"); throw new CommandError("Bad webhook name", "A webhook name must be between 3-64 characters.");
} }
const hookId = uuid(); const hookId = uuid();
const url = `${this.config.generic.urlPrefix}${this.config.generic.urlPrefix.endsWith('/') ? '' : '/'}${hookId}`; const url = `${this.config.generic.urlPrefix}${this.config.generic.urlPrefix.endsWith('/') ? '' : '/'}${hookId}`;
await GenericHookConnection.ensureRoomAccountData(this.roomId, this.as, hookId, name); await GenericHookConnection.ensureRoomAccountData(this.roomId, this.as, hookId, name);
await this.as.botClient.sendStateEvent(this.roomId, GenericHookConnection.CanonicalEventType, name, {hookId, name}); await this.as.botClient.sendStateEvent(this.roomId, GenericHookConnection.CanonicalEventType, name, {hookId, name});
const adminRoom = await this.getOrCreateAdminRoom(userId); const adminRoom = await this.getOrCreateAdminRoom(userId);
await adminRoom.sendNotice(`You have bridged a webhook. Please configure your webhook source to use \`${url}\``); await adminRoom.sendNotice(`You have bridged a webhook. Please configure your webhook source to use \`${url}\`.`);
return this.as.botClient.sendHtmlNotice(this.roomId, md.renderInline(`Room configured to bridge webhooks. See admin room for secret url.`)); return this.as.botClient.sendHtmlNotice(this.roomId, md.renderInline(`Room configured to bridge webhooks. See admin room for secret url.`));
} }
@botCommand("figma file", "Bridge a Figma file to the room", ["url"], [], true) @botCommand("figma file", "Bridge a Figma file to the room.", ["url"], [], true)
public async onFigma(userId: string, url: string) { public async onFigma(userId: string, url: string) {
if (!this.config.figma) { if (!this.config.figma) {
throw new CommandError("not-configured", "The bridge is not configured to support Figma"); throw new CommandError("not-configured", "The bridge is not configured to support Figma.");
} }
if (!this.config.checkPermission(userId, "figma", BridgePermissionLevel.manageConnections)) { if (!this.config.checkPermission(userId, "figma", BridgePermissionLevel.manageConnections)) {
throw new CommandError('You are not permitted to provision connections for Figma'); throw new CommandError('You are not permitted to provision connections for Figma.');
} }
if (!await this.as.botClient.userHasPowerLevelFor(userId, this.roomId, "", true)) { 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 setup new integrations."); throw new CommandError("not-configured", "You must be able to set state in a room ('Change settings') in order to set up new integrations.");
} }
if (!await this.as.botClient.userHasPowerLevelFor(this.as.botUserId, this.roomId, GitHubRepoConnection.CanonicalEventType, true)) { 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 setup a bridge in this room. Please promote me to an Admin/Moderator"); throw new CommandError("Bot lacks power level to set room state", "I do not have permission to set up a bridge in this room. Please promote me to an Admin/Moderator.");
} }
const res = /https:\/\/www\.figma\.com\/file\/(\w+).+/.exec(url); const res = /https:\/\/www\.figma\.com\/file\/(\w+).+/.exec(url);
if (!res) { if (!res) {
throw new CommandError("Invalid Figma url", "The Figma file url you entered was not valid. It should be in the format of `https://figma.com/file/FILEID/...`"); throw new CommandError("Invalid Figma url", "The Figma file url you entered was not valid. It should be in the format of `https://figma.com/file/FILEID/...`.");
} }
const [, fileId] = res; const [, fileId] = res;
await this.as.botClient.sendStateEvent(this.roomId, FigmaFileConnection.CanonicalEventType, fileId, {fileId}); await this.as.botClient.sendStateEvent(this.roomId, FigmaFileConnection.CanonicalEventType, fileId, {fileId});

View File

@ -20,22 +20,22 @@ export function generateGitHubOAuthUrl(clientId: string, redirectUri: string, st
} }
export class GitHubBotCommands extends AdminRoomCommandHandler { export class GitHubBotCommands extends AdminRoomCommandHandler {
@botCommand("github login", {help: "Login to GitHub", category: "github"}) @botCommand("github login", {help: "Log in to GitHub", category: "github"})
public async loginCommand() { public async loginCommand() {
if (!this.config.github) { if (!this.config.github) {
throw new CommandError("no-github-support", "The bridge is not configured with GitHub support"); throw new CommandError("no-github-support", "The bridge is not configured with GitHub support.");
} }
if (!this.config.github.oauth) { if (!this.config.github.oauth) {
throw new CommandError("no-github-support", "The bridge is not configured with GitHub OAuth support"); throw new CommandError("no-github-support", "The bridge is not configured with GitHub OAuth support.");
} }
const state = this.tokenStore.createStateForOAuth(this.userId); const state = this.tokenStore.createStateForOAuth(this.userId);
return this.sendNotice(`To login, open ${generateGitHubOAuthUrl(this.config.github.oauth.client_id, this.config.github.oauth.redirect_uri, state)} to link your account to the bridge`); return this.sendNotice(`Open ${generateGitHubOAuthUrl(this.config.github.oauth.client_id, this.config.github.oauth.redirect_uri, state)} to link your account to the bridge.`);
} }
@botCommand("github setpersonaltoken", {help: "Set your personal access token for GitHub", requiredArgs: ['accessToken'], category: "github"}) @botCommand("github setpersonaltoken", {help: "Set your personal access token for GitHub", requiredArgs: ['accessToken'], category: "github"})
public async setGHPersonalAccessToken(accessToken: string) { public async setGHPersonalAccessToken(accessToken: string) {
if (!this.config.github) { if (!this.config.github) {
throw new CommandError("no-github-support", "The bridge is not configured with GitHub support"); throw new CommandError("no-github-support", "The bridge is not configured with GitHub support.");
} }
let me; let me;
try { try {
@ -46,18 +46,18 @@ export class GitHubBotCommands extends AdminRoomCommandHandler {
await this.sendNotice("Could not authenticate with GitHub. Is your token correct?"); await this.sendNotice("Could not authenticate with GitHub. Is your token correct?");
return; return;
} }
await this.sendNotice(`Connected as ${me.data.login}. Token stored`); await this.sendNotice(`Connected as ${me.data.login}. Token stored.`);
await this.tokenStore.storeUserToken("github", this.userId, JSON.stringify({access_token: accessToken, token_type: 'pat'} as GitHubOAuthToken)); await this.tokenStore.storeUserToken("github", this.userId, JSON.stringify({access_token: accessToken, token_type: 'pat'} as GitHubOAuthToken));
} }
@botCommand("github hastoken", {help: "Check if you have a token stored for GitHub", category: "github"}) @botCommand("github hastoken", {help: "Check if you have a token stored for GitHub", category: "github"})
public async hasPersonalToken() { public async hasPersonalToken() {
if (!this.config.github) { if (!this.config.github) {
throw new CommandError("no-github-support", "The bridge is not configured with GitHub support"); throw new CommandError("no-github-support", "The bridge is not configured with GitHub support.");
} }
const result = await this.tokenStore.getUserToken("github", this.userId); const result = await this.tokenStore.getUserToken("github", this.userId);
if (result === null) { if (result === null) {
await this.sendNotice("You do not currently have a token stored"); await this.sendNotice("You do not currently have a token stored.");
return; return;
} }
await this.sendNotice("A token is stored for your GitHub account."); await this.sendNotice("A token is stored for your GitHub account.");

View File

@ -7,22 +7,22 @@ import { CLOUD_INSTANCE } from "./Client";
const log = new LogWrapper('JiraBotCommands'); const log = new LogWrapper('JiraBotCommands');
export class JiraBotCommands extends AdminRoomCommandHandler { export class JiraBotCommands extends AdminRoomCommandHandler {
@botCommand("jira login", {help: "Login to JIRA", category: "jira"}) @botCommand("jira login", {help: "Log in to JIRA", category: "jira"})
public async loginCommand() { public async loginCommand() {
if (!this.config.jira?.oauth || !this.tokenStore.jiraOAuth) { if (!this.config.jira?.oauth || !this.tokenStore.jiraOAuth) {
this.sendNotice(`Bot is not configured with JIRA OAuth support`); this.sendNotice(`Bot is not configured with JIRA OAuth support.`);
return; return;
} }
const state = this.tokenStore.createStateForOAuth(this.userId); const state = this.tokenStore.createStateForOAuth(this.userId);
const url = await this.tokenStore.jiraOAuth?.getAuthUrl(state); const url = await this.tokenStore.jiraOAuth?.getAuthUrl(state);
await this.sendNotice(`To login, open ${url} to link your account to the bridge`); await this.sendNotice(`Open ${url} to link your account to the bridge.`);
} }
@botCommand("jira logout", {help: "Clear any login information", category: "jira"}) @botCommand("jira logout", {help: "Clear any login information", category: "jira"})
public async logout() { public async logout() {
if (!this.config.jira?.oauth || !this.tokenStore.jiraOAuth) { if (!this.config.jira?.oauth || !this.tokenStore.jiraOAuth) {
this.sendNotice(`Bot is not configured with JIRA OAuth support`); this.sendNotice(`Bot is not configured with JIRA OAuth support.`);
return; return;
} }
if (await this.tokenStore.clearUserToken("jira", this.userId, this.config.jira.url || CLOUD_INSTANCE)) { if (await this.tokenStore.clearUserToken("jira", this.userId, this.config.jira.url || CLOUD_INSTANCE)) {
@ -34,13 +34,13 @@ export class JiraBotCommands extends AdminRoomCommandHandler {
@botCommand("jira whoami", {help: "Determine JIRA identity", category: "jira"}) @botCommand("jira whoami", {help: "Determine JIRA identity", category: "jira"})
public async whoami() { public async whoami() {
if (!this.config.jira) { if (!this.config.jira) {
await this.sendNotice(`Bot is not configured with JIRA OAuth support`); await this.sendNotice(`Bot is not configured with JIRA OAuth support.`);
return; return;
} }
const client = await this.tokenStore.getJiraForUser(this.userId, this.config.jira.url); const client = await this.tokenStore.getJiraForUser(this.userId, this.config.jira.url);
if (!client) { if (!client) {
await this.sendNotice(`You are not logged into JIRA`); await this.sendNotice(`You are not logged into JIRA.`);
return; return;
} }
// Get all resources for user // Get all resources for user
@ -49,10 +49,10 @@ export class JiraBotCommands extends AdminRoomCommandHandler {
resources = await client.getAccessibleResources(); resources = await client.getAccessibleResources();
} catch (ex) { } catch (ex) {
log.warn(`Could not request resources from JIRA API: `, ex); log.warn(`Could not request resources from JIRA API: `, ex);
await this.sendNotice(`Could not request JIRA resources due to an error`); await this.sendNotice(`Could not request JIRA resources due to an error.`);
return; return;
} }
let response = resources.length === 0 ? `You do not have any instances authorised with this bot` : 'You have access to the following instances:'; let response = resources.length === 0 ? `You do not have any instances authorised with this bot.` : 'You have access to the following instances:';
for (const resource of resources) { for (const resource of resources) {
const clientForResource = await client.getClientForResource(resource); const clientForResource = await client.getClientForResource(resource);
if (!clientForResource) { if (!clientForResource) {

View File

@ -134,7 +134,7 @@ export class JiraProvisionerRouter {
private async onOAuth(req: Request<undefined, undefined, undefined, {userId: string}>, res: Response<{url: string}>) { private async onOAuth(req: Request<undefined, undefined, undefined, {userId: string}>, res: Response<{url: string}>) {
if (!this.tokenStore.jiraOAuth) { if (!this.tokenStore.jiraOAuth) {
throw new ApiError('JIRA OAuth is disabled', ErrCode.DisabledFeature); throw new ApiError('JIRA OAuth is disabled.', ErrCode.DisabledFeature);
} }
const url = await this.tokenStore.jiraOAuth.getAuthUrl(this.tokenStore.createStateForOAuth(req.query.userId)); const url = await this.tokenStore.jiraOAuth.getAuthUrl(this.tokenStore.createStateForOAuth(req.query.userId));
res.send({ url }); res.send({ url });
@ -157,7 +157,7 @@ export class JiraProvisionerRouter {
} }
} catch (ex) { } catch (ex) {
log.warn(`Failed to fetch accessible resources for ${req.query.userId}`, ex); log.warn(`Failed to fetch accessible resources for ${req.query.userId}`, ex);
return next( new ApiError("Could not fetch accessible resources for JIRA user", ErrCode.Unknown)); return next( new ApiError("Could not fetch accessible resources for JIRA user.", ErrCode.Unknown));
} }
return res.send({ return res.send({
loggedIn: true, loggedIn: true,
@ -177,10 +177,10 @@ export class JiraProvisionerRouter {
resClient = await jiraUser.getClientForName(req.params.instanceName); resClient = await jiraUser.getClientForName(req.params.instanceName);
} catch (ex) { } catch (ex) {
log.warn(`Failed to fetch client for ${req.params.instanceName} for ${req.query.userId}`, ex); log.warn(`Failed to fetch client for ${req.params.instanceName} for ${req.query.userId}`, ex);
return next( new ApiError("Could not fetch accessible resources for JIRA user", ErrCode.Unknown)); return next( new ApiError("Could not fetch accessible resources for JIRA user.", ErrCode.Unknown));
} }
if (!resClient) { if (!resClient) {
return next( new ApiError("Instance not known or not accessible to this user", ErrCode.ForbiddenUser)); return next( new ApiError("Instance not known or not accessible to this user.", ErrCode.ForbiddenUser));
} }
const projects = []; const projects = [];
@ -195,7 +195,7 @@ export class JiraProvisionerRouter {
} }
} catch (ex) { } catch (ex) {
log.warn(`Failed to fetch accessible projects for ${req.params.instanceName} / ${req.query.userId}`, ex); log.warn(`Failed to fetch accessible projects for ${req.params.instanceName} / ${req.query.userId}`, ex);
return next( new ApiError("Could not fetch accessible projects for JIRA user", ErrCode.Unknown)); return next( new ApiError("Could not fetch accessible projects for JIRA user.", ErrCode.Unknown));
} }
return res.send(projects); return res.send(projects);

View File

@ -76,7 +76,7 @@ export class UserTokenStore {
public async storeUserToken(type: TokenType, userId: string, token: string, instanceUrl?: string): Promise<void> { public async storeUserToken(type: TokenType, userId: string, token: string, instanceUrl?: string): Promise<void> {
if (!this.config.checkPermission(userId, type, BridgePermissionLevel.login)) { if (!this.config.checkPermission(userId, type, BridgePermissionLevel.login)) {
throw new ApiError('User does not have permission to login to service', ErrCode.ForbiddenUser); throw new ApiError('User does not have permission to log in to service', ErrCode.ForbiddenUser);
} }
const key = tokenKey(type, userId, false, instanceUrl); const key = tokenKey(type, userId, false, instanceUrl);
const tokenParts: string[] = []; const tokenParts: string[] = [];

View File

@ -55,7 +55,7 @@ export async function ensureFigmaWebhooks(figmaConfig: BridgeConfigFigma, matrix
webhookDefinition = res.data as FigmaWebhookDefinition; webhookDefinition = res.data as FigmaWebhookDefinition;
await matrixClient.setAccountData(accountDataKey, {webhookId: webhookDefinition.id}); await matrixClient.setAccountData(accountDataKey, {webhookId: webhookDefinition.id});
} }
// Webhook is ready and setup // Webhook is ready and set up
} }
} }

View File

@ -45,7 +45,7 @@ Request the connection types enabled for this bridge.
"type": "JiraProject", // The name of the connection "type": "JiraProject", // The name of the connection
"eventType": "uk.half-shot.matrix-hookshot.jira.project", // Corresponds to the state type for the connection "eventType": "uk.half-shot.matrix-hookshot.jira.project", // Corresponds to the state type for the connection
"service": "jira", // or github, webhook. A human-readable service name to make things look pretty "service": "jira", // or github, webhook. A human-readable service name to make things look pretty
"botUserId": "@hookshot:yourdomain.com", // The bot mxid for the service. Currently this is the sender_localpart, but may change in the future. "botUserId": "@hookshot:yourdomain.com", // The bot mxid for the service. Currently, this is the sender_localpart, but may change in the future.
} }
} }
``` ```
@ -62,7 +62,7 @@ Request the connections for a given room. The `{roomId}` parameter is the target
"eventType": "uk.half-shot.matrix-hookshot.jira.project", // Corresponds to the state type in the connection "eventType": "uk.half-shot.matrix-hookshot.jira.project", // Corresponds to the state type in the connection
"id": "opaque-unique-id", // An opaque ID used to refer to this connection. Should **NOT** be assumed to be stable. "id": "opaque-unique-id", // An opaque ID used to refer to this connection. Should **NOT** be assumed to be stable.
"service": "jira", // or github, webhook. A human-readable service name to make things look pretty "service": "jira", // or github, webhook. A human-readable service name to make things look pretty
"botUserId": "@hookshot:yourdomain.com", // The bot mxid for the service. Currently this is the sender_localpart, but may change in the future. "botUserId": "@hookshot:yourdomain.com", // The bot mxid for the service. Currently, this is the sender_localpart, but may change in the future.
"config": { "config": {
// ... connection specific details, can be configured. // ... connection specific details, can be configured.
} }
@ -82,7 +82,7 @@ Request details of a single connection. The `{roomId}` parameter is the target M
"eventType": "uk.half-shot.matrix-hookshot.jira.project", // Corresponds to the state type in the connection "eventType": "uk.half-shot.matrix-hookshot.jira.project", // Corresponds to the state type in the connection
"id": "opaque-unique-id", // An opaque ID used to refer to this connection. Should **NOT** be assumed to be stable. "id": "opaque-unique-id", // An opaque ID used to refer to this connection. Should **NOT** be assumed to be stable.
"service": "jira", // or github, webhook. A human-readable service name to make things look pretty "service": "jira", // or github, webhook. A human-readable service name to make things look pretty
"botUserId": "@hookshot:yourdomain.com", // The bot mxid for the service. Currently this is the sender_localpart, but may change in the future. "botUserId": "@hookshot:yourdomain.com", // The bot mxid for the service. Currently, this is the sender_localpart, but may change in the future.
"config": { "config": {
// ... connection specific details, can be configured. // ... connection specific details, can be configured.
} }
@ -110,7 +110,7 @@ The body of the request is the configuration for the connection, which will be t
"eventType": "uk.half-shot.matrix-hookshot.jira.project", // Corresponds to the state type in the connection "eventType": "uk.half-shot.matrix-hookshot.jira.project", // Corresponds to the state type in the connection
"id": "opaque-unique-id", // An opaque ID used to refer to this connection. Should **NOT** be assumed to be stable. "id": "opaque-unique-id", // An opaque ID used to refer to this connection. Should **NOT** be assumed to be stable.
"service": "jira", // or github, webhook. A human-readable service name to make things look pretty "service": "jira", // or github, webhook. A human-readable service name to make things look pretty
"botUserId": "@hookshot:yourdomain.com", // The bot mxid for the service. Currently this is the sender_localpart, but may change in the future. "botUserId": "@hookshot:yourdomain.com", // The bot mxid for the service. Currently, this is the sender_localpart, but may change in the future.
"config": { "config": {
// ... connection specific details, can be configured. // ... connection specific details, can be configured.
} }
@ -138,7 +138,7 @@ The body of the request is the configuration for the connection, which will be t
"eventType": "uk.half-shot.matrix-hookshot.jira.project", // Corresponds to the state type in the connection "eventType": "uk.half-shot.matrix-hookshot.jira.project", // Corresponds to the state type in the connection
"id": "opaque-unique-id", // An opaque ID used to refer to this connection. Should **NOT** be assumed to be stable. "id": "opaque-unique-id", // An opaque ID used to refer to this connection. Should **NOT** be assumed to be stable.
"service": "jira", // or github, webhook. A human-readable service name to make things look pretty "service": "jira", // or github, webhook. A human-readable service name to make things look pretty
"botUserId": "@hookshot:yourdomain.com", // The bot mxid for the service. Currently this is the sender_localpart, but may change in the future. "botUserId": "@hookshot:yourdomain.com", // The bot mxid for the service. Currently, this is the sender_localpart, but may change in the future.
"config": { "config": {
// ... connection specific details, can be configured. // ... connection specific details, can be configured.
} }

View File

@ -185,10 +185,10 @@ export class Provisioner {
private getConnection(req: Request<{roomId: string, connectionId: string}>, res: Response<GetConnectionsResponseItem>) { private getConnection(req: Request<{roomId: string, connectionId: string}>, res: Response<GetConnectionsResponseItem>) {
const connection = this.connMan.getConnectionById(req.params.roomId, req.params.connectionId); const connection = this.connMan.getConnectionById(req.params.roomId, req.params.connectionId);
if (!connection) { if (!connection) {
throw new ApiError("Connection does not exist", ErrCode.NotFound); throw new ApiError("Connection does not exist.", ErrCode.NotFound);
} }
if (!connection.getProvisionerDetails) { if (!connection.getProvisionerDetails) {
throw new ApiError("Connection type does not support updates", ErrCode.UnsupportedOperation); throw new ApiError("Connection type does not support updates.", ErrCode.UnsupportedOperation);
} }
return res.send(connection.getProvisionerDetails()); return res.send(connection.getProvisionerDetails());
} }
@ -197,15 +197,15 @@ export class Provisioner {
// Need to figure out which connections are available // Need to figure out which connections are available
try { try {
if (!req.body || typeof req.body !== "object") { if (!req.body || typeof req.body !== "object") {
throw new ApiError("A JSON body must be provided", ErrCode.BadValue); throw new ApiError("A JSON body must be provided.", ErrCode.BadValue);
} }
const connection = await this.connMan.provisionConnection(req.params.roomId, req.query.userId, req.params.type, req.body); const connection = await this.connMan.provisionConnection(req.params.roomId, req.query.userId, req.params.type, req.body);
if (!connection.getProvisionerDetails) { if (!connection.getProvisionerDetails) {
throw new Error('Connection supported provisioning but not getProvisionerDetails'); throw new Error('Connection supported provisioning but not getProvisionerDetails.');
} }
res.send(connection.getProvisionerDetails()); res.send(connection.getProvisionerDetails());
} catch (ex) { } catch (ex) {
log.warn(`Failed to create connection for ${req.params.roomId}`, ex); log.warn(`Failed to create connection for ${req.params.roomId}.`, ex);
return next(ex); return next(ex);
} }
} }
@ -214,10 +214,10 @@ export class Provisioner {
try { try {
const connection = this.connMan.getConnectionById(req.params.roomId, req.params.connectionId); const connection = this.connMan.getConnectionById(req.params.roomId, req.params.connectionId);
if (!connection) { if (!connection) {
return next(new ApiError("Connection does not exist", ErrCode.NotFound)); return next(new ApiError("Connection does not exist.", ErrCode.NotFound));
} }
if (!connection.provisionerUpdateConfig || !connection.getProvisionerDetails) { if (!connection.provisionerUpdateConfig || !connection.getProvisionerDetails) {
return next(new ApiError("Connection type does not support updates", ErrCode.UnsupportedOperation)); return next(new ApiError("Connection type does not support updates.", ErrCode.UnsupportedOperation));
} }
await connection.provisionerUpdateConfig(req.query.userId, req.body); await connection.provisionerUpdateConfig(req.query.userId, req.body);
res.send(connection.getProvisionerDetails()); res.send(connection.getProvisionerDetails());
@ -230,10 +230,10 @@ export class Provisioner {
try { try {
const connection = this.connMan.getConnectionById(req.params.roomId, req.params.connectionId); const connection = this.connMan.getConnectionById(req.params.roomId, req.params.connectionId);
if (!connection) { if (!connection) {
return next(new ApiError("Connection does not exist", ErrCode.NotFound)); return next(new ApiError("Connection does not exist.", ErrCode.NotFound));
} }
if (!connection.onRemove) { if (!connection.onRemove) {
return next(new ApiError("Connection does not support removal", ErrCode.UnsupportedOperation)); return next(new ApiError("Connection does not support removal.", ErrCode.UnsupportedOperation));
} }
await this.connMan.purgeConnection(req.params.roomId, req.params.connectionId); await this.connMan.purgeConnection(req.params.roomId, req.params.connectionId);
res.send({ok: true}); res.send({ok: true});