From 39c6e81cbbd7676fabaddee551f8ba61bde520c3 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Sun, 21 Nov 2021 12:34:56 +0000 Subject: [PATCH] Lots of changes --- .dockerignore | 18 +- .gitignore | 11 +- Cargo.lock | 223 +++++++++++++++++++++++++ Cargo.toml | 18 ++ Dockerfile | 10 +- build.rs | 5 + package.json | 14 +- src/ConnectionManager.ts | 245 +++++++++++++++++++++++++++ src/Connections/IConnection.ts | 5 + src/Connections/JiraProject.ts | 12 +- src/FormatUtil.rs | 41 +++++ src/FormatUtil.ts | 19 +-- src/GithubBridge.ts | 296 ++++++++------------------------- src/Jira/Utils.ts | 7 +- src/Jira/mod.rs | 11 ++ src/Jira/types.rs | 29 ++++ src/Jira/utils.rs | 22 +++ src/lib.rs | 16 ++ src/libRs.ts | 19 +++ tests/FormatUtilTest.ts | 37 +++++ tests/jira/Utils.ts | 13 ++ yarn.lock | 229 ++++++++++++++++++++++++- 22 files changed, 1028 insertions(+), 272 deletions(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 build.rs create mode 100644 src/ConnectionManager.ts create mode 100644 src/FormatUtil.rs create mode 100644 src/Jira/mod.rs create mode 100644 src/Jira/types.rs create mode 100644 src/Jira/utils.rs create mode 100644 src/lib.rs create mode 100644 src/libRs.ts create mode 100644 tests/jira/Utils.ts diff --git a/.dockerignore b/.dockerignore index 9abc330c..2b1e5dfd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,10 +1,12 @@ -node_modules/ -lib/ -tests/ -config.yml -public/ .github/ -logging.txt -tsconfig.tsbuildinfo *.pem -registration.yml \ No newline at end of file +config.yml +lib/ +node_modules/ +public/ +registration.yml +tests/ +tsconfig.tsbuildinfo + +# Added by cargo +/target \ No newline at end of file diff --git a/.gitignore b/.gitignore index 14386ea4..5f03ae8b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,10 @@ -node_modules/ -lib/ - -config.yml *.pem +config.yml +lib/ +node_modules/ +public/ registration.yml tsconfig.tsbuildinfo -public/ \ No newline at end of file +# Added by cargo +/target \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..0c90c41b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,223 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "matrix-github" +version = "0.0.1" +dependencies = [ + "napi", + "napi-build", + "napi-derive", + "serde", + "serde_derive", + "serde_json", + "url", +] + +[[package]] +name = "napi" +version = "1.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9050238b713b3c5dd5ae1613da1ccefe4061c03992f9e9bbe43b7d473ba4bd3c" +dependencies = [ + "napi-sys", + "serde", + "serde_json", + "winapi", +] + +[[package]] +name = "napi-build" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87375bacff0768dd606ccf870eae936efd21e3245af9e7b37ae44f969d48be8a" + +[[package]] +name = "napi-derive" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ee880798e942fc785e2e234544b9db578019a1d7676f45dad7f38d432ab0fe4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "napi-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf20e0081fea04e044aa4adf74cfea8ddc0324eec2894b1c700f4cafc72a56" + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "proc-macro2" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "serde" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" + +[[package]] +name = "serde_derive" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063bf466a64011ac24040a49009724ee60a57da1b437617ceb32e53ad61bfb19" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tinyvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "unicode-bidi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..d29bcd0e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "matrix-github" +version = "0.0.1" +edition = "2018" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +napi = {version="1", features=["serde-json"]} +napi-derive = "1" +url = "2" +serde_json = "1" +serde = "1" +serde_derive = "*" + +[build-dependencies] +napi-build = "1" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 85fefea6..db1a9003 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,17 @@ # Stage 0: Build the thing -FROM node:16-alpine AS builder +# Need debian based image to make node happy +FROM node:16 AS builder COPY . /src WORKDIR /src +RUN apk add rustup +RUN rustup-init -y --target x86_64-unknown-linux-gnu +ENV PATH="/root/.cargo/bin:${PATH}" + + # will also build -RUN yarn +RUN yarn # Stage 1: The actual container FROM node:16-alpine diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..5666ae2e --- /dev/null +++ b/build.rs @@ -0,0 +1,5 @@ +extern crate napi_build; + +fn main() { + napi_build::setup(); +} \ No newline at end of file diff --git a/package.json b/package.json index d0becd04..298c7e23 100644 --- a/package.json +++ b/package.json @@ -7,21 +7,26 @@ "author": "Half-Shot", "license": "MIT", "private": false, + "napi": { + "name": "matrix-github-rs" + }, "scripts": { "build:web": "snowpack build", "build:app": "tsc --project tsconfig.json", + "build:app:rs": "napi build --release ./lib", "dev:web": "snowpack dev", - "build": "yarn run build:web && yarn run build:app", + "build": "yarn run build:web && yarn run build:app:rs && yarn run build:app", "prepare": "yarn build", "start": "node --require source-map-support/register lib/App/BridgeApp.js", "start:app": "node --require source-map-support/register lib/App/BridgeApp.js", "start:webhooks": "node --require source-map-support/register lib/App/GithubWebhookApp.js", "start:matrixsender": "node --require source-map-support/register lib/App/MatrixSenderApp.js", - "test": "mocha -r ts-node/register tests/*.ts", + "test": "mocha -r ts-node/register tests/*.ts tests/**/*.ts", "lint": "eslint -c .eslintrc.js src/**/*.ts", "generate-default-config": "node lib/Config/Defaults.js --config > config.sample.yml" }, "dependencies": { + "@node-rs/helper": "^1.2.1", "@octokit/auth-app": "^3.3.0", "@octokit/auth-token": "^2.4.5", "@octokit/rest": "^18.10.0", @@ -47,6 +52,7 @@ }, "devDependencies": { "@fontsource/open-sans": "^4.2.2", + "@napi-rs/cli": "^1.3.5", "@prefresh/snowpack": "^3.1.2", "@snowpack/plugin-typescript": "^1.2.1", "@types/chai": "^4.2.16", @@ -57,14 +63,14 @@ "@types/micromatch": "^4.0.1", "@types/mime": "^2.0.3", "@types/mocha": "^8.2.2", - "@types/node": "^12", "@types/node-emoji": "^1.8.1", + "@types/node": "^12", "@types/uuid": "^8.3.0", "@typescript-eslint/eslint-plugin": "^4.21.0", "@typescript-eslint/parser": "^4.21.0", "chai": "^4.3.4", - "eslint": "^7.24.0", "eslint-plugin-mocha": "^8.1.0", + "eslint": "^7.24.0", "mini.css": "^3.0.1", "preact": "^10.5.13", "snowpack": "^3.2.2", diff --git a/src/ConnectionManager.ts b/src/ConnectionManager.ts new file mode 100644 index 00000000..45bafc08 --- /dev/null +++ b/src/ConnectionManager.ts @@ -0,0 +1,245 @@ + + +/** + * Manages connections between Matrix rooms and the remote side. + */ + +import { Appservice, StateEvent } from "matrix-bot-sdk"; +import { CommentProcessor } from "./CommentProcessor"; +import { BridgeConfig, GitLabInstance } from "./Config/Config"; +import { GitHubDiscussionConnection, GitHubDiscussionSpace, GitHubIssueConnection, GitHubRepoConnection, GitHubUserSpace, GitLabIssueConnection, GitLabRepoConnection, IConnection } from "./Connections"; +import { GenericHookConnection } from "./Connections/GenericHook"; +import { JiraProjectConnection } from "./Connections/JiraProject"; +import { GithubInstance } from "./Github/GithubInstance"; +import { GitLabClient } from "./Gitlab/Client"; +import LogWrapper from "./LogWrapper"; +import { MessageSenderClient } from "./MatrixSender"; +import { UserTokenStore } from "./UserTokenStore"; + +const log = new LogWrapper("ConnectionManager"); + +export class ConnectionManager { + private connections: IConnection[] = []; + + constructor( + private readonly as: Appservice, + private readonly config: BridgeConfig, + private readonly tokenStore: UserTokenStore, + private readonly commentProcessor: CommentProcessor, + private readonly messageClient: MessageSenderClient, + private readonly github?: GithubInstance) { + + } + + /** + * Push a new connection to the manager, if this connection already + * exists then this will no-op. + * NOTE: The comparison only checks that the same object instance isn't present, + * but not if two instances exist with the same type/state. + * @param connection The connection instance to push. + */ + public push(...connections: IConnection[]) { + // NOTE: Double loop + for (const connection of connections) { + if (this.connections.find((c) => c !== connection)) { + this.connections.push(connection); + } + } + // Already exists, noop. + } + + public async createConnectionForState(roomId: string, state: StateEvent) { + log.debug(`Looking to create connection for ${roomId}`); + if (state.content.disabled === false) { + log.debug(`${roomId} has disabled state for ${state.type}`); + return; + } + + if (GitHubRepoConnection.EventTypes.includes(state.type)) { + if (!this.github) { + throw Error('GitHub is not configured'); + } + return new GitHubRepoConnection(roomId, this.as, state.content, this.tokenStore, state.stateKey); + } + + if (GitHubDiscussionConnection.EventTypes.includes(state.type)) { + if (!this.github) { + throw Error('GitHub is not configured'); + } + return new GitHubDiscussionConnection( + roomId, this.as, state.content, state.stateKey, this.tokenStore, this.commentProcessor, + this.messageClient, + ); + } + + if (GitHubDiscussionSpace.EventTypes.includes(state.type)) { + if (!this.github) { + throw Error('GitHub is not configured'); + } + + return new GitHubDiscussionSpace( + await this.as.botClient.getSpace(roomId), state.content, state.stateKey + ); + } + + if (GitHubIssueConnection.EventTypes.includes(state.type)) { + if (!this.github) { + throw Error('GitHub is not configured'); + } + const issue = new GitHubIssueConnection(roomId, this.as, state.content, state.stateKey || "", this.tokenStore, this.commentProcessor, this.messageClient, this.github); + await issue.syncIssueState(); + return issue; + } + + if (GitHubUserSpace.EventTypes.includes(state.type)) { + if (!this.github) { + throw Error('GitHub is not configured'); + } + return new GitHubUserSpace( + await this.as.botClient.getSpace(roomId), state.content, state.stateKey + ); + } + + if (GitLabRepoConnection.EventTypes.includes(state.type)) { + if (!this.config.gitlab) { + throw Error('GitLab is not configured'); + } + const instance = this.config.gitlab.instances[state.content.instance]; + if (!instance) { + throw Error('Instance name not recognised'); + } + return new GitLabRepoConnection(roomId, this.as, state.content, this.tokenStore, instance); + } + + if (GitLabIssueConnection.EventTypes.includes(state.type)) { + if (!this.config.gitlab) { + throw Error('GitLab is not configured'); + } + const instance = this.config.gitlab.instances[state.content.instance]; + return new GitLabIssueConnection( + roomId, + this.as, + state.content, + state.stateKey as string, + this.tokenStore, + this.commentProcessor, + this.messageClient, + instance); + } + + if (JiraProjectConnection.EventTypes.includes(state.type)) { + if (!this.config.jira) { + throw Error('JIRA is not configured'); + } + return new JiraProjectConnection(roomId, this.as, state.content, state.stateKey, this.commentProcessor, this.messageClient); + } + + if (GenericHookConnection.EventTypes.includes(state.type) && this.config.generic?.enabled) { + return new GenericHookConnection( + roomId, + state.content, + state.stateKey, + this.messageClient, + this.config.generic.allowJsTransformationFunctions + ); + } + return; + } + + public async createConnectionsForRoomId(roomId: string): Promise { + const state = await this.as.botClient.getRoomState(roomId); + const connections: IConnection[] = []; + for (const event of state) { + const conn = await this.createConnectionForState(roomId, new StateEvent(event)); + if (conn) { connections.push(conn); } + } + return connections; + } + + public getConnectionsForGithubIssue(org: string, repo: string, issueNumber: number): (GitHubIssueConnection|GitLabRepoConnection)[] { + org = org.toLowerCase(); + repo = repo.toLowerCase(); + return this.connections.filter((c) => (c instanceof GitHubIssueConnection && c.org === org && c.repo === repo && c.issueNumber === issueNumber) || + (c instanceof GitHubRepoConnection && c.org === org && c.repo === repo)) as (GitHubIssueConnection|GitLabRepoConnection)[]; + } + + public getConnectionsForGithubRepo(org: string, repo: string): GitHubRepoConnection[] { + org = org.toLowerCase(); + repo = repo.toLowerCase(); + return this.connections.filter((c) => (c instanceof GitHubRepoConnection && c.org === org && c.repo === repo)) as GitHubRepoConnection[]; + } + + public getConnectionsForGithubRepoDiscussion(owner: string, repo: string): GitHubDiscussionSpace[] { + owner = owner.toLowerCase(); + repo = repo.toLowerCase(); + return this.connections.filter((c) => (c instanceof GitHubDiscussionSpace && c.owner === owner && c.repo === repo)) as GitHubDiscussionSpace[]; + } + + public getConnectionForGithubUser(user: string): GitHubUserSpace { + return this.connections.find(c => c instanceof GitHubUserSpace && c.owner === user.toLowerCase()) as GitHubUserSpace; + } + + public getConnectionsForGithubDiscussion(owner: string, repo: string, discussionNumber: number) { + owner = owner.toLowerCase(); + repo = repo.toLowerCase(); + return this.connections.filter( + (c) => ( + c instanceof GitHubDiscussionConnection && + c.owner === owner && + c.repo === repo && + c.discussionNumber === discussionNumber + ) + ) as GitHubDiscussionConnection[]; + } + + public getConnectionsForGitLabIssueWebhook(repoHome: string, issueId: number) { + if (!this.config.gitlab) { + throw Error('GitLab configuration missing, cannot handle note'); + } + const res = GitLabClient.splitUrlIntoParts(this.config.gitlab.instances, repoHome); + if (!res) { + throw Error('No instance found for note'); + } + const instance = this.config.gitlab.instances[res[0]]; + return this.getConnectionsForGitLabIssue(instance, res[1], issueId); + } + + public getConnectionsForGitLabIssue(instance: GitLabInstance, projects: string[], issueNumber: number): GitLabIssueConnection[] { + return this.connections.filter((c) => ( + c instanceof GitLabIssueConnection && + c.issueNumber == issueNumber && + c.instanceUrl == instance.url && + c.projectPath == projects.join("/") + )) as GitLabIssueConnection[]; + } + + public getConnectionsForGitLabRepo(pathWithNamespace: string): GitLabRepoConnection[] { + pathWithNamespace = pathWithNamespace.toLowerCase(); + return this.connections.filter((c) => (c instanceof GitLabRepoConnection && c.path === pathWithNamespace)) as GitLabRepoConnection[]; + } + + public getConnectionsForJiraProject(projectId: string, eventName: string): JiraProjectConnection[] { + return this.connections.filter((c) => (c instanceof JiraProjectConnection && c.projectId === projectId && c.isInterestedInHookEvent(eventName))) as JiraProjectConnection[]; + } + + public getConnectionsForGenericWebhook(hookId: string): GenericHookConnection[] { + return this.connections.filter((c) => (c instanceof GenericHookConnection && c.hookId === hookId)) as GenericHookConnection[]; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public getAllConnectionsOfType(typeT: new (...params : any[]) => T): T[] { + return this.connections.filter((c) => (c instanceof typeT)) as T[]; + } + + public isRoomConnected(roomId: string): boolean { + return !!this.connections.find(c => c.roomId === roomId); + } + + public getAllConnectionsForRoom(roomId: string): IConnection[] { + return this.connections.filter(c => c.roomId === roomId); + } + + public getInterestedForRoomState(roomId: string, eventType: string, stateKey: string): IConnection[] { + return this.connections.filter(c => c.roomId === roomId && c.isInterestedInStateEvent(eventType, stateKey)); + } +} \ No newline at end of file diff --git a/src/Connections/IConnection.ts b/src/Connections/IConnection.ts index 5c5b8b4c..d8579b81 100644 --- a/src/Connections/IConnection.ts +++ b/src/Connections/IConnection.ts @@ -25,5 +25,10 @@ export interface IConnection { isInterestedInStateEvent: (eventType: string, stateKey: string) => boolean; + /** + * Is the connection interested in the event that is being sent from the remote side? + */ + isInterestedInHookEvent?: (eventType: string) => boolean; + toString(): string; } \ No newline at end of file diff --git a/src/Connections/JiraProject.ts b/src/Connections/JiraProject.ts index c9c3c143..ce57a334 100644 --- a/src/Connections/JiraProject.ts +++ b/src/Connections/JiraProject.ts @@ -6,10 +6,13 @@ import { MessageSenderClient } from "../MatrixSender" import { JiraIssueEvent } from "../Jira/WebhookTypes"; import { FormatUtil } from "../FormatUtil"; import markdownit from "markdown-it"; -import { generateWebLinkFromIssue } from "../Jira/Utils"; +import { generateJiraWebLinkFromIssue } from "../Jira/Utils"; +type JiraAllowedEventsNames = "issue.created"; +const JiraAllowedEvents: JiraAllowedEventsNames[] = ["issue.created"]; export interface JiraProjectConnectionState { id: string; + events?: JiraAllowedEventsNames[], } const log = new LogWrapper("JiraProjectConnection"); @@ -33,6 +36,10 @@ export class JiraProjectConnection implements IConnection { return this.state.id; } + public isInterestedInHookEvent(eventName: string) { + return !this.state.events || this.state.events?.includes(eventName as JiraAllowedEventsNames); + } + constructor(public readonly roomId: string, private readonly as: Appservice, private state: JiraProjectConnectionState, @@ -47,11 +54,12 @@ export class JiraProjectConnection implements IConnection { public async onJiraIssueCreated(data: JiraIssueEvent) { log.info(`onIssueCreated ${this.roomId} ${this.projectId} ${data.issue.id}`); + const creator = data.issue.fields.creator; if (!creator) { throw Error('No creator field'); } - const url = generateWebLinkFromIssue(data.issue); + const url = generateJiraWebLinkFromIssue(data.issue); const content = `${creator.displayName} created a new JIRA issue [${data.issue.key}](${url}): "${data.issue.fields.summary}"`; await this.as.botIntent.sendEvent(this.roomId, { msgtype: "m.notice", diff --git a/src/FormatUtil.rs b/src/FormatUtil.rs new file mode 100644 index 00000000..e63a89c4 --- /dev/null +++ b/src/FormatUtil.rs @@ -0,0 +1,41 @@ + +use napi::{CallContext, Env, Error as NapiError, JsObject, JsUnknown}; + +use crate::Jira::types::{JiraIssue, JiraIssueLight}; +use crate::Jira; + + +pub fn get_module(env: Env) -> Result { + let mut root_module = env.create_object()?; + root_module.create_named_method("get_partial_body_for_jira_issue", get_partial_body_for_jira_issue)?; + Ok(root_module) +} + +/// Generate a URL for a given Jira Issue object. +#[js_function(1)] +pub fn get_partial_body_for_jira_issue(ctx: CallContext) -> Result { + let jira_issue: JiraIssue = ctx.env.from_js_value(ctx.get::(0)?)?; + let light = JiraIssueLight { + _self: jira_issue._self, + key: jira_issue.key, + }; + let mut body = ctx.env.create_object()?; + let url = Jira::utils::generate_jira_web_link_from_issue(&light)?; + body.set_named_property("external_url", ctx.env.create_string_from_std(url)?)?; + + let mut jira_issue_result = ctx.env.create_object()?; + let mut jira_project = ctx.env.create_object()?; + + + jira_issue_result.set_named_property("id", ctx.env.create_string_from_std(jira_issue.id)?)?; + jira_issue_result.set_named_property("key", ctx.env.create_string_from_std(light.key)?)?; + jira_issue_result.set_named_property("api_url", ctx.env.create_string_from_std(light._self)?)?; + + jira_project.set_named_property("id", ctx.env.create_string_from_std(jira_issue.fields.project.id)?)?; + jira_project.set_named_property("key", ctx.env.create_string_from_std(jira_issue.fields.project.key)?)?; + jira_project.set_named_property("api_url", ctx.env.create_string_from_std(jira_issue.fields.project._self)?)?; + + body.set_named_property("uk.half-shot.matrix-github.jira.issue", jira_issue_result)?; + body.set_named_property("uk.half-shot.matrix-github.jira.project", jira_project)?; + Ok(body) +} diff --git a/src/FormatUtil.ts b/src/FormatUtil.ts index fb426aab..c22ed6ce 100644 --- a/src/FormatUtil.ts +++ b/src/FormatUtil.ts @@ -1,10 +1,12 @@ /* eslint-disable camelcase */ import { ProjectsListResponseData } from './Github/Types'; import emoji from "node-emoji"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import { contrastColor } from "contrast-color"; import { JiraIssue } from './Jira/Types'; -import { generateWebLinkFromIssue } from './Jira/Utils'; +import { format_util } from "./libRs"; + interface IMinimalRepository { id: number; full_name: string; @@ -104,19 +106,6 @@ export class FormatUtil { } public static getPartialBodyForJiraIssue(issue: JiraIssue) { - const url = generateWebLinkFromIssue(issue); - return { - "external_url": url, - "uk.half-shot.matrix-github.jira.issue": { - id: issue.id, - key: issue.key, - api_url: issue.self, - }, - "uk.half-shot.matrix-github.jira.project": { - id: issue.fields.project.id, - key: issue.fields.project.key, - api_url: issue.fields.project.self, - }, - }; + return format_util.get_partial_body_for_jira_issue(issue); } } diff --git a/src/GithubBridge.ts b/src/GithubBridge.ts index 113592f7..9415b7da 100644 --- a/src/GithubBridge.ts +++ b/src/GithubBridge.ts @@ -31,10 +31,12 @@ import { UserNotificationsEvent } from "./Notifications/UserNotificationWatcher" import { UserTokenStore } from "./UserTokenStore"; import * as GitHubWebhookTypes from "@octokit/webhooks-types"; import LogWrapper from "./LogWrapper"; +import { ConnectionManager } from "./ConnectionManager"; const log = new LogWrapper("GithubBridge"); export class GithubBridge { + private connectionManager?: ConnectionManager; private github?: GithubInstance; private as!: Appservice; private encryptedMatrixClient?: MatrixClient; @@ -45,193 +47,11 @@ export class GithubBridge { private tokenStore!: UserTokenStore; private messageClient!: MessageSenderClient; private widgetApi!: BridgeWidgetApi; - private connections: IConnection[] = []; private ready = false; constructor(private config: BridgeConfig, private registration: IAppserviceRegistration) { } - private async createConnectionForState(roomId: string, state: StateEvent) { - log.debug(`Looking to create connection for ${roomId}`); - if (state.content.disabled === false) { - log.debug(`${roomId} has disabled state for ${state.type}`); - return; - } - - if (GitHubRepoConnection.EventTypes.includes(state.type)) { - if (!this.github) { - throw Error('GitHub is not configured'); - } - return new GitHubRepoConnection(roomId, this.as, state.content, this.tokenStore, state.stateKey); - } - - if (GitHubDiscussionConnection.EventTypes.includes(state.type)) { - if (!this.github) { - throw Error('GitHub is not configured'); - } - return new GitHubDiscussionConnection( - roomId, this.as, state.content, state.stateKey, this.tokenStore, this.commentProcessor, - this.messageClient, - ); - } - - if (GitHubDiscussionSpace.EventTypes.includes(state.type)) { - if (!this.github) { - throw Error('GitHub is not configured'); - } - - return new GitHubDiscussionSpace( - await this.as.botClient.getSpace(roomId), state.content, state.stateKey - ); - } - - if (GitHubIssueConnection.EventTypes.includes(state.type)) { - if (!this.github) { - throw Error('GitHub is not configured'); - } - const issue = new GitHubIssueConnection(roomId, this.as, state.content, state.stateKey || "", this.tokenStore, this.commentProcessor, this.messageClient, this.github); - await issue.syncIssueState(); - return issue; - } - - if (GitHubUserSpace.EventTypes.includes(state.type)) { - if (!this.github) { - throw Error('GitHub is not configured'); - } - return new GitHubUserSpace( - await this.as.botClient.getSpace(roomId), state.content, state.stateKey - ); - } - - if (GitLabRepoConnection.EventTypes.includes(state.type)) { - if (!this.config.gitlab) { - throw Error('GitLab is not configured'); - } - const instance = this.config.gitlab.instances[state.content.instance]; - if (!instance) { - throw Error('Instance name not recognised'); - } - return new GitLabRepoConnection(roomId, this.as, state.content, this.tokenStore, instance); - } - - if (GitLabIssueConnection.EventTypes.includes(state.type)) { - if (!this.config.gitlab) { - throw Error('GitLab is not configured'); - } - const instance = this.config.gitlab.instances[state.content.instance]; - return new GitLabIssueConnection( - roomId, - this.as, - state.content, - state.stateKey as string, - this.tokenStore, - this.commentProcessor, - this.messageClient, - instance); - } - - if (JiraProjectConnection.EventTypes.includes(state.type)) { - if (!this.config.jira) { - throw Error('JIRA is not configured'); - } - return new JiraProjectConnection(roomId, this.as, state.content, state.stateKey, this.commentProcessor, this.messageClient); - } - - if (GenericHookConnection.EventTypes.includes(state.type) && this.config.generic?.enabled) { - return new GenericHookConnection( - roomId, - state.content, - state.stateKey, - this.messageClient, - this.config.generic.allowJsTransformationFunctions - ); - } - - return; - } - - private async createConnectionsForRoomId(roomId: string): Promise { - const state = await this.as.botClient.getRoomState(roomId); - const connections: IConnection[] = []; - for (const event of state) { - const conn = await this.createConnectionForState(roomId, new StateEvent(event)); - if (conn) { connections.push(conn); } - } - return connections; - } - - private getConnectionsForGithubIssue(org: string, repo: string, issueNumber: number): (GitHubIssueConnection|GitLabRepoConnection)[] { - org = org.toLowerCase(); - repo = repo.toLowerCase(); - return this.connections.filter((c) => (c instanceof GitHubIssueConnection && c.org === org && c.repo === repo && c.issueNumber === issueNumber) || - (c instanceof GitHubRepoConnection && c.org === org && c.repo === repo)) as (GitHubIssueConnection|GitLabRepoConnection)[]; - } - - private getConnectionsForGithubRepo(org: string, repo: string): GitHubRepoConnection[] { - org = org.toLowerCase(); - repo = repo.toLowerCase(); - return this.connections.filter((c) => (c instanceof GitHubRepoConnection && c.org === org && c.repo === repo)) as GitHubRepoConnection[]; - } - - private getConnectionsForGithubRepoDiscussion(owner: string, repo: string): GitHubDiscussionSpace[] { - owner = owner.toLowerCase(); - repo = repo.toLowerCase(); - return this.connections.filter((c) => (c instanceof GitHubDiscussionSpace && c.owner === owner && c.repo === repo)) as GitHubDiscussionSpace[]; - } - - private getConnectionForGithubUser(user: string): GitHubUserSpace { - return this.connections.find(c => c instanceof GitHubUserSpace && c.owner === user.toLowerCase()) as GitHubUserSpace; - } - - - private getConnectionsForGithubDiscussion(owner: string, repo: string, discussionNumber: number) { - owner = owner.toLowerCase(); - repo = repo.toLowerCase(); - return this.connections.filter( - (c) => ( - c instanceof GitHubDiscussionConnection && - c.owner === owner && - c.repo === repo && - c.discussionNumber === discussionNumber - ) - ) as GitHubDiscussionConnection[]; - } - - private getConnectionsForGitLabIssueWebhook(repoHome: string, issueId: number) { - if (!this.config.gitlab) { - throw Error('GitLab configuration missing, cannot handle note'); - } - const res = GitLabClient.splitUrlIntoParts(this.config.gitlab.instances, repoHome); - if (!res) { - throw Error('No instance found for note'); - } - const instance = this.config.gitlab.instances[res[0]]; - return this.getConnectionsForGitLabIssue(instance, res[1], issueId); - } - - private getConnectionsForGitLabIssue(instance: GitLabInstance, projects: string[], issueNumber: number): GitLabIssueConnection[] { - return this.connections.filter((c) => ( - c instanceof GitLabIssueConnection && - c.issueNumber == issueNumber && - c.instanceUrl == instance.url && - c.projectPath == projects.join("/") - )) as GitLabIssueConnection[]; - } - - private getConnectionsForGitLabRepo(pathWithNamespace: string): GitLabRepoConnection[] { - pathWithNamespace = pathWithNamespace.toLowerCase(); - return this.connections.filter((c) => (c instanceof GitLabRepoConnection && c.path === pathWithNamespace)) as GitLabRepoConnection[]; - } - - private getConnectionsForJiraProject(projectId: string): JiraProjectConnection[] { - return this.connections.filter((c) => (c instanceof JiraProjectConnection && c.projectId === projectId)) as JiraProjectConnection[]; - } - - private getConnectionsForGenericWebhook(hookId: string): GenericHookConnection[] { - return this.connections.filter((c) => (c instanceof GenericHookConnection && c.hookId === hookId)) as GenericHookConnection[]; - } - - public stop() { this.as.stop(); if(this.queue.stop) this.queue.stop(); @@ -300,6 +120,8 @@ export class GithubBridge { this.tokenStore = new UserTokenStore(this.config.passFile || "./passkey.pem", this.as.botIntent); await this.tokenStore.load(); + const connManager = this.connectionManager = new ConnectionManager(this.as, + this.config, this.tokenStore, this.commentProcessor, this.messageClient, this.github); this.as.on("query.room", async (roomAlias, cb) => { try { @@ -347,7 +169,7 @@ export class GithubBridge { this.queue.on("github.issue_comment.created", async ({ data }) => { const { repository, issue, owner } = validateRepoIssue(data); - const connections = this.getConnectionsForGithubIssue(owner, repository.name, issue.number); + const connections = connManager.getConnectionsForGithubIssue(owner, repository.name, issue.number); connections.map(async (c) => { try { if (c instanceof GitHubIssueConnection) @@ -360,7 +182,7 @@ export class GithubBridge { this.queue.on("github.issues.opened", async ({ data }) => { const { repository, owner } = validateRepoIssue(data); - const connections = this.getConnectionsForGithubRepo(owner, repository.name); + const connections = connManager.getConnectionsForGithubRepo(owner, repository.name); connections.map(async (c) => { try { await c.onIssueCreated(data); @@ -372,7 +194,7 @@ export class GithubBridge { this.queue.on("github.issues.edited", async ({ data }) => { const { repository, issue, owner } = validateRepoIssue(data); - const connections = this.getConnectionsForGithubIssue(owner, repository.name, issue.number); + const connections = connManager.getConnectionsForGithubIssue(owner, repository.name, issue.number); connections.map(async (c) => { try { // TODO: Needs impls @@ -386,7 +208,7 @@ export class GithubBridge { this.queue.on("github.issues.closed", async ({ data }) => { const { repository, issue, owner } = validateRepoIssue(data); - const connections = this.getConnectionsForGithubIssue(owner, repository.name, issue.number); + const connections = connManager.getConnectionsForGithubIssue(owner, repository.name, issue.number); connections.map(async (c) => { try { if (c instanceof GitHubIssueConnection || c instanceof GitHubRepoConnection) @@ -399,7 +221,7 @@ export class GithubBridge { this.queue.on("github.issues.reopened", async ({ data }) => { const { repository, issue, owner } = validateRepoIssue(data); - const connections = this.getConnectionsForGithubIssue(owner, repository.name, issue.number); + const connections = connManager.getConnectionsForGithubIssue(owner, repository.name, issue.number); connections.map(async (c) => { try { if (c instanceof GitHubIssueConnection || c instanceof GitHubRepoConnection) @@ -412,7 +234,7 @@ export class GithubBridge { this.queue.on("github.issues.edited", async ({ data }) => { const { repository, issue, owner } = validateRepoIssue(data); - const connections = this.getConnectionsForGithubRepo(owner, repository.name); + const connections = connManager.getConnectionsForGithubRepo(owner, repository.name); connections.map(async (c) => { try { await c.onIssueEdited(data); @@ -423,7 +245,7 @@ export class GithubBridge { }); this.queue.on("github.pull_request.opened", async ({ data }) => { - const connections = this.getConnectionsForGithubRepo(data.repository.owner.login, data.repository.name); + const connections = connManager.getConnectionsForGithubRepo(data.repository.owner.login, data.repository.name); connections.map(async (c) => { try { await c.onPROpened(data); @@ -434,7 +256,7 @@ export class GithubBridge { }); this.queue.on("github.pull_request.closed", async ({ data }) => { - const connections = this.getConnectionsForGithubRepo(data.repository.owner.login, data.repository.name); + const connections = connManager.getConnectionsForGithubRepo(data.repository.owner.login, data.repository.name); connections.map(async (c) => { try { await c.onPRClosed(data); @@ -445,7 +267,7 @@ export class GithubBridge { }); this.queue.on("github.pull_request.ready_for_review", async ({ data }) => { - const connections = this.getConnectionsForGithubRepo(data.repository.owner.login, data.repository.name); + const connections = connManager.getConnectionsForGithubRepo(data.repository.owner.login, data.repository.name); connections.map(async (c) => { try { await c.onPRReadyForReview(data); @@ -456,7 +278,7 @@ export class GithubBridge { }); this.queue.on("github.pull_request_review.submitted", async ({ data }) => { - const connections = this.getConnectionsForGithubRepo(data.repository.owner.login, data.repository.name); + const connections = connManager.getConnectionsForGithubRepo(data.repository.owner.login, data.repository.name); connections.map(async (c) => { try { await c.onPRReviewed(data); @@ -467,7 +289,7 @@ export class GithubBridge { }); this.queue.on("github.release.created", async ({ data }) => { - const connections = this.getConnectionsForGithubRepo(data.repository.owner.login, data.repository.name); + const connections = connManager.getConnectionsForGithubRepo(data.repository.owner.login, data.repository.name); connections.map(async (c) => { try { await c.onReleaseCreated(data); @@ -478,7 +300,7 @@ export class GithubBridge { }); this.queue.on("gitlab.merge_request.open", async (msg) => { - const connections = this.getConnectionsForGitLabRepo(msg.data.project.path_with_namespace); + const connections = connManager.getConnectionsForGitLabRepo(msg.data.project.path_with_namespace); connections.map(async (c) => { try { await c.onMergeRequestOpened(msg.data); @@ -489,7 +311,7 @@ export class GithubBridge { }); this.queue.on("gitlab.tag_push", async (msg) => { - const connections = this.getConnectionsForGitLabRepo(msg.data.project.path_with_namespace); + const connections = connManager.getConnectionsForGitLabRepo(msg.data.project.path_with_namespace); connections.map(async (c) => { try { await c.onMergeRequestOpened(msg.data); @@ -529,7 +351,7 @@ export class GithubBridge { }); this.queue.on("gitlab.note.created", async ({data}) => { - const connections = this.getConnectionsForGitLabIssueWebhook(data.repository.homepage, data.issue.iid); + const connections = connManager.getConnectionsForGitLabIssueWebhook(data.repository.homepage, data.issue.iid); connections.map(async (c) => { try { if (c.onCommentCreated) @@ -541,7 +363,7 @@ export class GithubBridge { }); this.queue.on("gitlab.issue.reopen", async ({data}) => { - const connections = this.getConnectionsForGitLabIssueWebhook(data.repository.homepage, data.object_attributes.iid); + const connections = connManager.getConnectionsForGitLabIssueWebhook(data.repository.homepage, data.object_attributes.iid); connections.map(async (c) => { try { await c.onIssueReopened(); @@ -552,7 +374,7 @@ export class GithubBridge { }); this.queue.on("gitlab.issue.close", async ({data}) => { - const connections = this.getConnectionsForGitLabIssueWebhook(data.repository.homepage, data.object_attributes.iid); + const connections = connManager.getConnectionsForGitLabIssueWebhook(data.repository.homepage, data.object_attributes.iid); connections.map(async (c) => { try { await c.onIssueClosed(); @@ -563,7 +385,7 @@ export class GithubBridge { }); this.queue.on("github.discussion_comment.created", async ({data}) => { - const connections = this.getConnectionsForGithubDiscussion(data.repository.owner.login, data.repository.name, data.discussion.number); + const connections = connManager.getConnectionsForGithubDiscussion(data.repository.owner.login, data.repository.name, data.discussion.number); connections.map(async (c) => { try { await c.onDiscussionCommentCreated(data); @@ -577,13 +399,13 @@ export class GithubBridge { if (!this.github) { return; } - const spaces = this.getConnectionsForGithubRepoDiscussion(data.repository.owner.login, data.repository.name); + const spaces = connManager.getConnectionsForGithubRepoDiscussion(data.repository.owner.login, data.repository.name); if (spaces.length === 0) { log.info(`Not creating discussion ${data.discussion.id} ${data.repository.owner.login}/${data.repository.name}, no target spaces`); // We don't want to create any discussions if we have no target spaces. return; } - let [discussionConnection] = this.getConnectionsForGithubDiscussion(data.repository.owner.login, data.repository.name, data.discussion.id); + let [discussionConnection] = connManager.getConnectionsForGithubDiscussion(data.repository.owner.login, data.repository.name, data.discussion.id); if (!discussionConnection) { try { // If we don't have an existing connection for this discussion (likely), then create one. @@ -597,7 +419,7 @@ export class GithubBridge { this.commentProcessor, this.messageClient, ); - this.connections.push(discussionConnection); + connManager.push(discussionConnection); } catch (ex) { log.error(ex); throw Error('Failed to create discussion room'); @@ -616,7 +438,7 @@ export class GithubBridge { this.queue.on("jira.issue_created", async ({data}) => { log.info(`JIRA issue created for project ${data.issue.fields.project.id}, issue id ${data.issue.id}`); const projectId = data.issue.fields.project.id; - const connections = this.getConnectionsForJiraProject(projectId); + const connections = connManager.getConnectionsForJiraProject(projectId, "jira.issue_created"); connections.forEach(async (c) => { try { @@ -629,7 +451,7 @@ export class GithubBridge { this.queue.on("generic-webhook.event", async ({data}) => { log.info(`Incoming generic hook ${data.hookId}`); - const connections = this.getConnectionsForGenericWebhook(data.hookId); + const connections = connManager.getConnectionsForGenericWebhook(data.hookId); connections.forEach(async (c) => { try { @@ -678,14 +500,14 @@ export class GithubBridge { log.debug("Fetching state for " + roomId); let connections: IConnection[]; try { - connections = await this.createConnectionsForRoomId(roomId); + connections = await connManager.createConnectionsForRoomId(roomId); } catch (ex) { log.error(`Unable to create connection for ${roomId}`, ex); continue; } if (connections.length) { log.info(`Room ${roomId} is connected to: ${connections.join(',')}`); - this.connections.push(...connections); + connManager.push(...connections); continue; } @@ -717,8 +539,8 @@ export class GithubBridge { } // Handle spaces - for (const discussion of this.connections.filter((c) => c instanceof GitHubDiscussionSpace) as GitHubDiscussionSpace[]) { - const user = this.getConnectionForGithubUser(discussion.owner); + for (const discussion of connManager.getAllConnectionsOfType(GitHubDiscussionSpace)) { + const user = connManager.getConnectionForGithubUser(discussion.owner); if (user) { await user.ensureDiscussionInSpace(discussion); } @@ -753,6 +575,10 @@ export class GithubBridge { } private async onRoomMessage(roomId: string, event: MatrixEvent) { + if (!this.connectionManager) { + // Not ready yet. + return; + } if (this.as.isNamespacedUser(event.sender)) { /* We ignore messages from our users */ return; @@ -762,7 +588,6 @@ export class GithubBridge { return; } log.info(`Got message roomId=${roomId} type=${event.type} from=${event.sender}`); - console.log(event); log.debug("Content:", JSON.stringify(event)); const adminRoom = this.adminRooms.get(roomId); @@ -784,7 +609,7 @@ export class GithubBridge { const issueNumber = ev.content["uk.half-shot.matrix-github.issue"]?.number; if (splitParts && issueNumber) { log.info(`Handling reply for ${splitParts}${issueNumber}`); - const connections = this.getConnectionsForGithubIssue(splitParts[0], splitParts[1], issueNumber); + const connections = this.connectionManager.getConnectionsForGithubIssue(splitParts[0], splitParts[1], issueNumber); await Promise.all(connections.map(async c => { if (c instanceof GitHubIssueConnection) { return c.onMatrixIssueComment(processedReply); @@ -806,7 +631,7 @@ export class GithubBridge { } } - for (const connection of this.connections.filter((c) => c.roomId === roomId)) { + for (const connection of this.connectionManager.getAllConnectionsForRoom(roomId)) { try { if (connection.onMessageEvent) { await connection.onMessageEvent(event); @@ -822,44 +647,59 @@ export class GithubBridge { // Only act on bot joins return; } - - const isRoomConnected = !!this.connections.find(c => c.roomId === roomId); + if (!this.connectionManager) { + // Not ready yet. + return; + } // Only fetch rooms we have no connections in yet. - if (!isRoomConnected) { - const connections = await this.createConnectionsForRoomId(roomId); - this.connections.push(...connections); + if (!this.connectionManager.isRoomConnected(roomId)) { + const connections = await this.connectionManager.createConnectionsForRoomId(roomId); + this.connectionManager.push(...connections); } } private async onRoomEvent(roomId: string, event: MatrixEvent) { + if (!this.connectionManager) { + // Not ready yet. + return; + } if (event.state_key) { // A state update, hurrah! - const existingConnection = this.connections.find((c) => c.roomId === roomId && c.isInterestedInStateEvent(event.type, event.state_key || "")); - if (existingConnection?.onStateUpdate) { - existingConnection.onStateUpdate(event); - } else if (!existingConnection) { + const existingConnections = this.connectionManager.getInterestedForRoomState(roomId, event.type, event.state_key); + for (const connection of existingConnections) { + try { + if (connection?.onStateUpdate) { + connection.onStateUpdate(event); + } + } catch (ex) { + log.warn(`Connection ${connection.toString()} failed to handle onStateUpdate:`, ex); + } + } + if (!existingConnections.length) { // Is anyone interested in this state? - const connection = await this.createConnectionForState(roomId, new StateEvent(event)); + const connection = await this.connectionManager.createConnectionForState(roomId, new StateEvent(event)); if (connection) { log.info(`New connected added to ${roomId}: ${connection.toString()}`); - this.connections.push(connection); + this.connectionManager.push(connection); } } return; } + + // We still want to react to our own state events. if (event.sender === this.as.botUserId) { // It's us return; } - for (const connection of this.connections.filter((c) => c.roomId === roomId)) { + for (const connection of this.connectionManager.getAllConnectionsForRoom(roomId)) { try { if (connection.onEvent) { await connection.onEvent(event); } } catch (ex) { - log.warn(`Connection ${connection.toString()} failed to handle event:`, ex); + log.warn(`Connection ${connection.toString()} failed to handle onEvent:`, ex); } } } @@ -922,7 +762,6 @@ export class GithubBridge { } } - res = GitHubUserSpace.QueryRoomRegex.exec(roomAlias); if (res) { if (!this.github) { @@ -1019,7 +858,8 @@ export class GithubBridge { adminRoom.on("settings.changed", this.onAdminRoomSettingsChanged.bind(this)); adminRoom.on("open.project", async (project: ProjectsGetResponseData) => { const connection = await GitHubProjectConnection.onOpenProject(project, this.as, adminRoom.userId); - this.connections.push(connection); + // TODO: Surely we only do this if we don't have one already? + this.connectionManager?.push(connection); }); // adminRoom.on("open.discussion", async (owner: string, repo: string, discussions: Discussion) => { // const connection = await GitHubDiscussionConnection.createDiscussionRoom( @@ -1028,7 +868,7 @@ export class GithubBridge { // this.connections.push(connection); // }); adminRoom.on("open.gitlab-issue", async (issueInfo: GetIssueOpts, res: GetIssueResponse, instanceName: string, instance: GitLabInstance) => { - const [ connection ] = this.getConnectionsForGitLabIssue(instance, issueInfo.projects, issueInfo.issue); + const [ connection ] = this.connectionManager?.getConnectionsForGitLabIssue(instance, issueInfo.projects, issueInfo.issue) || []; if (connection) { return this.as.botClient.inviteUser(adminRoom.userId, connection.roomId); } @@ -1042,7 +882,7 @@ export class GithubBridge { this.commentProcessor, this.messageClient ); - this.connections.push(newConnection); + this.connectionManager?.push(newConnection); return this.as.botClient.inviteUser(adminRoom.userId, newConnection.roomId); }); this.adminRooms.set(roomId, adminRoom); diff --git a/src/Jira/Utils.ts b/src/Jira/Utils.ts index e7dbcce7..7216c94e 100644 --- a/src/Jira/Utils.ts +++ b/src/Jira/Utils.ts @@ -1,6 +1,3 @@ -import { JiraIssue } from "./Types"; +import { jira } from "../libRs"; -export function generateWebLinkFromIssue(issue: JiraIssue) { - const { origin } = new URL(issue.self); - return `${origin}/browse/${issue.key}` -} \ No newline at end of file +export const generateJiraWebLinkFromIssue = jira.utils.generate_jira_web_link_from_issue; \ No newline at end of file diff --git a/src/Jira/mod.rs b/src/Jira/mod.rs new file mode 100644 index 00000000..b97af529 --- /dev/null +++ b/src/Jira/mod.rs @@ -0,0 +1,11 @@ +use napi::{Env, Error as NapiError, JsObject}; +pub mod utils; +pub mod types; + +pub fn get_module(env: Env) -> Result { + let mut root_module = env.create_object()?; + let mut utils_module = env.create_object()?; + utils_module.create_named_method("generate_jira_web_link_from_issue", utils::js_generate_jira_web_link_from_issue)?; + root_module.set_named_property("utils", utils_module)?; + Ok(root_module) +} \ No newline at end of file diff --git a/src/Jira/types.rs b/src/Jira/types.rs new file mode 100644 index 00000000..e66d0cc7 --- /dev/null +++ b/src/Jira/types.rs @@ -0,0 +1,29 @@ +#[derive(Serialize, Debug, Deserialize)] +pub struct JiraProject { + #[serde(rename = "self")] + pub _self: String, + pub id: String, + pub key: String, +} + +#[derive(Serialize, Debug, Deserialize)] + +pub struct JiraIssue { + #[serde(rename = "self")] + pub _self: String, + pub id: String, + pub key: String, + pub fields: JiraIssueFields, +} + +#[derive(Serialize, Debug, Deserialize)] +pub struct JiraIssueFields { + pub project: JiraProject +} + +#[derive(Serialize, Debug, Deserialize)] +pub struct JiraIssueLight { + #[serde(rename = "self")] + pub _self: String, + pub key: String, +} diff --git a/src/Jira/utils.rs b/src/Jira/utils.rs new file mode 100644 index 00000000..b4cde76f --- /dev/null +++ b/src/Jira/utils.rs @@ -0,0 +1,22 @@ +use napi::{CallContext, Error as NapiError, JsString, JsUnknown, Status}; +use url::{Url}; +use super::types::{JiraIssueLight}; + +/// Generate a URL for a given Jira Issue object. +#[js_function(1)] +pub fn js_generate_jira_web_link_from_issue(ctx: CallContext) -> Result { + let jira_issue: JiraIssueLight = ctx.env.from_js_value(ctx.get::(0)?)?; + match generate_jira_web_link_from_issue(&jira_issue) { + Ok(url) => ctx.env.create_string_from_std(url), + Err(err) => Err(NapiError::new(Status::Unknown, err.to_string())), + } +} + +/// Generate a URL for a given Jira Issue object. +pub fn generate_jira_web_link_from_issue(jira_issue: &JiraIssueLight) -> Result { + let result = Url::parse(&jira_issue._self); + match result { + Ok(url) => Ok(format!("{}://{}/browse/{}", url.scheme(), url.host_str().unwrap(), jira_issue.key)), + Err(err) => Err(NapiError::new(Status::Unknown, err.to_string())), + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..bbc748a1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,16 @@ +use napi::{Env, Error as NapiError, JsObject}; +mod Jira; +mod FormatUtil; + +#[macro_use] +extern crate napi_derive; + +#[macro_use] +extern crate serde_derive; + +#[module_exports] +fn init(mut exports: JsObject, env: Env) -> Result<(), NapiError> { + exports.set_named_property("jira", Jira::get_module(env)?)?; + exports.set_named_property("format_util", FormatUtil::get_module(env)?)?; + Ok(()) +} diff --git a/src/libRs.ts b/src/libRs.ts new file mode 100644 index 00000000..7e1ac3d2 --- /dev/null +++ b/src/libRs.ts @@ -0,0 +1,19 @@ +/* eslint-disable camelcase */ +import { JiraIssue } from "./Jira/Types"; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const rootModule = require('../lib/matrix-github-rs.node'); + +interface FormatUtil { + get_partial_body_for_jira_issue: (issue: JiraIssue) => Record +} + +interface JiraModule { + utils: { + generate_jira_web_link_from_issue: (issue: {self: string, key: string}) => string; + } +} + + +export const format_util = rootModule.format_util as FormatUtil; +export const jira = rootModule.jira as JiraModule; \ No newline at end of file diff --git a/tests/FormatUtilTest.ts b/tests/FormatUtilTest.ts index 65be1449..db7f7509 100644 --- a/tests/FormatUtilTest.ts +++ b/tests/FormatUtilTest.ts @@ -1,5 +1,6 @@ import { FormatUtil } from "../src/FormatUtil"; import { expect } from "chai"; +import { JiraIssue, JiraProject } from "../src/Jira/Types"; const SIMPLE_ISSUE = { id: 123, @@ -19,6 +20,27 @@ const SIMPLE_REPO = { html_url: "https://github.com/evilcorp/lab/issues/123", }; +const SIMPLE_JIRA_ISSUE = { + id: "test-issue", + self: "http://example-api.url.com/issue-url", + key: "TEST-001", + fields: { + summary: "summary", + issuetype: "foo", + project: { + self: "http://example-api.url.com/project-url", + id: "test-project", + key: "TEST", + name: "Test Project", + projectTypeKey: "project-type-key", + simplified: false, + avatarUrls: {} + } as JiraProject, + assignee: null, + priority: "1", + status: "open", + }, +} as JiraIssue; describe("FormatUtilTest", () => { it("correctly formats a repo room name", () => { @@ -36,4 +58,19 @@ describe("FormatUtilTest", () => { "Status: open | https://github.com/evilcorp/lab/issues/123", ); }); + it("should correctly format a JIRA issue", () => { + expect(FormatUtil.getPartialBodyForJiraIssue(SIMPLE_JIRA_ISSUE)).to.deep.equal({ + "external_url": "http://example-api.url.com/browse/TEST-001", + "uk.half-shot.matrix-github.jira.issue": { + "api_url": "http://example-api.url.com/issue-url", + "id": "test-issue", + "key": "TEST-001", + }, + "uk.half-shot.matrix-github.jira.project": { + "api_url": "http://example-api.url.com/project-url", + "id": "test-project", + "key": "TEST", + }, + }); + }); }); diff --git a/tests/jira/Utils.ts b/tests/jira/Utils.ts new file mode 100644 index 00000000..d64fa02d --- /dev/null +++ b/tests/jira/Utils.ts @@ -0,0 +1,13 @@ +import { expect } from "chai"; +import { generateJiraWebLinkFromIssue } from "../../src/Jira/Utils"; + +describe("Jira", () => { + describe("Utils", () => { + it("processes a jira issue into a URL", () => { + expect(generateJiraWebLinkFromIssue({ + self: "https://my-test-jira/", + key: "TEST-111", + })).to.equal("https://my-test-jira/browse/TEST-111"); + }); + }); +}); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 4d8063ad..35ffd4bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -266,6 +266,25 @@ resolved "https://registry.yarnpkg.com/@fontsource/open-sans/-/open-sans-4.2.2.tgz#59046f772136a44c5999e0189d33c6a26676630c" integrity sha512-NbsL1a9asJO6N/5kRxYPCy0kNhKMi9T75kl4QfIGtmpd/5IfB+UIAUxd9AICmCLaH4Osc2TImeTJj94xc9MNKg== +"@napi-rs/cli@^1.3.5": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@napi-rs/cli/-/cli-1.3.5.tgz#89e4d97127edc4ed10a06637a43d27a1ed3c288d" + integrity sha512-Z0KZIciemioYODTyO908v2AtL8Zg4sohQDD+dyHeHmOiOfaez/y/xQ8XnpOHc2W5fRidKUW+MVWyTtpLTbKsqw== + dependencies: + inquirer "^8.1.3" + +"@napi-rs/triples@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@napi-rs/triples/-/triples-1.0.3.tgz#76d6d0c3f4d16013c61e45dfca5ff1e6c31ae53c" + integrity sha512-jDJTpta+P4p1NZTFVLHJ/TLFVYVcOqv6l8xwOeBKNPMgY/zDYH/YH7SJbvrr/h1RcS9GzbPcLKGzpuK9cV56UA== + +"@node-rs/helper@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@node-rs/helper/-/helper-1.2.1.tgz#e079b05f21ff4329d82c4e1f71c0290e4ecdc70c" + integrity sha512-R5wEmm8nbuQU0YGGmYVjEc0OHtYsuXdpRG+Ut/3wZ9XAvQWyThN08bTh2cBJgoZxHQUPtvRfeQuxcAgLuiBISg== + dependencies: + "@napi-rs/triples" "^1.0.3" + "@nodelib/fs.scandir@2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69" @@ -856,6 +875,13 @@ ansi-colors@4.1.1, ansi-colors@^4.1.1: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== +ansi-escapes@^4.2.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + ansi-regex@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" @@ -999,6 +1025,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + basic-auth@~2.0.0, basic-auth@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" @@ -1035,6 +1066,15 @@ bitsyntax@~0.0.4: dependencies: buffer-more-ints "0.0.2" +bl@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + bluebird@^3.4.6, bluebird@^3.5.0: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" @@ -1130,6 +1170,14 @@ buffer-more-ints@0.0.2: resolved "https://registry.yarnpkg.com/buffer-more-ints/-/buffer-more-ints-0.0.2.tgz#26b3885d10fa13db7fc01aae3aab870199e0124c" integrity sha1-JrOIXRD6E9t/wBquOquHAZngEkw= +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -1206,6 +1254,19 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + check-error@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" @@ -1231,11 +1292,23 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + cli-spinners@^2.5.0: version "2.6.0" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.0.tgz#36c7dc98fb6a9a76bd6238ec3f77e2425627e939" integrity sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q== +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== + cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -1245,6 +1318,11 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= + cluster-key-slot@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d" @@ -1505,6 +1583,13 @@ default-browser-id@^2.0.0: pify "^2.3.0" untildify "^2.0.0" +defaults@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" + integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730= + dependencies: + clone "^1.0.2" + define-properties@^1.1.2, define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -1970,6 +2055,15 @@ extend@~3.0.2: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" @@ -2034,6 +2128,13 @@ fecha@^4.2.0: resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.0.tgz#3ffb6395453e3f3efff850404f0a59b6747f5f41" integrity sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg== +figures@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -2440,13 +2541,18 @@ iconv-lite@0.4.23: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@0.4.24: +iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" @@ -2483,7 +2589,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -2493,6 +2599,26 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +inquirer@^8.1.3: + version "8.2.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.0.tgz#f44f008dd344bbfc4b30031f45d984e034a3ac3a" + integrity sha512-0crLweprevJ02tTuA6ThpoAERAGyVILC4sS74uib58Xf/zSr1/ZWtmm7D5CI+bSQEaA04f0K7idaHpQbSWgiVQ== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.1" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.21" + mute-stream "0.0.8" + ora "^5.4.1" + run-async "^2.4.0" + rxjs "^7.2.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + ioredis@^4.26.0: version "4.26.0" resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.26.0.tgz#dbbfb5e5da085fc2b1de8174db50fa42f9fed66a" @@ -2582,6 +2708,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + is-negative-zero@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" @@ -2652,6 +2783,11 @@ is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + is-wsl@^2.1.1: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" @@ -2919,6 +3055,14 @@ log-symbols@4.0.0: dependencies: chalk "^4.0.0" +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + logform@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/logform/-/logform-2.2.0.tgz#40f036d19161fc76b68ab50fdc7fe495544492f2" @@ -3174,6 +3318,11 @@ ms@2.1.3, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + nanoid@3.1.20: version "3.1.20" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788" @@ -3305,7 +3454,7 @@ one-time@^1.0.0: dependencies: fn.name "1.x.x" -onetime@^5.1.2: +onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== @@ -3332,11 +3481,31 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" +ora@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== + dependencies: + bl "^4.1.0" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.5.0" + is-interactive "^1.0.0" + is-unicode-supported "^0.1.0" + log-symbols "^4.1.0" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + os-homedir@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + p-limit@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" @@ -3681,6 +3850,14 @@ resolve@^1.20.0: is-core-module "^2.2.0" path-parse "^1.0.6" +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + retry@0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" @@ -3705,6 +3882,11 @@ rollup@^2.34.0: optionalDependencies: fsevents "~2.3.1" +run-async@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -3712,6 +3894,13 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +rxjs@^7.2.0: + version "7.4.0" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.4.0.tgz#a12a44d7eebf016f5ff2441b87f28c9a51cebc68" + integrity sha512-7SQDi7xeTMCJpqViXh8gL/lebcwlp3d831F05+9B44A4B0WfsEwUQHR64gsH1kvJ+Ep/J9K2+n1hVl1CsGN23w== + dependencies: + tslib "~2.1.0" + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -3849,6 +4038,11 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +signal-exit@^3.0.2: + version "3.0.6" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.6.tgz#24e630c4b0f03fea446a2bd299e62b4a6ca8d0af" + integrity sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ== + signal-exit@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" @@ -4144,11 +4338,23 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + timer2@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/timer2/-/timer2-1.0.0.tgz#7a2441569c6564cb891f605788eef0377d89f5de" integrity sha512-UOZql+P2ET0da+B7V3/RImN3IhC5ghb+9cpecfUhmYGIm0z73dDr3A781nBLnFYmRzeT1AmoT4w9Lgr8n7n7xg== +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -4196,6 +4402,11 @@ tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" + integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== + tsscmp@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb" @@ -4237,6 +4448,11 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + type-fest@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" @@ -4379,6 +4595,13 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +wcwidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= + dependencies: + defaults "^1.0.3" + which-boxed-primitive@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"