Merge remote-tracking branch 'origin/main' into hs/rs-message-queues
@ -15,6 +15,8 @@ module.exports = {
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"camelcase": ["error", { "properties": "never", "ignoreDestructuring": true }],
|
||||
"no-console": "error"
|
||||
},
|
||||
@ -48,6 +50,7 @@ module.exports = {
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-unused-vars": ["error"],
|
||||
"no-useless-constructor": "off",
|
||||
"@typescript-eslint/no-useless-constructor": ["error"],
|
||||
|
6
.github/workflows/docker-hub-latest.yml
vendored
@ -10,6 +10,12 @@ on:
|
||||
branches: [ main ]
|
||||
paths-ignore:
|
||||
- changelog.d/**'
|
||||
merge_group:
|
||||
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
DOCKER_NAMESPACE: halfshot
|
||||
|
4
.github/workflows/docker-hub-release.yml
vendored
@ -10,6 +10,10 @@ env:
|
||||
DOCKER_NAMESPACE: halfshot
|
||||
PLATFORMS: linux/amd64,linux/arm64
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
docker-release:
|
||||
runs-on: ubuntu-latest
|
||||
|
4
.github/workflows/docs-latest.yml
vendored
@ -5,6 +5,10 @@ on:
|
||||
paths-ignore:
|
||||
- changelog.d/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-20.04
|
||||
|
4
.github/workflows/docs-release.yml
vendored
@ -4,6 +4,10 @@ on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-20.04
|
||||
|
27
.github/workflows/helm-lint.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
name: Helm Chart - Validate
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths-ignore:
|
||||
- changelog.d/**'
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
paths-ignore:
|
||||
- changelog.d/**'
|
||||
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
lint-helm:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Lint Helm
|
||||
uses: WyriHaximus/github-action-helm3@v3
|
||||
with:
|
||||
exec: helm lint ./helm/hookshot/
|
||||
|
||||
- name: Validate
|
||||
uses: nlamirault/helm-kubeconform-action@v0.1.0
|
||||
with:
|
||||
charts: ./helm/
|
56
.github/workflows/helm.yml
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
name: Helm Chart - Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'helm/**' # only execute if we have helm chart changes
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
# depending on default permission settings for your org (contents being read-only or read-write for workloads), you will have to add permissions
|
||||
# see: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "$GITHUB_ACTOR"
|
||||
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v3
|
||||
with:
|
||||
version: v3.10.0
|
||||
|
||||
- name: "Get app version from package.json"
|
||||
id: get_hookshot_version
|
||||
run: |
|
||||
echo "hookshot_version=$(cat package.json | yq .version)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set chart appVersion to current package.json version
|
||||
uses: mikefarah/yq@v4.34.1
|
||||
with:
|
||||
cmd: |
|
||||
yq -i '.appVersion="${{steps.get_hookshot_version.outputs.hookshot_version}}"' helm/hookshot/Chart.yaml
|
||||
|
||||
- name: Set values hookshot config to current config.sample.yml contents
|
||||
uses: mikefarah/yq@v4.34.1
|
||||
with:
|
||||
cmd: |
|
||||
yq -i eval-all 'select(fileIndex==0).hookshot.config = select(fileIndex==1) | select(fileIndex==0)' helm/hookshot/values.yaml config.sample.yml
|
||||
|
||||
- name: Run chart-releaser
|
||||
uses: helm/chart-releaser-action@v1.5.0
|
||||
env:
|
||||
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
with:
|
||||
config: helm/cr.yaml
|
||||
charts_dir: helm/
|
89
.github/workflows/main.yml
vendored
@ -11,6 +11,12 @@ on:
|
||||
- changelog.d/**'
|
||||
|
||||
workflow_dispatch:
|
||||
merge_group:
|
||||
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint-node:
|
||||
@ -34,6 +40,7 @@ jobs:
|
||||
profile: minimal
|
||||
components: rustfmt
|
||||
- run: cargo fmt --all -- --check
|
||||
- run: cargo clippy -- -Dwarnings
|
||||
|
||||
config:
|
||||
runs-on: ubuntu-latest
|
||||
@ -44,7 +51,7 @@ jobs:
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
- run: yarn # Need to build scripts to get rust bindings
|
||||
- run: yarn --silent ts-node src/Config/Defaults.ts --config | diff config.sample.yml -
|
||||
- run: yarn --silent ts-node src/config/Defaults.ts --config | diff config.sample.yml -
|
||||
|
||||
metrics-docs:
|
||||
runs-on: ubuntu-latest
|
||||
@ -62,7 +69,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
node_version: [18, 20]
|
||||
node_version: [20, 21]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node_version }}
|
||||
@ -73,5 +80,83 @@ jobs:
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: rust-cache
|
||||
- run: yarn
|
||||
- run: yarn test:cover
|
||||
|
||||
build-homerunner:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
homerunnersha: ${{ steps.gitsha.outputs.sha }}
|
||||
steps:
|
||||
- name: Checkout matrix-org/complement
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: matrix-org/complement
|
||||
- name: Get complement git sha
|
||||
id: gitsha
|
||||
run: echo sha=`git rev-parse --short HEAD` >> "$GITHUB_OUTPUT"
|
||||
- name: Cache homerunner
|
||||
id: cached
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: homerunner
|
||||
key: ${{ runner.os }}-homerunner-${{ steps.gitsha.outputs.sha }}
|
||||
- name: "Set Go Version"
|
||||
if: ${{ steps.cached.outputs.cache-hit != 'true' }}
|
||||
run: |
|
||||
echo "$GOROOT_1_18_X64/bin" >> $GITHUB_PATH
|
||||
echo "~/go/bin" >> $GITHUB_PATH
|
||||
# Build and install homerunner
|
||||
- name: Install Complement Dependencies
|
||||
if: ${{ steps.cached.outputs.cache-hit != 'true' }}
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install -y libolm3 libolm-dev
|
||||
- name: Build homerunner
|
||||
if: ${{ steps.cached.outputs.cache-hit != 'true' }}
|
||||
run: |
|
||||
go build ./cmd/homerunner
|
||||
|
||||
integration-test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
needs:
|
||||
- test
|
||||
- build-homerunner
|
||||
steps:
|
||||
- name: Install Complement Dependencies
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install -y libolm3
|
||||
- name: Load cached homerunner bin
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: homerunner
|
||||
key: ${{ runner.os }}-homerunner-${{ needs.build-synapse.outputs.homerunnersha }}
|
||||
fail-on-cache-miss: true # Shouldn't happen, we build this in the needs step.
|
||||
- name: Checkout matrix-hookshot
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: matrix-hookshot
|
||||
# Setup node & run tests
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: matrix-hookshot/.node-version
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: matrix-hookshot
|
||||
shared-key: rust-cache
|
||||
- name: Run Homerunner tests
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
HOMERUNNER_SPAWN_HS_TIMEOUT_SECS: 100
|
||||
HOMERUNNER_IMAGE: ghcr.io/element-hq/synapse/complement-synapse:latest
|
||||
NODE_OPTIONS: --dns-result-order ipv4first
|
||||
run: |
|
||||
docker pull $HOMERUNNER_IMAGE
|
||||
cd matrix-hookshot
|
||||
yarn --strict-semver --frozen-lockfile
|
||||
../homerunner &
|
||||
bash -ic 'yarn test:e2e'
|
1
.github/workflows/newsfile.yml
vendored
@ -3,6 +3,7 @@ name: Newsfile
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
merge_group:
|
||||
|
||||
jobs:
|
||||
changelog:
|
||||
|
@ -1 +1 @@
|
||||
18
|
||||
20
|
||||
|
259
CHANGELOG.md
@ -1,3 +1,262 @@
|
||||
5.1.2 (2024-01-02)
|
||||
==================
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix widget pinning to light theme. ([\#873](https://github.com/matrix-org/matrix-hookshot/issues/873))
|
||||
- Fix hookshot failing to format API errors.
|
||||
Only log a stacktrace of API errors on debug level logging, log limited error on info. ([\#874](https://github.com/matrix-org/matrix-hookshot/issues/874))
|
||||
- Fix GitHub events not working due to verification failures. ([\#875](https://github.com/matrix-org/matrix-hookshot/issues/875))
|
||||
|
||||
|
||||
Internal Changes
|
||||
----------------
|
||||
|
||||
- Fix spelling of "successfully". ([\#869](https://github.com/matrix-org/matrix-hookshot/issues/869))
|
||||
|
||||
|
||||
5.1.1 (2023-12-29)
|
||||
==================
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix widgets not loading when bound to the same listener as "webhooks". ([\#872](https://github.com/matrix-org/matrix-hookshot/issues/872))
|
||||
|
||||
|
||||
5.1.0 (2023-12-29)
|
||||
==================
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix feed widget not showing the true values for template / notify on failure. ([\#866](https://github.com/matrix-org/matrix-hookshot/issues/866))
|
||||
- Fix widgets failing with "Request timed out". ([\#870](https://github.com/matrix-org/matrix-hookshot/issues/870))
|
||||
|
||||
|
||||
Deprecations and Removals
|
||||
-------------------------
|
||||
|
||||
- The GoNEB migrator is being removed in this release. Users wishing to migrate from GoNEB deployments should use <=5.0.0 and then upgrade. ([\#867](https://github.com/matrix-org/matrix-hookshot/issues/867))
|
||||
|
||||
|
||||
Internal Changes
|
||||
----------------
|
||||
|
||||
- Integrate end to end testing. ([\#869](https://github.com/matrix-org/matrix-hookshot/issues/869))
|
||||
|
||||
|
||||
5.0.0 (2023-12-27)
|
||||
==================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Warn if the bot does not have permissions to talk in a room. ([\#852](https://github.com/matrix-org/matrix-hookshot/issues/852))
|
||||
- Support dark mode for the widget interface. ([\#863](https://github.com/matrix-org/matrix-hookshot/issues/863))
|
||||
- Add `webhook list` and `webhook remove` commands. ([\#866](https://github.com/matrix-org/matrix-hookshot/issues/866))
|
||||
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix notify on failure not being toggleable in the feeds widget interface. ([\#865](https://github.com/matrix-org/matrix-hookshot/issues/865))
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Documentation tidyups. ([\#855](https://github.com/matrix-org/matrix-hookshot/issues/855), [\#857](https://github.com/matrix-org/matrix-hookshot/issues/857), [\#858](https://github.com/matrix-org/matrix-hookshot/issues/858), [\#859](https://github.com/matrix-org/matrix-hookshot/issues/859), [\#860](https://github.com/matrix-org/matrix-hookshot/issues/860))
|
||||
- Generally tidy up and improve metrics documentation. ([\#856](https://github.com/matrix-org/matrix-hookshot/issues/856))
|
||||
|
||||
|
||||
Deprecations and Removals
|
||||
-------------------------
|
||||
|
||||
- Drop support for Node 18 and start supporting Node 21. ([\#862](https://github.com/matrix-org/matrix-hookshot/issues/862))
|
||||
|
||||
|
||||
4.7.0 (2023-12-06)
|
||||
==================
|
||||
|
||||
Internal Changes
|
||||
----------------
|
||||
|
||||
- Update the release script to examine the staged contents of package files when checking for consistency between Node & Rust package versions. ([\#846](https://github.com/matrix-org/matrix-hookshot/issues/846))
|
||||
- Use Node 20 (slim) for Docker image base. ([\#849](https://github.com/matrix-org/matrix-hookshot/issues/849))
|
||||
|
||||
|
||||
4.6.0 (2023-11-20)
|
||||
==================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Add new `webhookResponse` field to the transformation API to specify your own response data. See the documentation for help. ([\#839](https://github.com/matrix-org/matrix-hookshot/issues/839))
|
||||
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix version picker on docs site not loading. ([\#843](https://github.com/matrix-org/matrix-hookshot/issues/843))
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Add note about GitHub token scope for private vs. public repo notifications ([\#830](https://github.com/matrix-org/matrix-hookshot/issues/830))
|
||||
|
||||
|
||||
Internal Changes
|
||||
----------------
|
||||
|
||||
- Update the release script to check for consistency between Node & Rust package versions. ([\#819](https://github.com/matrix-org/matrix-hookshot/issues/819))
|
||||
- Chart version 0.1.14
|
||||
Do not populate optional values in default helm config, as default values are not valid. ([\#821](https://github.com/matrix-org/matrix-hookshot/issues/821))
|
||||
- Release chart version 0.1.15.
|
||||
Sample config now comments out optional parameters by default. ([\#826](https://github.com/matrix-org/matrix-hookshot/issues/826))
|
||||
|
||||
|
||||
4.5.1 (2023-09-26)
|
||||
==================
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix transformation scripts breaking if they include a `return` at the top level ([\#818](https://github.com/matrix-org/matrix-hookshot/issues/818))
|
||||
|
||||
|
||||
4.5.0 (2023-09-26)
|
||||
==================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Bridge Gitlab comment replies as Matrix threads. ([\#758](https://github.com/matrix-org/matrix-hookshot/issues/758))
|
||||
- Add generic webhook transformation JS snippet for Prometheus Alertmanager. ([\#808](https://github.com/matrix-org/matrix-hookshot/issues/808))
|
||||
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix a potential memory leak where Hookshot may hold onto certain requests forever in memory. ([\#814](https://github.com/matrix-org/matrix-hookshot/issues/814))
|
||||
- Fix feed metrics treating request failures as parsing failures. ([\#816](https://github.com/matrix-org/matrix-hookshot/issues/816))
|
||||
|
||||
|
||||
Deprecations and Removals
|
||||
-------------------------
|
||||
|
||||
- Drop support for the Sled crypto store format. Users must disable/remove the configuration key of `experimentalEncryption.useLegacySledStore`, and the crypto store will always use the SQLite format. If an existing SQLite store does not exist on bridge startup, one will be created. ([\#798](https://github.com/matrix-org/matrix-hookshot/issues/798))
|
||||
|
||||
|
||||
Internal Changes
|
||||
----------------
|
||||
|
||||
- Update the version number of Hookshot's Rust package. ([\#803](https://github.com/matrix-org/matrix-hookshot/issues/803))
|
||||
- Update eslint to a version that supports Typescript 5.1.3. ([\#815](https://github.com/matrix-org/matrix-hookshot/issues/815))
|
||||
- Use quickjs instead of vm2 for evaluating JS transformation functions. ([\#817](https://github.com/matrix-org/matrix-hookshot/issues/817))
|
||||
|
||||
|
||||
4.4.1 (2023-07-31)
|
||||
==================
|
||||
|
||||
It is **strongly** reccomended you upgrade your bridge, as this release contains security fixes.
|
||||
|
||||
🔒 Security
|
||||
-----------
|
||||
|
||||
- Fixes for GHSA-vc7j-h8xg-fv5x.
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Add more icons to GitHub repo hooks ([\#795](https://github.com/matrix-org/matrix-hookshot/issues/795))
|
||||
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix instructions for validating your config using Docker ([\#787](https://github.com/matrix-org/matrix-hookshot/issues/787))
|
||||
|
||||
|
||||
Internal Changes
|
||||
----------------
|
||||
|
||||
- Sort feed list alphabetically in bot command response ([\#791](https://github.com/matrix-org/matrix-hookshot/issues/791))
|
||||
- Update word-wrap from 1.2.3 to 1.2.4. ([\#799](https://github.com/matrix-org/matrix-hookshot/issues/799))
|
||||
- Update matrix-appservice-bridge to 9.0.1. ([\#800](https://github.com/matrix-org/matrix-hookshot/issues/800))
|
||||
|
||||
|
||||
4.4.0 (2023-06-28)
|
||||
==================
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Refactor Hookshot to use Redis for caching of feed information, massively improving memory usage.
|
||||
|
||||
Please note that this is a behavioural change: Hookshots configured to use in-memory caching (not Redis),
|
||||
will no longer bridge any RSS entries it may have missed during downtime, and will instead perform an initial
|
||||
sync (not reporting any entries) instead. ([\#786](https://github.com/matrix-org/matrix-hookshot/issues/786))
|
||||
|
||||
- Feeds now tries to find an HTML-type link before falling back to the first link when parsing atom feeds ([\#784](https://github.com/matrix-org/matrix-hookshot/issues/784))
|
||||
|
||||
|
||||
4.3.0 (2023-06-19)
|
||||
==================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Added basic helm chart to repository with GitHub Actions / chart-releaser builds ([\#719](https://github.com/matrix-org/matrix-hookshot/issues/719))
|
||||
- Feeds are now polled concurrently (defaulting to 4 feeds at a time). ([\#779](https://github.com/matrix-org/matrix-hookshot/issues/779))
|
||||
|
||||
|
||||
4.2.0 (2023-06-05)
|
||||
===================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Add support for uploading bot avatar images. ([\#767](https://github.com/matrix-org/matrix-hookshot/issues/767))
|
||||
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix confusing case where issue comments would be notified on if the issue event type is checked on GitHub connections. ([\#757](https://github.com/matrix-org/matrix-hookshot/issues/757))
|
||||
- Fix crash when failing to handle events, typically due to lacking permissions to send messages in a room. ([\#771](https://github.com/matrix-org/matrix-hookshot/issues/771))
|
||||
|
||||
|
||||
4.1.0 (2023-05-24)
|
||||
==================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Add support for notifying when a GitLab MR has a single review (rather than completed review). ([\#736](https://github.com/matrix-org/matrix-hookshot/issues/736))
|
||||
- Add support for Sentry tracing. ([\#754](https://github.com/matrix-org/matrix-hookshot/issues/754))
|
||||
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix feed message format when the item does not contain a title or link. ([\#737](https://github.com/matrix-org/matrix-hookshot/issues/737))
|
||||
- Fix HTML appearing in its escaped form in feed item summaries. ([\#738](https://github.com/matrix-org/matrix-hookshot/issues/738))
|
||||
- Fix Github comments not being rendered correctly as blockquotes. ([\#746](https://github.com/matrix-org/matrix-hookshot/issues/746))
|
||||
- Fix setup issues when the bot has PL 0 and room default isn't 0. ([\#755](https://github.com/matrix-org/matrix-hookshot/issues/755))
|
||||
|
||||
|
||||
Internal Changes
|
||||
----------------
|
||||
|
||||
- Apply non-style suggestions by `cargo clippy` to reduce allocations in the rust code. ([\#750](https://github.com/matrix-org/matrix-hookshot/issues/750))
|
||||
- Apply more Rust clippy suggestions, and run clippy in CI. ([\#753](https://github.com/matrix-org/matrix-hookshot/issues/753))
|
||||
- Update eslint to a version that supports Typescript 5. ([\#760](https://github.com/matrix-org/matrix-hookshot/issues/760))
|
||||
|
||||
|
||||
4.0.0 (2023-04-27)
|
||||
==================
|
||||
|
||||
|
1666
Cargo.lock
generated
14
Cargo.toml
@ -1,13 +1,13 @@
|
||||
[package]
|
||||
name = "matrix-hookshot"
|
||||
version = "1.8.1"
|
||||
version = "5.1.2"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
napi = {version="2.12.6", features=["serde-json", "napi6"]}
|
||||
napi = {version="2", features=["serde-json", "async", "napi6"]}
|
||||
napi-derive = "2"
|
||||
url = "2"
|
||||
serde_json = "1"
|
||||
@ -15,13 +15,15 @@ serde = "1"
|
||||
serde_derive = "1"
|
||||
contrast = "0"
|
||||
rgb = "0"
|
||||
md-5 = "0.8.0"
|
||||
hex = "0.4.3"
|
||||
rss = "2.0.3"
|
||||
md-5 = "0.10"
|
||||
hex = "0.4"
|
||||
rss = "2.0"
|
||||
atom_syndication = "0.12"
|
||||
glob = "0.3.1"
|
||||
uuid = {version="1.3.2", features=["v4", "fast-rng"]}
|
||||
redis = "0.23.0"
|
||||
ruma = { version = "0.9", features = ["events", "html"] }
|
||||
reqwest = "0.11"
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = "1"
|
||||
napi-build = "2"
|
||||
|
14
Dockerfile
@ -1,9 +1,11 @@
|
||||
# Stage 0: Build the thing
|
||||
# Need debian based image to build the native rust module
|
||||
# as musl doesn't support cdylib
|
||||
FROM node:18 AS builder
|
||||
FROM node:20-slim AS builder
|
||||
|
||||
# Needed in order to build rust FFI bindings.
|
||||
RUN apt-get update && apt-get install -y build-essential cmake curl pkg-config pkg-config libssl-dev
|
||||
|
||||
# We need rustup so we have a sensible rust version, the version packed with bullsye is too old
|
||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
@ -12,9 +14,6 @@ ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
ARG CARGO_NET_GIT_FETCH_WITH_CLI=false
|
||||
ENV CARGO_NET_GIT_FETCH_WITH_CLI=$CARGO_NET_GIT_FETCH_WITH_CLI
|
||||
|
||||
# Needed to build rust things for matrix-sdk-crypto-nodejs
|
||||
# See https://github.com/matrix-org/matrix-rust-sdk-bindings/blob/main/crates/matrix-sdk-crypto-nodejs/release/Dockerfile.linux#L5-L6
|
||||
RUN apt-get update && apt-get install -y build-essential cmake
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
@ -30,10 +29,12 @@ RUN yarn build
|
||||
|
||||
|
||||
# Stage 1: The actual container
|
||||
FROM node:18
|
||||
FROM node:20-slim
|
||||
|
||||
WORKDIR /bin/matrix-hookshot
|
||||
|
||||
RUN apt-get update && apt-get install -y openssl ca-certificates
|
||||
|
||||
COPY --from=builder /src/yarn.lock /src/package.json ./
|
||||
COPY --from=builder /cache/yarn /cache/yarn
|
||||
RUN yarn config set yarn-offline-mirror /cache/yarn
|
||||
@ -42,6 +43,7 @@ RUN yarn --network-timeout 600000 --production --pure-lockfile && yarn cache cle
|
||||
|
||||
COPY --from=builder /src/lib ./
|
||||
COPY --from=builder /src/public ./public
|
||||
COPY --from=builder /src/assets ./assets
|
||||
|
||||
VOLUME /data
|
||||
EXPOSE 9993
|
||||
|
@ -23,14 +23,14 @@ We richly support the following integrations:
|
||||
- [Jira](https://matrix-org.github.io/matrix-hookshot/latest/setup/jira.html)
|
||||
- [RSS/Atom feeds](https://matrix-org.github.io/matrix-hookshot/latest/setup/feeds.html)
|
||||
|
||||
Get started by reading the [the setup guide](https://matrix-org.github.io/matrix-hookshot/latest/setup.html)!
|
||||
Get started by reading [the setup guide](https://matrix-org.github.io/matrix-hookshot/latest/setup.html)!
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
Documentation can be found on [GitHub Pages](https://matrix-org.github.io/matrix-hookshot).
|
||||
|
||||
You can build the documentation yourself by:
|
||||
You can build the documentation yourself by typing:
|
||||
```sh
|
||||
# cargo install mdbook
|
||||
mdbook build
|
||||
|
BIN
assets/feeds_avatar.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
assets/figma_avatar.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
assets/github_avatar.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
assets/gitlab_avatar.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
assets/jira_avatar.png
Normal file
After Width: | Height: | Size: 12 KiB |
67
assets/src/feeds_avatar.svg
Normal file
@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 0.33333333 0.33333333"
|
||||
version="1.1"
|
||||
id="svg1630"
|
||||
inkscape:version="1.2.2 (b0a84865, 2022-12-01)"
|
||||
sodipodi:docname="feeds_avatar.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1632"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="in"
|
||||
showgrid="false"
|
||||
inkscape:zoom="4.28125"
|
||||
inkscape:cx="94.715328"
|
||||
inkscape:cy="-14.014599"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1302"
|
||||
inkscape:window-x="2560"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1627" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
style="fill:#f8991d;stroke-width:0.000783981;fill-opacity:1"
|
||||
id="rect16036"
|
||||
width="0.33333334"
|
||||
height="0.33333334"
|
||||
x="0"
|
||||
y="0" />
|
||||
<ellipse
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0113383;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path5270"
|
||||
cx="0.10655722"
|
||||
cy="0.22619501"
|
||||
rx="0.023283437"
|
||||
ry="0.022608556" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.00108608px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 0.08347222,0.16973219 -7.942e-5,-0.032879 c 0.070275,0.00367 0.10927918,0.0539491 0.11023648,0.11071368 h -0.032928 c -5.4632e-4,-0.049895 -0.034473,-0.07602 -0.07722906,-0.0778347 z"
|
||||
id="path5805"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.00108608px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 0.08434684,0.11703641 0.0833928,0.08363142 c 0.10798627,0.0050099 0.16581867,0.08337597 0.16654766,0.16607045 l -0.0338827,-4.7761e-4 c 0.001483,-0.052895 -0.0376492,-0.13061767 -0.13171092,-0.13218785 z"
|
||||
id="path5807"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
88
assets/src/figma_avatar.svg
Normal file
@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 0.33333333 0.33333333"
|
||||
version="1.1"
|
||||
id="svg1630"
|
||||
inkscape:version="1.2.2 (b0a84865, 2022-12-01)"
|
||||
sodipodi:docname="figma_avatar.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1632"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="in"
|
||||
showgrid="false"
|
||||
inkscape:zoom="4.2812503"
|
||||
inkscape:cx="62.715324"
|
||||
inkscape:cy="56.525544"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1274"
|
||||
inkscape:window-x="2560"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1627" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
style="fill:#ffffff;stroke-width:0.000783981;fill-opacity:1"
|
||||
id="rect16036"
|
||||
width="0.33333334"
|
||||
height="0.33333334"
|
||||
x="0"
|
||||
y="0" />
|
||||
<g
|
||||
id="g482"
|
||||
transform="translate(0.01500414)">
|
||||
<rect
|
||||
width="0.11111087"
|
||||
height="0.16666642"
|
||||
fill="#000000"
|
||||
fill-opacity="0"
|
||||
id="rect240"
|
||||
x="0.096107043"
|
||||
y="0.083333619"
|
||||
style="stroke-width:0.00116931" />
|
||||
<path
|
||||
d="m 0.15166251,0.16666672 c 0,-0.0153406 0.0124364,-0.027778 0.0277778,-0.027778 v 0 c 0.015341,0 0.0277777,0.0124366 0.0277777,0.027778 v 0 c 0,0.0153413 -0.0124368,0.0277779 -0.0277777,0.0277779 v 0 c -0.0153414,0 -0.0277778,-0.0124366 -0.0277778,-0.0277779 z"
|
||||
fill="#1abcfe"
|
||||
id="path242"
|
||||
style="stroke-width:0.000389771" />
|
||||
<path
|
||||
d="m 0.09610705,0.22222216 c 0,-0.0153406 0.0124365,-0.027778 0.0277777,-0.027778 h 0.0277778 v 0.027778 c 0,0.0153413 -0.0124364,0.027778 -0.0277778,0.027778 v 0 c -0.0153412,0 -0.0277777,-0.0124366 -0.0277777,-0.027778 z"
|
||||
fill="#0acf83"
|
||||
id="path244"
|
||||
style="stroke-width:0.000389771" />
|
||||
<path
|
||||
d="m 0.15166251,0.08333365 v 0.05555524 h 0.0277778 c 0.0153414,0 0.0277777,-0.0124366 0.0277777,-0.0277773 v 0 c 0,-0.01534131 -0.0124364,-0.02777796 -0.0277777,-0.02777796 z"
|
||||
fill="#ff7262"
|
||||
id="path246"
|
||||
style="stroke-width:0.000389771" />
|
||||
<path
|
||||
d="m 0.09610705,0.11111141 c 0,0.0153413 0.0124365,0.0277773 0.0277777,0.0277773 h 0.0277778 V 0.08333345 h -0.0277778 c -0.0153412,0 -0.0277777,0.01243665 -0.0277777,0.02777796 z"
|
||||
fill="#f24e1e"
|
||||
id="path248"
|
||||
style="stroke-width:0.000389771" />
|
||||
<path
|
||||
d="m 0.09610705,0.16666672 c 0,0.0153413 0.0124365,0.0277779 0.0277777,0.0277779 h 0.0277778 v -0.0555552 h -0.0277778 c -0.0153412,0 -0.0277777,0.0124366 -0.0277777,0.0277773 z"
|
||||
fill="#a259ff"
|
||||
id="path250"
|
||||
style="stroke-width:0.000389771" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.3 KiB |
57
assets/src/github_avatar.svg
Normal file
@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 0.33333333 0.33333333"
|
||||
version="1.1"
|
||||
id="svg1630"
|
||||
inkscape:version="1.2.2 (b0a84865, 2022-12-01)"
|
||||
sodipodi:docname="github_avatar.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1632"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="in"
|
||||
showgrid="false"
|
||||
inkscape:zoom="12.109204"
|
||||
inkscape:cx="39.350233"
|
||||
inkscape:cy="8.0104357"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1302"
|
||||
inkscape:window-x="2560"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1627" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
style="fill:#ffffff;stroke-width:0.000783981;fill-opacity:1"
|
||||
id="rect16036"
|
||||
width="0.33333334"
|
||||
height="0.33333334"
|
||||
x="0"
|
||||
y="0" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m 0.16677043,0.04375549 c -0.0691789,0 -0.1251031,0.0563338 -0.1251031,0.12602641 0,0.055709 0.03583264,0.10286558 0.085542,0.11955571 0.006214,0.001255 0.008491,-0.002712 0.008491,-0.006048 0,-0.002922 -2.0487e-4,-0.012936 -2.0487e-4,-0.0233708 -0.0348006,0.007513 -0.0420475,-0.0150232 -0.0420475,-0.0150232 -0.005592,-0.0146055 -0.0138795,-0.0183596 -0.0138795,-0.0183596 -0.0113895,-0.00772 8.2964e-4,-0.00772 8.2964e-4,-0.00772 0.0126345,8.3481e-4 0.0192646,0.0129359 0.0192646,0.0129359 0.0111825,0.0191945 0.0292028,0.0137715 0.0364524,0.0104325 0.001034,-0.008137 0.004351,-0.0137715 0.007872,-0.0169001 -0.0277559,-0.002922 -0.0569588,-0.0137715 -0.0569588,-0.0621797 0,-0.0137715 0.004968,-0.0250378 0.01284,-0.0338003 -0.001242,-0.003129 -0.005593,-0.0160681 0.001245,-0.0333854 0,0 0.010563,-0.003339 0.0343806,0.012936 a 0.12027864,0.12027267 0 0 1 0.031277,-0.004173 c 0.010563,0 0.0213311,0.001463 0.0312744,0.004173 0.0238202,-0.0162752 0.0343833,-0.012936 0.0343833,-0.012936 0.006837,0.0173176 0.002484,0.0302564 0.001241,0.0333854 0.008079,0.008763 0.012843,0.0200291 0.012843,0.0338003 0,0.0484087 -0.0292028,0.059048 -0.0571663,0.0621797 0.004558,0.003964 0.008491,0.0114735 0.008491,0.0233683 0,0.0169001 -2.0484e-4,0.0304638 -2.0484e-4,0.034635 0,0.003339 0.002279,0.007305 0.008491,0.006054 0.0497094,-0.0166952 0.085542,-0.0638492 0.085542,-0.11955826 2.0433e-4,-0.0696935 -0.0559249,-0.12602732 -0.12489634,-0.12602731 z"
|
||||
fill="#24292f"
|
||||
id="path361"
|
||||
style="stroke-width:0.00256072" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.0 KiB |
74
assets/src/gitlab_avatar.svg
Normal file
@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 0.33333333 0.33333333"
|
||||
version="1.1"
|
||||
id="svg1630"
|
||||
inkscape:version="1.2.2 (b0a84865, 2022-12-01)"
|
||||
sodipodi:docname="gitlab_avatar.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1632"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="in"
|
||||
showgrid="false"
|
||||
inkscape:zoom="17.125001"
|
||||
inkscape:cx="28.992699"
|
||||
inkscape:cy="10.218978"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1302"
|
||||
inkscape:window-x="2560"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1627" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
style="fill:#ffffff;stroke-width:0.000783981;fill-opacity:1"
|
||||
id="rect16036"
|
||||
width="0.33333334"
|
||||
height="0.33333334"
|
||||
x="0"
|
||||
y="0" />
|
||||
<g
|
||||
id="LOGO"
|
||||
transform="matrix(0.00130172,0,0,0.00130172,-0.08066014,-0.08067311)">
|
||||
<path
|
||||
class="cls-1"
|
||||
d="m 282.83,170.73 -0.27,-0.69 -26.14,-68.22 a 6.81,6.81 0 0 0 -2.69,-3.24 7,7 0 0 0 -8,0.43 7,7 0 0 0 -2.32,3.52 l -17.65,54 h -71.47 l -17.65,-54 a 6.86,6.86 0 0 0 -2.32,-3.53 7,7 0 0 0 -8,-0.43 6.87,6.87 0 0 0 -2.69,3.24 L 97.44,170 l -0.26,0.69 a 48.54,48.54 0 0 0 16.1,56.1 l 0.09,0.07 0.24,0.17 39.82,29.82 19.7,14.91 12,9.06 a 8.07,8.07 0 0 0 9.76,0 l 12,-9.06 19.7,-14.91 40.06,-30 0.1,-0.08 a 48.56,48.56 0 0 0 16.08,-56.04 z"
|
||||
id="path695"
|
||||
style="fill:#e24329" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="m 282.83,170.73 -0.27,-0.69 a 88.3,88.3 0 0 0 -35.15,15.8 L 190,229.25 c 19.55,14.79 36.57,27.64 36.57,27.64 l 40.06,-30 0.1,-0.08 a 48.56,48.56 0 0 0 16.1,-56.08 z"
|
||||
id="path697"
|
||||
style="fill:#fc6d26" />
|
||||
<path
|
||||
class="cls-3"
|
||||
d="m 153.43,256.89 19.7,14.91 12,9.06 a 8.07,8.07 0 0 0 9.76,0 l 12,-9.06 19.7,-14.91 c 0,0 -17.04,-12.89 -36.59,-27.64 -19.55,14.75 -36.57,27.64 -36.57,27.64 z"
|
||||
id="path699"
|
||||
style="fill:#fca326" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M 132.58,185.84 A 88.19,88.19 0 0 0 97.44,170 l -0.26,0.69 a 48.54,48.54 0 0 0 16.1,56.1 l 0.09,0.07 0.24,0.17 39.82,29.82 c 0,0 17,-12.85 36.57,-27.64 z"
|
||||
id="path701"
|
||||
style="fill:#fc6d26" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
90
assets/src/jira_avatar.svg
Normal file
@ -0,0 +1,90 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg22"
|
||||
sodipodi:docname="jira_avatar.svg"
|
||||
inkscape:version="1.2.2 (b0a84865, 2022-12-01)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview24"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="11.346854"
|
||||
inkscape:cx="12.999198"
|
||||
inkscape:cy="14.453345"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1302"
|
||||
inkscape:window-x="2560"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg22" />
|
||||
<rect
|
||||
style="opacity:1;fill:#0052cc;fill-opacity:1;stroke-width:0.941893"
|
||||
id="rect332"
|
||||
width="32"
|
||||
height="32"
|
||||
x="0"
|
||||
y="-1.3125472e-07" />
|
||||
<path
|
||||
d="M23.7199 7.64001H15.6399C15.6399 9.64001 17.2799 11.28 19.2799 11.28H20.7599V12.72C20.7599 14.72 22.3999 16.36 24.3999 16.36V8.32001C24.4399 7.96001 24.1199 7.64001 23.7199 7.64001Z"
|
||||
fill="white"
|
||||
id="path4" />
|
||||
<path
|
||||
d="M19.7199 11.64H11.6399C11.6399 13.64 13.2799 15.28 15.2799 15.28H16.7599V16.72C16.7599 18.72 18.3999 20.36 20.3999 20.36V12.36C20.4399 11.96 20.1199 11.64 19.7199 11.64Z"
|
||||
fill="url(#paint0_linear)"
|
||||
id="path6" />
|
||||
<path
|
||||
d="M15.7199 15.6801H7.63989C7.63989 17.6801 9.27988 19.3201 11.2799 19.3201H12.7599V20.7601C12.7599 22.7601 14.3999 24.4001 16.3999 24.4001V16.4001C16.4399 16.0001 16.1199 15.6801 15.7199 15.6801Z"
|
||||
fill="url(#paint1_linear)"
|
||||
id="path8" />
|
||||
<defs
|
||||
id="defs20">
|
||||
<linearGradient
|
||||
id="paint0_linear"
|
||||
x1="20.2597"
|
||||
y1="11.6708"
|
||||
x2="16.8183"
|
||||
y2="15.2196"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
offset="0.176"
|
||||
stop-color="white"
|
||||
stop-opacity="0.4"
|
||||
id="stop10" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="white"
|
||||
id="stop12" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear"
|
||||
x1="16.4844"
|
||||
y1="15.7246"
|
||||
x2="12.5054"
|
||||
y2="19.596"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
offset="0.176"
|
||||
stop-color="white"
|
||||
stop-opacity="0.4"
|
||||
id="stop15" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="white"
|
||||
id="stop17" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
79
assets/src/webhooks_avatar.svg
Normal file
@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 0.33333333 0.33333333"
|
||||
version="1.1"
|
||||
id="svg1630"
|
||||
inkscape:version="1.2.2 (b0a84865, 2022-12-01)"
|
||||
sodipodi:docname="webhooks_avatar.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1632"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="in"
|
||||
showgrid="false"
|
||||
inkscape:zoom="6.0546022"
|
||||
inkscape:cx="90.096753"
|
||||
inkscape:cy="-17.011853"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1302"
|
||||
inkscape:window-x="2560"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1627" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
style="fill:#ffffff;stroke-width:0.000783981;fill-opacity:1"
|
||||
id="rect16036"
|
||||
width="0.33333334"
|
||||
height="0.33333334"
|
||||
x="0"
|
||||
y="0" />
|
||||
<g
|
||||
id="g346"
|
||||
transform="matrix(0.015625,0,0,0.015625,-2.4512961,-2.5012285)">
|
||||
<g
|
||||
id="OvHZFw.tif"
|
||||
transform="matrix(0.12401029,0,0,0.12401029,159.54955,163.29764)">
|
||||
<g
|
||||
id="g325">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fill="#231f20"
|
||||
d="m 60.467,50.345 c -5.367,9.022 -10.509,17.759 -15.758,26.43 -1.348,2.226 -2.015,4.039 -0.938,6.869 2.973,7.817 -1.221,15.424 -9.104,17.489 C 27.233,103.081 19.99,98.195 18.515,90.236 17.208,83.191 22.675,76.285 30.442,75.184 31.093,75.091 31.757,75.08 32.851,74.998 36.657,68.616 40.556,62.079 44.666,55.186 37.235,47.797 32.812,39.159 33.791,28.456 34.483,20.89 37.458,14.352 42.896,8.993 53.311,-1.269 69.2,-2.931 81.463,4.946 93.241,12.512 98.635,27.25 94.037,39.864 90.57,38.924 87.079,37.976 83.241,36.935 84.685,29.922 83.617,23.624 78.887,18.229 75.762,14.667 71.752,12.8 67.192,12.112 58.051,10.731 49.076,16.604 46.413,25.576 43.39,35.759 47.965,44.077 60.467,50.345 Z"
|
||||
id="path319" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fill="#231f20"
|
||||
d="m 75.794,39.676 c 3.781,6.67 7.62,13.441 11.425,20.15 19.232,-5.95 33.732,4.696 38.934,16.094 6.283,13.768 1.988,30.075 -10.352,38.569 -12.666,8.72 -28.684,7.23 -39.906,-3.971 2.86,-2.394 5.734,-4.799 8.805,-7.368 11.084,7.179 20.778,6.841 27.975,-1.66 6.137,-7.252 6.004,-18.065 -0.311,-25.165 -7.288,-8.193 -17.05,-8.443 -28.85,-0.578 -4.895,-8.684 -9.875,-17.299 -14.615,-26.046 -1.598,-2.948 -3.363,-4.658 -6.965,-5.282 -6.016,-1.043 -9.9,-6.209 -10.133,-11.997 -0.229,-5.724 3.143,-10.898 8.414,-12.914 5.221,-1.997 11.348,-0.385 14.86,4.054 2.87,3.627 3.782,7.709 2.272,12.182 -0.42,1.247 -0.964,2.454 -1.553,3.932 z"
|
||||
id="path321" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fill="#231f20"
|
||||
d="m 84.831,94.204 c -7.605,0 -15.238,0 -23.152,0 -2.219,9.127 -7.012,16.496 -15.271,21.182 -6.42,3.642 -13.34,4.877 -20.705,3.688 C 12.143,116.887 1.055,104.68 0.079,90.934 -1.026,75.363 9.677,61.522 23.943,58.413 c 0.985,3.577 1.98,7.188 2.965,10.756 -13.089,6.678 -17.619,15.092 -13.956,25.613 3.225,9.259 12.385,14.334 22.331,12.371 10.157,-2.004 15.278,-10.445 14.653,-23.992 9.629,0 19.266,-0.1 28.896,0.049 3.76,0.059 6.663,-0.331 9.496,-3.646 4.664,-5.455 13.248,-4.963 18.271,0.189 5.133,5.265 4.887,13.737 -0.545,18.78 -5.241,4.866 -13.521,4.606 -18.424,-0.637 -1.008,-1.081 -1.802,-2.364 -2.799,-3.692 z"
|
||||
id="path323" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.0 KiB |
BIN
assets/webhooks_avatar.png
Normal file
After Width: | Height: | Size: 26 KiB |
@ -1 +0,0 @@
|
||||
Fix feed message format when the item does not contain a title or link.
|
1
changelog.d/876.feature
Normal file
@ -0,0 +1 @@
|
||||
Add command to list feeds in JSON and YAML format to easily export all feeds from a room.
|
@ -2,175 +2,27 @@
|
||||
|
||||
bridge:
|
||||
# Basic homeserver configuration
|
||||
|
||||
domain: example.com
|
||||
url: http://localhost:8008
|
||||
mediaUrl: https://example.com
|
||||
port: 9993
|
||||
bindAddress: 127.0.0.1
|
||||
github:
|
||||
# (Optional) Configure this to enable GitHub support
|
||||
|
||||
auth:
|
||||
# Authentication for the GitHub App.
|
||||
|
||||
id: 123
|
||||
privateKeyFile: github-key.pem
|
||||
webhook:
|
||||
# Webhook settings for the GitHub app.
|
||||
|
||||
secret: secrettoken
|
||||
oauth:
|
||||
# (Optional) Settings for allowing users to sign in via OAuth.
|
||||
|
||||
client_id: foo
|
||||
client_secret: bar
|
||||
redirect_uri: https://example.com/bridge_oauth/
|
||||
defaultOptions:
|
||||
# (Optional) Default options for GitHub connections.
|
||||
|
||||
showIssueRoomLink: false
|
||||
hotlinkIssues:
|
||||
prefix: "#"
|
||||
userIdPrefix:
|
||||
# (Optional) Prefix used when creating ghost users for GitHub accounts.
|
||||
|
||||
_github_
|
||||
gitlab:
|
||||
# (Optional) Configure this to enable GitLab support
|
||||
|
||||
instances:
|
||||
gitlab.com:
|
||||
url: https://gitlab.com
|
||||
webhook:
|
||||
secret: secrettoken
|
||||
publicUrl: https://example.com/hookshot/
|
||||
userIdPrefix:
|
||||
# (Optional) Prefix used when creating ghost users for GitLab accounts.
|
||||
|
||||
_gitlab_
|
||||
figma:
|
||||
# (Optional) Configure this to enable Figma support
|
||||
|
||||
publicUrl: https://example.com/hookshot/
|
||||
instances:
|
||||
your-instance:
|
||||
teamId: your-team-id
|
||||
accessToken: your-personal-access-token
|
||||
passcode: your-webhook-passcode
|
||||
jira:
|
||||
# (Optional) Configure this to enable Jira support. Only specify `url` if you are using a On Premise install (i.e. not atlassian.com)
|
||||
|
||||
webhook:
|
||||
# Webhook settings for JIRA
|
||||
|
||||
secret: secrettoken
|
||||
oauth:
|
||||
# (Optional) OAuth settings for connecting users to JIRA. See documentation for more information
|
||||
|
||||
client_id: foo
|
||||
client_secret: bar
|
||||
redirect_uri: https://example.com/bridge_oauth/
|
||||
generic:
|
||||
# (Optional) Support for generic webhook events.
|
||||
#'allowJsTransformationFunctions' will allow users to write short transformation snippets in code, and thus is unsafe in untrusted environments
|
||||
|
||||
|
||||
enabled: false
|
||||
enableHttpGet: false
|
||||
urlPrefix: https://example.com/webhook/
|
||||
userIdPrefix: _webhooks_
|
||||
allowJsTransformationFunctions: false
|
||||
waitForComplete: false
|
||||
feeds:
|
||||
# (Optional) Configure this to enable RSS/Atom feed support
|
||||
|
||||
enabled: false
|
||||
pollIntervalSeconds: 600
|
||||
pollTimeoutSeconds: 30
|
||||
provisioning:
|
||||
# (Optional) Provisioning API for integration managers
|
||||
|
||||
secret: "!secretToken"
|
||||
passFile:
|
||||
# A passkey used to encrypt tokens stored inside the bridge.
|
||||
# Run openssl genpkey -out passkey.pem -outform PEM -algorithm RSA -pkeyopt rsa_keygen_bits:4096 to generate
|
||||
|
||||
passkey.pem
|
||||
bot:
|
||||
# (Optional) Define profile information for the bot user
|
||||
|
||||
displayname: Hookshot Bot
|
||||
avatar: mxc://half-shot.uk/2876e89ccade4cb615e210c458e2a7a6883fe17d
|
||||
serviceBots:
|
||||
# (Optional) Define additional bot users for specific services
|
||||
|
||||
- localpart: feeds
|
||||
displayname: Feeds
|
||||
avatar: mxc://half-shot.uk/2876e89ccade4cb615e210c458e2a7a6883fe17d
|
||||
prefix: "!feeds"
|
||||
service: feeds
|
||||
metrics:
|
||||
# (Optional) Prometheus metrics support
|
||||
|
||||
enabled: true
|
||||
queue:
|
||||
# (Optional) Message queue / cache configuration options for large scale deployments.
|
||||
# For encryption to work, must be set to monolithic mode and have a host & port specified.
|
||||
|
||||
monolithic: true
|
||||
port: 6379
|
||||
host: localhost
|
||||
logging:
|
||||
# (Optional) Logging settings. You can have a severity debug,info,warn,error
|
||||
|
||||
# Logging settings. You can have a severity debug,info,warn,error
|
||||
level: info
|
||||
colorize: true
|
||||
json: false
|
||||
timestampFormat: HH:mm:ss:SSS
|
||||
widgets:
|
||||
# (Optional) EXPERIMENTAL support for complimentary widgets
|
||||
|
||||
addToAdminRooms: false
|
||||
disallowedIpRanges:
|
||||
- 127.0.0.0/8
|
||||
- 10.0.0.0/8
|
||||
- 172.16.0.0/12
|
||||
- 192.168.0.0/16
|
||||
- 100.64.0.0/10
|
||||
- 192.0.0.0/24
|
||||
- 169.254.0.0/16
|
||||
- 192.88.99.0/24
|
||||
- 198.18.0.0/15
|
||||
- 192.0.2.0/24
|
||||
- 198.51.100.0/24
|
||||
- 203.0.113.0/24
|
||||
- 224.0.0.0/4
|
||||
- ::1/128
|
||||
- fe80::/10
|
||||
- fc00::/7
|
||||
- 2001:db8::/32
|
||||
- ff00::/8
|
||||
- fec0::/10
|
||||
roomSetupWidget:
|
||||
addOnInvite: false
|
||||
publicUrl: https://example.com/widgetapi/v1/static/
|
||||
branding:
|
||||
widgetTitle: Hookshot Configuration
|
||||
permissions:
|
||||
# (Optional) Permissions for using the bridge. See docs/setup.md#permissions for help
|
||||
|
||||
- actor: example.com
|
||||
services:
|
||||
- service: "*"
|
||||
level: admin
|
||||
listeners:
|
||||
# (Optional) HTTP Listener configuration.
|
||||
# HTTP Listener configuration.
|
||||
# Bind resource endpoints to ports and addresses.
|
||||
# 'port' must be specified. Each listener must listen on a unique port.
|
||||
# 'bindAddress' will default to '127.0.0.1' if not specified, which may not be suited to Docker environments.
|
||||
# 'resources' may be any of webhooks, widgets, metrics, provisioning
|
||||
|
||||
- port: 9000
|
||||
bindAddress: 0.0.0.0
|
||||
resources:
|
||||
@ -185,3 +37,150 @@ listeners:
|
||||
resources:
|
||||
- widgets
|
||||
|
||||
#github:
|
||||
# # (Optional) Configure this to enable GitHub support
|
||||
# auth:
|
||||
# # Authentication for the GitHub App.
|
||||
# id: 123
|
||||
# privateKeyFile: github-key.pem
|
||||
# webhook:
|
||||
# # Webhook settings for the GitHub app.
|
||||
# secret: secrettoken
|
||||
# oauth:
|
||||
# # (Optional) Settings for allowing users to sign in via OAuth.
|
||||
# client_id: foo
|
||||
# client_secret: bar
|
||||
# redirect_uri: https://example.com/bridge_oauth/
|
||||
# defaultOptions:
|
||||
# # (Optional) Default options for GitHub connections.
|
||||
# showIssueRoomLink: false
|
||||
# hotlinkIssues:
|
||||
# prefix: "#"
|
||||
# userIdPrefix:
|
||||
# # (Optional) Prefix used when creating ghost users for GitHub accounts.
|
||||
# _github_
|
||||
|
||||
#gitlab:
|
||||
# # (Optional) Configure this to enable GitLab support
|
||||
# instances:
|
||||
# gitlab.com:
|
||||
# url: https://gitlab.com
|
||||
# webhook:
|
||||
# secret: secrettoken
|
||||
# publicUrl: https://example.com/hookshot/
|
||||
# userIdPrefix:
|
||||
# # (Optional) Prefix used when creating ghost users for GitLab accounts.
|
||||
# _gitlab_
|
||||
# commentDebounceMs:
|
||||
# # (Optional) Aggregate comments by waiting this many miliseconds before posting them to Matrix. Defaults to 5000 (5 seconds)
|
||||
# 5000
|
||||
|
||||
#figma:
|
||||
# # (Optional) Configure this to enable Figma support
|
||||
# publicUrl: https://example.com/hookshot/
|
||||
# instances:
|
||||
# your-instance:
|
||||
# teamId: your-team-id
|
||||
# accessToken: your-personal-access-token
|
||||
# passcode: your-webhook-passcode
|
||||
|
||||
#jira:
|
||||
# # (Optional) Configure this to enable Jira support. Only specify `url` if you are using a On Premise install (i.e. not atlassian.com)
|
||||
# webhook:
|
||||
# # Webhook settings for JIRA
|
||||
# secret: secrettoken
|
||||
# oauth:
|
||||
# # (Optional) OAuth settings for connecting users to JIRA. See documentation for more information
|
||||
# client_id: foo
|
||||
# client_secret: bar
|
||||
# redirect_uri: https://example.com/bridge_oauth/
|
||||
|
||||
#generic:
|
||||
# # (Optional) Support for generic webhook events.
|
||||
# #'allowJsTransformationFunctions' will allow users to write short transformation snippets in code, and thus is unsafe in untrusted environments
|
||||
|
||||
# enabled: false
|
||||
# enableHttpGet: false
|
||||
# urlPrefix: https://example.com/webhook/
|
||||
# userIdPrefix: _webhooks_
|
||||
# allowJsTransformationFunctions: false
|
||||
# waitForComplete: false
|
||||
|
||||
#feeds:
|
||||
# # (Optional) Configure this to enable RSS/Atom feed support
|
||||
# enabled: false
|
||||
# pollConcurrency: 4
|
||||
# pollIntervalSeconds: 600
|
||||
# pollTimeoutSeconds: 30
|
||||
|
||||
#provisioning:
|
||||
# # (Optional) Provisioning API for integration managers
|
||||
# secret: "!secretToken"
|
||||
|
||||
#bot:
|
||||
# # (Optional) Define profile information for the bot user
|
||||
# displayname: Hookshot Bot
|
||||
# avatar: mxc://half-shot.uk/2876e89ccade4cb615e210c458e2a7a6883fe17d
|
||||
|
||||
#serviceBots:
|
||||
# # (Optional) Define additional bot users for specific services
|
||||
# - localpart: feeds
|
||||
# displayname: Feeds
|
||||
# avatar: ./assets/feeds_avatar.png
|
||||
# prefix: "!feeds"
|
||||
# service: feeds
|
||||
|
||||
#metrics:
|
||||
# # (Optional) Prometheus metrics support
|
||||
# enabled: true
|
||||
|
||||
#queue:
|
||||
# # (Optional) Message queue / cache configuration options for large scale deployments.
|
||||
# # For encryption to work, must be set to monolithic mode and have a host & port specified.
|
||||
# monolithic: true
|
||||
# port: 6379
|
||||
# host: localhost
|
||||
|
||||
#widgets:
|
||||
# # (Optional) EXPERIMENTAL support for complimentary widgets
|
||||
# addToAdminRooms: false
|
||||
# disallowedIpRanges:
|
||||
# - 127.0.0.0/8
|
||||
# - 10.0.0.0/8
|
||||
# - 172.16.0.0/12
|
||||
# - 192.168.0.0/16
|
||||
# - 100.64.0.0/10
|
||||
# - 192.0.0.0/24
|
||||
# - 169.254.0.0/16
|
||||
# - 192.88.99.0/24
|
||||
# - 198.18.0.0/15
|
||||
# - 192.0.2.0/24
|
||||
# - 198.51.100.0/24
|
||||
# - 203.0.113.0/24
|
||||
# - 224.0.0.0/4
|
||||
# - ::1/128
|
||||
# - fe80::/10
|
||||
# - fc00::/7
|
||||
# - 2001:db8::/32
|
||||
# - ff00::/8
|
||||
# - fec0::/10
|
||||
# roomSetupWidget:
|
||||
# addOnInvite: false
|
||||
# publicUrl: https://example.com/widgetapi/v1/static/
|
||||
# branding:
|
||||
# widgetTitle: Hookshot Configuration
|
||||
|
||||
#sentry:
|
||||
# # (Optional) Configure Sentry error reporting
|
||||
# dsn: https://examplePublicKey@o0.ingest.sentry.io/0
|
||||
# environment: production
|
||||
|
||||
#permissions:
|
||||
# # (Optional) Permissions for using the bridge. See docs/setup.md#permissions for help
|
||||
# - actor: example.com
|
||||
# services:
|
||||
# - service: "*"
|
||||
# level: admin
|
||||
|
||||
|
||||
|
||||
|
68
contrib/jsTransformationFunctions/alertmanager.js
Normal file
@ -0,0 +1,68 @@
|
||||
/**
|
||||
* This is a transformation function for Prometheus Alertmanager webhooks.
|
||||
* https://prometheus.io/docs/alerting/latest/configuration/#webhook_config
|
||||
*
|
||||
* Creates a formatted `m.text` message with plaintext fallback, containing:
|
||||
* - alert status and severity
|
||||
* - alert name and description
|
||||
* - URL to the entity that caused the alert
|
||||
* The formatted message also contains a clickable link that silences the alert.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param status resolved or firing
|
||||
* @param severity from the labels of the alert
|
||||
* @returns colored text rendering of the status and severity
|
||||
*/
|
||||
function statusBadge(status, severity) {
|
||||
let statusColor;
|
||||
if (status === "resolved") {
|
||||
return `<font color='green'><b>[RESOLVED]</b></font>`;
|
||||
}
|
||||
|
||||
switch(severity) {
|
||||
case 'resolved':
|
||||
case 'critical':
|
||||
return `<font color='red'><b>[FIRING - CRITICAL]</b></font>`;
|
||||
case 'warning':
|
||||
return `<font color='orange'><b>[FIRING - WARNING]</b></font>`;
|
||||
default:
|
||||
return `<b>[${status.toUpperCase()}]</b>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param alert object from the webhook payload
|
||||
* @param externalURL from the webhook payload
|
||||
* @returns a formatted link that will silence the alert when clicked
|
||||
*/
|
||||
function silenceLink(alert, externalURL) {
|
||||
filters = []
|
||||
for (const [label, val] of Object.entries(alert.labels)) {
|
||||
filters.push(encodeURIComponent(`${label}="${val}"`));
|
||||
}
|
||||
return `<a href="${externalURL}#silences/new?filter={${filters.join(",")}}">silence</a>`;
|
||||
}
|
||||
|
||||
if (!data.alerts) {
|
||||
result = {
|
||||
version: 'v2',
|
||||
empty: true,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const plainErrors = [];
|
||||
const htmlErrors = [];
|
||||
const { externalURL, alerts } = data;
|
||||
|
||||
for (const alert of data.alerts) {
|
||||
plainErrors.push(`**[${alert.status.toUpperCase()} - ${alert.labels.severity}]** - ${alert.labels.alertname}: ${alert.annotations.description} [source](${alert.generatorURL})`);
|
||||
htmlErrors.push(`<p>${statusBadge(alert.status, alert.labels.severity)}</p><p><b>${alert.labels.alertname}</b>: ${alert.annotations.description.replaceAll("\n","<br\>")}</p><p><a href="${alert.generatorURL}">source</a> | ${silenceLink(alert, externalURL)}</p>`)
|
||||
result = {
|
||||
version: 'v2',
|
||||
plain: plainErrors.join(`\n\n`),
|
||||
html: htmlErrors.join(`<br/><br/>`),
|
||||
msgtype: 'm.text'
|
||||
};
|
||||
}
|
@ -17,6 +17,7 @@
|
||||
- [GitLab Project](./usage/room_configuration/gitlab_project.md)
|
||||
- [JIRA Project](./usage/room_configuration/jira_project.md)
|
||||
- [📊 Metrics](./metrics.md)
|
||||
- [Sentry](./sentry.md)
|
||||
|
||||
# 🧑💻 Development
|
||||
- [Contributing](./contributing.md)
|
||||
|
@ -11,7 +11,7 @@ window.addEventListener("load", () => {
|
||||
res.json()
|
||||
).then(releases => {
|
||||
selectElement.innerHTML = "";
|
||||
for (const version of ['latest', ...releases.map(r => r.tag_name).filter(s => s !== "0.1.0")]) {
|
||||
for (const version of ['latest', ...releases.map(r => r.tag_name).filter(s => s !== "0.1.0" && !s.startsWith("helm-"))]) {
|
||||
const option = document.createElement("option");
|
||||
option.innerHTML = version;
|
||||
selectElement.add(option);
|
||||
@ -37,4 +37,4 @@ window.addEventListener("load", () => {
|
||||
});
|
||||
|
||||
document.querySelector(".version-box").appendChild(selectElement);
|
||||
});
|
||||
});
|
||||
|
@ -29,25 +29,30 @@
|
||||
/* icons for headers */
|
||||
|
||||
.chapter > li:nth-child(3) > ol:nth-child(1) > li:nth-child(2) strong:after {
|
||||
content: ' ' url('/matrix-hookshot/latest/icons/feeds.png')
|
||||
content: ' ' url('/matrix-hookshot/latest/icons/feeds.png');
|
||||
}
|
||||
|
||||
.chapter > li:nth-child(3) > ol:nth-child(1) > li:nth-child(3) strong:after {
|
||||
content: ' ' url('/matrix-hookshot/latest/icons/figma.png')
|
||||
content: ' ' url('/matrix-hookshot/latest/icons/figma.png');
|
||||
}
|
||||
|
||||
.chapter > li:nth-child(3) > ol:nth-child(1) > li:nth-child(4) strong:after {
|
||||
content: ' ' url('/matrix-hookshot/latest/icons/github.png')
|
||||
content: ' ' url('/matrix-hookshot/latest/icons/github.png');
|
||||
}
|
||||
|
||||
.chapter > li:nth-child(3) > ol:nth-child(1) > li:nth-child(5) strong:after {
|
||||
content: ' ' url('/matrix-hookshot/latest/icons/gitlab.png')
|
||||
content: ' ' url('/matrix-hookshot/latest/icons/gitlab.png');
|
||||
}
|
||||
|
||||
.chapter > li:nth-child(3) > ol:nth-child(1) > li:nth-child(6) strong:after {
|
||||
content: ' ' url('/matrix-hookshot/latest/icons/jira.png')
|
||||
content: ' ' url('/matrix-hookshot/latest/icons/jira.png');
|
||||
}
|
||||
|
||||
.chapter > li:nth-child(3) > ol:nth-child(1) > li:nth-child(7) strong:after {
|
||||
content: ' ' url('/matrix-hookshot/latest/icons/webhooks.png')
|
||||
content: ' ' url('/matrix-hookshot/latest/icons/webhooks.png');
|
||||
}
|
||||
|
||||
.chapter li:nth-child(7) > a:nth-child(1) > strong:after {
|
||||
content: ' ' url('/matrix-hookshot/latest/icons/sentry.png');
|
||||
}
|
||||
|
||||
|
@ -6,19 +6,18 @@ Encryption support is <strong>HIGHLY EXPERIMENTAL AND SUBJECT TO CHANGE</strong>
|
||||
For more details, see <a href="https://github.com/matrix-org/matrix-hookshot/issues/594">issue 594</a>.
|
||||
</section>
|
||||
|
||||
Hookshot supports end-to-bridge encryption via [MSC3202](https://github.com/matrix-org/matrix-spec-proposals/pull/3202). As such, encryption requires hookshot to be connected to a homeserver that supports that MSC, such as [Synapse](#running-with-synapse).
|
||||
Hookshot supports end-to-bridge encryption via [MSC3202](https://github.com/matrix-org/matrix-spec-proposals/pull/3202). As such, encryption requires Hookshot to be connected to a homeserver that supports that MSC, such as [Synapse](#running-with-synapse).
|
||||
|
||||
## Enabling encryption in Hookshot
|
||||
|
||||
In order for hookshot to use encryption, it must be configured as follows:
|
||||
- The `experimentalEncryption.storagePath` setting must point to a directory that hookshot has permissions to write files into. If running with Docker, this path should be within a volume (for persistency). Hookshot uses this directory for its crypto store (i.e. long-lived state relating to its encryption keys).
|
||||
- By default, the crypto store uses an SQLite-based format. To use the legacy Sled-based format, set `experimentalEncryption.useLegacySledStore` to `true`, though this is not expected to be necessary and will be removed in a future release.
|
||||
- Once a crypto store of a particular type (SQLite or Sled) has been initialized, its files must not be modified, and hookshot cannot be configured to use another crypto store of the same type as one it has used before. If a crypto store's files get lost or corrupted, hookshot may fail to start up, or may be unable to decrypt command messages. To fix such issues, stop hookshot, then reset its crypto store by running `yarn start:resetcrypto`.
|
||||
In order for Hookshot to use encryption, it must be configured as follows:
|
||||
- The `experimentalEncryption.storagePath` setting must point to a directory that Hookshot has permissions to write files into. If running with Docker, this path should be within a volume (for persistency). Hookshot uses this directory for its crypto store (i.e. long-lived state relating to its encryption keys).
|
||||
- Once a crypto store has been initialized, its files must not be modified, and Hookshot cannot be configured to use another crypto store of the same type as one it has used before. If a crypto store's files get lost or corrupted, Hookshot may fail to start up, or may be unable to decrypt command messages. To fix such issues, stop Hookshot, then reset its crypto store by running `yarn start:resetcrypto`.
|
||||
- [Redis](./workers.md) must be enabled. Note that worker mode is not yet supported with encryption, so `queue.monolithic` must be set to `true`.
|
||||
|
||||
If you ever reset your homeserver's state, ensure you also reset hookshot's encryption state. This includes clearing the `experimentalEncryption.storagePath` directory and all worker state stored in your redis instance. Otherwise, hookshot may fail on start up with registration errors.
|
||||
If you ever reset your homeserver's state, ensure you also reset Hookshot's encryption state. This includes clearing the `experimentalEncryption.storagePath` directory and all worker state stored in your redis instance. Otherwise, Hookshot may fail on start up with registration errors.
|
||||
|
||||
Also ensure that hookshot's appservice registration file contains every line from `registration.sample.yml` that appears after the `If enabling encryption` comment. Note that changing the registration file may require restarting the homeserver that hookshot is connected to.
|
||||
Also ensure that Hookshot's appservice registration file contains every line from `registration.sample.yml` that appears after the `If enabling encryption` comment. Note that changing the registration file may require restarting the homeserver that Hookshot is connected to.
|
||||
|
||||
## Running with Synapse
|
||||
|
||||
|
@ -20,7 +20,7 @@ For example with this configuration:
|
||||
serviceBots:
|
||||
- localpart: feeds
|
||||
displayname: Feeds
|
||||
avatar: mxc://half-shot.uk/2876e89ccade4cb615e210c458e2a7a6883fe17d
|
||||
avatar: "./assets/feeds_avatar.png"
|
||||
prefix: "!feeds"
|
||||
service: feeds
|
||||
```
|
||||
|
@ -56,14 +56,14 @@ When `addOnInvite` is true, the bridge will add a widget to rooms when the bot i
|
||||
`disallowedIpRanges` describes which IP ranges should be disallowed when resolving homeserver IP addresses (for security reasons).
|
||||
Unless you know what you are doing, it is recommended to not include this key. The default blocked IPs are listed above for your convenience.
|
||||
|
||||
`publicUrl` should be set to the publicly reachable address for the widget `public` content. By default, hookshot hosts this content on the
|
||||
`publicUrl` should be set to the publicly reachable address for the widget `public` content. By default, Hookshot hosts this content on the
|
||||
`widgets` listener under `/widgetapi/v1/static`.
|
||||
|
||||
`branding` allows you to change the strings used for various bits of widget UI. At the moment you can:
|
||||
- Set `widgetTitle` to change the title of the widget that is created.
|
||||
|
||||
`openIdOverrides` allows you to configure the correct federation endpoints for a given set of Matrix server names. This is useful if you are
|
||||
testing/developing hookshot in a local dev environment. Production environments should not use this configuration (as their Matrix server name
|
||||
testing/developing Hookshot in a local dev environment. Production environments should not use this configuration (as their Matrix server name
|
||||
should be resolvable). The config takes a mapping of Matrix server name => base path for federation.
|
||||
E.g. if your server name was `my-local-server` and your federation was readable via http://localhost/_matrix/federation,
|
||||
you would put configure `my-local-server: "http://localhost"`.
|
||||
|
@ -9,12 +9,11 @@ This feature is <b>experimental</b> and should only be used when you are reachin
|
||||
|
||||
## Running in multi-process mode
|
||||
|
||||
You must first have a working redis instance somewhere which can talk between processes. For example, in Docker you can run:
|
||||
You must first have a working Redis instance somewhere which can talk between processes. For example, in Docker you can run:
|
||||
|
||||
`docker run --name github-bridge-redis -p 6379:6379 -d redis`.
|
||||
|
||||
|
||||
The processes should all share the same config, which should contain the correct config enable redis:
|
||||
The processes should all share the same config, which should contain the correct config to enable Redis:
|
||||
|
||||
```yaml
|
||||
queue:
|
||||
|
BIN
docs/icons/sentry.png
Normal file
After Width: | Height: | Size: 543 B |
@ -10,12 +10,14 @@ metrics:
|
||||
port: 9002
|
||||
```
|
||||
|
||||
Hookshot will then provide metrics on `127.0.0.1` at port `9002`.
|
||||
|
||||
An example dashboard that can be used with [Grafana](https://grafana.com) can be found at [/contrib/hookshot-dashboard.json](https://github.com/matrix-org/matrix-hookshot/blob/main/contrib/hookshot-dashboard.json).
|
||||
There are 3 variables at the top of the dashboard:
|
||||
|
||||

|
||||
|
||||
Select the Prometheus with your Hookshot metrics as Data Source. Set Interval to your scraping interval. Set 2x Interval to twice the Interval value ([why?](https://github.com/matrix-org/matrix-hookshot/pull/407#issuecomment-1186251618)).
|
||||
Select the Prometheus instance with your Hookshot metrics as Data Source. Set Interval to your scraping interval. Set 2x Interval to twice the Interval value ([why?](https://github.com/matrix-org/matrix-hookshot/pull/407#issuecomment-1186251618)).
|
||||
|
||||
Below is the generated list of Prometheus metrics for Hookshot.
|
||||
|
||||
@ -24,29 +26,29 @@ Below is the generated list of Prometheus metrics for Hookshot.
|
||||
| Metric | Help | Labels |
|
||||
|--------|------|--------|
|
||||
| hookshot_webhooks_http_request | Number of requests made to the hookshot webhooks handler | path, method |
|
||||
| hookshot_provisioning_http_request | Number of requests made to the hookshot webhooks handler | path, method |
|
||||
| hookshot_provisioning_http_request | Number of requests made to the hookshot provisioner handler | path, method |
|
||||
| hookshot_queue_event_pushes | Number of events pushed through the queue | event |
|
||||
| hookshot_connection_event_failed | The number of events that failed to process | event, connectionId |
|
||||
| hookshot_connections | The number of active hookshot connections | service |
|
||||
| hookshot_connection_event_failed | Number of events that failed to process | event, connectionId |
|
||||
| hookshot_connections | Number of active hookshot connections | service |
|
||||
| hookshot_notifications_push | Number of notifications pushed | service |
|
||||
| hookshot_notifications_service_up | Is the notification service up or down | service |
|
||||
| hookshot_notifications_service_up | Whether the notification service is up or down | service |
|
||||
| hookshot_notifications_watchers | Number of notifications watchers running | service |
|
||||
| hookshot_feeds_count | The number of RSS feeds that hookshot is subscribed to | |
|
||||
| hookshot_feeds_fetch_ms | The time taken for hookshot to fetch all feeds | |
|
||||
| hookshot_feeds_failing | The number of RSS feeds that hookshot is failing to read | reason |
|
||||
| hookshot_feeds_count | Number of RSS feeds that hookshot is subscribed to | |
|
||||
| hookshot_feeds_fetch_ms | Time taken for hookshot to fetch all feeds | |
|
||||
| hookshot_feeds_failing | Number of RSS feeds that hookshot is failing to read | reason |
|
||||
## matrix
|
||||
| Metric | Help | Labels |
|
||||
|--------|------|--------|
|
||||
| matrix_api_calls | The number of Matrix client API calls made | method |
|
||||
| matrix_api_calls_failed | The number of Matrix client API calls which failed | method |
|
||||
| matrix_appservice_events | The number of events sent over the AS API | |
|
||||
| matrix_appservice_decryption_failed | The number of events sent over the AS API that failed to decrypt | |
|
||||
| matrix_api_calls | Number of Matrix client API calls made | method |
|
||||
| matrix_api_calls_failed | Number of Matrix client API calls which failed | method |
|
||||
| matrix_appservice_events | Number of events sent over the AS API | |
|
||||
| matrix_appservice_decryption_failed | Number of events sent over the AS API that failed to decrypt | |
|
||||
## feed
|
||||
| Metric | Help | Labels |
|
||||
|--------|------|--------|
|
||||
| feed_count | (Deprecated) The number of RSS feeds that hookshot is subscribed to | |
|
||||
| feed_fetch_ms | (Deprecated) The time taken for hookshot to fetch all feeds | |
|
||||
| feed_failing | (Deprecated) The number of RSS feeds that hookshot is failing to read | reason |
|
||||
| feed_count | (Deprecated) Number of RSS feeds that hookshot is subscribed to | |
|
||||
| feed_fetch_ms | (Deprecated) Time taken for hookshot to fetch all feeds | |
|
||||
| feed_failing | (Deprecated) Number of RSS feeds that hookshot is failing to read | reason |
|
||||
## process
|
||||
| Metric | Help | Labels |
|
||||
|--------|------|--------|
|
||||
@ -70,6 +72,8 @@ Below is the generated list of Prometheus metrics for Hookshot.
|
||||
| nodejs_eventloop_lag_p50_seconds | The 50th percentile of the recorded event loop delays. | |
|
||||
| nodejs_eventloop_lag_p90_seconds | The 90th percentile of the recorded event loop delays. | |
|
||||
| nodejs_eventloop_lag_p99_seconds | The 99th percentile of the recorded event loop delays. | |
|
||||
| nodejs_active_resources | Number of active resources that are currently keeping the event loop alive, grouped by async resource type. | type |
|
||||
| nodejs_active_resources_total | Total number of active resources. | |
|
||||
| nodejs_active_handles | Number of active libuv handles grouped by handle type. Every handle type is C++ class name. | type |
|
||||
| nodejs_active_handles_total | Total number of active handles. | |
|
||||
| nodejs_active_requests | Number of active libuv requests grouped by request type. Every request type is C++ class name. | type |
|
||||
|
14
docs/sentry.md
Normal file
@ -0,0 +1,14 @@
|
||||
Sentry
|
||||
======
|
||||
|
||||
Hookshot supports [Sentry](https://sentry.io/welcome/) error reporting.
|
||||
|
||||
You can configure Sentry by adding the following to your config:
|
||||
|
||||
```yaml
|
||||
sentry:
|
||||
dsn: https://examplePublicKey@o0.ingest.sentry.io/0 # The DSN for your Sentry project.
|
||||
environment: production # The environment sentry is being used in. Can be omitted.
|
||||
```
|
||||
|
||||
Sentry will automatically include the name of your homeserver as the `serverName` reported.
|
@ -5,13 +5,12 @@ This page explains how to set up Hookshot for use with a Matrix homeserver.
|
||||
|
||||
## Requirements
|
||||
|
||||
Hookshot is fairly light on resources, and can run in as low as 100MB or so of memory.
|
||||
Hookshot is fairly light on resources, and can run in as low as 100 MB or so of memory.
|
||||
Hookshot memory requirements may increase depending on the traffic and the number of rooms bridged.
|
||||
|
||||
You **must** have administrative access to an existing homeserver in order to set up Hookshot, as
|
||||
Hookshot requires the homeserver to be configured with its appservice registration.
|
||||
|
||||
|
||||
## Local installation
|
||||
|
||||
This bridge requires at least Node 16 and Rust installed.
|
||||
@ -47,6 +46,11 @@ docker run \
|
||||
|
||||
Where `/etc/matrix-hookshot` would contain the configuration files `config.yml` and `registration.yml`. The `passKey` file should also be stored alongside these files. In your config, you should use the path `/data/passkey.pem`.
|
||||
|
||||
## Installation via Helm
|
||||
|
||||
There's now a basic chart defined in [helm/hookshot](/helm/hookshot/) that can be used to deploy the Hookshot Docker container in a Kubernetes-native way.
|
||||
|
||||
More information on this method is available [here](https://github.com/matrix-org/matrix-hookshot/helm/hookshot/README.md)
|
||||
|
||||
## Configuration
|
||||
|
||||
@ -56,16 +60,18 @@ Copy the `config.sample.yml` to a new file `config.yml`. The sample config is al
|
||||
You should read and fill this in as the bridge will not start without a complete config.
|
||||
|
||||
You may validate your config without starting the service by running `yarn validate-config`.
|
||||
For Docker you can run `docker run --rm -v /absolute-path-to/config.yml:/config.yml halfshot/matrix-hookshot node Config/Config.js /config.yml`
|
||||
For Docker you can run `docker run --rm -v /absolute-path-to/config.yml:/config.yml halfshot/matrix-hookshot node config/Config.js /config.yml`
|
||||
|
||||
Copy `registration.sample.yml` into `registration.yml` and fill in:
|
||||
- At a minimum, you will need to replace the `as_token` and `hs_token` and change the domain part of the namespaces. The sample config can be also found at our [github repo](https://raw.githubusercontent.com/matrix-org/matrix-hookshot/main/registration.sample.yml) for your convienence.
|
||||
|
||||
At a minimum, you will need to replace the `as_token` and `hs_token` and change the domain part of the namespaces. The sample config can be also found at our [github repo](https://raw.githubusercontent.com/matrix-org/matrix-hookshot/main/registration.sample.yml) for your convienence.
|
||||
|
||||
You will need to link the registration file to the homeserver. Consult your homeserver documentation
|
||||
on how to add appservices. [Synapse documents the process here](https://matrix-org.github.io/synapse/latest/application_services.html).
|
||||
|
||||
### Homeserver Configuration
|
||||
|
||||
In addition to providing the registration file above, you also need to tell Hookshot how to reach the homeserver which is hosting it. For clarity, hookshot expects to be able to connect to an existing homeserver which has the Hookshot registration file configured.
|
||||
In addition to providing the registration file above, you also need to tell Hookshot how to reach the homeserver which is hosting it. For clarity, Hookshot expects to be able to connect to an existing homeserver which has the Hookshot registration file configured.
|
||||
|
||||
```yaml
|
||||
bridge:
|
||||
@ -76,10 +82,9 @@ bridge:
|
||||
bindAddress: 127.0.0.1 # The address which Hookshot will bind to. Docker users should set this to `0.0.0.0`.
|
||||
```
|
||||
|
||||
The `port` and `bindAddress` must not conflict with the other listeners in the bridge config. This listeners should **not** be reachable
|
||||
The `port` and `bindAddress` must not conflict with the other listeners in the bridge config. This listener should **not** be reachable
|
||||
over the internet to users, as it's intended to be used by the homeserver exclusively. This service listens on `/_matrix/app/`.
|
||||
|
||||
|
||||
### Permissions
|
||||
|
||||
The bridge supports fine grained permission control over what services a user can access.
|
||||
@ -94,6 +99,7 @@ permissions:
|
||||
```
|
||||
|
||||
You must configure a set of "actors" with access to services. An `actor` can be:
|
||||
|
||||
- A MxID (also known as a User ID) e.g. `"@Half-Shot:half-shot.uk"`
|
||||
- A homeserver domain e.g. `matrix.org`
|
||||
- A roomId. This will allow any member of this room to complete actions. e.g. `"!TlZdPIYrhwNvXlBiEk:half-shot.uk"`
|
||||
@ -101,7 +107,8 @@ You must configure a set of "actors" with access to services. An `actor` can be:
|
||||
|
||||
MxIDs. room IDs and `*` **must** be wrapped in quotes.
|
||||
|
||||
Each permission set can have a services. The `service` field can be:
|
||||
Each permission set can have a service. The `service` field can be:
|
||||
|
||||
- `github`
|
||||
- `gitlab`
|
||||
- `jira`
|
||||
@ -111,13 +118,14 @@ Each permission set can have a services. The `service` field can be:
|
||||
- `*`, for any service.
|
||||
|
||||
The `level` can be:
|
||||
- `commands` Can run commands within connected rooms, but NOT log in to the bridge.
|
||||
- `login` All the above, and can also log in to the bridge.
|
||||
- `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).
|
||||
- `admin` All permissions. This allows you to perform administrative tasks like deleting connections from all rooms.
|
||||
|
||||
When permissions are checked, if a user matches any of the permission set and one
|
||||
- `commands` Can run commands within connected rooms, but NOT log in to the bridge.
|
||||
- `login` All the above, and can also log in to the bridge.
|
||||
- `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).
|
||||
- `admin` All permissions. This allows you to perform administrative tasks like deleting connections from all rooms.
|
||||
|
||||
When permissions are checked, if a user matches any of the permissions set and one
|
||||
of those grants the right level for a service, they are allowed access. If none of the
|
||||
definitions match, they are denied.
|
||||
|
||||
@ -186,9 +194,21 @@ At a minimum, you should bind the `webhooks` resource to a port and address. You
|
||||
port, or one on each. Each listener MUST listen on a unique port.
|
||||
|
||||
You will also need to make this port accessible to the internet so services like GitHub can reach the bridge. It
|
||||
is recommended to factor hookshot into your load balancer configuration, but currently this process is left as an
|
||||
is recommended to factor Hookshot into your load balancer configuration, but currently this process is left as an
|
||||
exercise to the user.
|
||||
|
||||
However, if you use Nginx, have a look at this example:
|
||||
|
||||
```
|
||||
location ~ ^/widgetapi(.*)$ {
|
||||
set $backend "127.0.0.1:9002";
|
||||
proxy_pass http://$backend/widgetapi$1$is_args$args;
|
||||
}
|
||||
```
|
||||
|
||||
This will pass all requests at `/widgetapi` to Hookshot.
|
||||
|
||||
|
||||
In terms of API endpoints:
|
||||
|
||||
- The `webhooks` resource handles resources under `/`, so it should be on its own listener.
|
||||
@ -202,7 +222,6 @@ Please note that the appservice HTTP listener is configured <strong>separately</
|
||||
in the upstream library. See <a href="https://github.com/turt2live/matrix-bot-sdk/issues/191">this issue</a> for details.
|
||||
</section>
|
||||
|
||||
|
||||
### Services configuration
|
||||
|
||||
You will need to configure some services. Each service has its own documentation file inside the setup subdirectory.
|
||||
@ -230,7 +249,6 @@ logging:
|
||||
timestampFormat: HH:mm:ss:SSS
|
||||
```
|
||||
|
||||
|
||||
#### JSON Logging
|
||||
|
||||
Enabling the `json` option will configure hookshot to output structured JSON logs. The schema looks like:
|
||||
@ -259,4 +277,4 @@ Enabling the `json` option will configure hookshot to output structured JSON log
|
||||
"retrying in 5s"
|
||||
]
|
||||
}
|
||||
```
|
||||
```
|
||||
|
@ -19,40 +19,44 @@ Each feed will only be checked once, regardless of the number of rooms to which
|
||||
|
||||
No entries will be bridged upon the “initial sync” -- all entries that exist at the moment of setup will be considered to be already seen.
|
||||
|
||||
Please note that Hookshot **must** be configured with Redis to retain seen entries between restarts. By default, Hookshot will
|
||||
run an "initial sync" on each startup and will not process any entries from feeds from before the first sync.
|
||||
|
||||
## Usage
|
||||
|
||||
### Adding new feeds
|
||||
|
||||
To add a feed to your room:
|
||||
|
||||
- Invite the bot user to the room.
|
||||
- Make sure the bot able to send state events (usually the Moderator power level in clients)
|
||||
- Say `!hookshot feed <URL>` where `<URL>` links to an RSS/Atom feed you want to subscribe to.
|
||||
- Invite the bot user to the room.
|
||||
- Make sure the bot able to send state events (usually the Moderator power level in clients)
|
||||
- Say `!hookshot feed <URL>` where `<URL>` links to an RSS/Atom feed you want to subscribe to.
|
||||
|
||||
### Listing feeds
|
||||
|
||||
You can list all feeds that a room you're in is currently subscribed to with `!hookshot feed list`.
|
||||
It requires no special permissions from the user issuing the command.
|
||||
It requires no special permissions from the user issuing the command. Optionally you can format the list as `json` or
|
||||
`yaml` with `!hookshot feed list <format>`.
|
||||
|
||||
### Removing feeds
|
||||
|
||||
To remove a feed from a room, say `!hookshot feed remove <URL>`, with the URL specifying which feed you want to unsubscribe from.
|
||||
|
||||
|
||||
### Feed templates
|
||||
|
||||
You can optionally give a feed a specific template to use when sending a message into a room. A template
|
||||
may include any of the following tokens:
|
||||
|
||||
|Token |Description |
|
||||
|----------|--------------------------------------------|
|
||||
|$FEEDNAME | Either the label, title or url of the feed.|
|
||||
|$FEEDURL | The URL of the feed. |
|
||||
|$FEEDTITLE| The title of the feed. |
|
||||
|$TITLE | The title of the feed entry. |
|
||||
|$LINK | The link of the feed entry. |
|
||||
|$AUTHOR | The author of the feed entry. |
|
||||
|$DATE | The publish date (`pubDate`) of the entry. |
|
||||
|$SUMMARY | The summary of the entry. |
|
||||
| Token | Description |
|
||||
| ---------- | ---------------------------------------------------------- |
|
||||
| $FEEDNAME | Either the label, title or url of the feed. |
|
||||
| $FEEDURL | The URL of the feed. |
|
||||
| $FEEDTITLE | The title of the feed. |
|
||||
| $TITLE | The title of the feed entry. |
|
||||
| $URL | The URL of the feed entry. |
|
||||
| $LINK | The link of the feed entry. Formatted as `[$TITLE]($URL)`. |
|
||||
| $AUTHOR | The author of the feed entry. |
|
||||
| $DATE | The publish date (`pubDate`) of the entry. |
|
||||
| $SUMMARY | The summary of the entry. |
|
||||
|
||||
If not specified, the default template is `New post in $FEEDNAME: $LINK`.
|
||||
|
@ -75,6 +75,7 @@ If the body *also* contains a `username` key, then the message will be prepended
|
||||
|
||||
If the body does NOT contain a `text` field, the full payload will be sent to the room. This can be adapted into a message by creating a **JavaScript transformation function**.
|
||||
|
||||
|
||||
### Payload formats
|
||||
|
||||
If the request is a `POST`/`PUT`, the body of the request will be decoded and stored inside the event. Currently, Hookshot supports:
|
||||
@ -101,6 +102,15 @@ to a string representation of that value. This change is <strong>not applied</st
|
||||
variable, so it will contain proper float values.
|
||||
</section>
|
||||
|
||||
### Wait for complete
|
||||
|
||||
It is possible to choose whether a webhook response should be instant, or after hookshot has handled the message. The reason
|
||||
for this is that some services expect a quick response time (like Slack) whereas others will wait for the request to complete. You
|
||||
can specify this either globally in your config, or on the widget with `waitForComplete`.
|
||||
|
||||
If you make use of the `webhookResponse` feature, you will need to enable `waitForComplete` as otherwise hookshot will
|
||||
immeditately respond with it's default response values.
|
||||
|
||||
## JavaScript Transformations
|
||||
|
||||
<section class="notice">
|
||||
@ -142,6 +152,11 @@ The `v2` api expects an object to be returned from the `result` variable.
|
||||
"plain": "Some text", // The plaintext value to be used for the Matrix message.
|
||||
"html": "<b>Some</b> text", // The HTML value to be used for the Matrix message. If not provided, plain will be interpreted as markdown.
|
||||
"msgtype": "some.type", // The message type, such as m.notice or m.text, to be used for the Matrix message. If not provided, m.notice will be used.
|
||||
"webhookResponse": { // Optional response to send to the webhook requestor. All fields are optional. Defaults listed.
|
||||
"body": "{ \"ok\": true }",
|
||||
"contentType": "application/json",
|
||||
"statusCode": 200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -20,8 +20,9 @@ To authenticate with a personal access token:
|
||||
1. Click **Generate new token**
|
||||
1. Give it a good name, and a sensible expiration date. For scopes you will need:
|
||||
- Repo (to access repo information)
|
||||
- public_repo
|
||||
- repo:status
|
||||
- If you want notifications for private repos, you need `repo: Full control of private repositories`. If you just want notifications for public repos, you only need:
|
||||
- repo:status
|
||||
- public_repo
|
||||
- Workflow (if you want to be able to launch workflows / GitHub actions from Matrix)
|
||||
- Notifications (if you want to bridge in your notifications to Matrix)
|
||||
- User
|
||||
|
@ -48,6 +48,7 @@ the events marked as default below will be enabled. Otherwise, this is ignored.
|
||||
- merge_request.open *
|
||||
- merge_request.review.comments *
|
||||
- merge_request.review *
|
||||
- merge_request.review.individual
|
||||
- push *
|
||||
- release *
|
||||
- release.created *
|
||||
|
2
helm/cr.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
release-name-template: "helm-{{ .Name }}-{{ .Version }}"
|
6
helm/ct.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
remote: origin
|
||||
target-branch: main
|
||||
chart-repos: []
|
||||
chart-dirs:
|
||||
- helm
|
||||
validate-maintainers: false
|
1
helm/hookshot/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.tgz
|
24
helm/hookshot/.helmignore
Normal file
@ -0,0 +1,24 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
*.tgz
|
7
helm/hookshot/.yamllint
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
extends: default
|
||||
rules:
|
||||
line-length:
|
||||
level: warning
|
||||
max: 120
|
||||
braces: disable
|
22
helm/hookshot/Chart.yaml
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
apiVersion: v2
|
||||
name: hookshot
|
||||
description: Deploy a Matrix Hookshot instance to Kubernetes
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.1.15
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "0.0.0-replaced-by-ci"
|
122
helm/hookshot/README.md
Normal file
@ -0,0 +1,122 @@
|
||||
# hookshot
|
||||
|
||||
  
|
||||
Deploy a Matrix Hookshot instance to Kubernetes
|
||||
|
||||
Status: Beta
|
||||
|
||||
## About
|
||||
|
||||
This chart creates a basic Hookshot deployment inside Kubernetes.
|
||||
|
||||
# Installation
|
||||
|
||||
You'll need to have the Helm repository added to your local environment:
|
||||
|
||||
``` bash
|
||||
helm repo add hookshot https://matrix-org.github.io/matrix-hookshot
|
||||
helm repo update
|
||||
```
|
||||
|
||||
Which should allow you to see the Hookshot chart in the repo:
|
||||
|
||||
``` bash
|
||||
helm search repo hookshot
|
||||
|
||||
NAME CHART VERSION APP VERSION DESCRIPTION
|
||||
matrix-org/hookshot 0.1.13 1.16.0 A Helm chart for Kubernetes
|
||||
```
|
||||
|
||||
Before you can install, however, you'll need to make sure to configure Hookshot properly.
|
||||
|
||||
# Configuration
|
||||
|
||||
You'll need to create a `values.yaml` for your deployment of this chart. You can use the [included defaults](./values.yaml) as a starting point.
|
||||
|
||||
## Helm Values
|
||||
|
||||
To configure Hookshot-specific parameters, the value `.Values.hookshot.config` accepts an arbitrary YAML map as configuration. This gets templated into the container by [templates/configmap.yaml](./templates/configmap.yaml) - thus anything you can set in the [Example Configuration](https://matrix-org.github.io/matrix-hookshot/latest/setup/sample-configuration.html) can be set here.
|
||||
|
||||
## Existing configuration
|
||||
|
||||
If you have an existing configuration file for Hookshot, you can create a configmap like so:
|
||||
|
||||
``` bash
|
||||
kubectl create --namespace "your hookshot namespace" configmap hookshot-custom-config --from-file=config.yml --from-file=registration.yml --from-file=passkey.pem
|
||||
```
|
||||
|
||||
Note that the filenames must remain as listed based on the templating done in [templates/configmap.yaml](./templates/configmap.yaml)
|
||||
|
||||
Once created, you can set `.Values.hookshot.existingConfigMap` to `custom-hookshot-config` (or whichever name you chose for your secret) and set `.Values.hookshot.config` to `{}` or null to prevent confusion with the default parameters.
|
||||
|
||||
# Installation
|
||||
|
||||
Once you have your `values.yaml` file ready you can install the chart like this:
|
||||
|
||||
``` bash
|
||||
helm install hookshot --create-namespace --namespace hookshot matrix-org/hookshot -f values.yaml
|
||||
```
|
||||
|
||||
And upgrades can be done via:
|
||||
|
||||
``` bash
|
||||
helm upgrade hookshot --namespace hookshot matrix-org/hookshot -f values.yaml
|
||||
```
|
||||
|
||||
# External access
|
||||
|
||||
You'll need to configure your Ingress connectivity according to your environment. This chart should be compatible with most Ingress controllers and has been tested successfully with [ingress-nginx](https://github.com/kubernetes/ingress-nginx) and EKS ALB. You should also ensure that you have a way to provision certificates i.e. [cert-manager](https://cert-manager.io/) as HTTPS is required for appservice traffic.
|
||||
|
||||
## Values
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| affinity | object | `{}` | Affinity settings for deployment |
|
||||
| autoscaling.enabled | bool | `false` | |
|
||||
| fullnameOverride | string | `""` | Full name override for helm chart |
|
||||
| hookshot.config | object | `{"bridge":{"bindAddress":"0.0.0.0","domain":"example.com","port":9002,"url":"https://example.com"},"generic":{"allowJsTransformationFunctions":true,"enableHttpGet":false,"enabled":true,"urlPrefix":"https://example.com/","userIdPrefix":"_webhooks_","waitForComplete":false},"listeners":[{"bindAddress":"0.0.0.0","port":9000,"resources":["webhooks","widgets"]},{"bindAddress":"0.0.0.0","port":9001,"resources":["metrics"]}],"logging":{"colorize":false,"json":false,"level":"info","timestampFormat":"HH:mm:ss:SSS"},"metrics":{"enabled":true},"passFile":"/data/passkey.pem","widgets":{"addToAdminRooms":false,"branding":{"widgetTitle":"Hookshot Configuration"},"publicUrl":"https://webhook-hookshot.example.com/widgetapi/v1/static","roomSetupWidget":{"addOnInvite":false},"setRoomName":false}}` | Raw Hookshot configuration. Gets templated into a YAML file and then loaded unless an existingConfigMap is specified. |
|
||||
| hookshot.existingConfigMap | string | `nil` | Name of existing ConfigMap with valid Hookshot configuration |
|
||||
| hookshot.passkey | string | `""` | |
|
||||
| hookshot.registration.as_token | string | `""` | |
|
||||
| hookshot.registration.hs_token | string | `""` | |
|
||||
| hookshot.registration.id | string | `"matrix-hookshot"` | |
|
||||
| hookshot.registration.namespaces.rooms | list | `[]` | |
|
||||
| hookshot.registration.namespaces.users | list | `[]` | |
|
||||
| hookshot.registration.rate_limited | bool | `false` | |
|
||||
| hookshot.registration.sender_localpart | string | `"hookshot"` | |
|
||||
| hookshot.registration.url | string | `"http://example.com"` | |
|
||||
| image.pullPolicy | string | `"IfNotPresent"` | Pull policy for Hookshot image |
|
||||
| image.repository | string | `"halfshot/matrix-hookshot"` | Repository to pull hookshot image from |
|
||||
| image.tag | string | `nil` | Image tag to pull. Defaults to chart's appVersion value as set in Chart.yaml |
|
||||
| imagePullSecrets | list | `[]` | List of names of k8s secrets to be used as ImagePullSecrets for the pod |
|
||||
| ingress.appservice.annotations | object | `{}` | Annotations for appservice ingress |
|
||||
| ingress.appservice.className | string | `""` | Ingress class name for appservice ingress |
|
||||
| ingress.appservice.enabled | bool | `false` | Enable ingress for appservice |
|
||||
| ingress.appservice.hosts | list | `[]` | Host configuration for appservice ingress |
|
||||
| ingress.appservice.tls | list | `[]` | TLS configuration for appservice ingress |
|
||||
| ingress.webhook.annotations | object | `{}` | Annotations for webhook ingress |
|
||||
| ingress.webhook.className | string | `""` | Ingress class name for webhook ingress |
|
||||
| ingress.webhook.enabled | bool | `false` | Enable ingress for webhook |
|
||||
| ingress.webhook.hosts | list | `[]` | Host configuration for webhook ingress |
|
||||
| ingress.webhook.tls | list | `[]` | TLS configuration for webhook ingress |
|
||||
| nameOverride | string | `""` | Name override for helm chart |
|
||||
| nodeSelector | object | `{}` | Node selector parameters |
|
||||
| podAnnotations | object | `{}` | Extra annotations for Hookshot pod |
|
||||
| podSecurityContext | object | `{}` | Pod security context settings |
|
||||
| replicaCount | int | `1` | Number of replicas to deploy. Consequences of using multiple Hookshot replicas currently unknown. |
|
||||
| resources | object | `{}` | Pod resource requests / limits |
|
||||
| securityContext | object | `{}` | Security context settings |
|
||||
| service.annotations | object | `{}` | Extra annotations for service |
|
||||
| service.appservice.port | int | `9002` | Appservice port as configured in container |
|
||||
| service.labels | object | `{}` | Extra labels for service |
|
||||
| service.metrics.port | int | `9001` | Metrics port as configured in container |
|
||||
| service.port | int | `80` | Port for Hookshot service |
|
||||
| service.type | string | `"ClusterIP"` | Service type for Hookshot service |
|
||||
| service.webhook.port | int | `9000` | Webhook port as configured in container |
|
||||
| serviceAccount.annotations | object | `{}` | Annotations to add to the service account |
|
||||
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created |
|
||||
| serviceAccount.name | string | `""` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template |
|
||||
| tolerations | list | `[]` | Tolerations for deployment |
|
||||
|
||||
----------------------------------------------
|
||||
Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0)
|
74
helm/hookshot/README.md.gotmpl
Normal file
@ -0,0 +1,74 @@
|
||||
{{ template "chart.header" . }}
|
||||
{{ template "chart.deprecationWarning" . }}
|
||||
{{ template "chart.badgesSection" . }}
|
||||
{{ template "chart.description" . }}
|
||||
|
||||
Status: Beta
|
||||
|
||||
## About
|
||||
|
||||
This chart creates a basic Hookshot deployment inside Kubernetes.
|
||||
|
||||
# Installation
|
||||
|
||||
You'll need to have the Helm repository added to your local environment:
|
||||
|
||||
``` bash
|
||||
helm repo add hookshot https://matrix-org.github.io/matrix-hookshot
|
||||
helm repo update
|
||||
```
|
||||
|
||||
Which should allow you to see the Hookshot chart in the repo:
|
||||
|
||||
``` bash
|
||||
helm search repo hookshot
|
||||
|
||||
NAME CHART VERSION APP VERSION DESCRIPTION
|
||||
matrix-org/hookshot 0.1.13 1.16.0 A Helm chart for Kubernetes
|
||||
```
|
||||
|
||||
Before you can install, however, you'll need to make sure to configure Hookshot properly.
|
||||
|
||||
# Configuration
|
||||
|
||||
You'll need to create a `values.yaml` for your deployment of this chart. You can use the [included defaults](./values.yaml) as a starting point.
|
||||
|
||||
## Helm Values
|
||||
|
||||
To configure Hookshot-specific parameters, the value `.Values.hookshot.config` accepts an arbitrary YAML map as configuration. This gets templated into the container by [templates/configmap.yaml](./templates/configmap.yaml) - thus anything you can set in the [Example Configuration](https://matrix-org.github.io/matrix-hookshot/latest/setup/sample-configuration.html) can be set here.
|
||||
|
||||
## Existing configuration
|
||||
|
||||
If you have an existing configuration file for hookshot, you can create a configmap like so:
|
||||
|
||||
``` bash
|
||||
kubectl create --namespace "your hookshot namespace" configmap hookshot-custom-config --from-file=config.yml --from-file=registration.yml --from-file=passkey.pem
|
||||
```
|
||||
|
||||
Note that the filenames must remain as listed based on the templating done in [templates/configmap.yaml](./templates/configmap.yaml)
|
||||
|
||||
Once created, you can set `.Values.hookshot.existingConfigMap` to `custom-hookshot-config` (or whichever name you chose for your secret) and set `.Values.hookshot.config` to `{}` or null to prevent confusion with the default parameters.
|
||||
|
||||
# Installation
|
||||
|
||||
Once you have your `values.yaml` file ready you can install the chart like this:
|
||||
|
||||
``` bash
|
||||
helm install hookshot --create-namespace --namespace hookshot matrix-org/hookshot -f values.yaml
|
||||
```
|
||||
|
||||
And upgrades can be done via:
|
||||
|
||||
``` bash
|
||||
helm upgrade hookshot --namespace hookshot matrix-org/hookshot -f values.yaml
|
||||
```
|
||||
|
||||
# External access
|
||||
|
||||
You'll need to configure your Ingress connectivity according to your environment. This chart should be compatible with most Ingress controllers and has been tested successfully with [ingress-nginx](https://github.com/kubernetes/ingress-nginx) and EKS ALB. You should also ensure that you have a way to provision certificates i.e. [cert-manager](https://cert-manager.io/) as HTTPS is required for appservice traffic.
|
||||
|
||||
{{ template "chart.maintainersSection" . }}
|
||||
{{ template "chart.sourcesSection" . }}
|
||||
{{ template "chart.requirementsSection" . }}
|
||||
{{ template "chart.valuesSection" . }}
|
||||
{{ template "helm-docs.versionFooter" . }}
|
22
helm/hookshot/templates/NOTES.txt
Normal file
@ -0,0 +1,22 @@
|
||||
1. Get the application URL by running these commands:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range $host := .Values.ingress.hosts }}
|
||||
{{- range .paths }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "hookshot.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "hookshot.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "hookshot.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "hookshot.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||
{{- end }}
|
85
helm/hookshot/templates/_helpers.tpl
Normal file
@ -0,0 +1,85 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "hookshot.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "hookshot.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Helper for configmap name
|
||||
*/}}
|
||||
{{- define "hookshot.configMapName" -}}
|
||||
{{- if .Values.hookshot.existingConfigMap }}
|
||||
{{- printf "%s" .Values.hookshot.existingConfigMap -}}
|
||||
{{- else }}
|
||||
{{- printf "%s-config" (include "hookshot.fullname" .) | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "hookshot.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "hookshot.labels" -}}
|
||||
helm.sh/chart: {{ include "hookshot.chart" . }}
|
||||
{{ include "hookshot.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "hookshot.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "hookshot.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "hookshot.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "hookshot.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Allow the release namespace to be overridden for multi-namespace deployments in combined charts
|
||||
*/}}
|
||||
{{- define "hookshot.namespace" -}}
|
||||
{{- if .Values.namespaceOverride -}}
|
||||
{{- .Values.namespaceOverride -}}
|
||||
{{- else -}}
|
||||
{{- .Release.Namespace -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
160
helm/hookshot/templates/_pod.tpl
Normal file
@ -0,0 +1,160 @@
|
||||
{{- define "hookshot.pod" -}}
|
||||
{{- if .Values.schedulerName }}
|
||||
schedulerName: "{{ .Values.schedulerName }}"
|
||||
{{- end }}
|
||||
serviceAccountName: {{ template "hookshot.serviceAccountName" . }}
|
||||
automountServiceAccountToken: {{ .Values.serviceAccount.autoMount }}
|
||||
{{- if .Values.securityContext }}
|
||||
securityContext:
|
||||
{{ toYaml .Values.securityContext | indent 2 }}
|
||||
{{- end }}
|
||||
{{- if .Values.hostAliases }}
|
||||
hostAliases:
|
||||
{{ toYaml .Values.hostAliases | indent 2 }}
|
||||
{{- end }}
|
||||
{{- if .Values.priorityClassName }}
|
||||
priorityClassName: {{ .Values.priorityClassName }}
|
||||
{{- end }}
|
||||
initContainers:
|
||||
|
||||
{{- if .Values.image.pullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- $root := . }}
|
||||
{{- range .Values.image.pullSecrets }}
|
||||
- name: {{ tpl . $root }}
|
||||
{{- end}}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
{{- if .Values.image.sha }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}@sha256:{{ .Values.image.sha }}"
|
||||
{{- else }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
{{- end }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
{{- if .Values.command }}
|
||||
command:
|
||||
{{- range .Values.command }}
|
||||
- {{ . }}
|
||||
{{- end }}
|
||||
{{- end}}
|
||||
{{- if .Values.containerSecurityContext }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.containerSecurityContext | nindent 6 }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
{{- if or (and (not .Values.hookshot.existingConfigMap) (.Values.hookshot.config)) (.Values.hookshot.existingConfigMap) }}
|
||||
- name: config
|
||||
mountPath: "/data"
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: webhook
|
||||
containerPort: 9000
|
||||
protocol: TCP
|
||||
- name: metrics
|
||||
containerPort: 9001
|
||||
protocol: TCP
|
||||
- name: appservice
|
||||
containerPort: 9002
|
||||
protocol: TCP
|
||||
env:
|
||||
|
||||
envFrom:
|
||||
{{- if .Values.envFromSecret }}
|
||||
- secretRef:
|
||||
name: {{ tpl .Values.envFromSecret . }}
|
||||
{{- end }}
|
||||
{{- if .Values.envRenderSecret }}
|
||||
- secretRef:
|
||||
name: {{ template "hookshot.fullname" . }}-env
|
||||
{{- end }}
|
||||
{{- range .Values.envFromSecrets }}
|
||||
- secretRef:
|
||||
name: {{ tpl .name $ }}
|
||||
optional: {{ .optional | default false }}
|
||||
{{- end }}
|
||||
{{- range .Values.envFromConfigMaps }}
|
||||
- configMapRef:
|
||||
name: {{ tpl .name $ }}
|
||||
optional: {{ .optional | default false }}
|
||||
{{- end }}
|
||||
livenessProbe:
|
||||
{{ toYaml .Values.livenessProbe | indent 6 }}
|
||||
readinessProbe:
|
||||
{{ toYaml .Values.readinessProbe | indent 6 }}
|
||||
{{- if .Values.lifecycleHooks }}
|
||||
lifecycle: {{ tpl (.Values.lifecycleHooks | toYaml) . | nindent 6 }}
|
||||
{{- end }}
|
||||
resources:
|
||||
{{ toYaml .Values.resources | indent 6 }}
|
||||
{{- with .Values.extraContainers }}
|
||||
{{ tpl . $ | indent 2 }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{ toYaml . | indent 2 }}
|
||||
{{- end }}
|
||||
{{- $root := . }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{ tpl (toYaml .) $root | indent 2 }}
|
||||
{{- end }}
|
||||
{{- with .Values.topologySpreadConstraints }}
|
||||
topologySpreadConstraints:
|
||||
{{ toYaml . | indent 2 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{ toYaml . | indent 2 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: {{ template "hookshot.configMapName" . }}
|
||||
{{- $root := . }}
|
||||
{{- range .Values.extraConfigmapMounts }}
|
||||
- name: {{ tpl .name $root }}
|
||||
configMap:
|
||||
name: {{ tpl .configMap $root }}
|
||||
{{- if .items }}
|
||||
items: {{ toYaml .items | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- range .Values.extraSecretMounts }}
|
||||
{{- if .secretName }}
|
||||
- name: {{ .name }}
|
||||
secret:
|
||||
secretName: {{ .secretName }}
|
||||
defaultMode: {{ .defaultMode }}
|
||||
{{- if .items }}
|
||||
items: {{ toYaml .items | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- else if .projected }}
|
||||
- name: {{ .name }}
|
||||
projected: {{- toYaml .projected | nindent 6 }}
|
||||
{{- else if .csi }}
|
||||
- name: {{ .name }}
|
||||
csi: {{- toYaml .csi | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range .Values.extraVolumeMounts }}
|
||||
- name: {{ .name }}
|
||||
{{- if .existingClaim }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .existingClaim }}
|
||||
{{- else if .hostPath }}
|
||||
hostPath:
|
||||
path: {{ .hostPath }}
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range .Values.extraEmptyDirMounts }}
|
||||
- name: {{ .name }}
|
||||
emptyDir: {}
|
||||
{{- end -}}
|
||||
{{- if .Values.extraContainerVolumes }}
|
||||
{{ tpl (toYaml .Values.extraContainerVolumes) . | indent 2 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
21
helm/hookshot/templates/configmap.yaml
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
{{- if not .Values.hookshot.existingConfigMap }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ template "hookshot.configMapName" . }}
|
||||
namespace: {{ template "hookshot.namespace" . }}
|
||||
labels:
|
||||
{{- include "hookshot.labels" . | nindent 4 }}
|
||||
{{- with .Values.annotations }}
|
||||
annotations:
|
||||
{{ toYaml . | indent 4 }}
|
||||
{{- end }}
|
||||
data:
|
||||
config.yml: |
|
||||
{{ toYaml .Values.hookshot.config | indent 4 }}
|
||||
registration.yml: |
|
||||
{{ toYaml .Values.hookshot.registration | indent 4 }}
|
||||
passkey.pem: |
|
||||
{{ .Values.hookshot.passkey | indent 4 }}
|
||||
{{- end }}
|
24
helm/hookshot/templates/deployment.yaml
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "hookshot.fullname" . }}
|
||||
labels:
|
||||
{{- include "hookshot.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "hookshot.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "hookshot.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- include "hookshot.pod" . | nindent 6 }}
|
29
helm/hookshot/templates/hpa.yaml
Normal file
@ -0,0 +1,29 @@
|
||||
---
|
||||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2beta1
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "hookshot.fullname" . }}
|
||||
labels:
|
||||
{{- include "hookshot.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "hookshot.fullname" . }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
62
helm/hookshot/templates/ingress-appservice.yaml
Normal file
@ -0,0 +1,62 @@
|
||||
---
|
||||
{{- if .Values.ingress.appservice.enabled -}}
|
||||
{{- $fullName := include "hookshot.fullname" . -}}
|
||||
{{- $svcPort := .Values.service.port -}}
|
||||
{{- if and .Values.ingress.appservice.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
{{- if not (hasKey .Values.ingress.appservice.annotations "kubernetes.io/ingress.class") }}
|
||||
{{- $_ := set .Values.ingress.appservice.annotations "kubernetes.io/ingress.class" .Values.ingress.appservice.className}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}-appservice
|
||||
labels:
|
||||
{{- include "hookshot.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.appservice.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if and .Values.ingress.appservice.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: {{ .Values.ingress.appservice.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.appservice.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.appservice.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.appservice.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
|
||||
pathType: {{ .pathType }}
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ $fullName }}
|
||||
port:
|
||||
number: {{ .port }}
|
||||
{{- else }}
|
||||
serviceName: {{ $fullName }}
|
||||
servicePort: {{ .port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
62
helm/hookshot/templates/ingress.yaml
Normal file
@ -0,0 +1,62 @@
|
||||
---
|
||||
{{- if .Values.ingress.webhook.enabled -}}
|
||||
{{- $fullName := include "hookshot.fullname" . -}}
|
||||
{{- $svcPort := .Values.service.port -}}
|
||||
{{- if and .Values.ingress.webhook.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
{{- if not (hasKey .Values.ingress.webhook.annotations "kubernetes.io/ingress.class") }}
|
||||
{{- $_ := set .Values.ingress.webhook.annotations "kubernetes.io/ingress.class" .Values.ingress.webhook.className}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
{{- include "hookshot.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.webhook.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if and .Values.ingress.webhook.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: {{ .Values.ingress.webhook.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.webhook.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.webhook.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.webhook.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
|
||||
pathType: {{ .pathType }}
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ $fullName }}
|
||||
port:
|
||||
number: {{ .port }}
|
||||
{{- else }}
|
||||
serviceName: {{ $fullName }}
|
||||
servicePort: {{ .port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
31
helm/hookshot/templates/service.yaml
Normal file
@ -0,0 +1,31 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "hookshot.fullname" . }}
|
||||
{{- with .Values.service.annotations }}
|
||||
annotations:
|
||||
{{ toYaml . | indent 4 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "hookshot.labels" . | nindent 4 }}
|
||||
{{- with .Values.service.labels }}
|
||||
{{ toYaml . | indent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.webhook.port }}
|
||||
targetPort: webhook
|
||||
protocol: TCP
|
||||
name: webhook
|
||||
- port: {{ .Values.service.metrics.port }}
|
||||
targetPort: metrics
|
||||
protocol: TCP
|
||||
name: metrics
|
||||
- port: {{ .Values.service.appservice.port }}
|
||||
targetPort: appservice
|
||||
protocol: TCP
|
||||
name: appservice
|
||||
selector:
|
||||
{{- include "hookshot.selectorLabels" . | nindent 4 }}
|
12
helm/hookshot/templates/serviceaccount.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "hookshot.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "hookshot.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
16
helm/hookshot/templates/tests/test-connection.yaml
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "hookshot.fullname" . }}-test-connection"
|
||||
labels:
|
||||
{{- include "hookshot.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": test
|
||||
spec:
|
||||
containers:
|
||||
- name: wget
|
||||
image: busybox
|
||||
command: ['wget']
|
||||
args: ['{{ include "hookshot.fullname" . }}:{{ .Values.service.webhook.port }}']
|
||||
restartPolicy: Never
|
314
helm/hookshot/values.yaml
Normal file
@ -0,0 +1,314 @@
|
||||
---
|
||||
# Note: This chart is released using the config.sample.yml file
|
||||
#
|
||||
# -- Number of replicas to deploy. Consequences of using multiple Hookshot replicas currently unknown.
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
# -- Repository to pull hookshot image from
|
||||
repository: halfshot/matrix-hookshot
|
||||
# -- Pull policy for Hookshot image
|
||||
pullPolicy: IfNotPresent
|
||||
# -- Image tag to pull. Defaults to chart's appVersion value as set in Chart.yaml
|
||||
tag:
|
||||
|
||||
# -- List of names of k8s secrets to be used as ImagePullSecrets for the pod
|
||||
imagePullSecrets: []
|
||||
|
||||
# -- Name override for helm chart
|
||||
nameOverride: ""
|
||||
|
||||
# -- Full name override for helm chart
|
||||
fullnameOverride: ""
|
||||
|
||||
serviceAccount:
|
||||
# -- Specifies whether a service account should be created
|
||||
create: true
|
||||
# -- Annotations to add to the service account
|
||||
annotations: {}
|
||||
# -- The name of the service account to use. If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
# -- Extra annotations for Hookshot pod
|
||||
podAnnotations: {}
|
||||
|
||||
# -- Pod security context settings
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
# -- Security context settings
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
service:
|
||||
# -- Service type for Hookshot service
|
||||
type: ClusterIP
|
||||
# -- Port for Hookshot service
|
||||
port: 80
|
||||
# -- Extra annotations for service
|
||||
annotations: {}
|
||||
# -- Extra labels for service
|
||||
labels: {}
|
||||
|
||||
webhook:
|
||||
# -- Webhook port as configured in container
|
||||
port: 9000
|
||||
metrics:
|
||||
# -- Metrics port as configured in container
|
||||
port: 9001
|
||||
appservice:
|
||||
# -- Appservice port as configured in container
|
||||
port: 9002
|
||||
|
||||
ingress:
|
||||
webhook:
|
||||
# -- Enable ingress for webhook
|
||||
enabled: false
|
||||
# -- Ingress class name for webhook ingress
|
||||
className: ""
|
||||
# -- Annotations for webhook ingress
|
||||
annotations: {}
|
||||
# -- Host configuration for webhook ingress
|
||||
hosts: []
|
||||
# -- TLS configuration for webhook ingress
|
||||
tls: []
|
||||
|
||||
appservice:
|
||||
# -- Enable ingress for appservice
|
||||
enabled: false
|
||||
# -- Ingress class name for appservice ingress
|
||||
className: ""
|
||||
# -- Annotations for appservice ingress
|
||||
annotations: {}
|
||||
# -- Host configuration for appservice ingress
|
||||
hosts: []
|
||||
# -- TLS configuration for appservice ingress
|
||||
tls: []
|
||||
|
||||
# -- Pod resource requests / limits
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
|
||||
# -- Node selector parameters
|
||||
nodeSelector: {}
|
||||
# -- Tolerations for deployment
|
||||
tolerations: []
|
||||
|
||||
# -- Affinity settings for deployment
|
||||
affinity: {}
|
||||
|
||||
hookshot:
|
||||
# -- Name of existing ConfigMap with valid Hookshot configuration
|
||||
existingConfigMap:
|
||||
|
||||
# -- Raw Hookshot configuration. Gets templated into a YAML file and then loaded unless an existingConfigMap is specified.
|
||||
config:
|
||||
bridge:
|
||||
# Basic homeserver configuration
|
||||
#
|
||||
domain: example.com
|
||||
url: http://localhost:8008
|
||||
mediaUrl: https://example.com
|
||||
port: 9993
|
||||
bindAddress: 127.0.0.1
|
||||
# github:
|
||||
# (Optional) Configure this to enable GitHub support
|
||||
#
|
||||
# auth:
|
||||
# Authentication for the GitHub App.
|
||||
#
|
||||
# id: 123
|
||||
# privateKeyFile: github-key.pem
|
||||
# webhook:
|
||||
# Webhook settings for the GitHub app.
|
||||
#
|
||||
# secret: secrettoken
|
||||
# oauth:
|
||||
# (Optional) Settings for allowing users to sign in via OAuth.
|
||||
#
|
||||
# client_id: foo
|
||||
# client_secret: bar
|
||||
# redirect_uri: https://example.com/bridge_oauth/
|
||||
# defaultOptions:
|
||||
# (Optional) Default options for GitHub connections.
|
||||
#
|
||||
# showIssueRoomLink: false
|
||||
# hotlinkIssues:
|
||||
# prefix: "#"
|
||||
# userIdPrefix: _github_
|
||||
# (Optional) Prefix used when creating ghost users for GitHub accounts.
|
||||
#
|
||||
# gitlab:
|
||||
# (Optional) Configure this to enable GitLab support
|
||||
#
|
||||
# instances:
|
||||
# gitlab.com:
|
||||
# url: https://gitlab.com
|
||||
# webhook:
|
||||
# secret: secrettoken
|
||||
# publicUrl: https://example.com/hookshot/
|
||||
# userIdPrefix: _gitlab_
|
||||
# (Optional) Prefix used when creating ghost users for GitLab accounts.
|
||||
#
|
||||
# figma:
|
||||
# (Optional) Configure this to enable Figma support
|
||||
#
|
||||
# publicUrl: https://example.com/hookshot/
|
||||
# instances:
|
||||
# your-instance:
|
||||
# teamId: your-team-id
|
||||
# accessToken: your-personal-access-token
|
||||
# passcode: your-webhook-passcode
|
||||
# jira:
|
||||
# (Optional) Configure this to enable Jira support. Only specify `url` if you are using a On Premise install (i.e. not atlassian.com)
|
||||
#
|
||||
# webhook:
|
||||
# Webhook settings for JIRA
|
||||
#
|
||||
# secret: secrettoken
|
||||
# oauth:
|
||||
# (Optional) OAuth settings for connecting users to JIRA. See documentation for more information
|
||||
#
|
||||
# client_id: foo
|
||||
# client_secret: bar
|
||||
# redirect_uri: https://example.com/bridge_oauth/
|
||||
generic:
|
||||
# (Optional) Support for generic webhook events.
|
||||
#'allowJsTransformationFunctions' will allow users to write short transformation snippets in code, and thus is unsafe in untrusted environments
|
||||
#
|
||||
#
|
||||
enabled: false
|
||||
enableHttpGet: false
|
||||
urlPrefix: https://example.com/webhook/
|
||||
userIdPrefix: _webhooks_
|
||||
allowJsTransformationFunctions: false
|
||||
waitForComplete: false
|
||||
feeds:
|
||||
# (Optional) Configure this to enable RSS/Atom feed support
|
||||
#
|
||||
enabled: false
|
||||
pollIntervalSeconds: 600
|
||||
pollTimeoutSeconds: 30
|
||||
# provisioning:
|
||||
# (Optional) Provisioning API for integration managers
|
||||
#
|
||||
# secret: "!secretToken"
|
||||
passFile: passkey.pem
|
||||
# A passkey used to encrypt tokens stored inside the bridge.
|
||||
# Run openssl genpkey -out passkey.pem -outform PEM -algorithm RSA -pkeyopt rsa_keygen_bits:4096 to generate
|
||||
#
|
||||
# bot:
|
||||
# (Optional) Define profile information for the bot user
|
||||
#
|
||||
# displayname: Hookshot Bot
|
||||
# avatar: mxc://half-shot.uk/2876e89ccade4cb615e210c458e2a7a6883fe17d
|
||||
# serviceBots:
|
||||
# (Optional) Define additional bot users for specific services
|
||||
#
|
||||
# - localpart: feeds
|
||||
# displayname: Feeds
|
||||
# avatar: mxc://half-shot.uk/2876e89ccade4cb615e210c458e2a7a6883fe17d
|
||||
# prefix: "!feeds"
|
||||
# service: feeds
|
||||
metrics:
|
||||
# (Optional) Prometheus metrics support
|
||||
#
|
||||
enabled: true
|
||||
# queue:
|
||||
# (Optional) Message queue / cache configuration options for large scale deployments.
|
||||
# For encryption to work, must be set to monolithic mode and have a host & port specified.
|
||||
#
|
||||
# monolithic: true
|
||||
# port: 6379
|
||||
# host: localhost
|
||||
logging:
|
||||
# (Optional) Logging settings. You can have a severity debug,info,warn,error
|
||||
#
|
||||
level: info
|
||||
colorize: true
|
||||
json: false
|
||||
timestampFormat: HH:mm:ss:SSS
|
||||
# widgets:
|
||||
# (Optional) EXPERIMENTAL support for complimentary widgets
|
||||
#
|
||||
# addToAdminRooms: false
|
||||
# disallowedIpRanges:
|
||||
# - 127.0.0.0/8
|
||||
# - 10.0.0.0/8
|
||||
# - 172.16.0.0/12
|
||||
# - 192.168.0.0/16
|
||||
# - 100.64.0.0/10
|
||||
# - 192.0.0.0/24
|
||||
# - 169.254.0.0/16
|
||||
# - 192.88.99.0/24
|
||||
# - 198.18.0.0/15
|
||||
# - 192.0.2.0/24
|
||||
# - 198.51.100.0/24
|
||||
# - 203.0.113.0/24
|
||||
# - 224.0.0.0/4
|
||||
# - ::1/128
|
||||
# - fe80::/10
|
||||
# - fc00::/7
|
||||
# - 2001:db8::/32
|
||||
# - ff00::/8
|
||||
# - fec0::/10
|
||||
# roomSetupWidget:
|
||||
# addOnInvite: false
|
||||
# publicUrl: https://example.com/widgetapi/v1/static/
|
||||
# branding:
|
||||
# widgetTitle: Hookshot Configuration
|
||||
# permissions:
|
||||
# (Optional) Permissions for using the bridge. See docs/setup.md#permissions for help
|
||||
#
|
||||
# - actor: example.com
|
||||
# services:
|
||||
# - service: "*"
|
||||
# level: admin
|
||||
listeners:
|
||||
# (Optional) HTTP Listener configuration.
|
||||
# Bind resource endpoints to ports and addresses.
|
||||
# 'port' must be specified. Each listener must listen on a unique port.
|
||||
# 'bindAddress' will default to '127.0.0.1' if not specified, which may not be suited to Docker environments.
|
||||
# 'resources' may be any of webhooks, widgets, metrics, provisioning
|
||||
#
|
||||
- port: 9000
|
||||
bindAddress: 0.0.0.0
|
||||
resources:
|
||||
- webhooks
|
||||
- port: 9001
|
||||
bindAddress: 127.0.0.1
|
||||
resources:
|
||||
- metrics
|
||||
- provisioning
|
||||
- port: 9002
|
||||
bindAddress: 0.0.0.0
|
||||
resources:
|
||||
- widgets
|
||||
registration:
|
||||
id: matrix-hookshot
|
||||
as_token: ""
|
||||
hs_token: ""
|
||||
namespaces:
|
||||
rooms: []
|
||||
users: []
|
||||
sender_localpart: hookshot
|
||||
url: "http://example.com"
|
||||
rate_limited: false
|
||||
passkey: ""
|
21
jest.config.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* For a detailed explanation regarding each configuration property, visit:
|
||||
* https://jestjs.io/docs/configuration
|
||||
*/
|
||||
|
||||
import type {Config} from 'jest';
|
||||
|
||||
const config: Config = {
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
rootDir: "spec",
|
||||
testTimeout: 60000,
|
||||
transform: {
|
||||
'^.+\\.tsx?$': [
|
||||
'ts-jest',
|
||||
{
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
98
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-hookshot",
|
||||
"version": "4.0.0",
|
||||
"version": "5.1.2",
|
||||
"description": "A bridge between Matrix and multiple project management services, such as GitHub, GitLab and JIRA.",
|
||||
"main": "lib/app.js",
|
||||
"repository": "https://github.com/matrix-org/matrix-hookshot",
|
||||
@ -10,7 +10,7 @@
|
||||
"name": "matrix-hookshot-rs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20"
|
||||
},
|
||||
"scripts": {
|
||||
"build:web": "vite build",
|
||||
@ -32,80 +32,88 @@
|
||||
"start:matrixsender": "node --require source-map-support/register lib/App/MatrixSenderApp.js",
|
||||
"start:resetcrypto": "node --require source-map-support/register lib/App/ResetCryptoStore.js",
|
||||
"test": "mocha -r ts-node/register tests/init.ts tests/*.ts tests/**/*.ts",
|
||||
"test:e2e": "yarn node --experimental-vm-modules $(yarn bin jest)",
|
||||
"test:cover": "nyc --reporter=lcov --reporter=text yarn test",
|
||||
"lint": "yarn run lint:js && yarn run lint:rs",
|
||||
"lint:js": "eslint -c .eslintrc.js 'src/**/*.ts' 'tests/**/*.ts' 'web/**/*.ts' 'web/**/*.tsx'",
|
||||
"lint:rs": "cargo fmt --all -- --check",
|
||||
"lint:rs:apply": "cargo fmt --all",
|
||||
"generate-default-config": "ts-node src/Config/Defaults.ts --config > config.sample.yml",
|
||||
"validate-config": "ts-node src/Config/Config.ts"
|
||||
"lint:rs": "cargo fmt --all -- --check && cargo clippy -- -Dwarnings",
|
||||
"lint:rs:apply": "cargo clippy --fix && cargo fmt --all",
|
||||
"generate-default-config": "ts-node src/config/Defaults.ts --config > config.sample.yml",
|
||||
"validate-config": "ts-node src/config/Config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@octokit/auth-app": "^3.3.0",
|
||||
"@octokit/auth-token": "^2.4.5",
|
||||
"@octokit/rest": "^18.10.0",
|
||||
"@octokit/webhooks": "^9.1.2",
|
||||
"@octokit/auth-app": "^6.0.2",
|
||||
"@octokit/auth-token": "^4.0.0",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@octokit/webhooks": "^12.0.10",
|
||||
"@sentry/node": "^7.52.1",
|
||||
"@vector-im/compound-design-tokens": "^0.1.0",
|
||||
"@vector-im/compound-web": "^0.9.4",
|
||||
"ajv": "^8.11.0",
|
||||
"axios": "^0.24.0",
|
||||
"axios": "^1.6.3",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.3",
|
||||
"express": "^4.18.2",
|
||||
"figma-js": "^1.14.0",
|
||||
"http-status-codes": "^2.2.0",
|
||||
"ioredis": "^5.2.3",
|
||||
"jira-client": "^8.0.0",
|
||||
"markdown-it": "^12.3.2",
|
||||
"matrix-appservice-bridge": "^9.0.0",
|
||||
"matrix-bot-sdk": "npm:@vector-im/matrix-bot-sdk@^0.6.6-element.1",
|
||||
"matrix-widget-api": "^1.0.0",
|
||||
"micromatch": "^4.0.4",
|
||||
"mime": "^3.0.0",
|
||||
"node-emoji": "^1.11.0",
|
||||
"nyc": "^15.1.0",
|
||||
"jira-client": "^8.2.2",
|
||||
"markdown-it": "^14.0.0",
|
||||
"matrix-appservice-bridge": "^9.0.1",
|
||||
"matrix-bot-sdk": "npm:@vector-im/matrix-bot-sdk@^0.7.0-specific-device-2",
|
||||
"matrix-widget-api": "^1.6.0",
|
||||
"micromatch": "^4.0.5",
|
||||
"mime": "^4.0.1",
|
||||
"node-emoji": "^2.1.3",
|
||||
"p-queue": "^6.6.2",
|
||||
"prom-client": "^14.0.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"preact-render-to-string": "^6.3.1",
|
||||
"prom-client": "^15.1.0",
|
||||
"quickjs-emscripten": "^0.26.0",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"string-argv": "^0.3.1",
|
||||
"tiny-typed-emitter": "^2.1.0",
|
||||
"vm2": "^3.9.17",
|
||||
"winston": "^3.3.3",
|
||||
"xml2js": "^0.5.0",
|
||||
"yaml": "^2.2.2"
|
||||
"vite-plugin-magical-svg": "^1.1.1",
|
||||
"winston": "^3.11.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"yaml": "^2.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/lang-javascript": "^6.0.2",
|
||||
"@napi-rs/cli": "^2.2.0",
|
||||
"@napi-rs/cli": "^2.13.2",
|
||||
"@preact/preset-vite": "^2.2.0",
|
||||
"@tsconfig/node18": "^2.0.0",
|
||||
"@rollup/plugin-alias": "^5.1.0",
|
||||
"@tsconfig/node18": "^18.2.2",
|
||||
"@types/ajv": "^1.0.0",
|
||||
"@types/chai": "^4.2.22",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/jira-client": "^7.1.0",
|
||||
"@types/markdown-it": "^12.2.3",
|
||||
"@types/markdown-it": "^13.0.7",
|
||||
"@types/micromatch": "^4.0.1",
|
||||
"@types/mime": "^2.0.3",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node-emoji": "^1.8.1",
|
||||
"@types/uuid": "^8.3.3",
|
||||
"@types/mime": "^3.0.4",
|
||||
"@types/mocha": "^10.0.6",
|
||||
"@types/node": "20.10.6",
|
||||
"@types/xml2js": "^0.4.11",
|
||||
"@types/node": "18",
|
||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||
"@typescript-eslint/parser": "^5.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
||||
"@typescript-eslint/parser": "^6.17.0",
|
||||
"@uiw/react-codemirror": "^4.12.3",
|
||||
"chai": "^4.3.4",
|
||||
"eslint": "^8.3.0",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-config-preact": "^1.3.0",
|
||||
"eslint-plugin-mocha": "^9.0.0",
|
||||
"eslint-plugin-mocha": "^10.1.0",
|
||||
"homerunner-client": "^1.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"mini.css": "^3.0.1",
|
||||
"mocha": "^8.2.1",
|
||||
"mocha": "^10.2.0",
|
||||
"nyc": "^15.1.0",
|
||||
"preact": "^10.5.15",
|
||||
"rimraf": "^3.0.2",
|
||||
"sass": "^1.51.0",
|
||||
"rimraf": "^5.0.5",
|
||||
"sass": "^1.69.6",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.1.4",
|
||||
"vite-svg-loader": "^4.0.0"
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.10"
|
||||
}
|
||||
}
|
||||
|
@ -35,12 +35,14 @@ metrics:
|
||||
port: 9002
|
||||
\`\`\`
|
||||
|
||||
Hookshot will then provide metrics on \`127.0.0.1\` at port \`9002\`.
|
||||
|
||||
An example dashboard that can be used with [Grafana](https://grafana.com) can be found at [/contrib/hookshot-dashboard.json](https://github.com/matrix-org/matrix-hookshot/blob/main/contrib/hookshot-dashboard.json).
|
||||
There are 3 variables at the top of the dashboard:
|
||||
|
||||

|
||||
|
||||
Select the Prometheus with your Hookshot metrics as Data Source. Set Interval to your scraping interval. Set 2x Interval to twice the Interval value ([why?](https://github.com/matrix-org/matrix-hookshot/pull/407#issuecomment-1186251618)).
|
||||
Select the Prometheus instance with your Hookshot metrics as Data Source. Set Interval to your scraping interval. Set 2x Interval to twice the Interval value ([why?](https://github.com/matrix-org/matrix-hookshot/pull/407#issuecomment-1186251618)).
|
||||
|
||||
Below is the generated list of Prometheus metrics for Hookshot.
|
||||
|
||||
|
@ -3,23 +3,41 @@
|
||||
|
||||
if ! command -v jq &> /dev/null
|
||||
then
|
||||
echo "You must install jq to use this script"
|
||||
exit
|
||||
echo "You must install jq to use this script" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION=`jq -r .version package.json`
|
||||
VERSION=`jq -r .version <(git show :package.json)`
|
||||
|
||||
function parseCargoVersion {
|
||||
awk '$1 == "version" {gsub("\"", "", $3); print $3}' $1
|
||||
}
|
||||
CARGO_TOML_VERSION=`parseCargoVersion <(git show :Cargo.toml)`
|
||||
if [[ $VERSION != $CARGO_TOML_VERSION ]]; then
|
||||
echo "Node & Rust package versions do not match." >&2
|
||||
echo "Node version (package.json): ${VERSION}" >&2
|
||||
echo "Rust version (Cargo.toml): ${CARGO_TOML_VERSION}" >&2
|
||||
exit 2
|
||||
fi
|
||||
CARGO_LOCK_VERSION=`parseCargoVersion <(grep -A1 matrix-hookshot <(git show :Cargo.lock))`
|
||||
if [[ $CARGO_TOML_VERSION != $CARGO_LOCK_VERSION ]]; then
|
||||
echo "Rust package version does not match the lockfile." >&2
|
||||
echo "Rust version (Cargo.toml): ${CARGO_TOML_VERSION}" >&2
|
||||
echo "Lockfile version (Cargo.lock): ${CARGO_LOCK_VERSION}" >&2
|
||||
exit 3
|
||||
fi
|
||||
TAG="$VERSION"
|
||||
HEAD_BRANCH=`git remote show origin | sed -n '/HEAD branch/s/.*: //p'`
|
||||
REPO_NAME=`git remote show origin -n | grep -m 1 -oP '(?<=git@github.com:)(.*)(?=.git)'`
|
||||
|
||||
if [[ "`git branch --show-current`" != $HEAD_BRANCH ]]; then
|
||||
echo "You must be on the develop branch to run this command."
|
||||
exit 1
|
||||
echo "You must be on the $HEAD_BRANCH branch to run this command." >&2
|
||||
exit 4
|
||||
fi
|
||||
|
||||
if [ $(git tag -l "$TAG") ]; then
|
||||
echo "Tag $TAG already exists, not continuing."
|
||||
exit 1
|
||||
echo "Tag $TAG already exists, not continuing." >&2
|
||||
exit 5
|
||||
fi
|
||||
|
||||
echo "Drafting a new release"
|
||||
|
62
spec/basic.spec.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { MessageEventContent } from "matrix-bot-sdk";
|
||||
import { E2ESetupTestTimeout, E2ETestEnv } from "./util/e2e-test";
|
||||
import { describe, it, beforeEach, afterEach } from "@jest/globals";
|
||||
import { expect } from "chai";
|
||||
|
||||
describe('Basic test setup', () => {
|
||||
let testEnv: E2ETestEnv;
|
||||
|
||||
beforeEach(async () => {
|
||||
testEnv = await E2ETestEnv.createTestEnv({matrixLocalparts: ['user']});
|
||||
await testEnv.setUp();
|
||||
}, E2ESetupTestTimeout);
|
||||
|
||||
afterEach(() => {
|
||||
return testEnv?.tearDown();
|
||||
});
|
||||
|
||||
it('should be able to invite the bot to a room', async () => {
|
||||
const user = testEnv.getUser('user');
|
||||
const roomId = await user.createRoom({ name: 'Test room', invite:[testEnv.botMxid] });
|
||||
await user.waitForRoomJoin({sender: testEnv.botMxid, roomId });
|
||||
await user.sendText(roomId, "!hookshot help");
|
||||
const msg = await user.waitForRoomEvent<MessageEventContent>({
|
||||
eventType: 'm.room.message', sender: testEnv.botMxid, roomId
|
||||
});
|
||||
// Expect help text.
|
||||
expect(msg.data.content.body).to.include('!hookshot help` - This help text\n');
|
||||
});
|
||||
|
||||
// TODO: Move test to it's own generic connections file.
|
||||
it('should be able to setup a webhook', async () => {
|
||||
const user = testEnv.getUser('user');
|
||||
const testRoomId = await user.createRoom({ name: 'Test room', invite:[testEnv.botMxid] });
|
||||
await user.waitForRoomJoin({sender: testEnv.botMxid, roomId: testRoomId });
|
||||
await user.setUserPowerLevel(testEnv.botMxid, testRoomId, 50);
|
||||
await user.sendText(testRoomId, "!hookshot webhook test-webhook");
|
||||
const inviteResponse = await user.waitForRoomInvite({sender: testEnv.botMxid});
|
||||
await user.waitForRoomEvent<MessageEventContent>({
|
||||
eventType: 'm.room.message', sender: testEnv.botMxid, roomId: testRoomId,
|
||||
body: 'Room configured to bridge webhooks. See admin room for secret url.'
|
||||
});
|
||||
const webhookUrlMessage = user.waitForRoomEvent<MessageEventContent>({
|
||||
eventType: 'm.room.message', sender: testEnv.botMxid, roomId: inviteResponse.roomId
|
||||
});
|
||||
await user.joinRoom(inviteResponse.roomId);
|
||||
const msgData = (await webhookUrlMessage).data.content.body;
|
||||
const webhookUrl = msgData.split('\n')[2];
|
||||
const webhookNotice = user.waitForRoomEvent<MessageEventContent>({
|
||||
eventType: 'm.room.message', sender: testEnv.botMxid, roomId: testRoomId, body: 'Hello world!'
|
||||
});
|
||||
|
||||
// Send a webhook
|
||||
await fetch(webhookUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({text: 'Hello world!'})
|
||||
});
|
||||
|
||||
// And await the notice.
|
||||
await webhookNotice;
|
||||
});
|
||||
});
|
143
spec/github.spec.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import { E2ESetupTestTimeout, E2ETestEnv } from "./util/e2e-test";
|
||||
import { describe, it, beforeEach, afterEach } from "@jest/globals";
|
||||
import { createHmac, randomUUID } from "crypto";
|
||||
import { GitHubRepoConnection, GitHubRepoConnectionState } from "../src/Connections";
|
||||
import { MessageEventContent } from "matrix-bot-sdk";
|
||||
import { getBridgeApi } from "./util/bridge-api";
|
||||
import { Server, createServer } from "http";
|
||||
|
||||
describe('GitHub', () => {
|
||||
let testEnv: E2ETestEnv;
|
||||
let githubServer: Server;
|
||||
const webhooksPort = 9500 + E2ETestEnv.workerId;
|
||||
const githubPort = 9700 + E2ETestEnv.workerId;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Fake out enough of a GitHub API to get past startup. Later
|
||||
// tests might make more use of this.
|
||||
githubServer = createServer((req, res) => {
|
||||
if (req.method === 'GET' && req.url === '/api/v3/app') {
|
||||
res.writeHead(200, undefined, { "content-type": 'application/json'});
|
||||
res.write(JSON.stringify({}));
|
||||
} else if (req.method === 'GET' && req.url === '/api/v3/app/installations?per_page=100&page=1') {
|
||||
res.writeHead(200, undefined, { "content-type": 'application/json'});
|
||||
res.write(JSON.stringify([]));
|
||||
} else {
|
||||
console.log('Unknown request', req.method, req.url);
|
||||
res.writeHead(404);
|
||||
}
|
||||
res.end();
|
||||
}).listen(githubPort);
|
||||
testEnv = await E2ETestEnv.createTestEnv({matrixLocalparts: ['user'], config: {
|
||||
github: {
|
||||
webhook: {
|
||||
secret: randomUUID(),
|
||||
},
|
||||
// So we can mock out the URL
|
||||
enterpriseUrl: `http://localhost:${githubPort}`,
|
||||
auth: {
|
||||
privateKeyFile: 'replaced',
|
||||
id: '1234',
|
||||
}
|
||||
},
|
||||
widgets: {
|
||||
publicUrl: `http://localhost:${webhooksPort}`
|
||||
},
|
||||
listeners: [{
|
||||
port: webhooksPort,
|
||||
bindAddress: '0.0.0.0',
|
||||
// Bind to the SAME listener to ensure we don't have conflicts.
|
||||
resources: ['webhooks', 'widgets'],
|
||||
}],
|
||||
}});
|
||||
await testEnv.setUp();
|
||||
}, E2ESetupTestTimeout);
|
||||
|
||||
afterEach(() => {
|
||||
githubServer?.close();
|
||||
return testEnv?.tearDown();
|
||||
});
|
||||
|
||||
it.only('should be able to handle a GitHub event', async () => {
|
||||
const user = testEnv.getUser('user');
|
||||
const bridgeApi = await getBridgeApi(testEnv.opts.config?.widgets?.publicUrl!, user);
|
||||
const testRoomId = await user.createRoom({ name: 'Test room', invite:[testEnv.botMxid] });
|
||||
await user.setUserPowerLevel(testEnv.botMxid, testRoomId, 50);
|
||||
await user.waitForRoomJoin({sender: testEnv.botMxid, roomId: testRoomId });
|
||||
// Now hack in a GitHub connection.
|
||||
await testEnv.app.appservice.botClient.sendStateEvent(testRoomId, GitHubRepoConnection.CanonicalEventType, "my-test", {
|
||||
org: 'my-org',
|
||||
repo: 'my-repo'
|
||||
} satisfies GitHubRepoConnectionState);
|
||||
|
||||
// Wait for connection to be accepted.
|
||||
await new Promise<void>(r => {
|
||||
let interval: NodeJS.Timeout;
|
||||
interval = setInterval(() => {
|
||||
bridgeApi.getConnectionsForRoom(testRoomId).then(conns => {
|
||||
if (conns.length > 0) {
|
||||
clearInterval(interval);
|
||||
r();
|
||||
}
|
||||
})
|
||||
}, 500);
|
||||
});
|
||||
|
||||
const webhookNotice = user.waitForRoomEvent<MessageEventContent>({
|
||||
eventType: 'm.room.message', sender: testEnv.botMxid, roomId: testRoomId
|
||||
});
|
||||
|
||||
const webhookPayload = JSON.stringify({
|
||||
"action": "opened",
|
||||
"number": 1,
|
||||
"pull_request": {
|
||||
id: 1,
|
||||
"url": "https://api.github.com/repos/my-org/my-repo/pulls/1",
|
||||
"html_url": "https://github.com/my-org/my-repo/pulls/1",
|
||||
"number": 1,
|
||||
"state": "open",
|
||||
"locked": false,
|
||||
"title": "My test pull request",
|
||||
"user": {
|
||||
"login": "alice",
|
||||
},
|
||||
},
|
||||
repository: {
|
||||
id: 1,
|
||||
"html_url": "https://github.com/my-org/my-repo",
|
||||
name: 'my-repo',
|
||||
full_name: 'my-org/my-repo',
|
||||
owner: {
|
||||
login: 'my-org',
|
||||
}
|
||||
},
|
||||
sender: {
|
||||
login: 'alice',
|
||||
}
|
||||
});
|
||||
|
||||
const hmac = createHmac('sha256', testEnv.opts.config?.github?.webhook.secret!);
|
||||
hmac.write(webhookPayload);
|
||||
hmac.end();
|
||||
|
||||
// Send a webhook
|
||||
const req = await fetch(`http://localhost:${webhooksPort}/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-github-event': 'pull_request',
|
||||
'X-Hub-Signature-256': `sha256=${hmac.read().toString('hex')}`,
|
||||
'X-GitHub-Delivery': randomUUID(),
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: webhookPayload,
|
||||
});
|
||||
expect(req.status).toBe(200);
|
||||
expect(await req.text()).toBe('OK');
|
||||
|
||||
// And await the notice.
|
||||
const { body } = (await webhookNotice).data.content;
|
||||
expect(body).toContain('**alice** opened a new PR');
|
||||
expect(body).toContain('https://github.com/my-org/my-repo/pulls/1');
|
||||
expect(body).toContain('My test pull request');
|
||||
});
|
||||
});
|
14
spec/util/bridge-api.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { MatrixClient } from "matrix-bot-sdk";
|
||||
import { BridgeAPI } from "../../web/BridgeAPI";
|
||||
import { WidgetApi } from "matrix-widget-api";
|
||||
|
||||
export async function getBridgeApi(publicUrl: string, user: MatrixClient) {
|
||||
return BridgeAPI.getBridgeAPI(publicUrl, {
|
||||
requestOpenIDConnectToken: () => {
|
||||
return user.getOpenIDConnectToken()
|
||||
},
|
||||
} as unknown as WidgetApi, {
|
||||
getItem() { return null},
|
||||
setItem() { },
|
||||
} as unknown as Storage);
|
||||
}
|
273
spec/util/e2e-test.ts
Normal file
@ -0,0 +1,273 @@
|
||||
import { ComplementHomeServer, createHS, destroyHS } from "./homerunner";
|
||||
import { IAppserviceRegistration, MatrixClient, MembershipEventContent, PowerLevelsEventContent } from "matrix-bot-sdk";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { BridgeConfig, BridgeConfigRoot } from "../../src/config/Config";
|
||||
import { start } from "../../src/App/BridgeApp";
|
||||
import { RSAKeyPairOptions, generateKeyPair } from "node:crypto";
|
||||
import path from "node:path";
|
||||
|
||||
const WAIT_EVENT_TIMEOUT = 10000;
|
||||
export const E2ESetupTestTimeout = 60000;
|
||||
|
||||
interface Opts {
|
||||
matrixLocalparts?: string[];
|
||||
config?: Partial<BridgeConfigRoot>,
|
||||
}
|
||||
|
||||
export class E2ETestMatrixClient extends MatrixClient {
|
||||
|
||||
public async waitForPowerLevel(
|
||||
roomId: string, expected: Partial<PowerLevelsEventContent>,
|
||||
): Promise<{roomId: string, data: {
|
||||
sender: string, type: string, state_key?: string, content: PowerLevelsEventContent, event_id: string,
|
||||
}}> {
|
||||
return this.waitForEvent('room.event', (eventRoomId: string, eventData: {
|
||||
sender: string, type: string, content: Record<string, unknown>, event_id: string, state_key: string,
|
||||
}) => {
|
||||
if (eventRoomId !== roomId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (eventData.type !== "m.room.power_levels") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (eventData.state_key !== "") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Check only the keys we care about
|
||||
for (const [key, value] of Object.entries(expected)) {
|
||||
const evValue = eventData.content[key] ?? undefined;
|
||||
const sortOrder = value !== null && typeof value === "object" ? Object.keys(value).sort() : undefined;
|
||||
const jsonLeft = JSON.stringify(evValue, sortOrder);
|
||||
const jsonRight = JSON.stringify(value, sortOrder);
|
||||
if (jsonLeft !== jsonRight) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
console.info(
|
||||
// eslint-disable-next-line max-len
|
||||
`${eventRoomId} ${eventData.event_id} ${eventData.sender}`
|
||||
);
|
||||
return {roomId: eventRoomId, data: eventData};
|
||||
}, `Timed out waiting for powerlevel from in ${roomId}`)
|
||||
}
|
||||
|
||||
public async waitForRoomEvent<T extends object = Record<string, unknown>>(
|
||||
opts: {eventType: string, sender: string, roomId?: string, stateKey?: string, body?: string}
|
||||
): Promise<{roomId: string, data: {
|
||||
sender: string, type: string, state_key?: string, content: T, event_id: string,
|
||||
}}> {
|
||||
const {eventType, sender, roomId, stateKey} = opts;
|
||||
return this.waitForEvent('room.event', (eventRoomId: string, eventData: {
|
||||
sender: string, type: string, state_key?: string, content: T, event_id: string,
|
||||
}) => {
|
||||
if (eventData.sender !== sender) {
|
||||
return undefined;
|
||||
}
|
||||
if (eventData.type !== eventType) {
|
||||
return undefined;
|
||||
}
|
||||
if (roomId && eventRoomId !== roomId) {
|
||||
return undefined;
|
||||
}
|
||||
if (stateKey !== undefined && eventData.state_key !== stateKey) {
|
||||
return undefined;
|
||||
}
|
||||
const body = 'body' in eventData.content && eventData.content.body;
|
||||
if (opts.body && body !== opts.body) {
|
||||
return undefined;
|
||||
}
|
||||
console.info(
|
||||
// eslint-disable-next-line max-len
|
||||
`${eventRoomId} ${eventData.event_id} ${eventData.type} ${eventData.sender} ${eventData.state_key ?? body ?? ''}`
|
||||
);
|
||||
return {roomId: eventRoomId, data: eventData};
|
||||
}, `Timed out waiting for ${eventType} from ${sender} in ${roomId || "any room"}`)
|
||||
}
|
||||
|
||||
public async waitForRoomJoin(
|
||||
opts: {sender: string, roomId?: string}
|
||||
): Promise<{roomId: string, data: unknown}> {
|
||||
const {sender, roomId} = opts;
|
||||
return this.waitForEvent('room.event', (eventRoomId: string, eventData: {
|
||||
sender: string,
|
||||
state_key: string,
|
||||
content: MembershipEventContent,
|
||||
}) => {
|
||||
if (eventData.state_key !== sender) {
|
||||
return;
|
||||
}
|
||||
if (roomId && eventRoomId !== roomId) {
|
||||
return;
|
||||
}
|
||||
if (eventData.content.membership !== "join") {
|
||||
return;
|
||||
}
|
||||
return {roomId: eventRoomId, data: eventData};
|
||||
}, `Timed out waiting for join to ${roomId || "any room"} from ${sender}`)
|
||||
}
|
||||
|
||||
public async waitForRoomInvite(
|
||||
opts: {sender: string, roomId?: string}
|
||||
): Promise<{roomId: string, data: unknown}> {
|
||||
const {sender, roomId} = opts;
|
||||
return this.waitForEvent('room.invite', (eventRoomId: string, eventData: {
|
||||
sender: string
|
||||
}) => {
|
||||
if (eventData.sender !== sender) {
|
||||
return undefined;
|
||||
}
|
||||
if (roomId && eventRoomId !== roomId) {
|
||||
return undefined;
|
||||
}
|
||||
return {roomId: eventRoomId, data: eventData};
|
||||
}, `Timed out waiting for invite to ${roomId || "any room"} from ${sender}`)
|
||||
}
|
||||
|
||||
public async waitForEvent<T>(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
emitterType: string, filterFn: (...args: any[]) => T|undefined, timeoutMsg: string)
|
||||
: Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let timer: NodeJS.Timeout;
|
||||
const fn = (...args: unknown[]) => {
|
||||
const data = filterFn(...args);
|
||||
if (data) {
|
||||
clearTimeout(timer);
|
||||
resolve(data);
|
||||
}
|
||||
};
|
||||
timer = setTimeout(() => {
|
||||
this.removeListener(emitterType, fn);
|
||||
reject(new Error(timeoutMsg));
|
||||
}, WAIT_EVENT_TIMEOUT);
|
||||
this.on(emitterType, fn)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class E2ETestEnv {
|
||||
|
||||
static get workerId() {
|
||||
return parseInt(process.env.JEST_WORKER_ID ?? '0');
|
||||
}
|
||||
|
||||
static async createTestEnv(opts: Opts): Promise<E2ETestEnv> {
|
||||
const workerID = this.workerId;
|
||||
const { matrixLocalparts, config: providedConfig } = opts;
|
||||
const keyPromise = new Promise<string>((resolve, reject) => generateKeyPair("rsa", {
|
||||
modulusLength: 4096,
|
||||
privateKeyEncoding: {
|
||||
type: "pkcs8",
|
||||
format: "pem",
|
||||
},
|
||||
publicKeyEncoding: {
|
||||
format: "pem",
|
||||
type: "pkcs1",
|
||||
}
|
||||
} satisfies RSAKeyPairOptions<"pem", "pem">, (err, _, privateKey) => {
|
||||
if (err) { reject(err) } else { resolve(privateKey) }
|
||||
}));
|
||||
|
||||
// Configure homeserver and bots
|
||||
const [homeserver, dir, privateKey] = await Promise.all([
|
||||
createHS([...matrixLocalparts || []], workerID),
|
||||
mkdtemp('hookshot-int-test'),
|
||||
keyPromise,
|
||||
]);
|
||||
const keyPath = path.join(dir, 'key.pem');
|
||||
await writeFile(keyPath, privateKey, 'utf-8');
|
||||
const webhooksPort = 9500 + workerID;
|
||||
|
||||
if (providedConfig?.widgets) {
|
||||
providedConfig.widgets.openIdOverrides = {
|
||||
'hookshot': homeserver.url,
|
||||
}
|
||||
}
|
||||
|
||||
if (providedConfig?.github) {
|
||||
providedConfig.github.auth.privateKeyFile = keyPath;
|
||||
}
|
||||
|
||||
const config = new BridgeConfig({
|
||||
bridge: {
|
||||
domain: homeserver.domain,
|
||||
url: homeserver.url,
|
||||
port: homeserver.appPort,
|
||||
bindAddress: '0.0.0.0',
|
||||
},
|
||||
logging: {
|
||||
level: 'info',
|
||||
},
|
||||
queue: {
|
||||
monolithic: true,
|
||||
},
|
||||
// Always enable webhooks so that hookshot starts.
|
||||
generic: {
|
||||
enabled: true,
|
||||
urlPrefix: `http://localhost:${webhooksPort}/webhook`,
|
||||
},
|
||||
listeners: [{
|
||||
port: webhooksPort,
|
||||
bindAddress: '0.0.0.0',
|
||||
resources: ['webhooks'],
|
||||
}],
|
||||
passFile: keyPath,
|
||||
...providedConfig,
|
||||
});
|
||||
const registration: IAppserviceRegistration = {
|
||||
as_token: homeserver.asToken,
|
||||
hs_token: homeserver.hsToken,
|
||||
sender_localpart: 'hookshot',
|
||||
namespaces: {
|
||||
users: [{
|
||||
regex: `@hookshot:${homeserver.domain}`,
|
||||
exclusive: true,
|
||||
}],
|
||||
rooms: [],
|
||||
aliases: [],
|
||||
}
|
||||
};
|
||||
const app = await start(config, registration);
|
||||
app.listener.finaliseListeners();
|
||||
|
||||
return new E2ETestEnv(homeserver, app, opts, config, dir);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
public readonly homeserver: ComplementHomeServer,
|
||||
public app: Awaited<ReturnType<typeof start>>,
|
||||
public readonly opts: Opts,
|
||||
private readonly config: BridgeConfig,
|
||||
private readonly dir: string,
|
||||
) { }
|
||||
|
||||
public get botMxid() {
|
||||
return `@hookshot:${this.homeserver.domain}`;
|
||||
}
|
||||
|
||||
public async setUp(): Promise<void> {
|
||||
await this.app.bridgeApp.start();
|
||||
}
|
||||
|
||||
public async tearDown(): Promise<void> {
|
||||
await this.app.bridgeApp.stop();
|
||||
await this.app.listener.stop();
|
||||
await this.app.storage.disconnect?.();
|
||||
this.homeserver.users.forEach(u => u.client.stop());
|
||||
await destroyHS(this.homeserver.id);
|
||||
await rm(this.dir, { recursive: true });
|
||||
}
|
||||
|
||||
public getUser(localpart: string) {
|
||||
const u = this.homeserver.users.find(u => u.userId === `@${localpart}:${this.homeserver.domain}`);
|
||||
if (!u) {
|
||||
throw Error("User missing from test");
|
||||
}
|
||||
return u.client;
|
||||
}
|
||||
}
|
134
spec/util/homerunner.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import { MatrixClient } from "matrix-bot-sdk";
|
||||
import { createHash, createHmac, randomUUID } from "crypto";
|
||||
import { Homerunner } from "homerunner-client";
|
||||
import { E2ETestMatrixClient } from "./e2e-test";
|
||||
|
||||
const HOMERUNNER_IMAGE = process.env.HOMERUNNER_IMAGE || 'ghcr.io/element-hq/synapse/complement-synapse:latest';
|
||||
export const DEFAULT_REGISTRATION_SHARED_SECRET = (
|
||||
process.env.REGISTRATION_SHARED_SECRET || 'complement'
|
||||
);
|
||||
const COMPLEMENT_HOSTNAME_RUNNING_COMPLEMENT = (
|
||||
process.env.COMPLEMENT_HOSTNAME_RUNNING_COMPLEMENT || "host.docker.internal"
|
||||
);
|
||||
|
||||
const homerunner = new Homerunner.Client();
|
||||
|
||||
export interface ComplementHomeServer {
|
||||
id: string,
|
||||
url: string,
|
||||
domain: string,
|
||||
users: {userId: string, accessToken: string, deviceId: string, client: E2ETestMatrixClient}[]
|
||||
asToken: string,
|
||||
hsToken: string,
|
||||
appPort: number,
|
||||
}
|
||||
|
||||
async function waitForHomerunner() {
|
||||
let attempts = 0;
|
||||
do {
|
||||
attempts++;
|
||||
console.log(`Waiting for homerunner to be ready (${attempts}/100)`);
|
||||
try {
|
||||
await homerunner.health();
|
||||
break;
|
||||
}
|
||||
catch (ex) {
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
}
|
||||
} while (attempts < 100)
|
||||
if (attempts === 100) {
|
||||
throw Error('Homerunner was not ready after 100 attempts');
|
||||
}
|
||||
}
|
||||
|
||||
export async function createHS(localparts: string[] = [], workerId: number): Promise<ComplementHomeServer> {
|
||||
await waitForHomerunner();
|
||||
|
||||
const appPort = 49600 + workerId;
|
||||
const blueprint = `hookshot_integration_test_${Date.now()}`;
|
||||
const asToken = randomUUID();
|
||||
const hsToken = randomUUID();
|
||||
const blueprintResponse = await homerunner.create({
|
||||
base_image_uri: HOMERUNNER_IMAGE,
|
||||
blueprint: {
|
||||
Name: blueprint,
|
||||
Homeservers: [{
|
||||
Name: 'hookshot',
|
||||
Users: localparts.map(localpart => ({Localpart: localpart, DisplayName: localpart})),
|
||||
ApplicationServices: [{
|
||||
ID: 'hookshot',
|
||||
URL: `http://${COMPLEMENT_HOSTNAME_RUNNING_COMPLEMENT}:${appPort}`,
|
||||
SenderLocalpart: 'hookshot',
|
||||
RateLimited: false,
|
||||
...{ASToken: asToken,
|
||||
HSToken: hsToken},
|
||||
}]
|
||||
}],
|
||||
}
|
||||
});
|
||||
const [homeserverName, homeserver] = Object.entries(blueprintResponse.homeservers)[0];
|
||||
// Skip AS user.
|
||||
const users = Object.entries(homeserver.AccessTokens)
|
||||
.filter(([_uId, accessToken]) => accessToken !== asToken)
|
||||
.map(([userId, accessToken]) => ({
|
||||
userId: userId,
|
||||
accessToken,
|
||||
deviceId: homeserver.DeviceIDs[userId],
|
||||
client: new E2ETestMatrixClient(homeserver.BaseURL, accessToken),
|
||||
})
|
||||
);
|
||||
|
||||
// Start syncing proactively.
|
||||
await Promise.all(users.map(u => u.client.start()));
|
||||
return {
|
||||
users,
|
||||
id: blueprint,
|
||||
url: homeserver.BaseURL,
|
||||
domain: homeserverName,
|
||||
asToken,
|
||||
appPort,
|
||||
hsToken,
|
||||
};
|
||||
}
|
||||
|
||||
export function destroyHS(
|
||||
id: string
|
||||
): Promise<void> {
|
||||
return homerunner.destroy(id);
|
||||
}
|
||||
|
||||
export async function registerUser(
|
||||
homeserverUrl: string,
|
||||
user: { username: string, admin: boolean },
|
||||
sharedSecret = DEFAULT_REGISTRATION_SHARED_SECRET,
|
||||
): Promise<{mxid: string, client: MatrixClient}> {
|
||||
const registerUrl: string = (() => {
|
||||
const url = new URL(homeserverUrl);
|
||||
url.pathname = '/_synapse/admin/v1/register';
|
||||
return url.toString();
|
||||
})();
|
||||
|
||||
const nonce = await fetch(registerUrl, { method: 'GET' }).then(res => res.json()).then((res) => (res as any).nonce);
|
||||
const password = createHash('sha256')
|
||||
.update(user.username)
|
||||
.update(sharedSecret)
|
||||
.digest('hex');
|
||||
const hmac = createHmac('sha1', sharedSecret)
|
||||
.update(nonce).update("\x00")
|
||||
.update(user.username).update("\x00")
|
||||
.update(password).update("\x00")
|
||||
.update(user.admin ? 'admin' : 'notadmin')
|
||||
.digest('hex');
|
||||
return await fetch(registerUrl, { method: "POST", body: JSON.stringify(
|
||||
{
|
||||
nonce,
|
||||
username: user.username,
|
||||
password,
|
||||
admin: user.admin,
|
||||
mac: hmac,
|
||||
}
|
||||
)}).then(res => res.json()).then(res => ({
|
||||
mxid: (res as {user_id: string}).user_id,
|
||||
client: new MatrixClient(homeserverUrl, (res as {access_token: string}).access_token),
|
||||
})).catch(err => { console.log(err.response.body); throw new Error(`Failed to register user: ${err}`); });
|
||||
}
|
37
spec/widgets.spec.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { E2ESetupTestTimeout, E2ETestEnv } from "./util/e2e-test";
|
||||
import { describe, it, beforeEach, afterEach } from "@jest/globals";
|
||||
import { getBridgeApi } from "./util/bridge-api";
|
||||
|
||||
describe('Widgets', () => {
|
||||
let testEnv: E2ETestEnv;
|
||||
|
||||
beforeEach(async () => {
|
||||
const webhooksPort = 9500 + E2ETestEnv.workerId;
|
||||
testEnv = await E2ETestEnv.createTestEnv({matrixLocalparts: ['user'], config: {
|
||||
widgets: {
|
||||
publicUrl: `http://localhost:${webhooksPort}`
|
||||
},
|
||||
listeners: [{
|
||||
port: webhooksPort,
|
||||
bindAddress: '0.0.0.0',
|
||||
// Bind to the SAME listener to ensure we don't have conflicts.
|
||||
resources: ['webhooks', 'widgets'],
|
||||
}],
|
||||
|
||||
}});
|
||||
await testEnv.setUp();
|
||||
}, E2ESetupTestTimeout);
|
||||
|
||||
afterEach(() => {
|
||||
return testEnv?.tearDown();
|
||||
});
|
||||
|
||||
it('should be able to authenticate with the widget API', async () => {
|
||||
const user = testEnv.getUser('user');
|
||||
const bridgeApi = await getBridgeApi(testEnv.opts.config?.widgets?.publicUrl!, user);
|
||||
expect(await bridgeApi.verify()).toEqual({
|
||||
"type": "widget",
|
||||
"userId": "@user:hookshot",
|
||||
});
|
||||
});
|
||||
});
|
@ -2,20 +2,20 @@
|
||||
import "reflect-metadata";
|
||||
import { AdminAccountData, AdminRoomCommandHandler, Category } from "./AdminRoomCommandHandler";
|
||||
import { botCommand, compileBotCommands, handleCommand, BotCommands, HelpFunction } from "./BotCommands";
|
||||
import { BridgeConfig, BridgePermissionLevel } from "./Config/Config";
|
||||
import { BridgeConfig, BridgePermissionLevel } from "./config/Config";
|
||||
import { BridgeRoomState, BridgeRoomStateGitHub } from "./Widgets/BridgeWidgetInterface";
|
||||
import { Endpoints } from "@octokit/types";
|
||||
import { GitHubDiscussionSpace, GitHubIssueConnection, GitHubRepoConnection } from "./Connections";
|
||||
import { ConnectionManager } from "./ConnectionManager";
|
||||
import { FormatUtil } from "./FormatUtil";
|
||||
import { GetUserResponse } from "./Gitlab/Types";
|
||||
import { GitHubBotCommands } from "./Github/AdminCommands";
|
||||
import { GithubGraphQLClient } from "./Github/GithubInstance";
|
||||
import { GitHubBotCommands } from "./github/AdminCommands";
|
||||
import { GithubGraphQLClient } from "./github/GithubInstance";
|
||||
import { GitLabClient } from "./Gitlab/Client";
|
||||
import { Intent } from "matrix-bot-sdk";
|
||||
import { JiraBotCommands } from "./Jira/AdminCommands";
|
||||
import { JiraBotCommands } from "./jira/AdminCommands";
|
||||
import { NotifFilter, NotificationFilterStateContent } from "./NotificationFilters";
|
||||
import { ProjectsListResponseData } from "./Github/Types";
|
||||
import { ProjectsListResponseData } from "./github/Types";
|
||||
import { UserTokenStore } from "./UserTokenStore";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import markdown from "markdown-it";
|
||||
|
@ -1,6 +1,6 @@
|
||||
import EventEmitter from "events";
|
||||
import { Intent } from "matrix-bot-sdk";
|
||||
import { BridgeConfig } from "./Config/Config";
|
||||
import { BridgeConfig } from "./config/Config";
|
||||
import { UserTokenStore } from "./UserTokenStore";
|
||||
|
||||
|
||||
|
@ -1,23 +1,20 @@
|
||||
import { Bridge } from "../Bridge";
|
||||
|
||||
import { BridgeConfig, parseRegistrationFile } from "../Config/Config";
|
||||
import { BridgeConfig, parseRegistrationFile } from "../config/Config";
|
||||
import { Webhooks } from "../Webhooks";
|
||||
import { MatrixSender } from "../MatrixSender";
|
||||
import { UserNotificationWatcher } from "../Notifications/UserNotificationWatcher";
|
||||
import { ListenerService } from "../ListenerService";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import { LogService } from "matrix-bot-sdk";
|
||||
import { Logger, getBridgeVersion } from "matrix-appservice-bridge";
|
||||
import { IAppserviceRegistration, LogService } from "matrix-bot-sdk";
|
||||
import { getAppservice } from "../appservice";
|
||||
import BotUsersManager from "../Managers/BotUsersManager";
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { GenericHookConnection } from "../Connections";
|
||||
|
||||
Logger.configure({console: "info"});
|
||||
const log = new Logger("App");
|
||||
|
||||
async function start() {
|
||||
const configFile = process.argv[2] || "./config.yml";
|
||||
const registrationFile = process.argv[3] || "./registration.yml";
|
||||
const config = await BridgeConfig.parseConfig(configFile, process.env);
|
||||
const registration = await parseRegistrationFile(registrationFile);
|
||||
export async function start(config: BridgeConfig, registration: IAppserviceRegistration) {
|
||||
const listener = new ListenerService(config.listeners);
|
||||
listener.start();
|
||||
Logger.configure({
|
||||
@ -37,6 +34,21 @@ async function start() {
|
||||
userNotificationWatcher.start();
|
||||
}
|
||||
|
||||
if (config.sentry) {
|
||||
Sentry.init({
|
||||
dsn: config.sentry.dsn,
|
||||
environment: config.sentry.environment,
|
||||
release: getBridgeVersion(),
|
||||
serverName: config.bridge.domain,
|
||||
includeLocalVariables: true,
|
||||
});
|
||||
log.info("Sentry reporting enabled");
|
||||
}
|
||||
|
||||
if (config.generic?.allowJsTransformationFunctions) {
|
||||
await GenericHookConnection.initialiseQuickJS();
|
||||
}
|
||||
|
||||
const botUsersManager = new BotUsersManager(config, appservice);
|
||||
|
||||
const bridgeApp = new Bridge(config, listener, appservice, storage, botUsersManager);
|
||||
@ -48,22 +60,32 @@ async function start() {
|
||||
// Don't care to await this, as the process is about to end
|
||||
storage.disconnect?.();
|
||||
});
|
||||
await bridgeApp.start();
|
||||
|
||||
// XXX: Since the webhook listener listens on /, it must listen AFTER other resources
|
||||
// have bound themselves.
|
||||
if (config.queue.monolithic) {
|
||||
const webhookHandler = new Webhooks(config);
|
||||
listener.bindResource('webhooks', webhookHandler.expressRouter);
|
||||
}
|
||||
return {
|
||||
appservice,
|
||||
bridgeApp,
|
||||
storage,
|
||||
listener,
|
||||
};
|
||||
}
|
||||
|
||||
start().catch((ex) => {
|
||||
if (Logger.root.configured) {
|
||||
log.error("BridgeApp encountered an error and has stopped:", ex);
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("BridgeApp encountered an error and has stopped", ex);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
async function startFromFile() {
|
||||
const configFile = process.argv[2] || "./config.yml";
|
||||
const registrationFile = process.argv[3] || "./registration.yml";
|
||||
const config = await BridgeConfig.parseConfig(configFile, process.env);
|
||||
const registration = await parseRegistrationFile(registrationFile);
|
||||
const { bridgeApp, listener } = await start(config, registration);
|
||||
await bridgeApp.start();
|
||||
listener.finaliseListeners();
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
startFromFile().catch((ex) => {
|
||||
if (Logger.root.configured) {
|
||||
log.error("BridgeApp encountered an error and has stopped:", ex);
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("BridgeApp encountered an error and has stopped", ex);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { BridgeConfig } from "../Config/Config";
|
||||
import { BridgeConfig } from "../config/Config";
|
||||
import { Webhooks } from "../Webhooks";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import { UserNotificationWatcher } from "../Notifications/UserNotificationWatcher";
|
||||
@ -30,6 +30,7 @@ async function start() {
|
||||
}
|
||||
const webhookHandler = new Webhooks(config);
|
||||
listener.bindResource('webhooks', webhookHandler.expressRouter);
|
||||
listener.finaliseListeners();
|
||||
const userWatcher = new UserNotificationWatcher(config);
|
||||
userWatcher.start();
|
||||
process.once("SIGTERM", () => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { BridgeConfig, parseRegistrationFile } from "../Config/Config";
|
||||
import { BridgeConfig, parseRegistrationFile } from "../config/Config";
|
||||
import { MatrixSender } from "../MatrixSender";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import Metrics from "../Metrics";
|
||||
@ -32,6 +32,7 @@ async function start() {
|
||||
listener.bindResource('metrics', Metrics.expressRouter);
|
||||
}
|
||||
}
|
||||
listener.finaliseListeners();
|
||||
sender.listen();
|
||||
process.once("SIGTERM", () => {
|
||||
log.error("Got SIGTERM");
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { rm } from "fs/promises";
|
||||
|
||||
import { BridgeConfig, parseRegistrationFile } from "../Config/Config";
|
||||
import { BridgeConfig, parseRegistrationFile } from "../config/Config";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import { LogService, MatrixClient } from "matrix-bot-sdk";
|
||||
import { getAppservice } from "../appservice";
|
||||
|
@ -1,11 +1,11 @@
|
||||
import markdown from "markdown-it";
|
||||
import stringArgv from "string-argv";
|
||||
import { ApiError } from "./api";
|
||||
import { CommandError } from "./errors";
|
||||
import { MatrixMessageContent } from "./MatrixEvent";
|
||||
import { BridgePermissionLevel } from "./Config/Config";
|
||||
import { BridgePermissionLevel } from "./config/Config";
|
||||
import { PermissionCheckFn } from "./Connections";
|
||||
|
||||
const stringArgv = import("string-argv");
|
||||
const md = new markdown();
|
||||
|
||||
export const botCommandSymbol = Symbol("botCommandMetadata");
|
||||
@ -124,7 +124,7 @@ export async function handleCommand(
|
||||
}
|
||||
command = command.substring(prefix.length);
|
||||
}
|
||||
const parts = stringArgv(command);
|
||||
const parts = (await stringArgv).parseArgsStringToArgv(command);
|
||||
for (let i = parts.length; i > 0; i--) {
|
||||
const prefix = parts.slice(0, i).join(" ").toLowerCase();
|
||||
// We have a match!
|
||||
|
151
src/Bridge.ts
@ -2,44 +2,46 @@ import { AdminAccountData } from "./AdminRoomCommandHandler";
|
||||
import { AdminRoom, BRIDGE_ROOM_TYPE, LEGACY_BRIDGE_ROOM_TYPE } from "./AdminRoom";
|
||||
import { Appservice, RichRepliesPreprocessor, IRichReplyMetadata, StateEvent, EventKind, PowerLevelsEvent, Intent } from "matrix-bot-sdk";
|
||||
import BotUsersManager from "./Managers/BotUsersManager";
|
||||
import { BridgeConfig, BridgePermissionLevel, GitLabInstance } from "./Config/Config";
|
||||
import { BridgeConfig, BridgePermissionLevel, GitLabInstance } from "./config/Config";
|
||||
import { BridgeWidgetApi } from "./Widgets/BridgeWidgetApi";
|
||||
import { CommentProcessor } from "./CommentProcessor";
|
||||
import { ConnectionManager } from "./ConnectionManager";
|
||||
import { GetIssueResponse, GetIssueOpts } from "./Gitlab/Types"
|
||||
import { GithubInstance } from "./Github/GithubInstance";
|
||||
import { GithubInstance } from "./github/GithubInstance";
|
||||
import { IBridgeStorageProvider } from "./Stores/StorageProvider";
|
||||
import { IConnection, GitHubDiscussionSpace, GitHubDiscussionConnection, GitHubUserSpace, JiraProjectConnection, GitLabRepoConnection,
|
||||
GitHubIssueConnection, GitHubProjectConnection, GitHubRepoConnection, GitLabIssueConnection, FigmaFileConnection, FeedConnection, GenericHookConnection } from "./Connections";
|
||||
GitHubIssueConnection, GitHubProjectConnection, GitHubRepoConnection, GitLabIssueConnection, FigmaFileConnection, FeedConnection, GenericHookConnection, WebhookResponse } from "./Connections";
|
||||
import { IGitLabWebhookIssueStateEvent, IGitLabWebhookMREvent, IGitLabWebhookNoteEvent, IGitLabWebhookPushEvent, IGitLabWebhookReleaseEvent, IGitLabWebhookTagPushEvent, IGitLabWebhookWikiPageEvent } from "./Gitlab/WebhookTypes";
|
||||
import { JiraIssueEvent, JiraIssueUpdatedEvent, JiraVersionEvent } from "./Jira/WebhookTypes";
|
||||
import { JiraOAuthResult } from "./Jira/Types";
|
||||
import { JiraIssueEvent, JiraIssueUpdatedEvent, JiraVersionEvent } from "./jira/WebhookTypes";
|
||||
import { JiraOAuthResult } from "./jira/Types";
|
||||
import { MatrixEvent, MatrixMemberContent, MatrixMessageContent } from "./MatrixEvent";
|
||||
import { MessageQueue, createMessageQueue } from "./MessageQueue";
|
||||
import { MessageQueue, MessageQueueMessageOut, createMessageQueue } from "./MessageQueue";
|
||||
import { MessageSenderClient } from "./MatrixSender";
|
||||
import { NotifFilter, NotificationFilterStateContent } from "./NotificationFilters";
|
||||
import { NotificationProcessor } from "./NotificationsProcessor";
|
||||
import { NotificationsEnableEvent, NotificationsDisableEvent } from "./Webhooks";
|
||||
import { GitHubOAuthToken, GitHubOAuthTokenResponse, ProjectsGetResponseData } from "./Github/Types";
|
||||
import { NotificationsEnableEvent, NotificationsDisableEvent, Webhooks } from "./Webhooks";
|
||||
import { GitHubOAuthToken, GitHubOAuthTokenResponse, ProjectsGetResponseData } from "./github/Types";
|
||||
import { retry } from "./PromiseUtil";
|
||||
import { UserNotificationsEvent } from "./Notifications/UserNotificationWatcher";
|
||||
import { UserTokenStore } from "./UserTokenStore";
|
||||
import * as GitHubWebhookTypes from "@octokit/webhooks-types";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import { Provisioner } from "./provisioning/provisioner";
|
||||
import { JiraProvisionerRouter } from "./Jira/Router";
|
||||
import { GitHubProvisionerRouter } from "./Github/Router";
|
||||
import { JiraProvisionerRouter } from "./jira/Router";
|
||||
import { GitHubProvisionerRouter } from "./github/Router";
|
||||
import { OAuthRequest } from "./WebhookTypes";
|
||||
import { promises as fs } from "fs";
|
||||
import Metrics from "./Metrics";
|
||||
import { FigmaEvent, ensureFigmaWebhooks } from "./figma";
|
||||
import { ListenerService } from "./ListenerService";
|
||||
import { SetupConnection } from "./Connections/SetupConnection";
|
||||
import { JiraOAuthRequestCloud, JiraOAuthRequestOnPrem, JiraOAuthRequestResult } from "./Jira/OAuth";
|
||||
import { JiraOAuthRequestCloud, JiraOAuthRequestOnPrem, JiraOAuthRequestResult } from "./jira/OAuth";
|
||||
import { GenericWebhookEvent, GenericWebhookEventResult } from "./generic/types";
|
||||
import { SetupWidget } from "./Widgets/SetupWidget";
|
||||
import { FeedEntry, FeedError, FeedReader, FeedSuccess } from "./feeds/FeedReader";
|
||||
import PQueue from "p-queue";
|
||||
import * as Sentry from '@sentry/node';
|
||||
|
||||
const log = new Logger("Bridge");
|
||||
|
||||
export class Bridge {
|
||||
@ -365,6 +367,18 @@ export class Bridge {
|
||||
(c, data) => c.onMergeRequestReviewed(data),
|
||||
);
|
||||
|
||||
this.bindHandlerToQueue<IGitLabWebhookMREvent, GitLabRepoConnection>(
|
||||
"gitlab.merge_request.approval",
|
||||
(data) => connManager.getConnectionsForGitLabRepo(data.project.path_with_namespace),
|
||||
(c, data) => c.onMergeRequestIndividualReview(data),
|
||||
);
|
||||
|
||||
this.bindHandlerToQueue<IGitLabWebhookMREvent, GitLabRepoConnection>(
|
||||
"gitlab.merge_request.unapproval",
|
||||
(data) => connManager.getConnectionsForGitLabRepo(data.project.path_with_namespace),
|
||||
(c, data) => c.onMergeRequestIndividualReview(data),
|
||||
);
|
||||
|
||||
this.bindHandlerToQueue<IGitLabWebhookMREvent, GitLabRepoConnection>(
|
||||
"gitlab.merge_request.update",
|
||||
(data) => connManager.getConnectionsForGitLabRepo(data.project.path_with_namespace),
|
||||
@ -597,18 +611,25 @@ export class Bridge {
|
||||
return;
|
||||
}
|
||||
let successful: boolean|null = null;
|
||||
if (this.config.generic?.waitForComplete) {
|
||||
successful = await c.onGenericHook(data.hookData);
|
||||
}
|
||||
await this.queue.push<GenericWebhookEventResult>({
|
||||
data: {successful},
|
||||
sender: "Bridge",
|
||||
eventName: `response.${id}`,
|
||||
});
|
||||
didPush = true;
|
||||
if (!this.config.generic?.waitForComplete) {
|
||||
let response: WebhookResponse|undefined;
|
||||
if (this.config.generic?.waitForComplete || c.waitForComplete) {
|
||||
const result = await c.onGenericHook(data.hookData);
|
||||
successful = result.successful;
|
||||
response = result.response;
|
||||
await this.queue.push<GenericWebhookEventResult>({
|
||||
data: {successful, response},
|
||||
sender: "Bridge",
|
||||
eventName: "response.generic-webhook.event",
|
||||
});
|
||||
} else {
|
||||
await this.queue.push<GenericWebhookEventResult>({
|
||||
data: {},
|
||||
sender: "Bridge",
|
||||
eventName: "response.generic-webhook.event",
|
||||
});
|
||||
await c.onGenericHook(data.hookData);
|
||||
}
|
||||
didPush = true;
|
||||
}
|
||||
catch (ex) {
|
||||
log.warn(`Failed to handle generic webhook`, ex);
|
||||
@ -756,28 +777,46 @@ export class Bridge {
|
||||
this.config.feeds,
|
||||
this.connectionManager,
|
||||
this.queue,
|
||||
// Use default bot when storing account data
|
||||
this.as.botClient,
|
||||
this.storage,
|
||||
);
|
||||
}
|
||||
|
||||
const webhookHandler = new Webhooks(this.config);
|
||||
this.listener.bindResource('webhooks', webhookHandler.expressRouter);
|
||||
|
||||
await this.as.begin();
|
||||
log.info(`Bridge is now ready. Found ${this.connectionManager.size} connections`);
|
||||
this.ready = true;
|
||||
}
|
||||
|
||||
private async handleHookshotEvent<EventType, ConnType extends IConnection>(msg: MessageQueueMessageOut<EventType>, connection: ConnType, handler: (c: ConnType, data: EventType) => Promise<unknown>|unknown) {
|
||||
try {
|
||||
await handler(connection, msg.data);
|
||||
} catch (e) {
|
||||
Sentry.withScope((scope) => {
|
||||
scope.setTransactionName('handleHookshotEvent');
|
||||
scope.setTags({
|
||||
eventType: msg.eventName,
|
||||
roomId: connection.roomId,
|
||||
});
|
||||
scope.setContext("connection", {
|
||||
id: connection.connectionId,
|
||||
});
|
||||
log.warn(`Connection ${connection.toString()} failed to handle ${msg.eventName}:`, e);
|
||||
Metrics.connectionsEventFailed.inc({ event: msg.eventName, connectionId: connection.connectionId });
|
||||
Sentry.captureException(e, scope);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async bindHandlerToQueue<EventType, ConnType extends IConnection>(event: string, connectionFetcher: (data: EventType) => ConnType[], handler: (c: ConnType, data: EventType) => Promise<unknown>|unknown) {
|
||||
const connectionFetcherBound = connectionFetcher.bind(this);
|
||||
this.queue.on<EventType>(event, (msg) => {
|
||||
const connections = connectionFetcher.bind(this)(msg.data);
|
||||
const connections = connectionFetcherBound(msg.data);
|
||||
log.debug(`${event} for ${connections.map(c => c.toString()).join(', ') || '[empty]'}`);
|
||||
connections.forEach(async (connection) => {
|
||||
try {
|
||||
await handler(connection, msg.data);
|
||||
} catch (ex) {
|
||||
Metrics.connectionsEventFailed.inc({ event, connectionId: connection.connectionId });
|
||||
log.warn(`Connection ${connection.toString()} failed to handle ${event}:`, ex);
|
||||
}
|
||||
})
|
||||
connections.forEach((connection) => {
|
||||
void this.handleHookshotEvent(msg, connection, handler);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -888,12 +927,24 @@ export class Bridge {
|
||||
if (!adminRoom) {
|
||||
let handled = false;
|
||||
for (const connection of this.connectionManager.getAllConnectionsForRoom(roomId)) {
|
||||
const scope = new Sentry.Scope();
|
||||
scope.setTransactionName('onRoomMessage');
|
||||
scope.setTags({
|
||||
eventId: event.event_id,
|
||||
sender: event.sender,
|
||||
eventType: event.type,
|
||||
roomId: connection.roomId,
|
||||
});
|
||||
scope.setContext("connection", {
|
||||
id: connection.connectionId,
|
||||
});
|
||||
try {
|
||||
if (connection.onMessageEvent) {
|
||||
handled = await connection.onMessageEvent(event, checkPermission, processedReplyMetadata);
|
||||
}
|
||||
} catch (ex) {
|
||||
log.warn(`Connection ${connection.toString()} failed to handle message:`, ex);
|
||||
Sentry.captureException(ex, scope);
|
||||
}
|
||||
if (handled) {
|
||||
break;
|
||||
@ -1060,6 +1111,17 @@ export class Bridge {
|
||||
if (!this.connectionManager.verifyStateEventForConnection(connection, state, true)) {
|
||||
continue;
|
||||
}
|
||||
const scope = new Sentry.Scope();
|
||||
scope.setTransactionName('onStateUpdate');
|
||||
scope.setTags({
|
||||
eventId: event.event_id,
|
||||
sender: event.sender,
|
||||
eventType: event.type,
|
||||
roomId: connection.roomId,
|
||||
});
|
||||
scope.setContext("connection", {
|
||||
id: connection.connectionId,
|
||||
});
|
||||
try {
|
||||
// Empty object == redacted
|
||||
if (event.content.disabled === true || Object.keys(event.content).length === 0) {
|
||||
@ -1086,9 +1148,9 @@ export class Bridge {
|
||||
if (this.config.widgets?.roomSetupWidget?.addOnInvite && event.type === "m.room.power_levels" && event.state_key === "" && !this.connectionManager.isRoomConnected(roomId)) {
|
||||
log.debug(`${roomId} got a new powerlevel change and isn't connected to any connections, testing to see if we should create a setup widget`)
|
||||
const plEvent = new PowerLevelsEvent(event);
|
||||
const currentPl = plEvent.content.users?.[botUser.userId] || plEvent.defaultUserLevel;
|
||||
const previousPl = plEvent.previousContent?.users?.[botUser.userId] || plEvent.previousContent?.users_default;
|
||||
const requiredPl = plEvent.content.events?.["im.vector.modular.widgets"] || plEvent.defaultStateEventLevel;
|
||||
const currentPl = plEvent.content.users?.[botUser.userId] ?? plEvent.defaultUserLevel;
|
||||
const previousPl = plEvent.previousContent?.users?.[botUser.userId] ?? plEvent.previousContent?.users_default;
|
||||
const requiredPl = plEvent.content.events?.["im.vector.modular.widgets"] ?? plEvent.defaultStateEventLevel;
|
||||
if (currentPl !== previousPl && currentPl >= requiredPl) {
|
||||
// PL changed for bot user, check to see if the widget can be created.
|
||||
try {
|
||||
@ -1110,11 +1172,24 @@ export class Bridge {
|
||||
}
|
||||
|
||||
for (const connection of this.connectionManager.getAllConnectionsForRoom(roomId)) {
|
||||
if (!connection.onEvent) {
|
||||
continue;
|
||||
}
|
||||
const scope = new Sentry.Scope();
|
||||
scope.setTransactionName('onRoomEvent');
|
||||
scope.setTags({
|
||||
eventId: event.event_id,
|
||||
sender: event.sender,
|
||||
eventType: event.type,
|
||||
roomId: connection.roomId,
|
||||
});
|
||||
scope.setContext("connection", {
|
||||
id: connection.connectionId,
|
||||
});
|
||||
try {
|
||||
if (connection.onEvent) {
|
||||
await connection.onEvent(event);
|
||||
}
|
||||
await connection.onEvent(event);
|
||||
} catch (ex) {
|
||||
Sentry.captureException(ex, scope);
|
||||
log.warn(`Connection ${connection.toString()} failed to handle onEvent:`, ex);
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { Appservice } from "matrix-bot-sdk";
|
||||
import markdown from "markdown-it";
|
||||
import mime from "mime";
|
||||
import emoji from "node-emoji";
|
||||
import { MatrixMessageContent, MatrixEvent } from "./MatrixEvent";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import axios from "axios";
|
||||
import { FormatUtil } from "./FormatUtil";
|
||||
import { IssuesGetCommentResponseData, ReposGetResponseData, IssuesGetResponseData } from "./Github/Types"
|
||||
import { IssuesGetCommentResponseData, ReposGetResponseData, IssuesGetResponseData } from "./github/Types"
|
||||
import { IGitLabWebhookNoteEvent } from "./Gitlab/WebhookTypes";
|
||||
|
||||
const REGEX_MENTION = /(^|\s)(@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})(\s|$)/ig;
|
||||
@ -14,6 +13,7 @@ const REGEX_MATRIX_MENTION = /<a href="https:\/\/matrix\.to\/#\/(.+)">(.*)<\/a>/
|
||||
const REGEX_IMAGES = /!\[.*]\((.*\.(\w+))\)/gm;
|
||||
const md = new markdown();
|
||||
const log = new Logger("CommentProcessor");
|
||||
const mime = import('mime');
|
||||
|
||||
interface IMatrixCommentEvent extends MatrixMessageContent {
|
||||
// eslint-disable-next-line camelcase
|
||||
@ -131,7 +131,7 @@ export class CommentProcessor {
|
||||
let match = REGEX_IMAGES.exec(bodyCopy);
|
||||
while (match) {
|
||||
bodyCopy = bodyCopy.replace(match[1], "");
|
||||
const contentType = mime.getType(match[1]) || "none";
|
||||
const contentType = (await mime).default.getType(match[1]) || "none";
|
||||
if (
|
||||
!contentType.startsWith("image") &&
|
||||
!contentType.startsWith("video") &&
|
||||
@ -145,7 +145,7 @@ export class CommentProcessor {
|
||||
try {
|
||||
const { data, headers } = await axios.get(rawUrl, {responseType: "arraybuffer"});
|
||||
const imageData = data;
|
||||
const contentType = headers["content-type"] || mime.getType(rawUrl) || "application/octet-stream";
|
||||
const contentType = headers["content-type"] || (await mime).default.getType(rawUrl) || "application/octet-stream";
|
||||
let url;
|
||||
if (convertToMxc) {
|
||||
url = await this.as.botIntent.underlyingClient.uploadContent(imageData, contentType);
|
||||
|
@ -6,15 +6,15 @@
|
||||
|
||||
import { Appservice, Intent, StateEvent } from "matrix-bot-sdk";
|
||||
import { ApiError, ErrCode } from "./api";
|
||||
import { BridgeConfig, BridgePermissionLevel, GitLabInstance } from "./Config/Config";
|
||||
import { BridgeConfig, BridgePermissionLevel, GitLabInstance } from "./config/Config";
|
||||
import { CommentProcessor } from "./CommentProcessor";
|
||||
import { ConnectionDeclaration, ConnectionDeclarations, GenericHookConnection, GitHubDiscussionConnection, GitHubDiscussionSpace, GitHubIssueConnection, GitHubProjectConnection, GitHubRepoConnection, GitHubUserSpace, GitLabIssueConnection, GitLabRepoConnection, IConnection, IConnectionState, JiraProjectConnection } from "./Connections";
|
||||
import { FigmaFileConnection, FeedConnection } from "./Connections";
|
||||
import { GetConnectionTypeResponseItem } from "./provisioning/api";
|
||||
import { GitLabClient } from "./Gitlab/Client";
|
||||
import { GithubInstance } from "./Github/GithubInstance";
|
||||
import { GithubInstance } from "./github/GithubInstance";
|
||||
import { IBridgeStorageProvider } from "./Stores/StorageProvider";
|
||||
import { JiraProject, JiraVersion } from "./Jira/Types";
|
||||
import { JiraProject, JiraVersion } from "./jira/Types";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import { MessageSenderClient } from "./MatrixSender";
|
||||
import { UserTokenStore } from "./UserTokenStore";
|
||||
|
@ -1,14 +1,18 @@
|
||||
import {Intent, StateEvent} from "matrix-bot-sdk";
|
||||
import { IConnection, IConnectionState, InstantiateConnectionOpts } from ".";
|
||||
import { ApiError, ErrCode } from "../api";
|
||||
import { FeedEntry, FeedError, FeedReader} from "../feeds/FeedReader";
|
||||
import { FeedEntry, FeedError} from "../feeds/FeedReader";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import { BaseConnection } from "./BaseConnection";
|
||||
import markdown from "markdown-it";
|
||||
import { Connection, ProvisionConnectionOpts } from "./IConnection";
|
||||
import { GetConnectionsResponseItem } from "../provisioning/api";
|
||||
import { readFeed, sanitizeHtml } from "../libRs";
|
||||
import UserAgent from "../UserAgent";
|
||||
const log = new Logger("FeedConnection");
|
||||
const md = new markdown();
|
||||
const md = new markdown({
|
||||
html: true,
|
||||
});
|
||||
|
||||
export interface LastResultOk {
|
||||
timestamp: number;
|
||||
@ -22,10 +26,10 @@ export interface LastResultFail {
|
||||
|
||||
|
||||
export interface FeedConnectionState extends IConnectionState {
|
||||
url: string;
|
||||
label?: string;
|
||||
template?: string;
|
||||
notifyOnFailure?: boolean;
|
||||
url: string;
|
||||
label: string|undefined;
|
||||
template: string|undefined;
|
||||
notifyOnFailure: boolean|undefined;
|
||||
}
|
||||
|
||||
export interface FeedConnectionSecrets {
|
||||
@ -35,7 +39,7 @@ export interface FeedConnectionSecrets {
|
||||
export type FeedResponseItem = GetConnectionsResponseItem<FeedConnectionState, FeedConnectionSecrets>;
|
||||
|
||||
const MAX_LAST_RESULT_ITEMS = 5;
|
||||
const VALIDATION_FETCH_TIMEOUT_MS = 5000;
|
||||
const VALIDATION_FETCH_TIMEOUT_S = 5;
|
||||
const MAX_SUMMARY_LENGTH = 512;
|
||||
const MAX_TEMPLATE_LENGTH = 1024;
|
||||
|
||||
@ -65,7 +69,10 @@ export class FeedConnection extends BaseConnection implements IConnection {
|
||||
}
|
||||
|
||||
try {
|
||||
await FeedReader.fetchFeed(url, {}, VALIDATION_FETCH_TIMEOUT_MS);
|
||||
await readFeed(url, {
|
||||
userAgent: UserAgent,
|
||||
pollTimeoutSeconds: VALIDATION_FETCH_TIMEOUT_S,
|
||||
});
|
||||
} catch (ex) {
|
||||
throw new ApiError(`Could not read feed from URL: ${ex.message}`, ErrCode.BadValue);
|
||||
}
|
||||
@ -90,8 +97,11 @@ export class FeedConnection extends BaseConnection implements IConnection {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof data.notifyOnFailure !== 'undefined' && typeof data.notifyOnFailure !== 'boolean') {
|
||||
throw new ApiError('notifyOnFailure must be a boolean', ErrCode.BadValue);
|
||||
}
|
||||
|
||||
return { url, label: data.label, template: data.template };
|
||||
return { url, label: data.label, template: data.template, notifyOnFailure: data.notifyOnFailure };
|
||||
}
|
||||
|
||||
static async provisionConnection(roomId: string, _userId: string, data: Record<string, unknown> = {}, { intent, config }: ProvisionConnectionOpts) {
|
||||
@ -126,6 +136,8 @@ export class FeedConnection extends BaseConnection implements IConnection {
|
||||
config: {
|
||||
url: this.feedUrl,
|
||||
label: this.state.label,
|
||||
template: this.state.template,
|
||||
notifyOnFailure: this.state.notifyOnFailure,
|
||||
},
|
||||
secrets: {
|
||||
lastResults: this.lastResults,
|
||||
@ -182,6 +194,15 @@ export class FeedConnection extends BaseConnection implements IConnection {
|
||||
}
|
||||
|
||||
public async handleFeedEntry(entry: FeedEntry): Promise<void> {
|
||||
// We will need to tidy this up.
|
||||
if (this.state.template?.match(/\$SUMMARY\b/) && entry.summary) {
|
||||
// This might be massive and cause us to fail to send the message
|
||||
// so confine to a maximum size.
|
||||
if (entry.summary.length > MAX_SUMMARY_LENGTH) {
|
||||
entry.summary = entry.summary.substring(0, MAX_SUMMARY_LENGTH) + "…";
|
||||
}
|
||||
entry.summary = sanitizeHtml(entry.summary);
|
||||
}
|
||||
|
||||
let message;
|
||||
if (this.state.template) {
|
||||
@ -194,12 +215,6 @@ export class FeedConnection extends BaseConnection implements IConnection {
|
||||
message = this.templateFeedEntry(DEFAULT_TEMPLATE, entry);
|
||||
}
|
||||
|
||||
// This might be massive and cause us to fail to send the message
|
||||
// so confine to a maximum size.
|
||||
if (entry.summary?.length ?? 0 > MAX_SUMMARY_LENGTH) {
|
||||
entry.summary = entry.summary?.substring(0, MAX_SUMMARY_LENGTH) + "…" ?? null;
|
||||
}
|
||||
|
||||
await this.intent.sendEvent(this.roomId, {
|
||||
msgtype: 'm.notice',
|
||||
format: "org.matrix.custom.html",
|
||||
|
@ -5,7 +5,7 @@ import { BaseConnection } from "./BaseConnection";
|
||||
import { IConnection, IConnectionState } from ".";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import { IBridgeStorageProvider } from "../Stores/StorageProvider";
|
||||
import { BridgeConfig } from "../Config/Config";
|
||||
import { BridgeConfig } from "../config/Config";
|
||||
import { Connection, InstantiateConnectionOpts, ProvisionConnectionOpts } from "./IConnection";
|
||||
import { ConfigGrantChecker, GrantChecker } from "../grants/GrantCheck";
|
||||
|
||||
|
@ -2,13 +2,13 @@ import { Connection, IConnection, IConnectionState, InstantiateConnectionOpts, P
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import { MessageSenderClient } from "../MatrixSender"
|
||||
import markdownit from "markdown-it";
|
||||
import { VMScript as Script, NodeVM } from "vm2";
|
||||
import { QuickJSWASMModule, newQuickJSWASMModule, shouldInterruptAfterDeadline } from "quickjs-emscripten";
|
||||
import { MatrixEvent } from "../MatrixEvent";
|
||||
import { Appservice, Intent, StateEvent } from "matrix-bot-sdk";
|
||||
import { ApiError, ErrCode } from "../api";
|
||||
import { BaseConnection } from "./BaseConnection";
|
||||
import { GetConnectionsResponseItem } from "../provisioning/api";
|
||||
import { BridgeConfigGenericWebhooks } from "../Config/Config";
|
||||
import { BridgeConfigGenericWebhooks } from "../config/Config";
|
||||
import { ensureUserIsInRoom } from "../IntentUtils";
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
@ -21,7 +21,11 @@ export interface GenericHookConnectionState extends IConnectionState {
|
||||
* The name given in the provisioning UI and displaynames.
|
||||
*/
|
||||
name: string;
|
||||
transformationFunction?: string;
|
||||
transformationFunction: string|undefined;
|
||||
/**
|
||||
* Should the webhook only respond on completion.
|
||||
*/
|
||||
waitForComplete: boolean|undefined;
|
||||
}
|
||||
|
||||
export interface GenericHookSecrets {
|
||||
@ -45,12 +49,19 @@ export interface GenericHookAccountData {
|
||||
[hookId: string]: string;
|
||||
}
|
||||
|
||||
export interface WebhookResponse {
|
||||
body: string;
|
||||
contentType?: string;
|
||||
statusCode?: number;
|
||||
}
|
||||
|
||||
interface WebhookTransformationResult {
|
||||
version: string;
|
||||
plain?: string;
|
||||
html?: string;
|
||||
msgtype?: string;
|
||||
empty?: boolean;
|
||||
webhookResponse?: WebhookResponse;
|
||||
}
|
||||
|
||||
const log = new Logger("GenericHookConnection");
|
||||
@ -65,6 +76,11 @@ const SANITIZE_MAX_BREADTH = 50;
|
||||
*/
|
||||
@Connection
|
||||
export class GenericHookConnection extends BaseConnection implements IConnection {
|
||||
private static quickModule?: QuickJSWASMModule;
|
||||
|
||||
public static async initialiseQuickJS() {
|
||||
GenericHookConnection.quickModule = await newQuickJSWASMModule();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a JSON payload is compatible with Matrix JSON requirements, such
|
||||
@ -107,27 +123,30 @@ export class GenericHookConnection extends BaseConnection implements IConnection
|
||||
return obj;
|
||||
}
|
||||
|
||||
static validateState(state: Record<string, unknown>, allowJsTransformationFunctions?: boolean): GenericHookConnectionState {
|
||||
const {name, transformationFunction} = state;
|
||||
let transformationFunctionResult: string|undefined;
|
||||
if (transformationFunction) {
|
||||
if (allowJsTransformationFunctions !== undefined && !allowJsTransformationFunctions) {
|
||||
throw new ApiError('Transformation functions are not allowed', ErrCode.DisabledFeature);
|
||||
}
|
||||
if (typeof transformationFunction !== "string") {
|
||||
throw new ApiError('Transformation functions must be a string', ErrCode.BadValue);
|
||||
}
|
||||
transformationFunctionResult = transformationFunction;
|
||||
}
|
||||
static validateState(state: Record<string, unknown>): GenericHookConnectionState {
|
||||
const {name, transformationFunction, waitForComplete} = state;
|
||||
if (!name) {
|
||||
throw new ApiError('Missing name', ErrCode.BadValue);
|
||||
}
|
||||
if (typeof name !== "string" || name.length < 3 || name.length > 64) {
|
||||
throw new ApiError("'name' must be a string between 3-64 characters long", ErrCode.BadValue);
|
||||
}
|
||||
if (waitForComplete !== undefined && typeof waitForComplete !== "boolean") {
|
||||
throw new ApiError("'waitForComplete' must be a boolean", ErrCode.BadValue);
|
||||
}
|
||||
// Use !=, not !==, to check for both undefined and null
|
||||
if (transformationFunction != undefined) {
|
||||
if (!this.quickModule) {
|
||||
throw new ApiError('Transformation functions are not allowed', ErrCode.DisabledFeature);
|
||||
}
|
||||
if (typeof transformationFunction !== "string") {
|
||||
throw new ApiError('Transformation functions must be a string', ErrCode.BadValue);
|
||||
}
|
||||
}
|
||||
return {
|
||||
name,
|
||||
...(transformationFunctionResult && {transformationFunction: transformationFunctionResult}),
|
||||
transformationFunction: transformationFunction || undefined,
|
||||
waitForComplete,
|
||||
};
|
||||
}
|
||||
|
||||
@ -163,7 +182,7 @@ export class GenericHookConnection extends BaseConnection implements IConnection
|
||||
throw Error('Generic Webhooks are not configured');
|
||||
}
|
||||
const hookId = randomUUID();
|
||||
const validState = GenericHookConnection.validateState(data, config.generic.allowJsTransformationFunctions || false);
|
||||
const validState = GenericHookConnection.validateState(data);
|
||||
await GenericHookConnection.ensureRoomAccountData(roomId, intent, hookId, validState.name);
|
||||
await intent.underlyingClient.sendStateEvent(roomId, this.CanonicalEventType, validState.name, validState);
|
||||
const connection = new GenericHookConnection(roomId, validState, hookId, validState.name, messageClient, config.generic, as, intent);
|
||||
@ -197,8 +216,11 @@ export class GenericHookConnection extends BaseConnection implements IConnection
|
||||
GenericHookConnection.LegacyCanonicalEventType,
|
||||
];
|
||||
|
||||
private transformationFunction?: Script;
|
||||
private transformationFunction?: string;
|
||||
private cachedDisplayname?: string;
|
||||
/**
|
||||
* @param state Should be a pre-validated state object returned by {@link validateState}
|
||||
*/
|
||||
constructor(
|
||||
roomId: string,
|
||||
private state: GenericHookConnectionState,
|
||||
@ -210,11 +232,19 @@ export class GenericHookConnection extends BaseConnection implements IConnection
|
||||
private readonly intent: Intent,
|
||||
) {
|
||||
super(roomId, stateKey, GenericHookConnection.CanonicalEventType);
|
||||
if (state.transformationFunction && config.allowJsTransformationFunctions) {
|
||||
this.transformationFunction = new Script(state.transformationFunction);
|
||||
if (state.transformationFunction && GenericHookConnection.quickModule) {
|
||||
this.transformationFunction = state.transformationFunction;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should the webhook handler wait for this to finish before
|
||||
* sending a response back.
|
||||
*/
|
||||
public get waitForComplete(): boolean {
|
||||
return this.state.waitForComplete ?? false;
|
||||
}
|
||||
|
||||
public get priority(): number {
|
||||
return this.state.priority || super.priority;
|
||||
}
|
||||
@ -259,12 +289,26 @@ export class GenericHookConnection extends BaseConnection implements IConnection
|
||||
}
|
||||
|
||||
public async onStateUpdate(stateEv: MatrixEvent<unknown>) {
|
||||
const validatedConfig = GenericHookConnection.validateState(stateEv.content as Record<string, unknown>, this.config.allowJsTransformationFunctions || false);
|
||||
const validatedConfig = GenericHookConnection.validateState(stateEv.content as Record<string, unknown>);
|
||||
if (validatedConfig.transformationFunction) {
|
||||
try {
|
||||
this.transformationFunction = new Script(validatedConfig.transformationFunction);
|
||||
} catch (ex) {
|
||||
await this.messageClient.sendMatrixText(this.roomId, 'Could not compile transformation function:' + ex, "m.text", this.intent.userId);
|
||||
const ctx = GenericHookConnection.quickModule!.newContext();
|
||||
const codeEvalResult = ctx.evalCode(`function f(data) {${validatedConfig.transformationFunction}}`, undefined, { compileOnly: true });
|
||||
if (codeEvalResult.error) {
|
||||
const errorString = JSON.stringify(ctx.dump(codeEvalResult.error), null, 2);
|
||||
codeEvalResult.error.dispose();
|
||||
ctx.dispose();
|
||||
|
||||
const errorPrefix = "Could not compile transformation function:";
|
||||
await this.intent.sendEvent(this.roomId, {
|
||||
msgtype: "m.text",
|
||||
body: errorPrefix + "\n\n```json\n\n" + errorString + "\n\n```",
|
||||
formatted_body: `<p>${errorPrefix}</p><p><pre><code class=\\"language-json\\">${errorString}</code></pre></p>`,
|
||||
format: "org.matrix.custom.html",
|
||||
});
|
||||
} else {
|
||||
codeEvalResult.value.dispose();
|
||||
ctx.dispose();
|
||||
this.transformationFunction = validatedConfig.transformationFunction;
|
||||
}
|
||||
} else {
|
||||
this.transformationFunction = undefined;
|
||||
@ -281,8 +325,10 @@ export class GenericHookConnection extends BaseConnection implements IConnection
|
||||
} else if (typeof safeData?.text === "string") {
|
||||
msg.plain = safeData.text;
|
||||
} else {
|
||||
msg.plain = "Received webhook data:\n\n" + "```json\n\n" + JSON.stringify(data, null, 2) + "\n\n```";
|
||||
msg.html = `<p>Received webhook data:</p><p><pre><code class=\\"language-json\\">${JSON.stringify(data, null, 2)}</code></pre></p>`
|
||||
const dataString = JSON.stringify(data, null, 2);
|
||||
const dataPrefix = "Received webhook data:";
|
||||
msg.plain = dataPrefix + "\n\n```json\n\n" + dataString + "\n\n```";
|
||||
msg.html = `<p>${dataPrefix}</p><p><pre><code class=\\"language-json\\">${dataString}</code></pre></p>`
|
||||
}
|
||||
|
||||
if (typeof safeData?.html === "string") {
|
||||
@ -300,52 +346,76 @@ export class GenericHookConnection extends BaseConnection implements IConnection
|
||||
return msg;
|
||||
}
|
||||
|
||||
public executeTransformationFunction(data: unknown): {plain: string, html?: string, msgtype?: string}|null {
|
||||
public executeTransformationFunction(data: unknown): {content?: {plain: string, html?: string, msgtype?: string}, webhookResponse?: WebhookResponse} {
|
||||
if (!this.transformationFunction) {
|
||||
throw Error('Transformation function not defined');
|
||||
}
|
||||
const vm = new NodeVM({
|
||||
console: 'off',
|
||||
wrapper: 'none',
|
||||
wasm: false,
|
||||
eval: false,
|
||||
timeout: TRANSFORMATION_TIMEOUT_MS,
|
||||
});
|
||||
vm.setGlobal('HookshotApiVersion', 'v2');
|
||||
vm.setGlobal('data', data);
|
||||
vm.run(this.transformationFunction);
|
||||
const result = vm.getGlobal('result');
|
||||
let result;
|
||||
const ctx = GenericHookConnection.quickModule!.newContext();
|
||||
ctx.runtime.setInterruptHandler(shouldInterruptAfterDeadline(Date.now() + TRANSFORMATION_TIMEOUT_MS));
|
||||
try {
|
||||
ctx.setProp(ctx.global, 'HookshotApiVersion', ctx.newString('v2'));
|
||||
const ctxResult = ctx.evalCode(`const data = ${JSON.stringify(data)};\n(() => { ${this.state.transformationFunction} })();`);
|
||||
|
||||
if (ctxResult.error) {
|
||||
const e = Error(`Transformation failed to run: ${JSON.stringify(ctx.dump(ctxResult.error))}`);
|
||||
ctxResult.error.dispose();
|
||||
throw e;
|
||||
} else {
|
||||
const value = ctx.getProp(ctx.global, 'result');
|
||||
result = ctx.dump(value);
|
||||
value.dispose();
|
||||
ctxResult.value.dispose();
|
||||
}
|
||||
} finally {
|
||||
ctx.global.dispose();
|
||||
ctx.dispose();
|
||||
}
|
||||
|
||||
// Legacy v1 api
|
||||
if (typeof result === "string") {
|
||||
return {plain: `Received webhook: ${result}`};
|
||||
return {content: {plain: `Received webhook: ${result}`}};
|
||||
} else if (typeof result !== "object") {
|
||||
return {plain: `No content`};
|
||||
return {content: {plain: `No content`}};
|
||||
}
|
||||
const transformationResult = result as WebhookTransformationResult;
|
||||
if (transformationResult.version !== "v2") {
|
||||
throw Error("Result returned from transformation didn't specify version = v2");
|
||||
}
|
||||
|
||||
if (transformationResult.empty) {
|
||||
return null; // No-op
|
||||
let content;
|
||||
if (!transformationResult.empty) {
|
||||
if (typeof transformationResult.plain !== "string") {
|
||||
throw Error("Result returned from transformation didn't provide a string value for plain");
|
||||
}
|
||||
if (transformationResult.html !== undefined && typeof transformationResult.html !== "string") {
|
||||
throw Error("Result returned from transformation didn't provide a string value for html");
|
||||
}
|
||||
if (transformationResult.msgtype !== undefined && typeof transformationResult.msgtype !== "string") {
|
||||
throw Error("Result returned from transformation didn't provide a string value for msgtype");
|
||||
}
|
||||
content = {
|
||||
plain: transformationResult.plain,
|
||||
html: transformationResult.html,
|
||||
msgtype: transformationResult.msgtype,
|
||||
};
|
||||
}
|
||||
|
||||
const plain = transformationResult.plain;
|
||||
if (typeof plain !== "string") {
|
||||
throw Error("Result returned from transformation didn't provide a string value for plain");
|
||||
}
|
||||
if (transformationResult.html && typeof transformationResult.html !== "string") {
|
||||
throw Error("Result returned from transformation didn't provide a string value for html");
|
||||
}
|
||||
if (transformationResult.msgtype && typeof transformationResult.msgtype !== "string") {
|
||||
throw Error("Result returned from transformation didn't provide a string value for msgtype");
|
||||
if (transformationResult.webhookResponse) {
|
||||
if (typeof transformationResult.webhookResponse.body !== "string") {
|
||||
throw Error("Result returned from transformation didn't provide a string value for webhookResponse.body");
|
||||
}
|
||||
if (transformationResult.webhookResponse.statusCode !== undefined && typeof transformationResult.webhookResponse.statusCode !== "number" && Number.isInteger(transformationResult.webhookResponse.statusCode)) {
|
||||
throw Error("Result returned from transformation didn't provide a number value for webhookResponse.statusCode");
|
||||
}
|
||||
if (transformationResult.webhookResponse.contentType !== undefined && typeof transformationResult.webhookResponse.contentType !== "string") {
|
||||
throw Error("Result returned from transformation didn't provide a contentType value for msgtype");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
plain: plain,
|
||||
html: transformationResult.html,
|
||||
msgtype: transformationResult.msgtype,
|
||||
content,
|
||||
webhookResponse: transformationResult.webhookResponse,
|
||||
}
|
||||
}
|
||||
|
||||
@ -354,45 +424,49 @@ export class GenericHookConnection extends BaseConnection implements IConnection
|
||||
* @param data Structured data. This may either be a string, or an object.
|
||||
* @returns `true` if the webhook completed, or `false` if it failed to complete
|
||||
*/
|
||||
public async onGenericHook(data: unknown): Promise<boolean> {
|
||||
public async onGenericHook(data: unknown): Promise<{successful: boolean, response?: WebhookResponse}> {
|
||||
log.info(`onGenericHook ${this.roomId} ${this.hookId}`);
|
||||
let content: {plain: string, html?: string, msgtype?: string};
|
||||
let success = true;
|
||||
let content: {plain: string, html?: string, msgtype?: string}|undefined;
|
||||
let webhookResponse: WebhookResponse|undefined;
|
||||
let successful = true;
|
||||
if (!this.transformationFunction) {
|
||||
content = this.transformHookData(data);
|
||||
} else {
|
||||
try {
|
||||
const potentialContent = this.executeTransformationFunction(data);
|
||||
if (potentialContent === null) {
|
||||
// Explitly no action
|
||||
return true;
|
||||
}
|
||||
content = potentialContent;
|
||||
const result = this.executeTransformationFunction(data);
|
||||
content = result.content;
|
||||
webhookResponse = result.webhookResponse;
|
||||
} catch (ex) {
|
||||
log.warn(`Failed to run transformation function`, ex);
|
||||
content = {plain: `Webhook received but failed to process via transformation function`};
|
||||
success = false;
|
||||
successful = false;
|
||||
}
|
||||
}
|
||||
|
||||
const sender = this.getUserId();
|
||||
const senderIntent = this.as.getIntentForUserId(sender);
|
||||
await this.ensureDisplayname(senderIntent);
|
||||
if (content) {
|
||||
const sender = this.getUserId();
|
||||
const senderIntent = this.as.getIntentForUserId(sender);
|
||||
await this.ensureDisplayname(senderIntent);
|
||||
|
||||
await ensureUserIsInRoom(senderIntent, this.intent.underlyingClient, this.roomId);
|
||||
|
||||
// Matrix cannot handle float data, so make sure we parse out any floats.
|
||||
const safeData = GenericHookConnection.sanitiseObjectForMatrixJSON(data);
|
||||
|
||||
await this.messageClient.sendMatrixMessage(this.roomId, {
|
||||
msgtype: content.msgtype || "m.notice",
|
||||
body: content.plain,
|
||||
// render can output redundant trailing newlines, so trim it.
|
||||
formatted_body: content.html || md.render(content.plain).trim(),
|
||||
format: "org.matrix.custom.html",
|
||||
"uk.half-shot.hookshot.webhook_data": safeData,
|
||||
}, 'm.room.message', sender);
|
||||
}
|
||||
|
||||
await ensureUserIsInRoom(senderIntent, this.intent.underlyingClient, this.roomId);
|
||||
|
||||
// Matrix cannot handle float data, so make sure we parse out any floats.
|
||||
const safeData = GenericHookConnection.sanitiseObjectForMatrixJSON(data);
|
||||
|
||||
await this.messageClient.sendMatrixMessage(this.roomId, {
|
||||
msgtype: content.msgtype || "m.notice",
|
||||
body: content.plain,
|
||||
// render can output redundant trailing newlines, so trim it.
|
||||
formatted_body: content.html || md.render(content.plain).trim(),
|
||||
format: "org.matrix.custom.html",
|
||||
"uk.half-shot.hookshot.webhook_data": safeData,
|
||||
}, 'm.room.message', sender);
|
||||
return success;
|
||||
return {
|
||||
successful,
|
||||
response: webhookResponse,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@ -412,6 +486,7 @@ export class GenericHookConnection extends BaseConnection implements IConnection
|
||||
id: this.connectionId,
|
||||
config: {
|
||||
transformationFunction: this.state.transformationFunction,
|
||||
waitForComplete: this.waitForComplete,
|
||||
name: this.state.name,
|
||||
},
|
||||
...(showSecrets ? { secrets: {
|
||||
@ -437,7 +512,7 @@ export class GenericHookConnection extends BaseConnection implements IConnection
|
||||
public async provisionerUpdateConfig(userId: string, config: Record<string, unknown>) {
|
||||
// Apply previous state to the current config, as provisioners might not return "unknown" keys.
|
||||
config = { ...this.state, ...config };
|
||||
const validatedConfig = GenericHookConnection.validateState(config, this.config.allowJsTransformationFunctions || false);
|
||||
const validatedConfig = GenericHookConnection.validateState(config);
|
||||
await this.intent.underlyingClient.sendStateEvent(this.roomId, GenericHookConnection.CanonicalEventType, this.stateKey,
|
||||
{
|
||||
...validatedConfig,
|
||||
|
@ -9,10 +9,10 @@ import { Discussion } from "@octokit/webhooks-types";
|
||||
import emoji from "node-emoji";
|
||||
import markdown from "markdown-it";
|
||||
import { DiscussionCommentCreatedEvent } from "@octokit/webhooks-types";
|
||||
import { GithubGraphQLClient } from "../Github/GithubInstance";
|
||||
import { GithubGraphQLClient } from "../github/GithubInstance";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import { BaseConnection } from "./BaseConnection";
|
||||
import { BridgeConfig, BridgeConfigGitHub } from "../Config/Config";
|
||||
import { BridgeConfig, BridgeConfigGitHub } from "../config/Config";
|
||||
import { ConfigGrantChecker, GrantChecker } from "../grants/GrantCheck";
|
||||
import QuickLRU from "@alloc/quick-lru";
|
||||
export interface GitHubDiscussionConnectionState {
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { Connection, IConnection, InstantiateConnectionOpts } from "./IConnection";
|
||||
import { Appservice, Space, StateEvent } from "matrix-bot-sdk";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import { ReposGetResponseData } from "../Github/Types";
|
||||
import { ReposGetResponseData } from "../github/Types";
|
||||
import axios from "axios";
|
||||
import { GitHubDiscussionConnection } from "./GithubDiscussion";
|
||||
import { GithubInstance } from "../Github/GithubInstance";
|
||||
import { GithubInstance } from "../github/GithubInstance";
|
||||
import { BaseConnection } from "./BaseConnection";
|
||||
import { ConfigGrantChecker, GrantChecker } from "../grants/GrantCheck";
|
||||
import { BridgeConfig } from "../Config/Config";
|
||||
import { BridgeConfig } from "../config/Config";
|
||||
|
||||
const log = new Logger("GitHubDiscussionSpace");
|
||||
|
||||
|
@ -9,11 +9,11 @@ import { MessageSenderClient } from "../MatrixSender";
|
||||
import { ensureUserIsInRoom, getIntentForUser } from "../IntentUtils";
|
||||
import { FormatUtil } from "../FormatUtil";
|
||||
import axios from "axios";
|
||||
import { GithubInstance } from "../Github/GithubInstance";
|
||||
import { IssuesGetCommentResponseData, IssuesGetResponseData, ReposGetResponseData} from "../Github/Types";
|
||||
import { GithubInstance } from "../github/GithubInstance";
|
||||
import { IssuesGetCommentResponseData, IssuesGetResponseData, ReposGetResponseData} from "../github/Types";
|
||||
import { IssuesEditedEvent, IssueCommentCreatedEvent } from "@octokit/webhooks-types";
|
||||
import { BaseConnection } from "./BaseConnection";
|
||||
import { BridgeConfigGitHub } from "../Config/Config";
|
||||
import { BridgeConfigGitHub } from "../config/Config";
|
||||
|
||||
export interface GitHubIssueConnectionState {
|
||||
org: string;
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Connection, IConnection, InstantiateConnectionOpts } from "./IConnection";
|
||||
import { Appservice, Intent, StateEvent } from "matrix-bot-sdk";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import { ProjectsGetResponseData } from "../Github/Types";
|
||||
import { ProjectsGetResponseData } from "../github/Types";
|
||||
import { BaseConnection } from "./BaseConnection";
|
||||
import { ConfigGrantChecker, GrantChecker } from "../grants/GrantCheck";
|
||||
import { BridgeConfig } from "../Config/Config";
|
||||
import { BridgeConfig } from "../config/Config";
|
||||
|
||||
export interface GitHubProjectConnectionState {
|
||||
// eslint-disable-next-line camelcase
|
||||
|
@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import { Appservice, Intent, IRichReplyMetadata, StateEvent } from "matrix-bot-sdk";
|
||||
import { BotCommands, botCommand, compileBotCommands, HelpFunction } from "../BotCommands";
|
||||
import { CommentProcessor } from "../CommentProcessor";
|
||||
@ -13,22 +12,22 @@ import { IssuesOpenedEvent, IssuesReopenedEvent, IssuesEditedEvent, PullRequestO
|
||||
import { MatrixMessageContent, MatrixEvent, MatrixReactionContent } from "../MatrixEvent";
|
||||
import { MessageSenderClient } from "../MatrixSender";
|
||||
import { CommandError, NotLoggedInError } from "../errors";
|
||||
import { NAMELESS_ORG_PLACEHOLDER, ReposGetResponseData } from "../Github/Types";
|
||||
import { ReposGetResponseData } from "../github/Types";
|
||||
import { UserTokenStore } from "../UserTokenStore";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import emoji from "node-emoji";
|
||||
import { emojify } from "node-emoji";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import markdown from "markdown-it";
|
||||
import { CommandConnection } from "./CommandConnection";
|
||||
import { GithubInstance } from "../Github/GithubInstance";
|
||||
import { getNameForGitHubAccount, GithubInstance } from "../github/GithubInstance";
|
||||
import { GitHubIssueConnection } from "./GithubIssue";
|
||||
import { BridgeConfigGitHub } from "../Config/Config";
|
||||
import { BridgeConfigGitHub } from "../config/Config";
|
||||
import { ApiError, ErrCode, ValidatorApiError } from "../api";
|
||||
import { PermissionCheckFn } from ".";
|
||||
import { GitHubRepoMessageBody, MinimalGitHubIssue } from "../libRs";
|
||||
import Ajv, { JSONSchemaType } from "ajv";
|
||||
import { HookFilter } from "../HookFilter";
|
||||
import { GitHubGrantChecker } from "../Github/GrantChecker";
|
||||
import { GitHubGrantChecker } from "../github/GrantChecker";
|
||||
|
||||
const log = new Logger("GitHubRepoConnection");
|
||||
const md = new markdown();
|
||||
@ -316,13 +315,14 @@ const EMOJI_TO_REVIEW_STATE = {
|
||||
};
|
||||
|
||||
const WORKFLOW_CONCLUSION_TO_NOTICE: Record<WorkflowRunCompletedEvent["workflow_run"]["conclusion"], string> = {
|
||||
success: "completed sucessfully 🎉",
|
||||
success: "completed successfully 🎉",
|
||||
failure: "failed 😟",
|
||||
neutral: "completed neutrally 😐",
|
||||
cancelled: "was cancelled 🙅",
|
||||
timed_out: "timed out ⏰",
|
||||
action_required: "requires further action 🖱️",
|
||||
stale: "completed, but is stale 🍞"
|
||||
stale: "completed, but is stale 🍞",
|
||||
skipped: "skipped ⏭️"
|
||||
}
|
||||
|
||||
const TRUNCATE_COMMENT_SIZE = 256;
|
||||
@ -669,7 +669,7 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
if (this.showIssueRoomLink) {
|
||||
message += ` [Issue Room](https://matrix.to/#/${this.as.getAlias(GitHubIssueConnection.generateAliasLocalpart(this.org, this.repo, issue.number))})`;
|
||||
}
|
||||
const content = emoji.emojify(message);
|
||||
const content = emojify(message);
|
||||
await this.intent.sendEvent(this.roomId, {
|
||||
msgtype: "m.notice",
|
||||
body: content ,
|
||||
@ -698,7 +698,7 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
)
|
||||
);
|
||||
// Typescript is dumb.
|
||||
// @ts-ignore - property is used
|
||||
// @ts-expect-error - property is used
|
||||
const reviewEvent = reviewKey && EMOJI_TO_REVIEW_STATE[reviewKey];
|
||||
if (body && repoInfo && pullRequestId && reviewEvent) {
|
||||
log.info(`Handling reply to PR ${pullRequestId}`);
|
||||
@ -862,7 +862,7 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
inputs: workflowArgs,
|
||||
});
|
||||
} catch (ex) {
|
||||
const httpError = ex as AxiosError;
|
||||
const httpError = ex as AxiosError<{message: string}>;
|
||||
if (httpError.response?.data) {
|
||||
throw new CommandError(httpError.response?.data.message, httpError.response?.data.message);
|
||||
}
|
||||
@ -885,7 +885,8 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
}
|
||||
const orgRepoName = event.repository.full_name;
|
||||
|
||||
let message = `**${event.issue.user.login}** created new issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${event.issue.title}"`;
|
||||
const icon = '📥';
|
||||
let message = emojify(`${icon} **${event.issue.user.login}** created new issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${event.issue.title}"`);
|
||||
message += (event.issue.assignee ? ` assigned to ${event.issue.assignee.login}` : '');
|
||||
if (this.showIssueRoomLink) {
|
||||
const appInstance = await this.githubInstance.getSafeOctokitForRepo(this.org, this.repo);
|
||||
@ -895,7 +896,7 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
log.warn(`Cannot show issue room link, no app install for ${orgRepoName}`);
|
||||
}
|
||||
}
|
||||
const content = emoji.emojify(message);
|
||||
const content = emojify(message);
|
||||
const labels = FormatUtil.formatLabels(event.issue.labels?.map(l => ({ name: l.name, description: l.description || undefined, color: l.color || undefined })));
|
||||
await this.intent.sendEvent(this.roomId, {
|
||||
msgtype: "m.notice",
|
||||
@ -907,12 +908,13 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
}
|
||||
|
||||
public async onIssueCommentCreated(event: IssueCommentCreatedEvent) {
|
||||
if (this.hookFilter.shouldSkip('issue.comment.created', 'issue.comment', 'issue') || !this.matchesLabelFilter(event.issue)) {
|
||||
if (this.hookFilter.shouldSkip('issue.comment.created', 'issue.comment') || !this.matchesLabelFilter(event.issue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let message = `**${event.comment.user.login}** [commented](${event.issue.html_url}) on [${event.repository.full_name}#${event.issue.number}](${event.issue.html_url}) `;
|
||||
message += "\n > " + event.comment.body.substring(0, TRUNCATE_COMMENT_SIZE) + (event.comment.body.length > TRUNCATE_COMMENT_SIZE ? "…" : "");
|
||||
|
||||
const icon = '🗣';
|
||||
let message = emojify(`${icon} **${event.comment.user.login}** [commented](${event.issue.html_url}) on [${event.repository.full_name}#${event.issue.number}](${event.issue.html_url}) `);
|
||||
message += "\n> " + event.comment.body.substring(0, TRUNCATE_COMMENT_SIZE) + (event.comment.body.length > TRUNCATE_COMMENT_SIZE ? "…" : "");
|
||||
|
||||
await this.intent.sendEvent(this.roomId, {
|
||||
msgtype: "m.notice",
|
||||
@ -960,7 +962,8 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
}
|
||||
}
|
||||
}
|
||||
const content = `**${event.sender.login}** ${state} issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${emoji.emojify(event.issue.title)}"${withComment}`;
|
||||
const icon = state === 'reopened' ? '🔷' : '⬛';
|
||||
const content = emojify(`${icon} **${event.sender.login}** ${state} issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${emojify(event.issue.title)}"${withComment}`);
|
||||
await this.intent.sendEvent(this.roomId, {
|
||||
msgtype: "m.notice",
|
||||
body: content,
|
||||
@ -979,7 +982,8 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
}
|
||||
log.info(`onIssueEdited ${this.roomId} ${this.org}/${this.repo} #${event.issue.number}`);
|
||||
const orgRepoName = event.repository.full_name;
|
||||
const content = `**${event.sender.login}** edited issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${emoji.emojify(event.issue.title)}"`;
|
||||
const icon = '✏';
|
||||
const content = emojify(`${icon} **${event.sender.login}** edited issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${emojify(event.issue.title)}"`);
|
||||
await this.intent.sendEvent(this.roomId, {
|
||||
msgtype: "m.notice",
|
||||
body: content,
|
||||
@ -1010,7 +1014,8 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
}
|
||||
const orgRepoName = event.repository.full_name;
|
||||
const {plain, html} = FormatUtil.formatLabels(event.issue.labels?.map(l => ({ name: l.name, description: l.description || undefined, color: l.color || undefined })));
|
||||
const content = `**${event.sender.login}** labeled issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${emoji.emojify(event.issue.title)}"`;
|
||||
const icon = '🗃';
|
||||
const content = emojify(`${icon} **${event.sender.login}** labeled issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${emojify(event.issue.title)}"`);
|
||||
this.intent.sendEvent(this.roomId, {
|
||||
msgtype: "m.notice",
|
||||
body: content + (plain.length > 0 ? ` with labels ${plain}`: ""),
|
||||
@ -1067,7 +1072,8 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
diffContentHtml = `\n<pre><code class="language-diff">${diff.data}\n</code></pre>`;
|
||||
}
|
||||
}
|
||||
const content = emoji.emojify(`**${event.sender.login}** ${verb} a new PR [${orgRepoName}#${event.pull_request.number}](${event.pull_request.html_url}): "${event.pull_request.title}"`);
|
||||
const icon = verb === 'opened' ? '🔵' : '⚪';
|
||||
const content = emojify(`${icon} **${event.sender.login}** ${verb} a new PR [${orgRepoName}#${event.pull_request.number}](${event.pull_request.html_url}): "${event.pull_request.title}"`);
|
||||
const labels = FormatUtil.formatLabels(event.pull_request.labels?.map(l => ({ name: l.name, description: l.description || undefined, color: l.color || undefined })));
|
||||
await this.intent.sendEvent(this.roomId, {
|
||||
msgtype: "m.notice",
|
||||
@ -1091,7 +1097,8 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
throw Error('No repository content!');
|
||||
}
|
||||
const orgRepoName = event.repository.full_name;
|
||||
const content = emoji.emojify(`**${event.sender.login}** has marked [${orgRepoName}#${event.pull_request.number}](${event.pull_request.html_url}) as ready to review "${event.pull_request.title}"`);
|
||||
const icon = '🔬';
|
||||
const content = emojify(`${icon} **${event.sender.login}** has marked [${orgRepoName}#${event.pull_request.number}](${event.pull_request.html_url}) as ready to review "${event.pull_request.title}"`);
|
||||
await this.intent.sendEvent(this.roomId, {
|
||||
msgtype: "m.notice",
|
||||
body: content,
|
||||
@ -1124,7 +1131,7 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
// We don't recongnise this state, run away!
|
||||
return;
|
||||
}
|
||||
const content = emoji.emojify(`**${event.sender.login}** ${emojiForReview} ${event.review.state.toLowerCase()} [${orgRepoName}#${event.pull_request.number}](${event.pull_request.html_url}) "${event.pull_request.title}"`);
|
||||
const content = emojify(`${emojiForReview} **${event.sender.login}** ${event.review.state.toLowerCase()} [${orgRepoName}#${event.pull_request.number}](${event.pull_request.html_url}) "${event.pull_request.title}"`);
|
||||
await this.intent.sendEvent(this.roomId, {
|
||||
msgtype: "m.notice",
|
||||
body: content,
|
||||
@ -1170,7 +1177,8 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
}
|
||||
}
|
||||
|
||||
const content = emoji.emojify(`**${event.sender.login}** ${verb} PR [${orgRepoName}#${event.pull_request.number}](${event.pull_request.html_url}): "${event.pull_request.title}"${withComment}`);
|
||||
const icon = verb === 'merged' ? '✳' : '⚫';
|
||||
const content = emojify(`${icon} **${event.sender.login}** ${verb} PR [${orgRepoName}#${event.pull_request.number}](${event.pull_request.html_url}): "${event.pull_request.title}"${withComment}`);
|
||||
await this.intent.sendEvent(this.roomId, {
|
||||
msgtype: "m.notice",
|
||||
body: content,
|
||||
@ -1197,7 +1205,8 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
throw Error('No repository content!');
|
||||
}
|
||||
const orgRepoName = event.repository.full_name;
|
||||
let content = `**${event.sender.login}** 🪄 released [${event.release.name ?? event.release.tag_name}](${event.release.html_url}) for ${orgRepoName}`;
|
||||
const icon = '📣';
|
||||
let content = emojify(`${icon} **${event.sender.login}** released [${event.release.name ?? event.release.tag_name}](${event.release.html_url}) for ${orgRepoName}`);
|
||||
if (event.release.body) {
|
||||
content += `\n\n${event.release.body}`
|
||||
}
|
||||
@ -1222,8 +1231,9 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
if (!event.repository) {
|
||||
throw Error('No repository content!');
|
||||
}
|
||||
const icon = '📝';
|
||||
const orgRepoName = event.repository.full_name;
|
||||
let content = `**${event.sender.login}** 🪄 drafted release [${event.release.name ?? event.release.tag_name}](${event.release.html_url}) for ${orgRepoName}`;
|
||||
let content = emojify(`${icon} **${event.sender.login}** drafted release [${event.release.name ?? event.release.tag_name}](${event.release.html_url}) for ${orgRepoName}`);
|
||||
if (event.release.body) {
|
||||
content += `\n\n${event.release.body}`
|
||||
}
|
||||
@ -1259,7 +1269,8 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
|
||||
log.info(`onWorkflowCompleted ${this.roomId} ${this.org}/${this.repo} '${workflowRun.id}'`);
|
||||
const orgRepoName = event.repository.full_name;
|
||||
const content = `Workflow **${event.workflow.name}** [${WORKFLOW_CONCLUSION_TO_NOTICE[workflowRun.conclusion]}](${workflowRun.html_url}) for ${orgRepoName} on branch \`${workflowRun.head_branch}\``;
|
||||
const icon = '☑';
|
||||
const content = emojify(`${icon} Workflow **${event.workflow.name}** [${WORKFLOW_CONCLUSION_TO_NOTICE[workflowRun.conclusion]}](${workflowRun.html_url}) for ${orgRepoName} on branch \`${workflowRun.head_branch}\``);
|
||||
await this.intent.sendEvent(this.roomId, {
|
||||
msgtype: "m.notice",
|
||||
body: content,
|
||||
@ -1419,7 +1430,7 @@ export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnection
|
||||
for (const install of installs.data.installations) {
|
||||
if (install.account) {
|
||||
results.push({
|
||||
name: install.account.login || NAMELESS_ORG_PLACEHOLDER, // org or user name
|
||||
name: getNameForGitHubAccount(install.account), // org or user name
|
||||
});
|
||||
} else {
|
||||
log.debug(`Skipping install ${install.id}, has no attached account`);
|
||||
|
@ -3,10 +3,10 @@ import { Appservice, Space, StateEvent } from "matrix-bot-sdk";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import axios from "axios";
|
||||
import { GitHubDiscussionSpace } from ".";
|
||||
import { GithubInstance } from "../Github/GithubInstance";
|
||||
import { GithubInstance } from "../github/GithubInstance";
|
||||
import { BaseConnection } from "./BaseConnection";
|
||||
import { ConfigGrantChecker, GrantChecker } from "../grants/GrantCheck";
|
||||
import { BridgeConfig } from "../Config/Config";
|
||||
import { BridgeConfig } from "../config/Config";
|
||||
|
||||
const log = new Logger("GitHubOwnerSpace");
|
||||
|
||||
|
@ -5,7 +5,7 @@ import { UserTokenStore } from "../UserTokenStore";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import { CommentProcessor } from "../CommentProcessor";
|
||||
import { MessageSenderClient } from "../MatrixSender";
|
||||
import { BridgeConfig, BridgeConfigGitLab, GitLabInstance } from "../Config/Config";
|
||||
import { BridgeConfig, BridgeConfigGitLab, GitLabInstance } from "../config/Config";
|
||||
import { GetIssueResponse } from "../Gitlab/Types";
|
||||
import { IGitLabWebhookNoteEvent } from "../Gitlab/WebhookTypes";
|
||||
import { ensureUserIsInRoom, getIntentForUser } from "../IntentUtils";
|
||||
|
@ -1,18 +1,16 @@
|
||||
// We need to instantiate some functions which are not directly called, which confuses typescript.
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import { UserTokenStore } from "../UserTokenStore";
|
||||
import { Appservice, Intent, StateEvent } from "matrix-bot-sdk";
|
||||
import { BotCommands, botCommand, compileBotCommands } from "../BotCommands";
|
||||
import { MatrixEvent, MatrixMessageContent } from "../MatrixEvent";
|
||||
import markdown from "markdown-it";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import { BridgeConfigGitLab, GitLabInstance } from "../Config/Config";
|
||||
import { BridgeConfigGitLab, GitLabInstance } from "../config/Config";
|
||||
import { IGitlabMergeRequest, IGitlabProject, IGitlabUser, IGitLabWebhookMREvent, IGitLabWebhookNoteEvent, IGitLabWebhookPushEvent, IGitLabWebhookReleaseEvent, IGitLabWebhookTagPushEvent, IGitLabWebhookWikiPageEvent } from "../Gitlab/WebhookTypes";
|
||||
import { CommandConnection } from "./CommandConnection";
|
||||
import { Connection, IConnection, IConnectionState, InstantiateConnectionOpts, ProvisionConnectionOpts } from "./IConnection";
|
||||
import { ConnectionWarning, GetConnectionsResponseItem } from "../provisioning/api";
|
||||
import { ErrCode, ApiError, ValidatorApiError } from "../api"
|
||||
import { AccessLevel } from "../Gitlab/Types";
|
||||
import { AccessLevel, SerializedGitlabDiscussionThreads } from "../Gitlab/Types";
|
||||
import Ajv, { JSONSchemaType } from "ajv";
|
||||
import { CommandError } from "../errors";
|
||||
import QuickLRU from "@alloc/quick-lru";
|
||||
@ -59,8 +57,6 @@ const log = new Logger("GitLabRepoConnection");
|
||||
const md = new markdown();
|
||||
|
||||
const PUSH_MAX_COMMITS = 5;
|
||||
const MRRCOMMENT_DEBOUNCE_MS = 5000;
|
||||
|
||||
|
||||
export type GitLabRepoResponseItem = GetConnectionsResponseItem<GitLabRepoConnectionState>;
|
||||
|
||||
@ -70,6 +66,7 @@ type AllowedEventsNames =
|
||||
"merge_request.close" |
|
||||
"merge_request.merge" |
|
||||
"merge_request.review" |
|
||||
"merge_request.review.individual" |
|
||||
"merge_request.ready_for_review" |
|
||||
"merge_request.review.comments" |
|
||||
`merge_request.${string}` |
|
||||
@ -86,6 +83,7 @@ const AllowedEvents: AllowedEventsNames[] = [
|
||||
"merge_request.close",
|
||||
"merge_request.merge",
|
||||
"merge_request.review",
|
||||
"merge_request.review.individual",
|
||||
"merge_request.ready_for_review",
|
||||
"merge_request.review.comments",
|
||||
"merge_request",
|
||||
@ -203,7 +201,7 @@ export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnection
|
||||
throw new ValidatorApiError(validator.errors);
|
||||
}
|
||||
|
||||
static async createConnectionForState(roomId: string, event: StateEvent<Record<string, unknown>>, {as, intent, tokenStore, config}: InstantiateConnectionOpts) {
|
||||
static async createConnectionForState(roomId: string, event: StateEvent<Record<string, unknown>>, {as, intent, storage, tokenStore, config}: InstantiateConnectionOpts) {
|
||||
if (!config.gitlab) {
|
||||
throw Error('GitLab is not configured');
|
||||
}
|
||||
@ -212,7 +210,13 @@ export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnection
|
||||
if (!instance) {
|
||||
throw Error('Instance name not recognised');
|
||||
}
|
||||
return new GitLabRepoConnection(roomId, event.stateKey, as, config.gitlab, intent, state, tokenStore, instance);
|
||||
|
||||
const connection = new GitLabRepoConnection(roomId, event.stateKey, as, config.gitlab, intent, state, tokenStore, instance, storage);
|
||||
|
||||
const discussionThreads = await storage.getGitlabDiscussionThreads(connection.connectionId);
|
||||
connection.setDiscussionThreads(discussionThreads);
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
public static async assertUserHasAccessToProject(
|
||||
@ -240,7 +244,12 @@ export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnection
|
||||
return permissionLevel;
|
||||
}
|
||||
|
||||
public static async provisionConnection(roomId: string, requester: string, data: Record<string, unknown>, { as, config, intent, tokenStore, getAllConnectionsOfType }: ProvisionConnectionOpts) {
|
||||
public static async provisionConnection(
|
||||
roomId: string,
|
||||
requester: string,
|
||||
data: Record<string, unknown>,
|
||||
{ as, config, intent, storage, tokenStore, getAllConnectionsOfType }: ProvisionConnectionOpts
|
||||
) {
|
||||
if (!config.gitlab) {
|
||||
throw Error('GitLab is not configured');
|
||||
}
|
||||
@ -258,7 +267,8 @@ export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnection
|
||||
|
||||
const project = await client.projects.get(validData.path);
|
||||
const stateEventKey = `${validData.instance}/${validData.path}`;
|
||||
const connection = new GitLabRepoConnection(roomId, stateEventKey, as, gitlabConfig, intent, validData, tokenStore, instance);
|
||||
const connection = new GitLabRepoConnection(roomId, stateEventKey, as, gitlabConfig, intent, validData, tokenStore, instance, storage);
|
||||
|
||||
const existingConnections = getAllConnectionsOfType(GitLabRepoConnection);
|
||||
const existing = existingConnections.find(c => c.roomId === roomId && c.instance.url === connection.instance.url && c.path === connection.path);
|
||||
|
||||
@ -380,21 +390,19 @@ export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnection
|
||||
private readonly debounceMRComments = new Map<string, {
|
||||
commentCount: number,
|
||||
commentNotes?: string[],
|
||||
discussions: string[],
|
||||
author: string,
|
||||
timeout: NodeJS.Timeout,
|
||||
approved?: boolean,
|
||||
skip?: boolean,
|
||||
}>();
|
||||
|
||||
/**
|
||||
* GitLab provides NO threading information in its webhook response objects,
|
||||
* so we need to determine if we've seen a comment for a line before, and
|
||||
* skip it if we have (because it's probably a reply).
|
||||
*/
|
||||
private readonly mergeRequestSeenDiscussionIds = new QuickLRU<string, undefined>({ maxSize: 100 });
|
||||
private readonly discussionThreads = new QuickLRU<string, Promise<string|undefined>>({ maxSize: 100});
|
||||
|
||||
private readonly hookFilter: HookFilter<AllowedEventsNames>;
|
||||
|
||||
private readonly grantChecker;
|
||||
private readonly commentDebounceMs: number;
|
||||
|
||||
constructor(
|
||||
roomId: string,
|
||||
@ -405,6 +413,7 @@ export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnection
|
||||
state: ConnectionStateValidated,
|
||||
private readonly tokenStore: UserTokenStore,
|
||||
private readonly instance: GitLabInstance,
|
||||
private readonly storage: IBridgeStorageProvider,
|
||||
) {
|
||||
super(
|
||||
roomId,
|
||||
@ -425,7 +434,8 @@ export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnection
|
||||
this.hookFilter = new HookFilter(
|
||||
state.enableHooks ?? DefaultHooks,
|
||||
);
|
||||
}
|
||||
this.commentDebounceMs = config.commentDebounceMs;
|
||||
}
|
||||
|
||||
public get path() {
|
||||
return this.state.path.toLowerCase();
|
||||
@ -721,7 +731,7 @@ ${data.description}`;
|
||||
});
|
||||
}
|
||||
|
||||
private renderDebouncedMergeRequest(uniqueId: string, mergeRequest: IGitlabMergeRequest, project: IGitlabProject) {
|
||||
private async renderDebouncedMergeRequest(uniqueId: string, mergeRequest: IGitlabMergeRequest, project: IGitlabProject) {
|
||||
const result = this.debounceMRComments.get(uniqueId);
|
||||
if (!result) {
|
||||
// Always defined, but for type checking purposes.
|
||||
@ -737,26 +747,52 @@ ${data.description}`;
|
||||
comments = ` with ${result.commentCount} comments`;
|
||||
}
|
||||
|
||||
let approvalState = 'commented on';
|
||||
if (result.approved === true) {
|
||||
approvalState = '✅ approved'
|
||||
} else if (result.approved === false) {
|
||||
approvalState = '🔴 unapproved';
|
||||
let relation;
|
||||
const discussionWithThread = result.discussions.find(discussionId => this.discussionThreads.has(discussionId));
|
||||
if (discussionWithThread) {
|
||||
const threadEventId = await this.discussionThreads.get(discussionWithThread)?.catch(() => { /* already logged */ });
|
||||
if (threadEventId) {
|
||||
relation = {
|
||||
"m.relates_to": {
|
||||
"event_id": threadEventId,
|
||||
"rel_type": "m.thread"
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let content = `**${result.author}** ${approvalState} MR [${orgRepoName}#${mergeRequest.iid}](${mergeRequest.url}): "${mergeRequest.title}"${comments}`;
|
||||
let action = relation ? 'replied' : 'commented on'; // this is the only place we need this, approve/unapprove don't appear in discussions
|
||||
if (result.approved === true) {
|
||||
action = '✅ approved'
|
||||
} else if (result.approved === false) {
|
||||
action = '🔴 unapproved';
|
||||
}
|
||||
|
||||
const target = relation ? '' : ` MR [${orgRepoName}#${mergeRequest.iid}](${mergeRequest.url}): "${mergeRequest.title}"`;
|
||||
let content = `**${result.author}** ${action}${target} ${comments}`;
|
||||
|
||||
if (result.commentNotes) {
|
||||
content += "\n\n> " + result.commentNotes.join("\n\n> ");
|
||||
}
|
||||
|
||||
this.intent.sendEvent(this.roomId, {
|
||||
const eventPromise = this.intent.sendEvent(this.roomId, {
|
||||
msgtype: "m.notice",
|
||||
body: content,
|
||||
formatted_body: md.renderInline(content),
|
||||
format: "org.matrix.custom.html",
|
||||
...relation,
|
||||
}).catch(ex => {
|
||||
log.error('Failed to send MR review message', ex);
|
||||
return undefined;
|
||||
});
|
||||
|
||||
for (const discussionId of result.discussions) {
|
||||
if (!this.discussionThreads.has(discussionId)) {
|
||||
this.discussionThreads.set(discussionId, eventPromise);
|
||||
}
|
||||
}
|
||||
void this.persistDiscussionThreads().catch(ex => {
|
||||
log.error(`Failed to persistently store Gitlab discussion threads for connection ${this.connectionId}:`, ex);
|
||||
});
|
||||
}
|
||||
|
||||
@ -768,6 +804,7 @@ ${data.description}`;
|
||||
commentCount: number,
|
||||
commentNotes?: string[],
|
||||
approved?: boolean,
|
||||
discussionId?: string,
|
||||
/**
|
||||
* If the MR contains only comments, skip it.
|
||||
*/
|
||||
@ -787,16 +824,20 @@ ${data.description}`;
|
||||
if (!opts.skip) {
|
||||
existing.skip = false;
|
||||
}
|
||||
existing.timeout = setTimeout(() => this.renderDebouncedMergeRequest(uniqueId, mergeRequest, project), MRRCOMMENT_DEBOUNCE_MS);
|
||||
if (opts.discussionId) {
|
||||
existing.discussions.push(opts.discussionId);
|
||||
}
|
||||
existing.timeout = setTimeout(() => this.renderDebouncedMergeRequest(uniqueId, mergeRequest, project), this.commentDebounceMs);
|
||||
return;
|
||||
}
|
||||
this.debounceMRComments.set(uniqueId, {
|
||||
commentCount: commentCount,
|
||||
commentNotes: commentNotes,
|
||||
discussions: opts.discussionId ? [opts.discussionId] : [],
|
||||
skip: opts.skip,
|
||||
approved,
|
||||
author: user.name,
|
||||
timeout: setTimeout(() => this.renderDebouncedMergeRequest(uniqueId, mergeRequest, project), MRRCOMMENT_DEBOUNCE_MS),
|
||||
timeout: setTimeout(() => this.renderDebouncedMergeRequest(uniqueId, mergeRequest, project), this.commentDebounceMs),
|
||||
});
|
||||
}
|
||||
|
||||
@ -806,10 +847,6 @@ ${data.description}`;
|
||||
}
|
||||
log.info(`onMergeRequestReviewed ${this.roomId} ${this.instance}/${this.path} ${event.object_attributes.iid}`);
|
||||
this.validateMREvent(event);
|
||||
if (event.object_attributes.action !== "approved" && event.object_attributes.action !== "unapproved") {
|
||||
// Not interested.
|
||||
return;
|
||||
}
|
||||
this.debounceMergeRequestReview(
|
||||
event.user,
|
||||
event.object_attributes,
|
||||
@ -822,17 +859,24 @@ ${data.description}`;
|
||||
);
|
||||
}
|
||||
|
||||
private shouldHandleMRComment(event: IGitLabWebhookNoteEvent) {
|
||||
// Check to see if this line has had a comment before
|
||||
if (event.object_attributes.discussion_id) {
|
||||
if (this.mergeRequestSeenDiscussionIds.has(event.object_attributes.discussion_id)) {
|
||||
// If it has, this is probably a reply. Skip repeated replies.
|
||||
return false;
|
||||
}
|
||||
// Otherwise, record that we have seen the line and continue (it's probably a genuine comment).
|
||||
this.mergeRequestSeenDiscussionIds.set(event.object_attributes.discussion_id, undefined);
|
||||
|
||||
public async onMergeRequestIndividualReview(event: IGitLabWebhookMREvent) {
|
||||
if (this.hookFilter.shouldSkip('merge_request', 'merge_request.review.individual') || !this.matchesLabelFilter(event)) {
|
||||
return;
|
||||
}
|
||||
return true;
|
||||
|
||||
log.info(`onMergeRequestReviewed ${this.roomId} ${this.instance}/${this.path} ${event.object_attributes.iid}`);
|
||||
this.validateMREvent(event);
|
||||
this.debounceMergeRequestReview(
|
||||
event.user,
|
||||
event.object_attributes,
|
||||
event.project,
|
||||
{
|
||||
commentCount: 0,
|
||||
approved: "approved" === event.object_attributes.action,
|
||||
skip: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public async onCommentCreated(event: IGitLabWebhookNoteEvent) {
|
||||
@ -844,13 +888,11 @@ ${data.description}`;
|
||||
// Not a MR comment
|
||||
return;
|
||||
}
|
||||
if (!this.shouldHandleMRComment(event)) {
|
||||
// Skip it.
|
||||
return;
|
||||
}
|
||||
|
||||
this.debounceMergeRequestReview(event.user, event.merge_request, event.project, {
|
||||
commentCount: 1,
|
||||
commentNotes: this.state.includeCommentBody ? [event.object_attributes.note] : undefined,
|
||||
discussionId: event.object_attributes.discussion_id,
|
||||
skip: this.hookFilter.shouldSkip('merge_request.review.comments'),
|
||||
});
|
||||
}
|
||||
@ -894,6 +936,24 @@ ${data.description}`;
|
||||
}
|
||||
// TODO: Clean up webhooks
|
||||
}
|
||||
|
||||
private setDiscussionThreads(discussionThreads: SerializedGitlabDiscussionThreads): void {
|
||||
for (const { discussionId, eventId } of discussionThreads) {
|
||||
this.discussionThreads.set(discussionId, Promise.resolve(eventId));
|
||||
}
|
||||
}
|
||||
|
||||
private async persistDiscussionThreads(): Promise<void> {
|
||||
const serialized: SerializedGitlabDiscussionThreads = [];
|
||||
for (const [discussionId, eventIdPromise] of this.discussionThreads.entriesAscending()) {
|
||||
const eventId = await eventIdPromise.catch(() => { /* logged elsewhere */ });
|
||||
if (eventId) {
|
||||
serialized.push({ discussionId, eventId });
|
||||
}
|
||||
|
||||
}
|
||||
return this.storage.setGitlabDiscussionThreads(this.connectionId, serialized);
|
||||
}
|
||||
}
|
||||
|
||||
// Typescript doesn't understand Prototypes very well yet.
|
||||
|
@ -2,12 +2,12 @@ import { MatrixEvent, MatrixMessageContent } from "../MatrixEvent";
|
||||
import { IssuesOpenedEvent, IssuesEditedEvent } from "@octokit/webhooks-types";
|
||||
import { ConnectionWarning, GetConnectionsResponseItem } from "../provisioning/api";
|
||||
import { Appservice, Intent, IRichReplyMetadata, StateEvent } from "matrix-bot-sdk";
|
||||
import { BridgeConfig, BridgePermissionLevel } from "../Config/Config";
|
||||
import { BridgeConfig, BridgePermissionLevel } from "../config/Config";
|
||||
import { UserTokenStore } from "../UserTokenStore";
|
||||
import { CommentProcessor } from "../CommentProcessor";
|
||||
import { MessageSenderClient } from "../MatrixSender";
|
||||
import { IBridgeStorageProvider } from "../Stores/StorageProvider";
|
||||
import { GithubInstance } from "../Github/GithubInstance";
|
||||
import { GithubInstance } from "../github/GithubInstance";
|
||||
import "reflect-metadata";
|
||||
|
||||
export type PermissionCheckFn = (service: string, level: BridgePermissionLevel) => boolean;
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Connection, IConnection, IConnectionState, InstantiateConnectionOpts, ProvisionConnectionOpts } from "./IConnection";
|
||||
import { Appservice, Intent, StateEvent } from "matrix-bot-sdk";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import { JiraIssueEvent, JiraIssueUpdatedEvent, JiraVersionEvent } from "../Jira/WebhookTypes";
|
||||
import { JiraIssueEvent, JiraIssueUpdatedEvent, JiraVersionEvent } from "../jira/WebhookTypes";
|
||||
import { FormatUtil } from "../FormatUtil";
|
||||
import markdownit from "markdown-it";
|
||||
import { generateJiraWebLinkFromIssue, generateJiraWebLinkFromVersion } from "../Jira";
|
||||
import { JiraProject, JiraVersion } from "../Jira/Types";
|
||||
import { generateJiraWebLinkFromIssue, generateJiraWebLinkFromVersion } from "../jira";
|
||||
import { JiraProject, JiraVersion } from "../jira/Types";
|
||||
import { botCommand, BotCommands, compileBotCommands } from "../BotCommands";
|
||||
import { MatrixMessageContent } from "../MatrixEvent";
|
||||
import { CommandConnection } from "./CommandConnection";
|
||||
@ -14,10 +14,10 @@ import { CommandError, NotLoggedInError } from "../errors";
|
||||
import { ApiError, ErrCode } from "../api";
|
||||
import JiraApi from "jira-client";
|
||||
import { GetConnectionsResponseItem } from "../provisioning/api";
|
||||
import { BridgeConfigJira } from "../Config/Config";
|
||||
import { HookshotJiraApi } from "../Jira/Client";
|
||||
import { BridgeConfigJira } from "../config/Config";
|
||||
import { HookshotJiraApi } from "../jira/Client";
|
||||
import { GrantChecker } from "../grants/GrantCheck";
|
||||
import { JiraGrantChecker } from "../Jira/GrantChecker";
|
||||
import { JiraGrantChecker } from "../jira/GrantChecker";
|
||||
|
||||
type JiraAllowedEventsNames =
|
||||
"issue_created" |
|
||||
|
@ -1,9 +1,9 @@
|
||||
// We need to instantiate some functions which are not directly called, which confuses typescript.
|
||||
import { BotCommands, botCommand, compileBotCommands, HelpFunction } from "../BotCommands";
|
||||
import { CommandConnection } from "./CommandConnection";
|
||||
import { GenericHookConnection, GitHubRepoConnection, JiraProjectConnection, JiraProjectConnectionState } from ".";
|
||||
import { GenericHookConnection, GenericHookConnectionState, GitHubRepoConnection, JiraProjectConnection, JiraProjectConnectionState } from ".";
|
||||
import { CommandError } from "../errors";
|
||||
import { BridgePermissionLevel } from "../Config/Config";
|
||||
import { BridgePermissionLevel } from "../config/Config";
|
||||
import markdown from "markdown-it";
|
||||
import { FigmaFileConnection } from "./FigmaFileConnection";
|
||||
import { FeedConnection, FeedConnectionState } from "./FeedConnection";
|
||||
@ -14,6 +14,7 @@ import { GitLabRepoConnection } from "./GitlabRepo";
|
||||
import { IConnection, IConnectionState, ProvisionConnectionOpts } from "./IConnection";
|
||||
import { ApiError, Logger } from "matrix-appservice-bridge";
|
||||
import { Intent } from "matrix-bot-sdk";
|
||||
import YAML from 'yaml';
|
||||
const md = new markdown();
|
||||
const log = new Logger("SetupConnection");
|
||||
|
||||
@ -231,6 +232,58 @@ export class SetupConnection extends CommandConnection {
|
||||
return this.client.sendNotice(this.roomId, `Room configured to bridge webhooks. See admin room for secret url.`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@botCommand("webhook list", { help: "Show webhooks currently configured.", category: "generic"})
|
||||
public async onWebhookList() {
|
||||
const webhooks: GenericHookConnectionState[] = await this.client.getRoomState(this.roomId).catch((err: any) => {
|
||||
if (err.body.errcode === 'M_NOT_FOUND') {
|
||||
return []; // not an error to us
|
||||
}
|
||||
throw err;
|
||||
}).then(events =>
|
||||
events.filter(
|
||||
(ev: any) => ev.type === GenericHookConnection.CanonicalEventType && ev.content.name
|
||||
).map(ev => ev.content)
|
||||
);
|
||||
|
||||
if (webhooks.length === 0) {
|
||||
return this.client.sendHtmlNotice(this.roomId, md.renderInline('No webhooks configured'));
|
||||
} else {
|
||||
const feedDescriptions = webhooks.sort(
|
||||
(a, b) => a.name.localeCompare(b.name)
|
||||
).map(feed => {
|
||||
return feed.name;
|
||||
});
|
||||
|
||||
return this.client.sendHtmlNotice(this.roomId, md.render(
|
||||
'Webhooks configured:\n\n' +
|
||||
feedDescriptions.map(desc => ` - ${desc}`).join('\n')
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@botCommand("webhook remove", { help: "Remove a webhook from the room.", requiredArgs: ["name"], includeUserId: true, category: "generic"})
|
||||
public async onWebhookRemove(userId: string, name: string) {
|
||||
await this.checkUserPermissions(userId, "generic", GenericHookConnection.CanonicalEventType);
|
||||
|
||||
const event = await this.client.getRoomStateEvent(this.roomId, GenericHookConnection.CanonicalEventType, name).catch((err: any) => {
|
||||
if (err.body.errcode === 'M_NOT_FOUND') {
|
||||
return null; // not an error to us
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
if (!event || event.disabled === true || Object.keys(event).length === 0) {
|
||||
throw new CommandError("Invalid webhook name", `No webhook by the name of "${name}" is configured.`);
|
||||
}
|
||||
|
||||
await this.client.sendStateEvent(this.roomId, GenericHookConnection.CanonicalEventType, name, {
|
||||
disabled: true
|
||||
});
|
||||
|
||||
return this.client.sendHtmlNotice(this.roomId, md.renderInline(`Removed webhook \`${name}\``));
|
||||
}
|
||||
|
||||
@botCommand("figma file", { help: "Bridge a Figma file to the room.", requiredArgs: ["url"], includeUserId: true, category: "figma"})
|
||||
public async onFigma(userId: string, url: string) {
|
||||
if (!this.config.figma) {
|
||||
@ -274,8 +327,11 @@ export class SetupConnection extends CommandConnection {
|
||||
return this.client.sendHtmlNotice(this.roomId, md.renderInline(`Room configured to bridge \`${url}\``));
|
||||
}
|
||||
|
||||
@botCommand("feed list", { help: "Show feeds currently subscribed to.", category: "feeds"})
|
||||
public async onFeedList() {
|
||||
@botCommand("feed list", { help: "Show feeds currently subscribed to. Supported formats `json` and `yaml`.", optionalArgs: ["format"], category: "feeds"})
|
||||
public async onFeedList(format?: string) {
|
||||
const useJsonFormat = format?.toLowerCase() === 'json';
|
||||
const useYamlFormat = format?.toLowerCase() === 'yaml';
|
||||
|
||||
const feeds: FeedConnectionState[] = await this.client.getRoomState(this.roomId).catch((err: any) => {
|
||||
if (err.body.errcode === 'M_NOT_FOUND') {
|
||||
return []; // not an error to us
|
||||
@ -290,17 +346,28 @@ export class SetupConnection extends CommandConnection {
|
||||
if (feeds.length === 0) {
|
||||
return this.client.sendHtmlNotice(this.roomId, md.renderInline('Not subscribed to any feeds'));
|
||||
} else {
|
||||
const feedDescriptions = feeds.map(feed => {
|
||||
const feedDescriptions = feeds.sort(
|
||||
(a, b) => (a.label ?? a.url).localeCompare(b.label ?? b.url)
|
||||
).map(feed => {
|
||||
if (useJsonFormat || useYamlFormat) {
|
||||
return feed;
|
||||
}
|
||||
if (feed.label) {
|
||||
return `[${feed.label}](${feed.url})`;
|
||||
}
|
||||
return feed.url;
|
||||
});
|
||||
|
||||
return this.client.sendHtmlNotice(this.roomId, md.render(
|
||||
'Currently subscribed to these feeds:\n\n' +
|
||||
feedDescriptions.map(desc => ` - ${desc}`).join('\n')
|
||||
));
|
||||
let message = 'Currently subscribed to these feeds:\n';
|
||||
if (useJsonFormat) {
|
||||
message += `\`\`\`json\n${JSON.stringify(feedDescriptions, null, 4)}\n\`\`\``
|
||||
} else if (useYamlFormat) {
|
||||
message += `\`\`\`yaml\n${YAML.stringify(feedDescriptions)}\`\`\``
|
||||
} else {
|
||||
message += feedDescriptions.map(desc => `- ${desc}`).join('\n')
|
||||
}
|
||||
|
||||
return this.client.sendHtmlNotice(this.roomId, md.render(message));
|
||||
}
|
||||
}
|
||||
|
||||
|