Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1f8cbaade | |||
| 5f29cd2c1c |
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"plugins": [
|
||||
// this transforms async functions into generator functions, which
|
||||
// are then made to use the regenerator module by babel's
|
||||
// transform-regnerator plugin (which is enabled by es2015).
|
||||
"transform-async-to-bluebird",
|
||||
|
||||
// This makes sure that the regenerator runtime is available to
|
||||
// the transpiled code.
|
||||
"transform-runtime",
|
||||
],
|
||||
}
|
||||
@@ -21,6 +21,3 @@ insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 4
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
_docs
|
||||
-185
@@ -1,185 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: ["matrix-org", "import", "jsdoc", "n", "@vitest"],
|
||||
extends: ["plugin:matrix-org/babel", "plugin:import/typescript"],
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"],
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: true,
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
// NOTE: These rules are frozen and new rules should not be added here.
|
||||
// New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/
|
||||
rules: {
|
||||
"no-var": ["error"],
|
||||
"prefer-rest-params": ["error"],
|
||||
"prefer-spread": ["error"],
|
||||
"one-var": ["error"],
|
||||
"padded-blocks": ["error"],
|
||||
"no-extend-native": ["error"],
|
||||
"camelcase": ["error"],
|
||||
"no-multi-spaces": ["error", { ignoreEOLComments: true }],
|
||||
"space-before-function-paren": [
|
||||
"error",
|
||||
{
|
||||
anonymous: "never",
|
||||
named: "never",
|
||||
asyncArrow: "always",
|
||||
},
|
||||
],
|
||||
"arrow-parens": "off",
|
||||
"prefer-promise-reject-errors": "off",
|
||||
"no-constant-condition": "off",
|
||||
"no-async-promise-executor": "off",
|
||||
// We use a `logger` intermediary module
|
||||
"no-console": "error",
|
||||
|
||||
// restrict EventEmitters to force callers to use TypedEventEmitter
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
name: "events",
|
||||
message: "Please use TypedEventEmitter instead",
|
||||
},
|
||||
],
|
||||
|
||||
"no-restricted-properties": [
|
||||
"error",
|
||||
{
|
||||
object: "window",
|
||||
property: "setImmediate",
|
||||
message: "Use setTimeout instead.",
|
||||
},
|
||||
],
|
||||
"no-restricted-globals": [
|
||||
"error",
|
||||
{
|
||||
name: "setImmediate",
|
||||
message: "Use setTimeout instead.",
|
||||
},
|
||||
{
|
||||
name: "global",
|
||||
message: "Use globalThis instead.",
|
||||
},
|
||||
],
|
||||
|
||||
"import/no-restricted-paths": [
|
||||
"error",
|
||||
{
|
||||
zones: [
|
||||
{
|
||||
target: "./src/",
|
||||
from: "./src/index.ts",
|
||||
message:
|
||||
"The package index is dynamic between src and lib depending on " +
|
||||
"whether release or development, target the specific module or matrix.ts instead",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["**/*.ts"],
|
||||
plugins: ["eslint-plugin-tsdoc"],
|
||||
extends: ["plugin:matrix-org/typescript"],
|
||||
rules: {
|
||||
// TypeScript has its own version of this
|
||||
"@babel/no-invalid-this": "off",
|
||||
|
||||
// We're okay being explicit at the moment
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
// We disable this while we're transitioning
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
// We'd rather not do this but we do
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
// We're okay with assertion errors when we ask for them
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-empty-object-type": [
|
||||
"error",
|
||||
{
|
||||
// We do this sometimes to brand interfaces
|
||||
allowInterfaces: "with-single-extends",
|
||||
},
|
||||
],
|
||||
|
||||
"quotes": "off",
|
||||
// We use a `logger` intermediary module
|
||||
"no-console": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/**/*.ts"],
|
||||
rules: {
|
||||
"jsdoc/no-types": "error",
|
||||
"jsdoc/empty-tags": "error",
|
||||
"jsdoc/check-property-names": "error",
|
||||
"jsdoc/check-values": "error",
|
||||
// These need a bit more work before we can enable
|
||||
// "jsdoc/check-param-names": "error",
|
||||
// "jsdoc/check-indentation": "error",
|
||||
// Ensure .ts extension on imports outside of tests
|
||||
"n/file-extension-in-import": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
tryExtensions: [".ts"],
|
||||
},
|
||||
],
|
||||
"no-extra-boolean-cast": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["spec/**/*.ts"],
|
||||
extends: ["plugin:@vitest/legacy-recommended"],
|
||||
rules: {
|
||||
// We don't need super strict typing in test utilities
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-member-accessibility": "off",
|
||||
"@typescript-eslint/no-empty-object-type": "off",
|
||||
|
||||
// Disabled tests are a reality for now but as soon as all of the xits are
|
||||
// eliminated, we should enforce this.
|
||||
"@vitest/no-disabled-tests": "off",
|
||||
// Used in some crypto tests.
|
||||
"@vitest/no-standalone-expect": [
|
||||
"error",
|
||||
{
|
||||
additionalTestBlockFunctions: ["beforeAll", "beforeEach"],
|
||||
},
|
||||
],
|
||||
"@vitest/expect-expect": [
|
||||
"error",
|
||||
{
|
||||
assertFunctionNames: [
|
||||
"expect",
|
||||
"expectDevices",
|
||||
"assert.isTrue",
|
||||
"assert.isFalse",
|
||||
"passwordTest",
|
||||
"compareHeaders",
|
||||
"doTest",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Enable stricter promise rules for the MatrixRTC codebase
|
||||
files: ["src/matrixrtc/**/*.ts", "spec/unit/matrixrtc/*.ts"],
|
||||
rules: {
|
||||
// Encourage proper usage of Promises:
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/no-misused-promises": "error",
|
||||
"@typescript-eslint/require-await": "error",
|
||||
"@typescript-eslint/await-thenable": "error",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
module.exports = {
|
||||
parserOptions: {
|
||||
ecmaVersion: 2017,
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
}
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
|
||||
// babel's transform-runtime converts references to ES6 globals such as
|
||||
// Promise and Map to core-js polyfills, so we can use ES6 globals.
|
||||
es6: true,
|
||||
},
|
||||
extends: ["eslint:recommended", "google"],
|
||||
rules: {
|
||||
// rules we've always adhered to or now do
|
||||
"max-len": ["error", {
|
||||
code: 90,
|
||||
ignoreComments: true,
|
||||
}],
|
||||
curly: ["error", "multi-line"],
|
||||
"prefer-const": ["error"],
|
||||
"comma-dangle": ["error", {
|
||||
arrays: "always-multiline",
|
||||
objects: "always-multiline",
|
||||
imports: "always-multiline",
|
||||
exports: "always-multiline",
|
||||
functions: "always-multiline",
|
||||
}],
|
||||
|
||||
// loosen jsdoc requirements a little
|
||||
"require-jsdoc": ["error", {
|
||||
require: {
|
||||
FunctionDeclaration: false,
|
||||
}
|
||||
}],
|
||||
"valid-jsdoc": ["error", {
|
||||
requireParamDescription: false,
|
||||
requireReturn: false,
|
||||
requireReturnDescription: false,
|
||||
}],
|
||||
|
||||
// rules we do not want from eslint-recommended
|
||||
"no-console": ["off"],
|
||||
"no-constant-condition": ["off"],
|
||||
"no-empty": ["error", { "allowEmptyCatch": true }],
|
||||
|
||||
// rules we do not want from the google styleguide
|
||||
"object-curly-spacing": ["off"],
|
||||
"spaced-comment": ["off"],
|
||||
|
||||
// in principle we prefer single quotes, but life is too short
|
||||
quotes: ["off"],
|
||||
|
||||
// rules we'd ideally like to adhere to, but the current
|
||||
// code does not (in most cases because it's still ES5)
|
||||
// we set these to warnings, and assert that the number
|
||||
// of warnings doesn't exceed a given threshold
|
||||
"no-var": ["warn"],
|
||||
"brace-style": ["warn", "1tbs", {"allowSingleLine": true}],
|
||||
"prefer-rest-params": ["warn"],
|
||||
"prefer-spread": ["warn"],
|
||||
"one-var": ["warn"],
|
||||
"padded-blocks": ["warn"],
|
||||
"no-extend-native": ["warn"],
|
||||
"camelcase": ["warn"],
|
||||
"no-multi-spaces": ["error", { "ignoreEOLComments": true }],
|
||||
"space-before-function-paren": ["error", {
|
||||
"anonymous": "never",
|
||||
"named": "never",
|
||||
"asyncArrow": "always",
|
||||
}],
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
# Minor white-space adjustments
|
||||
1d1d59c75744e1f6a2be1cb3e0d1bd9ded5f8025
|
||||
# Import ordering and spacing: eslint-plugin-import
|
||||
80aaa6c32b50601f82e0c991c24e5a4590f39463
|
||||
# Minor white-space adjustment
|
||||
8fb036ba2d01fab66dc4373802ccf19b5cac8541
|
||||
# Minor white-space adjustment
|
||||
b63de6a902a9e1f8ffd7697dea33820fc04f028e
|
||||
3ca84cfc491b0987eec1f13f13cae58d2032bf54
|
||||
# Conform to new typescript eslint rules
|
||||
a87858840b57514603f63e2abbbda4f107f05a77
|
||||
5cf6684129a921295f5593173f16f192336fe0a2
|
||||
# Comply with new member-delimiter-style rule
|
||||
b2ad957d298720d3e026b6bd91be0c403338361a
|
||||
# Fix semicolons in TS files
|
||||
e2ec8952e38b8fea3f0ccaa09ecb42feeba0d923
|
||||
# Migrate to `eslint-plugin-matrix-org`
|
||||
# and `babel/...` to `@babel/...` migration
|
||||
09fac77ce0d9bcf6637088c29afab84084f0e739
|
||||
102704e91a70643bcc09721e14b0d909f0ef55c6
|
||||
# Eslint formatting
|
||||
cec00cd303787fa9008b6c48826e75ed438036fa
|
||||
# Minor eslint changes
|
||||
68bb8182e4e62d8f450f80c408c4b231b8725f1b
|
||||
c979ff6696e30ab8983ac416a3590996d84d3560
|
||||
f4a7395e3a3751a1a8e92dd302c49175a3296ad2
|
||||
# eslint --fix for dangley commas on function calls
|
||||
423175f5397910b0afe3112d6fb18283fc7d27d4
|
||||
# eslint ---fix for prefer-const
|
||||
7bca05af644e8b997dae81e568a3913d8f18d7ca
|
||||
# Fix linting on tests
|
||||
cee7f7a280a8c20bafc21c0a2911f60851f7a7ca
|
||||
# eslint --fix
|
||||
0fa9f7c6098822db1ae214f352fd1fe5c248b02c
|
||||
# eslint --fix for lots of white-space
|
||||
5abf6b9f208801c5022a47023150b5846cb0b309
|
||||
# eslint --fix
|
||||
7ed65407e6cdf292ce3cf659310c68d19dcd52b2
|
||||
# Switch to ESLint from JSHint (Google eslint rules as a base)
|
||||
e057956ede9ad1a931ff8050c411aca7907e0394
|
||||
# prettier
|
||||
349c2c2587c2885bb69eda4aa078b5383724cf5e
|
||||
@@ -1,17 +0,0 @@
|
||||
* @matrix-org/element-web-reviewers
|
||||
/.github/workflows/** @matrix-org/element-web-team
|
||||
/package.json @matrix-org/element-web-team
|
||||
/pnpm-lock.yaml @matrix-org/element-web-team
|
||||
/scripts/** @matrix-org/element-web-team
|
||||
/src/webrtc @matrix-org/element-call-reviewers
|
||||
/src/matrixrtc @matrix-org/element-call-reviewers
|
||||
/spec/*/webrtc @matrix-org/element-call-reviewers
|
||||
/spec/*/matrixrtc @matrix-org/element-call-reviewers
|
||||
|
||||
/src/crypto-api @matrix-org/element-crypto-web-reviewers
|
||||
/src/crypto @matrix-org/element-crypto-web-reviewers
|
||||
/src/rust-crypto @matrix-org/element-crypto-web-reviewers
|
||||
/spec/integ/crypto @matrix-org/element-crypto-web-reviewers
|
||||
/spec/unit/crypto.spec.ts @matrix-org/element-crypto-web-reviewers
|
||||
/spec/unit/crypto @matrix-org/element-crypto-web-reviewers
|
||||
/spec/unit/rust-crypto @matrix-org/element-crypto-web-reviewers
|
||||
@@ -1,2 +0,0 @@
|
||||
patreon: matrixdotorg
|
||||
liberapay: matrixdotorg
|
||||
@@ -1,8 +0,0 @@
|
||||
<!-- Thanks for submitting a PR! Please ensure the following requirements are met in order for us to review your PR -->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Tests written for new code (and old code if feasible).
|
||||
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
|
||||
- [ ] Linter and other CI checks pass.
|
||||
- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md)).
|
||||
@@ -1,28 +0,0 @@
|
||||
name: Sign Release Tarball
|
||||
description: Generates signature for release tarball and uploads it as a release asset
|
||||
inputs:
|
||||
gpg-fingerprint:
|
||||
description: Fingerprint of the GPG key to use for signing the tarball.
|
||||
required: true
|
||||
upload-url:
|
||||
description: GitHub release upload URL to upload the signature file to.
|
||||
required: true
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Generate tarball signature
|
||||
shell: bash
|
||||
run: |
|
||||
git -c tar.tar.gz.command='gzip -cn' archive --format=tar.gz --prefix="${REPO#*/}-${VERSION#v}/" -o "/tmp/${VERSION}.tar.gz" "${VERSION}"
|
||||
gpg -u "$GPG_FINGERPRINT" --armor --output "${VERSION}.tar.gz.asc" --detach-sig "/tmp/${VERSION}.tar.gz"
|
||||
rm "/tmp/${VERSION}.tar.gz"
|
||||
env:
|
||||
GPG_FINGERPRINT: ${{ inputs.gpg-fingerprint }}
|
||||
REPO: ${{ github.repository }}
|
||||
|
||||
- name: Upload tarball signature
|
||||
if: ${{ inputs.upload-url }}
|
||||
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ env.VERSION }}.tar.gz.asc
|
||||
@@ -1,41 +0,0 @@
|
||||
name: Upload release assets
|
||||
description: Uploads assets to an existing release and optionally signs them
|
||||
inputs:
|
||||
gpg-fingerprint:
|
||||
description: Fingerprint of the GPG key to use for signing the assets, if any.
|
||||
required: false
|
||||
upload-url:
|
||||
description: GitHub release upload URL to upload the assets to.
|
||||
required: true
|
||||
asset-path:
|
||||
description: |
|
||||
The path to the asset you want to upload, if any. You can use glob patterns here.
|
||||
Will be GPG signed and an `.asc` file included in the release artifacts if `gpg-fingerprint` is set.
|
||||
required: true
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Sign assets
|
||||
if: inputs.gpg-fingerprint
|
||||
shell: bash
|
||||
run: |
|
||||
for FILE in $ASSET_PATH
|
||||
do
|
||||
gpg -u "$GPG_FINGERPRINT" --armor --output "$FILE".asc --detach-sig "$FILE"
|
||||
done
|
||||
env:
|
||||
GPG_FINGERPRINT: ${{ inputs.gpg-fingerprint }}
|
||||
ASSET_PATH: ${{ inputs.asset-path }}
|
||||
|
||||
- name: Upload asset signatures
|
||||
if: inputs.gpg-fingerprint
|
||||
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ inputs.asset-path }}.asc
|
||||
|
||||
- name: Upload assets
|
||||
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ inputs.asset-path }}
|
||||
@@ -1,46 +0,0 @@
|
||||
- name: "A-Element-R"
|
||||
description: "Issues affecting the port of Element's crypto layer to Rust"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Packaging"
|
||||
description: "Packaging, signing, releasing"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Technical-Debt"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Testing"
|
||||
description: "Testing, code coverage, etc."
|
||||
color: "bfd4f2"
|
||||
- name: "backport staging"
|
||||
description: "Label to automatically backport PR to staging branch"
|
||||
color: "B60205"
|
||||
- name: "Dependencies"
|
||||
description: "Pull requests that update a dependency file"
|
||||
color: "0366d6"
|
||||
- name: "Easy"
|
||||
color: "5dc9f7"
|
||||
- name: "Sponsored"
|
||||
color: "ffc8f4"
|
||||
- name: "T-Deprecation"
|
||||
description: "A pull request that makes something deprecated"
|
||||
color: "98e6ae"
|
||||
- name: "T-Other"
|
||||
description: "Questions, user support, anything else"
|
||||
color: "98e6ae"
|
||||
- name: "X-Blocked"
|
||||
color: "ff7979"
|
||||
- name: "X-Breaking-Change"
|
||||
color: "ff7979"
|
||||
- name: "X-Reverted"
|
||||
description: "PR has been reverted"
|
||||
color: "F68AA3"
|
||||
- name: "X-Upcoming-Release-Blocker"
|
||||
description: "This does not affect the current release cycle but will affect the next one"
|
||||
color: "e99695"
|
||||
- name: "Z-Community-PR"
|
||||
description: "Issue is solved by a community member's PR"
|
||||
color: "ededed"
|
||||
- name: "Z-Flaky-Test"
|
||||
description: "A test is raising false alarms"
|
||||
color: "ededed"
|
||||
- name: "Z-Skip-Coverage"
|
||||
description: "Skip SonarQube coverage for this PR"
|
||||
color: "ededed"
|
||||
@@ -1,35 +0,0 @@
|
||||
name-template: "v$RESOLVED_VERSION"
|
||||
tag-template: "v$RESOLVED_VERSION"
|
||||
change-template: "* $TITLE ([#$NUMBER]($URL)). Contributed by @$AUTHOR."
|
||||
categories:
|
||||
- title: "🚨 BREAKING CHANGES"
|
||||
label: "X-Breaking-Change"
|
||||
- title: "🦖 Deprecations"
|
||||
label: "T-Deprecation"
|
||||
- title: "✨ Features"
|
||||
label: "T-Enhancement"
|
||||
- title: "🐛 Bug Fixes"
|
||||
label: "T-Defect"
|
||||
- title: "🧰 Maintenance"
|
||||
label: "Dependencies"
|
||||
collapse-after: 5
|
||||
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
|
||||
version-resolver:
|
||||
major:
|
||||
labels:
|
||||
- "X-Breaking-Change"
|
||||
default: minor
|
||||
exclude-labels:
|
||||
- "T-Task"
|
||||
- "X-Reverted"
|
||||
- "backport staging"
|
||||
exclude-contributors:
|
||||
- "RiotRobot"
|
||||
template: |
|
||||
$CHANGES
|
||||
#no-changes-template: ""
|
||||
prerelease: true
|
||||
prerelease-identifier: rc
|
||||
include-pre-releases: false
|
||||
stable-ref: master
|
||||
staging-ref: staging
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["github>matrix-org/renovate-config-element-web"]
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
name: Backport
|
||||
on:
|
||||
# Privilege escalation necessary to enable backporting PRs from forks
|
||||
# 🚨 We must not execute any checked out code here.
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers]
|
||||
types:
|
||||
- closed
|
||||
- labeled
|
||||
branches:
|
||||
- develop
|
||||
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
name: Backport
|
||||
runs-on: ubuntu-24.04
|
||||
# Only react to merged PRs for security reasons.
|
||||
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
|
||||
if: >
|
||||
github.event.pull_request.merged
|
||||
&& (
|
||||
github.event.action == 'closed'
|
||||
|| (
|
||||
github.event.action == 'labeled'
|
||||
&& contains(github.event.label.name, 'backport')
|
||||
)
|
||||
)
|
||||
steps:
|
||||
- uses: tibdex/backport@9565281eda0731b1d20c4025c43339fb0a23812e # v2
|
||||
with:
|
||||
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
|
||||
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
|
||||
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
@@ -1,38 +0,0 @@
|
||||
name: Deploy documentation PR preview
|
||||
|
||||
on:
|
||||
# Privilege escalation necessary to publish to Netlify
|
||||
# 🚨 We must not execute any checked out code here.
|
||||
workflow_run: # zizmor: ignore[dangerous-triggers]
|
||||
workflows: ["Static Analysis"]
|
||||
types:
|
||||
- completed
|
||||
permissions: {}
|
||||
jobs:
|
||||
netlify:
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
actions: read
|
||||
deployments: write
|
||||
steps:
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
name: docs
|
||||
path: docs
|
||||
|
||||
- name: 📤 Deploy to Netlify
|
||||
uses: matrix-org/netlify-pr-preview@9805cd123fc9a7e421e35340a05e1ebc5dee46b5 # v3
|
||||
with:
|
||||
path: docs
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
site_id: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
desc: Documentation preview
|
||||
deployment_env: PR Documentation Preview
|
||||
environment: PR Documentation Preview
|
||||
@@ -1,34 +0,0 @@
|
||||
# Triggers after the "Downstream artifacts" build has finished, to run the
|
||||
# element-web playwright tests (with access to repo secrets)
|
||||
|
||||
name: Element Web End to End Tests
|
||||
on:
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
pull_request: {}
|
||||
|
||||
# For now at least, we don't run this or the downstream-end-to-end-tests against pushes
|
||||
# to develop or master.
|
||||
#
|
||||
#push:
|
||||
# branches: [develop, master]
|
||||
permissions: {} # No permissions required
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
|
||||
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
playwright:
|
||||
name: Playwright
|
||||
uses: element-hq/element-web/.github/workflows/build-and-test.yaml@develop # zizmor: ignore[unpinned-uses]
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
contents: read
|
||||
with:
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
# We only want to run the playwright tests on merge queue to prevent regressions
|
||||
# from creeping in. They take a long time to run and consume multiple concurrent runners.
|
||||
skip: ${{ github.event_name != 'merge_group' }}
|
||||
@@ -1,26 +0,0 @@
|
||||
name: Notify Downstream Projects
|
||||
on:
|
||||
push:
|
||||
branches: [develop]
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
jobs:
|
||||
notify-downstream:
|
||||
# Only respect triggers from our develop branch, ignore that of forks
|
||||
if: github.repository == 'matrix-org/matrix-js-sdk'
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- repo: element-hq/element-web
|
||||
event: element-web-notify
|
||||
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Notify element-web repo that a new SDK build is on develop so it can CI against it
|
||||
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
repository: ${{ matrix.repo }}
|
||||
event-type: ${{ matrix.event }}
|
||||
@@ -1,104 +0,0 @@
|
||||
name: Pull Request
|
||||
on:
|
||||
# Privilege escalation necessary access members of the review teams
|
||||
# 🚨 We must not execute any checked out code here, and be careful around use of user-controlled inputs.
|
||||
# FIXME: only `community-prs` job needs this privilege, so it should be in its own workflow file.
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers]
|
||||
types: [opened, edited, labeled, unlabeled, synchronize]
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
workflow_call:
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
jobs:
|
||||
changelog:
|
||||
name: Preview Changelog
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5
|
||||
if: github.event_name != 'merge_group'
|
||||
with:
|
||||
labels: |
|
||||
X-Breaking-Change
|
||||
T-Deprecation
|
||||
T-Enhancement
|
||||
T-Defect
|
||||
T-Task
|
||||
Dependencies
|
||||
mode: minimum
|
||||
count: 1
|
||||
|
||||
prevent-blocked:
|
||||
name: Prevent Blocked
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Add notice
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
|
||||
with:
|
||||
script: |
|
||||
core.setFailed("Preventing merge whilst PR is marked blocked!");
|
||||
|
||||
community-prs:
|
||||
name: Label Community PRs
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.event.action == 'opened'
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check membership
|
||||
if: github.event.pull_request.user.login != 'renovate[bot]' && github.event.pull_request.user.login != 'dependabot[bot]'
|
||||
uses: tspascoal/get-user-teams-membership@57e9f42acd78f4d0f496b3be4368fc5f62696662 # v3
|
||||
id: teams
|
||||
with:
|
||||
username: ${{ github.event.pull_request.user.login }}
|
||||
organization: matrix-org
|
||||
team: Core Team
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
- name: Add label
|
||||
if: steps.teams.outputs.isTeamMember == 'false'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ['Z-Community-PR']
|
||||
});
|
||||
|
||||
close-if-fork-develop:
|
||||
name: Forbid develop branch fork contributions
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
pull-requests: write
|
||||
if: >
|
||||
github.event.action == 'opened' &&
|
||||
github.event.pull_request.head.ref == 'develop' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository
|
||||
steps:
|
||||
- name: Close pull request
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: "Thanks for opening this pull request, unfortunately we do not accept contributions from the main" +
|
||||
" branch of your fork, please re-open once you switch to an alternative branch for everyone's sanity." +
|
||||
" See https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md",
|
||||
});
|
||||
|
||||
github.rest.pulls.update({
|
||||
pull_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'closed'
|
||||
});
|
||||
@@ -1,38 +0,0 @@
|
||||
name: Release Sanity checks
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: false
|
||||
inputs:
|
||||
repository:
|
||||
type: string
|
||||
required: false
|
||||
default: ${{ github.repository }}
|
||||
description: "The repository (in form owner/repo) to check for release blockers"
|
||||
|
||||
permissions: {}
|
||||
jobs:
|
||||
checks:
|
||||
name: Sanity checks
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check for X-Release-Blocker label on any open issues or PRs
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
env:
|
||||
REPO: ${{ inputs.repository }}
|
||||
with:
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { REPO } = process.env;
|
||||
const { data } = await github.rest.search.issuesAndPullRequests({
|
||||
q: `repo:${REPO} label:X-Release-Blocker is:open`,
|
||||
per_page: 50,
|
||||
});
|
||||
|
||||
if (data.total_count) {
|
||||
data.items.forEach(item => {
|
||||
core.error(`Release blocker: ${item.html_url}`);
|
||||
});
|
||||
core.setFailed(`Found release blockers!`);
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
# Workflow used by other workflows to generate draft releases.
|
||||
name: Release Drafter Reusable
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
include-changes:
|
||||
description: Project to include changelog entries from in this release.
|
||||
type: string
|
||||
required: false
|
||||
concurrency: release-drafter-action
|
||||
permissions: {}
|
||||
jobs:
|
||||
draft:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: staging
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install Deps
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- uses: t3chguy/release-drafter@105e541c2c3d857f032bd522c0764694758fabad
|
||||
id: draft-release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
disable-autolabeler: true
|
||||
|
||||
- name: Get actions scripts
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
persist-credentials: false
|
||||
path: .action-repo
|
||||
sparse-checkout: |
|
||||
.github/actions
|
||||
scripts/release
|
||||
|
||||
- name: Ingest upstream changes
|
||||
if: inputs.include-changes
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
||||
DEPENDENCY: ${{ inputs.include-changes }}
|
||||
VERSION: ${{ steps.draft-release.outputs.tag_name }}
|
||||
with:
|
||||
retries: 3
|
||||
script: |
|
||||
const { RELEASE_ID: releaseId, DEPENDENCY, VERSION } = process.env;
|
||||
const { owner, repo } = context.repo;
|
||||
const script = require("./.action-repo/scripts/release/merge-release-notes.cjs");
|
||||
|
||||
let deps = [];
|
||||
if (DEPENDENCY.includes("/")) {
|
||||
deps.push(DEPENDENCY.replace("$VERSION", VERSION))
|
||||
} else {
|
||||
const fromVersion = JSON.parse((await github.request(`https://raw.githubusercontent.com/${owner}/${repo}/master/package.json`)).data).dependencies[DEPENDENCY];
|
||||
const toVersion = require("./package.json").dependencies[DEPENDENCY];
|
||||
|
||||
if (toVersion.endsWith("#develop")) {
|
||||
core.warning(`${DEPENDENCY} will be kept at ${fromVersion}`, { title: "Develop dependency found" });
|
||||
} else {
|
||||
deps.push([DEPENDENCY, fromVersion, toVersion]);
|
||||
}
|
||||
}
|
||||
|
||||
if (deps.length) {
|
||||
const notes = await script({
|
||||
github,
|
||||
releaseId,
|
||||
dependencies: deps,
|
||||
});
|
||||
|
||||
await github.rest.repos.updateRelease({
|
||||
owner,
|
||||
repo,
|
||||
release_id: releaseId,
|
||||
body: notes,
|
||||
tag_name: VERSION,
|
||||
});
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
# Generates the draft release for the js-sdk
|
||||
# Normally triggered whenever anything is merged to the staging branch, but
|
||||
# also has a workflow dispatch trigger in case it needs running manually due
|
||||
# to failures / workflow updates etc.
|
||||
name: Release Drafter
|
||||
on:
|
||||
push:
|
||||
branches: [staging]
|
||||
workflow_dispatch: {}
|
||||
concurrency: ${{ github.workflow }}
|
||||
permissions: {}
|
||||
jobs:
|
||||
draft:
|
||||
permissions:
|
||||
contents: write
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop # zizmor: ignore[unpinned-uses]
|
||||
@@ -1,91 +0,0 @@
|
||||
# Gitflow merge-back master->develop
|
||||
name: Merge master -> develop
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
workflow_call:
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
inputs:
|
||||
dependencies:
|
||||
description: List of dependencies to reset.
|
||||
type: string
|
||||
required: false
|
||||
dir:
|
||||
description: The directory to release
|
||||
type: string
|
||||
default: "."
|
||||
concurrency: ${{ github.workflow }}
|
||||
permissions: {} # Uses ELEMENT_BOT_TOKEN
|
||||
jobs:
|
||||
merge:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
- name: Get actions scripts
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
persist-credentials: false
|
||||
path: .action-repo
|
||||
sparse-checkout: |
|
||||
scripts/release
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: Set up git
|
||||
run: |
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
|
||||
- name: Merge to develop
|
||||
run: |
|
||||
git checkout develop
|
||||
git merge -X ours master
|
||||
|
||||
- name: Reset dependencies
|
||||
if: inputs.dependencies
|
||||
working-directory: ${{ inputs.dir }}
|
||||
run: |
|
||||
while IFS= read -r PACKAGE; do
|
||||
[ -z "$PACKAGE" ] && continue
|
||||
|
||||
CURRENT_VERSION=$(cat package.json | jq -r .dependencies[\"$PACKAGE\"])
|
||||
echo "Current $PACKAGE version is $CURRENT_VERSION"
|
||||
|
||||
if [[ "$CURRENT_VERSION" == "null" ]]
|
||||
then
|
||||
echo "Unable to find $PACKAGE in package.json"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$CURRENT_VERSION" == *"#develop" ]]
|
||||
then
|
||||
echo "Not updating dependency $PACKAGE"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Resetting $PACKAGE to develop branch..."
|
||||
pnpm add "github:matrix-org/$PACKAGE#develop"
|
||||
git add -u
|
||||
git commit -m "Reset $PACKAGE back to develop branch"
|
||||
done <<< "$DEPENDENCIES"
|
||||
env:
|
||||
DEPENDENCIES: ${{ inputs.dependencies }}
|
||||
|
||||
- name: Push changes
|
||||
run: git push origin develop
|
||||
@@ -1,332 +0,0 @@
|
||||
name: Release Make
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
GPG_PASSPHRASE:
|
||||
required: false
|
||||
GPG_PRIVATE_KEY:
|
||||
required: false
|
||||
inputs:
|
||||
final:
|
||||
description: Make final release
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
npm:
|
||||
description: Publish to npm
|
||||
type: boolean
|
||||
default: false
|
||||
gpg-fingerprint:
|
||||
description: Fingerprint of the GPG key to use for signing the git tag and assets, if any.
|
||||
type: string
|
||||
required: false
|
||||
asset-path:
|
||||
description: |
|
||||
The path to the asset you want to upload, if any. You can use glob patterns here.
|
||||
Will be GPG signed and an `.asc` file included in the release artifacts if `gpg-fingerprint` is set.
|
||||
Relative to `dir`.
|
||||
type: string
|
||||
required: false
|
||||
expected-asset-count:
|
||||
description: The number of expected assets, including signatures, excluding generated zip & tarball.
|
||||
type: number
|
||||
required: false
|
||||
dist-dir:
|
||||
description: The directory to release
|
||||
type: string
|
||||
default: "."
|
||||
version-dirs:
|
||||
description: Directories in which to update package.json `version` field
|
||||
type: string
|
||||
required: false
|
||||
outputs:
|
||||
npm-id:
|
||||
description: "The npm package@version string we published"
|
||||
value: ${{ jobs.npm.outputs.id }}
|
||||
permissions: {}
|
||||
jobs:
|
||||
checks:
|
||||
name: Sanity checks
|
||||
permissions:
|
||||
issues: read
|
||||
pull-requests: read
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop # zizmor: ignore[unpinned-uses]
|
||||
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-24.04
|
||||
environment: Release
|
||||
needs: checks
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Load GPG key
|
||||
id: gpg
|
||||
if: inputs.gpg-fingerprint
|
||||
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
fingerprint: ${{ inputs.gpg-fingerprint }}
|
||||
|
||||
- name: Get draft release
|
||||
id: draft-release
|
||||
uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # 1.2.5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
draft: true
|
||||
latest: true
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: staging
|
||||
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
- name: Get actions scripts
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
persist-credentials: false
|
||||
path: .action-repo
|
||||
sparse-checkout: |
|
||||
.github/actions
|
||||
scripts/release
|
||||
|
||||
- name: Prepare variables
|
||||
id: prepare
|
||||
working-directory: ${{ inputs.dist-dir }}
|
||||
run: |
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
HAS_DIST=0
|
||||
jq -e .scripts.dist package.json >/dev/null 2>&1 && HAS_DIST=1
|
||||
echo "has-dist-script=$HAS_DIST" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
VERSION: ${{ steps.draft-release.outputs.tag_name }}
|
||||
|
||||
- name: Finalise version
|
||||
if: inputs.final
|
||||
run: echo "VERSION=$(echo $VERSION | cut -d- -f1)" >> $GITHUB_ENV
|
||||
|
||||
- name: Check version number not in use
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
with:
|
||||
script: |
|
||||
const { VERSION } = process.env;
|
||||
github.rest.repos.getReleaseByTag({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
tag: VERSION,
|
||||
}).then(() => {
|
||||
core.setFailed(`Version ${VERSION} already exists`);
|
||||
}).catch(() => {
|
||||
// This is fine, we expect there to not be any release with this version yet
|
||||
});
|
||||
|
||||
- name: Set up git
|
||||
run: |
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: ${{ inputs.dist-dir }}/package.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: Handle develop dependencies
|
||||
working-directory: ${{ inputs.dist-dir }}
|
||||
run: |
|
||||
ret=0
|
||||
cat package.json | jq -r '.dependencies | to_entries | .[] | "\(.key) \(.value)"' | grep '#develop$' | while read -r dep ; do
|
||||
IFS=" "
|
||||
PACKAGE=${dep[0]}
|
||||
VERSION=${dep[1]}
|
||||
|
||||
echo "::warning title=Develop dependency found::$DEPENDENCY will be kept at $VERSION"
|
||||
pnpm add "$PACKAGE@$VERSION" --save-exact
|
||||
git add -u
|
||||
git commit -m "Keep $PACKAGE at $VERSION"
|
||||
done
|
||||
|
||||
- name: Bump package.json versions
|
||||
run: |
|
||||
for DIR in $DIRS; do
|
||||
pnpm version -C "$DIR" --no-git-tag-version "${VERSION#v}"
|
||||
git add "$DIR"/package.json
|
||||
done
|
||||
env:
|
||||
DIRS: ${{ inputs.version-dirs || inputs.dist-dir }}
|
||||
|
||||
- name: Add to CHANGELOG.md
|
||||
if: inputs.final
|
||||
run: |
|
||||
mv CHANGELOG.md CHANGELOG.md.old
|
||||
HEADER="Changes in [${VERSION#v}](https://github.com/${{ github.repository }}/releases/tag/$VERSION) ($(date '+%Y-%m-%d'))"
|
||||
|
||||
{
|
||||
echo "$HEADER"
|
||||
printf '=%.0s' $(seq ${#HEADER})
|
||||
echo ""
|
||||
echo "$RELEASE_NOTES"
|
||||
echo ""
|
||||
} > CHANGELOG.md
|
||||
|
||||
cat CHANGELOG.md.old >> CHANGELOG.md
|
||||
rm CHANGELOG.md.old
|
||||
git add CHANGELOG.md
|
||||
env:
|
||||
RELEASE_NOTES: ${{ steps.draft-release.outputs.body }}
|
||||
|
||||
- name: Commit changes
|
||||
run: git commit -m "$VERSION"
|
||||
|
||||
- name: Build assets
|
||||
if: steps.prepare.outputs.has-dist-script == '1'
|
||||
working-directory: ${{ inputs.dist-dir }}
|
||||
run: DIST_VERSION="$VERSION" pnpm dist
|
||||
|
||||
- name: Upload release assets & signatures
|
||||
if: inputs.asset-path
|
||||
uses: ./.action-repo/.github/actions/upload-release-assets
|
||||
with:
|
||||
gpg-fingerprint: ${{ inputs.gpg-fingerprint }}
|
||||
upload-url: ${{ steps.draft-release.outputs.upload_url }}
|
||||
asset-path: ${{ inputs.dist-dir }}/${{ inputs.asset-path }}
|
||||
|
||||
- name: Create signed tag
|
||||
if: inputs.gpg-fingerprint
|
||||
run: |
|
||||
GIT_COMMITTER_EMAIL="$SIGNING_ID" GPG_TTY=$(tty) git tag -u "$SIGNING_ID" -m "Release $VERSION" "$VERSION"
|
||||
env:
|
||||
SIGNING_ID: ${{ steps.gpg.outputs.email }}
|
||||
|
||||
- name: Generate & upload tarball signature
|
||||
if: inputs.gpg-fingerprint
|
||||
uses: ./.action-repo/.github/actions/sign-release-tarball
|
||||
with:
|
||||
gpg-fingerprint: ${{ inputs.gpg-fingerprint }}
|
||||
upload-url: ${{ steps.draft-release.outputs.upload_url }}
|
||||
|
||||
# We defer pushing changes until after the release assets are built,
|
||||
# signed & uploaded to improve the atomicity of this action.
|
||||
- name: Push changes to staging
|
||||
run: |
|
||||
git push origin staging $TAG
|
||||
git reset --hard
|
||||
env:
|
||||
TAG: ${{ inputs.gpg-fingerprint && env.VERSION || '' }}
|
||||
|
||||
- name: Validate tarball signature
|
||||
if: inputs.gpg-fingerprint
|
||||
run: |
|
||||
wget https://github.com/$GITHUB_REPOSITORY/archive/refs/tags/$VERSION.tar.gz
|
||||
gpg --verify "$VERSION.tar.gz.asc" "$VERSION.tar.gz"
|
||||
|
||||
- name: Validate release has expected assets
|
||||
if: inputs.expected-asset-count
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
env:
|
||||
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
||||
EXPECTED_ASSET_COUNT: ${{ inputs.expected-asset-count }}
|
||||
with:
|
||||
retries: 3
|
||||
script: |
|
||||
const { RELEASE_ID: release_id, EXPECTED_ASSET_COUNT } = process.env;
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
const { data: release } = await github.rest.repos.getRelease({
|
||||
owner,
|
||||
repo,
|
||||
release_id,
|
||||
});
|
||||
|
||||
if (release.assets.length !== parseInt(EXPECTED_ASSET_COUNT, 10)) {
|
||||
core.setFailed(`Found ${release.assets.length} assets but expected ${EXPECTED_ASSET_COUNT}`);
|
||||
}
|
||||
|
||||
- name: Merge to master
|
||||
if: inputs.final
|
||||
run: |
|
||||
git checkout master
|
||||
git merge -X theirs staging
|
||||
git push origin master
|
||||
|
||||
- name: Publish release
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
env:
|
||||
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
||||
FINAL: ${{ inputs.final }}
|
||||
with:
|
||||
retries: 3
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
script: |
|
||||
const { RELEASE_ID: release_id, RELEASE_NOTES, VERSION, FINAL } = process.env;
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
const opts = {
|
||||
owner,
|
||||
repo,
|
||||
release_id,
|
||||
tag_name: VERSION,
|
||||
name: VERSION,
|
||||
draft: false,
|
||||
body: RELEASE_NOTES,
|
||||
};
|
||||
|
||||
if (FINAL == "true") {
|
||||
opts.prerelease = false;
|
||||
opts.make_latest = true;
|
||||
}
|
||||
|
||||
github.rest.repos.updateRelease(opts);
|
||||
|
||||
npm:
|
||||
name: Publish to npm
|
||||
needs: release
|
||||
if: inputs.npm
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop # zizmor: ignore[unpinned-uses]
|
||||
with:
|
||||
dir: ${{ inputs.dist-dir }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
post-release:
|
||||
name: Post release steps
|
||||
needs: release
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- id: repository
|
||||
run: echo "REPO=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Advance release blocker labels
|
||||
uses: garganshu/github-label-updater@3770d15ebfed2fe2cb06a241047bc340f774a7d1 # v1.0.0
|
||||
with:
|
||||
owner: ${{ github.repository_owner }}
|
||||
repo: ${{ steps.repository.outputs.REPO }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
filter-labels: X-Upcoming-Release-Blocker
|
||||
remove-labels: X-Upcoming-Release-Blocker
|
||||
add-labels: X-Release-Blocker
|
||||
|
||||
# - name: Wait for master->develop gitflow merge
|
||||
# if: inputs.final
|
||||
# uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
|
||||
# with:
|
||||
# ref: master
|
||||
# repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# wait-interval: 10
|
||||
# check-name: merge
|
||||
# allowed-conclusions: success
|
||||
@@ -1,53 +0,0 @@
|
||||
name: Publish to npm
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
dir:
|
||||
description: The directory to release
|
||||
type: string
|
||||
default: "."
|
||||
outputs:
|
||||
id:
|
||||
description: "The npm package@version string we published"
|
||||
value: ${{ jobs.npm.outputs.id }}
|
||||
permissions: {}
|
||||
jobs:
|
||||
npm:
|
||||
name: Publish to npm
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
outputs:
|
||||
id: ${{ steps.npm-publish.outputs.id }}
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: staging
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- name: 🔧 pnpm cache
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
node-version-file: ${{ inputs.dir }}/package.json
|
||||
|
||||
# Ensure npm 11.5.1 or later is installed
|
||||
- name: Update npm
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: 🚀 Publish to npm
|
||||
id: npm-publish
|
||||
working-directory: ${{ inputs.dir }}
|
||||
run: |
|
||||
npm publish --provenance --access public --tag "$TAG"
|
||||
release=$(jq -r '"\(.name)@\(.version)"' package.json)
|
||||
echo "id=$release" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
TAG: ${{ contains(steps.npm-publish.outputs.id, '-rc.') && 'next' || 'latest' }}
|
||||
@@ -1,116 +0,0 @@
|
||||
name: Release Process
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
mode:
|
||||
description: What type of release
|
||||
required: true
|
||||
default: rc
|
||||
type: choice
|
||||
options:
|
||||
- rc
|
||||
- final
|
||||
docs:
|
||||
description: Publish docs
|
||||
required: true
|
||||
type: boolean
|
||||
default: true
|
||||
npm:
|
||||
description: Publish to npm
|
||||
required: true
|
||||
type: boolean
|
||||
default: true
|
||||
concurrency: ${{ github.workflow }}
|
||||
permissions: {} # No permissions required
|
||||
jobs:
|
||||
release:
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop # zizmor: ignore[unpinned-uses,secrets-inherit]
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: read
|
||||
id-token: write
|
||||
secrets: inherit
|
||||
with:
|
||||
final: ${{ inputs.mode == 'final' }}
|
||||
npm: ${{ inputs.npm }}
|
||||
|
||||
bump-downstreams:
|
||||
name: Update npm dependency in downstream projects
|
||||
needs: release
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- repo: element-hq/element-web
|
||||
path: apps/web
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: ${{ matrix.repo }}
|
||||
ref: staging
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
persist-credentials: true
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Bump dependency
|
||||
env:
|
||||
DEPENDENCY: ${{ needs.release.outputs.npm-id }}
|
||||
DIR: ${{ matrix.path }}
|
||||
run: |
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
pnpm add -C "$DIR" "$DEPENDENCY" --save-exact
|
||||
git add "$DIR"/package.json pnpm-lock.yaml
|
||||
git commit -am"Upgrade dependency to $DEPENDENCY"
|
||||
git push origin staging
|
||||
|
||||
docs:
|
||||
name: Publish Documentation
|
||||
needs: release
|
||||
if: inputs.docs
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- name: 🔧 pnpm cache
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: 📖 Generate docs
|
||||
run: pnpm gendoc
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
|
||||
with:
|
||||
path: _docs
|
||||
|
||||
docs-deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-24.04
|
||||
needs: docs
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5
|
||||
@@ -1,102 +0,0 @@
|
||||
# Must only be called from a workflow_run in the context of the upstream repo
|
||||
name: SonarCloud
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
SONAR_TOKEN:
|
||||
required: true
|
||||
# No longer used
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: false
|
||||
inputs:
|
||||
sharded:
|
||||
type: boolean
|
||||
required: false
|
||||
description: "Whether to combine multiple LCOV and sonar-report files in coverage artifact"
|
||||
version-pkg-json-dir:
|
||||
type: string
|
||||
default: "."
|
||||
description: "Relative path of the directory containing package.json with the `version` to use."
|
||||
permissions: {}
|
||||
jobs:
|
||||
sonarqube:
|
||||
runs-on: ubuntu-24.04
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event != 'merge_group'
|
||||
permissions:
|
||||
actions: read
|
||||
statuses: write
|
||||
id-token: write # sonar
|
||||
steps:
|
||||
# We create the status here and then update it to success/failure in the `report` stage
|
||||
# This provides an easy link to this workflow_run from the PR before Sonarcloud is done.
|
||||
- uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- name: "🧮 Checkout code"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
persist-credentials: false
|
||||
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
if: ${{ !inputs.sharded }}
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
name: coverage
|
||||
path: coverage
|
||||
- name: 📥 Download sharded artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
if: inputs.sharded
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
pattern: coverage-*
|
||||
path: coverage
|
||||
- name: Check coverage artifact
|
||||
run: |
|
||||
if [ ! -d coverage ]; then
|
||||
echo "Coverage not found. Exiting with failure."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- id: extra_args
|
||||
run: |
|
||||
coverage=$(find coverage -type f -name '*lcov.info' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
|
||||
echo "sonar.javascript.lcov.reportPaths=$coverage" >> sonar-project.properties
|
||||
reports=$(find coverage -type f -name '*sonar-report*.xml' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
|
||||
echo "sonar.testExecutionReportPaths=$reports" >> sonar-project.properties
|
||||
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
id: sonarcloud
|
||||
uses: matrix-org/sonarcloud-workflow-action@13968a27c924fa19b1dacbce6ca3ff217daa775b
|
||||
# workflow_run fails report against the develop commit always, we don't want that for PRs
|
||||
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
|
||||
with:
|
||||
skip_checkout: true
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
skip_coverage_label: Z-Skip-Coverage
|
||||
version_cmd: "cat ${{ inputs.version-pkg-json-dir }}/package.json | jq -r .version"
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.SONAR_TOKEN }}
|
||||
|
||||
- uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
|
||||
if: always()
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: ${{ steps.sonarcloud.outcome == 'success' && 'success' || 'failure' }}
|
||||
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
@@ -1,26 +0,0 @@
|
||||
name: SonarQube
|
||||
on:
|
||||
# Privilege escalation necessary to call upon SonarCloud
|
||||
# 🚨 We must not execute any checked out code here.
|
||||
workflow_run: # zizmor: ignore[dangerous-triggers]
|
||||
workflows: ["Tests"]
|
||||
types:
|
||||
- completed
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: true
|
||||
permissions: {}
|
||||
jobs:
|
||||
sonarqube:
|
||||
name: 🩻 SonarQube
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'merge_group'
|
||||
permissions:
|
||||
actions: read
|
||||
statuses: write
|
||||
id-token: write # sonar
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop # zizmor: ignore[unpinned-uses]
|
||||
secrets:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
with:
|
||||
sharded: true
|
||||
@@ -1,199 +0,0 @@
|
||||
name: Static Analysis
|
||||
on:
|
||||
pull_request: {}
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
push:
|
||||
branches: [develop, master]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
permissions: {} # No permissions needed
|
||||
jobs:
|
||||
ts_lint:
|
||||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Typecheck
|
||||
run: "pnpm run lint:types"
|
||||
|
||||
js_lint:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Run Linter
|
||||
run: "pnpm run lint:js"
|
||||
|
||||
node_example_lint:
|
||||
name: "Node.js example"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Build Types
|
||||
run: "pnpm build:types"
|
||||
|
||||
- name: Install Example Deps
|
||||
run: "npm install"
|
||||
working-directory: "examples/node"
|
||||
|
||||
- name: Check Syntax
|
||||
run: "node --check app.js"
|
||||
working-directory: "examples/node"
|
||||
|
||||
- name: Typecheck
|
||||
run: "npx tsc"
|
||||
working-directory: "examples/node"
|
||||
|
||||
workflow_lint:
|
||||
name: "Workflow Lint"
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
security-events: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: Run Linter
|
||||
run: "pnpm lint:workflows"
|
||||
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
||||
|
||||
docs:
|
||||
name: "JSDoc Checker"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Generate Docs
|
||||
run: "pnpm run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: docs
|
||||
path: _docs
|
||||
# We'll only use this in a workflow_run, then we're done with it
|
||||
retention-days: 1
|
||||
|
||||
analyse_dead_code:
|
||||
name: "Analyse Dead Code"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: Run linter
|
||||
run: "pnpm run lint:knip"
|
||||
|
||||
element-web:
|
||||
name: Downstream tsc element-web
|
||||
if: github.event_name == 'merge_group'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: element-hq/element-web
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: "./scripts/layered.sh"
|
||||
env:
|
||||
# tell layered.sh to check out the right sha of the JS-SDK
|
||||
JS_SDK_GITHUB_BASE_REF: ${{ github.sha }}
|
||||
|
||||
- name: Typecheck
|
||||
working-directory: apps/web
|
||||
run: "pnpm run lint:types"
|
||||
|
||||
# Workflow consolidation job
|
||||
done:
|
||||
needs:
|
||||
- ts_lint
|
||||
- js_lint
|
||||
- node_example_lint
|
||||
- workflow_lint
|
||||
- docs
|
||||
- analyse_dead_code
|
||||
- element-web
|
||||
name: Static Analysis
|
||||
runs-on: ubuntu-24.04
|
||||
if: always()
|
||||
steps:
|
||||
- if: contains(needs.*.result , 'failure') || contains(needs.*.result, 'cancelled')
|
||||
run: exit 1
|
||||
@@ -1,22 +0,0 @@
|
||||
name: Sync labels
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
schedule:
|
||||
- cron: "0 1 * * *" # 1am every day
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- .github/labels.yml
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
jobs:
|
||||
sync-labels:
|
||||
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@7f2f93fb9b52ece7a0998f60e64862aa203c1746
|
||||
with:
|
||||
LABELS: |
|
||||
element-hq/element-meta
|
||||
.github/labels.yml
|
||||
DELETE: true
|
||||
WET: true
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
@@ -1,131 +0,0 @@
|
||||
name: Tests
|
||||
on:
|
||||
pull_request: {}
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
push:
|
||||
branches: [develop, master]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
env:
|
||||
ENABLE_COVERAGE: ${{ github.event_name != 'merge_group' }}
|
||||
permissions: {} # No permissions required
|
||||
jobs:
|
||||
test:
|
||||
name: "Vitest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
matrix:
|
||||
specs: [integ, unit]
|
||||
node: ["lts/*", 22]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- name: Setup Node
|
||||
id: setupNode
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Get number of CPU cores
|
||||
id: cpu-cores
|
||||
uses: SimenB/github-actions-cpu-cores@97ba232459a8e02ff6121db9362b09661c875ab8 # v2
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
pnpm test \
|
||||
--coverage=${ENABLE_COVERAGE} \
|
||||
--maxWorkers ${NUM_WORKERS} \
|
||||
./spec/${{ matrix.specs }}
|
||||
env:
|
||||
SHARD: ${{ matrix.specs }}
|
||||
NUM_WORKERS: ${{ steps.cpu-cores.outputs.count }}
|
||||
|
||||
- name: Move coverage files into place
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
run: mv coverage/lcov.info coverage/${NODE_VERSION}-${{ matrix.specs }}.lcov.info
|
||||
env:
|
||||
NODE_VERSION: ${{ steps.setupNode.outputs.node-version }}
|
||||
|
||||
- name: Upload Artifact
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: coverage-${{ matrix.specs }}-${{ matrix.node == 'lts/*' && 'lts' || matrix.node }}
|
||||
path: |
|
||||
coverage
|
||||
!coverage/lcov-report
|
||||
|
||||
# Dummy completion job to simplify branch protections
|
||||
complete:
|
||||
name: Tests
|
||||
needs: test
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- if: needs.test.result != 'skipped' && needs.test.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
element-web:
|
||||
name: Downstream test element-web
|
||||
if: github.event_name == 'merge_group'
|
||||
uses: element-hq/element-web/.github/workflows/tests.yml@develop # zizmor: ignore[unpinned-uses]
|
||||
permissions:
|
||||
statuses: write
|
||||
with:
|
||||
disable_coverage: true
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
|
||||
complement-crypto:
|
||||
name: "Run Complement Crypto tests"
|
||||
if: github.event_name == 'merge_group'
|
||||
permissions: read-all # zizmor: ignore[excessive-permissions]
|
||||
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main # zizmor: ignore[unpinned-uses]
|
||||
with:
|
||||
use_js_sdk: "."
|
||||
|
||||
# we need this so the job is reported properly when run in a merge queue
|
||||
downstream-complement-crypto:
|
||||
name: Downstream Complement Crypto tests
|
||||
runs-on: ubuntu-24.04
|
||||
if: always()
|
||||
needs:
|
||||
- complement-crypto
|
||||
steps:
|
||||
- if: needs.complement-crypto.result != 'skipped' && needs.complement-crypto.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
# Hook for branch protection to skip downstream testing outside of merge queues
|
||||
# and skip sonarcloud coverage within merge queues
|
||||
downstream:
|
||||
name: Downstream tests
|
||||
runs-on: ubuntu-24.04
|
||||
if: always()
|
||||
needs:
|
||||
- element-web
|
||||
permissions:
|
||||
statuses: write
|
||||
steps:
|
||||
- name: Skip SonarCloud on merge queues
|
||||
if: env.ENABLE_COVERAGE == 'false'
|
||||
uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
description: SonarCloud skipped
|
||||
context: SonarCloud Code Analysis
|
||||
sha: ${{ github.sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- if: needs.element-web.result != 'skipped' && needs.element-web.result != 'success'
|
||||
run: exit 1
|
||||
@@ -1,14 +0,0 @@
|
||||
name: Move new issues into Issue triage board
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
jobs:
|
||||
automate-project-columns-next:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
||||
with:
|
||||
project-url: https://github.com/orgs/element-hq/projects/120
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
@@ -1,11 +0,0 @@
|
||||
name: Move labelled issues to correct projects
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
jobs:
|
||||
call-triage-labelled:
|
||||
uses: element-hq/element-web/.github/workflows/triage-labelled.yml@6339bcda15c71d209303b18a06a9b1c021220bf9
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
@@ -1,22 +0,0 @@
|
||||
name: Close stale PRs
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
permissions: {}
|
||||
jobs:
|
||||
close:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
actions: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
|
||||
with:
|
||||
operations-per-run: 250
|
||||
days-before-issue-stale: -1
|
||||
days-before-issue-close: -1
|
||||
days-before-pr-stale: 180
|
||||
days-before-pr-close: 0
|
||||
close-pr-message: "This PR has been automatically closed because it has been stale for 180 days. If you wish to continue working on this PR, please ping a maintainer to reopen it."
|
||||
+6
-10
@@ -1,21 +1,17 @@
|
||||
/_docs
|
||||
.DS_Store
|
||||
/.jsdocbuild
|
||||
/.jsdoc
|
||||
|
||||
node_modules
|
||||
/.npmrc
|
||||
/*.log
|
||||
package-lock.json
|
||||
.lock-wscript
|
||||
build/Release
|
||||
coverage
|
||||
lib-cov
|
||||
out
|
||||
reports
|
||||
/dist
|
||||
/lib
|
||||
/specbuild
|
||||
|
||||
# tarball created by `npm pack` / `yarn pack`
|
||||
# version file and tarball created by 'npm pack'
|
||||
/git-revision.txt
|
||||
/matrix-js-sdk-*.tgz
|
||||
|
||||
.vscode
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
npx lint-staged
|
||||
@@ -0,0 +1,2 @@
|
||||
instrumentation:
|
||||
compact: false
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"*.(ts|tsx)": ["eslint --fix", "prettier --write"],
|
||||
"*.(py|md|yaml)": ["prettier --write"]
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/_docs
|
||||
.DS_Store
|
||||
|
||||
/.npmrc
|
||||
/*.log
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
.lock-wscript
|
||||
build/Release
|
||||
coverage
|
||||
lib-cov
|
||||
out
|
||||
/dist
|
||||
/lib
|
||||
/examples/browser/lib
|
||||
/examples/crypto-browser/lib
|
||||
/examples/voip/lib
|
||||
|
||||
# version file and tarball created by `npm pack` / `yarn pack`
|
||||
/git-revision.txt
|
||||
/matrix-js-sdk-*.tgz
|
||||
|
||||
.vscode
|
||||
.vscode/
|
||||
|
||||
# This file is owned, parsed, and generated by allchange, which doesn't comply with prettier
|
||||
/CHANGELOG.md
|
||||
|
||||
# These files are also autogenerated
|
||||
/spec/test-utils/test-data/index.ts
|
||||
/spec/test-utils/test_indexeddb_cryptostore_dump/dump.json
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = require("eslint-plugin-matrix-org/.prettierrc.js");
|
||||
@@ -0,0 +1,5 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "10.11.0"
|
||||
script:
|
||||
- ./travis.sh
|
||||
+1
-4605
File diff suppressed because it is too large
Load Diff
-241
@@ -1,241 +0,0 @@
|
||||
# Contributing code to matrix-js-sdk
|
||||
|
||||
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are
|
||||
willing to license their contributions under the same license as the project
|
||||
itself. We follow a simple 'inbound=outbound' model for contributions: the act
|
||||
of submitting an 'inbound' contribution means that the contributor agrees to
|
||||
license the code under the same terms as the project's overall 'outbound'
|
||||
license - in this case, Apache Software License v2 (see
|
||||
[LICENSE](LICENSE)).
|
||||
|
||||
## How to contribute
|
||||
|
||||
The preferred and easiest way to contribute changes to the project is to fork
|
||||
it on github, and then create a pull request to ask us to pull your changes
|
||||
into our repo (https://help.github.com/articles/using-pull-requests/)
|
||||
|
||||
We use GitHub's pull request workflow to review the contribution, and either
|
||||
ask you to make any refinements needed or merge it and make them ourselves.
|
||||
|
||||
Your PR should have a title that describes what change is being made. This
|
||||
is used for the text in the Changelog entry by default (see below), so a good
|
||||
title will tell a user succinctly what change is being made. "Fix bug where
|
||||
cows had five legs" and, "Add support for miniature horses" are examples of good
|
||||
titles. Don't include an issue number here: that belongs in the description.
|
||||
Definitely don't use the GitHub default of "Update file.ts".
|
||||
|
||||
As for your PR description, it should include these things:
|
||||
|
||||
- References to any bugs fixed by the change (in GitHub's `Fixes` notation)
|
||||
- Describe the why and what is changing in the PR description so it's easy for
|
||||
onlookers and reviewers to onboard and context switch. This information is
|
||||
also helpful when we come back to look at this in 6 months and ask "why did
|
||||
we do it like that?" we have a chance of finding out.
|
||||
- Why didn't it work before? Why does it work now? What use cases does it
|
||||
unlock?
|
||||
- If you find yourself adding information on how the code works or why you
|
||||
chose to do it the way you did, make sure this information is instead
|
||||
written as comments in the code itself.
|
||||
- Sometimes a PR can change considerably as it is developed. In this case,
|
||||
the description should be updated to reflect the most recent state of
|
||||
the PR. (It can be helpful to retain the old content under a suitable
|
||||
heading, for additional context.)
|
||||
- Include a step-by-step testing strategy so that a reviewer can check out the
|
||||
code locally and easily get to the point of testing your change.
|
||||
- Add comments to the diff for the reviewer that might help them to understand
|
||||
why the change is necessary or how they might better understand and review it.
|
||||
|
||||
### Changelogs
|
||||
|
||||
There's no need to manually add Changelog entries: we use information in the
|
||||
pull request to populate the information that goes into the changelogs our
|
||||
users see, both for Element Web itself and other projects on which it is based.
|
||||
This is picked up from both labels on the pull request and the `Notes:`
|
||||
annotation in the description. By default, the PR title will be used for the
|
||||
changelog entry, but you can specify more options, as follows.
|
||||
|
||||
To add a longer, more detailed description of the change for the changelog:
|
||||
|
||||
_Fix llama herding bug_
|
||||
|
||||
```
|
||||
Notes: Fix a bug (https://github.com/matrix-org/notaproject/issues/123) where the 'Herd' button would not herd more than 8 Llamas if the moon was in the waxing gibbous phase
|
||||
```
|
||||
|
||||
For some PRs, it's not useful to have an entry in the user-facing changelog (this is
|
||||
the default for PRs labelled with `T-Task`):
|
||||
|
||||
_Remove outdated comment from `Ungulates.ts`_
|
||||
|
||||
```
|
||||
Notes: none
|
||||
```
|
||||
|
||||
Sometimes, you're fixing a bug in a downstream project, in which case you want
|
||||
an entry in that project's changelog. You can do that too:
|
||||
|
||||
_Fix another herding bug_
|
||||
|
||||
```
|
||||
Notes: Fix a bug where the `herd()` function would only work on Tuesdays
|
||||
element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays
|
||||
```
|
||||
|
||||
This example is for Element Web. You can specify:
|
||||
|
||||
- element-web
|
||||
- element-desktop
|
||||
|
||||
If your PR introduces a breaking change, use the `Notes` section in the same
|
||||
way, additionally adding the `X-Breaking-Change` label (see below). There's no need
|
||||
to specify in the notes that it's a breaking change - this will be added
|
||||
automatically based on the label - but remember to tell the developer how to
|
||||
migrate:
|
||||
|
||||
_Remove legacy class_
|
||||
|
||||
```
|
||||
Notes: Remove legacy `Camelopard` class. `Giraffe` should be used instead.
|
||||
```
|
||||
|
||||
Other metadata can be added using labels.
|
||||
|
||||
- `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a _major_ version bump.
|
||||
- `T-Enhancement`: A new feature - adding this label will mean the change causes a _minor_ version bump.
|
||||
- `T-Defect`: A bug fix (in either code or docs).
|
||||
- `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one.
|
||||
|
||||
If you don't have permission to add labels, your PR reviewer(s) can work with you
|
||||
to add them: ask in the PR description or comments.
|
||||
|
||||
We use continuous integration, and all pull requests get automatically tested:
|
||||
if your change breaks the build, then the PR will show that there are failed
|
||||
checks, so please check back after a few minutes.
|
||||
|
||||
## Tests
|
||||
|
||||
Your PR should include tests.
|
||||
|
||||
For new user facing features in `matrix-js-sdk`, you
|
||||
must include comprehensive unit tests written in Vitest.
|
||||
The existing tests can be found under `spec/unit`
|
||||
|
||||
It's good practice to write tests alongside the code as it ensures the code is testable from
|
||||
the start, and gives you a fast feedback loop while you're developing the
|
||||
functionality. Unit tests are necessary even for bug fixes.
|
||||
|
||||
When writing unit tests, please aim for a high level of test coverage
|
||||
for new code - 80% or greater. If you cannot achieve that, please document
|
||||
why it's not possible in your PR.
|
||||
|
||||
Tests validate that your change works as intended and also document
|
||||
concisely what is being changed. Ideally, your new tests fail
|
||||
prior to your change, and succeed once it has been applied. You may
|
||||
find this simpler to achieve if you write the tests first.
|
||||
|
||||
If you're spiking some code that's experimental and not being used to support
|
||||
production features, exceptions can be made to requirements for tests.
|
||||
Note that tests will still be required in order to ship the feature, and it's
|
||||
strongly encouraged to think about tests early in the process, as adding
|
||||
tests later will become progressively more difficult.
|
||||
|
||||
If you're not sure how to approach writing tests for your change, ask for help
|
||||
in [#element-dev](https://matrix.to/#/#element-dev:matrix.org).
|
||||
|
||||
## Code style
|
||||
|
||||
Code style is documented in [code_style.md](./code_style.md).
|
||||
Contributors are encouraged to it and follow the principles set out there.
|
||||
|
||||
Please ensure your changes match the cosmetic style of the existing project,
|
||||
and **_never_** mix cosmetic and functional changes in the same commit, as it
|
||||
makes it horribly hard to review otherwise.
|
||||
|
||||
## Sign off
|
||||
|
||||
In order to have a concrete record that your contribution is intentional
|
||||
and you agree to license it under the same terms as the project's license, we've
|
||||
adopted the same lightweight approach that the Linux Kernel
|
||||
(https://www.kernel.org/doc/html/latest/process/submitting-patches.html), Docker
|
||||
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
||||
projects use: the DCO (Developer Certificate of Origin:
|
||||
http://developercertificate.org/). This is a simple declaration that you wrote
|
||||
the contribution or otherwise have the right to contribute it to Matrix:
|
||||
|
||||
```
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
```
|
||||
|
||||
If you agree to this for your contribution, then all that's needed is to
|
||||
include the line in your commit or pull request comment:
|
||||
|
||||
```
|
||||
Signed-off-by: Your Name <your@email.example.org>
|
||||
```
|
||||
|
||||
We accept contributions under a legally identifiable name, such as your name on
|
||||
government documentation or common-law names (names claimed by legitimate usage
|
||||
or repute). Unfortunately, we cannot accept anonymous contributions at this
|
||||
time.
|
||||
|
||||
Git allows you to add this signoff automatically when using the `-s` flag to
|
||||
`git commit`, which uses the name and email set in your `user.name` and
|
||||
`user.email` git configs.
|
||||
|
||||
If you forgot to sign off your commits before making your pull request and are
|
||||
on Git 2.17+ you can mass signoff using rebase:
|
||||
|
||||
```
|
||||
git rebase --signoff origin/develop
|
||||
```
|
||||
|
||||
# Review expectations
|
||||
|
||||
See https://github.com/vector-im/element-meta/wiki/Review-process
|
||||
|
||||
# Merge Strategy
|
||||
|
||||
The preferred method for merging pull requests is squash merging to keep the
|
||||
commit history trim, but it is up to the discretion of the team member merging
|
||||
the change. We do not support rebase merges due to `allchange` being unable to
|
||||
handle them. When merging make sure to leave the default commit title, or
|
||||
at least leave the PR number at the end in brackets like by default.
|
||||
When stacking pull requests, you may wish to do the following:
|
||||
|
||||
1. Branch from develop to your branch (branch1), push commits onto it and open a pull request
|
||||
2. Branch from your base branch (branch1) to your work branch (branch2), push commits and open a pull request configuring the base to be branch1, saying in the description that it is based on your other PR.
|
||||
3. Merge the first PR using a merge commit otherwise your stacked PR will need a rebase. Github will automatically adjust the base branch of your other PR to be develop.
|
||||
@@ -0,0 +1,120 @@
|
||||
Contributing code to matrix-js-sdk
|
||||
==================================
|
||||
|
||||
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are
|
||||
willing to license their contributions under the same license as the project
|
||||
itself. We follow a simple 'inbound=outbound' model for contributions: the act
|
||||
of submitting an 'inbound' contribution means that the contributor agrees to
|
||||
license the code under the same terms as the project's overall 'outbound'
|
||||
license - in this case, Apache Software License v2 (see `<LICENSE>`_).
|
||||
|
||||
How to contribute
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
The preferred and easiest way to contribute changes to the project is to fork
|
||||
it on github, and then create a pull request to ask us to pull your changes
|
||||
into our repo (https://help.github.com/articles/using-pull-requests/)
|
||||
|
||||
**The single biggest thing you need to know is: please base your changes on
|
||||
the develop branch - /not/ master.**
|
||||
|
||||
We use the master branch to track the most recent release, so that folks who
|
||||
blindly clone the repo and automatically check out master get something that
|
||||
works. Develop is the unstable branch where all the development actually
|
||||
happens: the workflow is that contributors should fork the develop branch to
|
||||
make a 'feature' branch for a particular contribution, and then make a pull
|
||||
request to merge this back into the matrix.org 'official' develop branch. We
|
||||
use github's pull request workflow to review the contribution, and either ask
|
||||
you to make any refinements needed or merge it and make them ourselves. The
|
||||
changes will then land on master when we next do a release.
|
||||
|
||||
We use Travis for continuous integration, and all pull requests get
|
||||
automatically tested by Travis: if your change breaks the build, then the PR
|
||||
will show that there are failed checks, so please check back after a few
|
||||
minutes.
|
||||
|
||||
Code style
|
||||
~~~~~~~~~~
|
||||
|
||||
The code-style for matrix-js-sdk is not formally documented, but contributors
|
||||
are encouraged to read the code style document for matrix-react-sdk
|
||||
(`<https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md>`_)
|
||||
and follow the principles set out there.
|
||||
|
||||
Please ensure your changes match the cosmetic style of the existing project,
|
||||
and **never** mix cosmetic and functional changes in the same commit, as it
|
||||
makes it horribly hard to review otherwise.
|
||||
|
||||
Attribution
|
||||
~~~~~~~~~~~
|
||||
|
||||
Everyone who contributes anything to Matrix is welcome to be listed in the
|
||||
AUTHORS.rst file for the project in question. Please feel free to include a
|
||||
change to AUTHORS.rst in your pull request to list yourself and a short
|
||||
description of the area(s) you've worked on. Also, we sometimes have swag to
|
||||
give away to contributors - if you feel that Matrix-branded apparel is missing
|
||||
from your life, please mail us your shipping address to matrix at matrix.org
|
||||
and we'll try to fix it :)
|
||||
|
||||
Sign off
|
||||
~~~~~~~~
|
||||
|
||||
In order to have a concrete record that your contribution is intentional
|
||||
and you agree to license it under the same terms as the project's license, we've adopted the
|
||||
same lightweight approach that the Linux Kernel
|
||||
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
|
||||
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
||||
projects use: the DCO (Developer Certificate of Origin:
|
||||
http://developercertificate.org/). This is a simple declaration that you wrote
|
||||
the contribution or otherwise have the right to contribute it to Matrix::
|
||||
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
|
||||
If you agree to this for your contribution, then all that's needed is to
|
||||
include the line in your commit or pull request comment::
|
||||
|
||||
Signed-off-by: Your Name <your@email.example.org>
|
||||
|
||||
...using your real name; unfortunately pseudonyms and anonymous contributions
|
||||
can't be accepted. Git makes this trivial - just use the -s flag when you do
|
||||
``git commit``, having first set ``user.name`` and ``user.email`` git configs
|
||||
(which you should have done anyway :)
|
||||
|
||||
If you forgot to sign off your commits before making your pull request and are on git 2.17+
|
||||
you can mass signoff using rebase::
|
||||
|
||||
git rebase --signoff origin/develop
|
||||
@@ -1,73 +1,50 @@
|
||||
[](https://www.npmjs.com/package/matrix-js-sdk)
|
||||

|
||||

|
||||
[](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
|
||||
[](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
|
||||
[](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
|
||||
[](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
|
||||
Matrix Javascript SDK
|
||||
=====================
|
||||
[](http://matrix.org/jenkins/job/JavascriptSDK/)
|
||||
|
||||
# Matrix JavaScript SDK
|
||||
This is the [Matrix](https://matrix.org) Client-Server v1/v2 alpha SDK for
|
||||
JavaScript. This SDK can be run in a browser or in Node.js.
|
||||
|
||||
This is the [Matrix](https://matrix.org) Client-Server SDK for JavaScript and TypeScript. This SDK can be run in a
|
||||
browser or in Node.js.
|
||||
Quickstart
|
||||
==========
|
||||
|
||||
---
|
||||
In a browser
|
||||
------------
|
||||
Download either the full or minified version from
|
||||
https://github.com/matrix-org/matrix-js-sdk/releases/latest and add that as a
|
||||
``<script>`` to your page. There will be a global variable ``matrixcs``
|
||||
attached to ``window`` through which you can access the SDK. See below for how to
|
||||
include libolm to enable end-to-end-encryption.
|
||||
|
||||
<picture>
|
||||
<source srcset="contrib/element-logo-light.png" media="(prefers-color-scheme: dark)">
|
||||
<source srcset="contrib/element-logo-dark.png" media="(prefers-color-scheme: light)">
|
||||
<img src="contrib/element-logo-fallback.png" alt="Element logo">
|
||||
</picture>
|
||||
Please check [the working browser example](examples/browser) for more information.
|
||||
|
||||
<br>
|
||||
In Node.js
|
||||
----------
|
||||
|
||||
Development and maintenance is proudly sponsored by [Element](https://element.io). Element uses the SDK in their flagship [web](https://github.com/element-hq/element-web) and [desktop](https://github.com/element-hq/element-desktop) clients.
|
||||
|
||||
The SDK is also the basis for multiple Matrix projects and we welcome contributions from all.
|
||||
|
||||
---
|
||||
|
||||
#### Minimum Matrix server version: v1.1
|
||||
|
||||
The Matrix specification is constantly evolving - while this SDK aims for maximum backwards compatibility, it only
|
||||
guarantees that a feature will be supported for at least 4 spec releases. For example, if a feature the js-sdk supports
|
||||
is removed in v1.4 then the feature is _eligible_ for removal from the SDK when v1.8 is released. This SDK has no
|
||||
guarantee on implementing all features of any particular spec release, currently. This can mean that the SDK will call
|
||||
endpoints from before Matrix 1.1, for example.
|
||||
|
||||
# Quickstart
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Servers may require or use authenticated endpoints for media (images, files, avatars, etc). See the
|
||||
> [Authenticated Media](#authenticated-media) section for information on how to enable support for this.
|
||||
|
||||
Using `pnpm` instead of `npm` is recommended. Please see the pnpm [install
|
||||
guide](https://pnpm.io/installation#using-corepack) if you do not have it already.
|
||||
|
||||
`pnpm add matrix-js-sdk`
|
||||
``npm install matrix-js-sdk``
|
||||
|
||||
```javascript
|
||||
import * as sdk from "matrix-js-sdk";
|
||||
const client = sdk.createClient({ baseUrl: "https://matrix.org" });
|
||||
client.publicRooms(function (err, data) {
|
||||
var sdk = require("matrix-js-sdk");
|
||||
var client = sdk.createClient("https://matrix.org");
|
||||
client.publicRooms(function(err, data) {
|
||||
console.log("Public Rooms: %s", JSON.stringify(data));
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
See [below](#end-to-end-encryption-support) for how to enable end-to-end-encryption, or check
|
||||
[the Node.js terminal app](https://github.com/matrix-org/matrix-js-sdk/tree/develop/examples/node) for a more complex example.
|
||||
See below for how to include libolm to enable end-to-end-encryption. Please check
|
||||
[the Node.js terminal app](examples/node) for a more complex example.
|
||||
|
||||
To start the client:
|
||||
|
||||
```javascript
|
||||
await client.startClient({ initialSyncLimit: 10 });
|
||||
await client.startClient({initialSyncLimit: 10});
|
||||
```
|
||||
|
||||
You can perform a call to `/sync` to get the current state of the client:
|
||||
|
||||
```javascript
|
||||
client.once(ClientEvent.sync, function (state, prevState, res) {
|
||||
if (state === "PREPARED") {
|
||||
client.once('sync', function(state, prevState, res) {
|
||||
if(state === 'PREPARED') {
|
||||
console.log("prepared");
|
||||
} else {
|
||||
console.log(state);
|
||||
@@ -79,9 +56,9 @@ client.once(ClientEvent.sync, function (state, prevState, res) {
|
||||
To send a message:
|
||||
|
||||
```javascript
|
||||
const content = {
|
||||
body: "message text",
|
||||
msgtype: "m.text",
|
||||
var content = {
|
||||
"body": "message text",
|
||||
"msgtype": "m.text"
|
||||
};
|
||||
client.sendEvent("roomId", "m.room.message", content, "", (err, res) => {
|
||||
console.log(err);
|
||||
@@ -91,178 +68,170 @@ client.sendEvent("roomId", "m.room.message", content, "", (err, res) => {
|
||||
To listen for message events:
|
||||
|
||||
```javascript
|
||||
client.on(RoomEvent.Timeline, function (event, room, toStartOfTimeline) {
|
||||
if (event.getType() !== "m.room.message") {
|
||||
return; // only use messages
|
||||
}
|
||||
console.log(event.event.content.body);
|
||||
client.on("Room.timeline", function(event, room, toStartOfTimeline) {
|
||||
if (event.getType() !== "m.room.message") {
|
||||
return; // only use messages
|
||||
}
|
||||
console.log(event.event.content.body);
|
||||
});
|
||||
```
|
||||
|
||||
By default, the `matrix-js-sdk` client uses the `MemoryStore` to store events as they are received. For example to iterate through the currently stored timeline for a room:
|
||||
By default, the `matrix-js-sdk` client uses the `MatrixInMemoryStore` to store events as they are received. For example to iterate through the currently stored timeline for a room:
|
||||
|
||||
```javascript
|
||||
Object.keys(client.store.rooms).forEach((roomId) => {
|
||||
client.getRoom(roomId).timeline.forEach((t) => {
|
||||
console.log(t.event);
|
||||
});
|
||||
client.getRoom(roomId).timeline.forEach(t => {
|
||||
console.log(t.event);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Authenticated media
|
||||
|
||||
Servers supporting [MSC3916](https://github.com/matrix-org/matrix-spec-proposals/pull/3916) (Matrix 1.11) will require clients, like
|
||||
yours, to include an `Authorization` header when `/download`ing or `/thumbnail`ing media. For NodeJS environments this
|
||||
may be as easy as the following code snippet, though web browsers may need to use [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)
|
||||
to append the header when using the endpoints in `<img />` elements and similar.
|
||||
|
||||
```javascript
|
||||
const downloadUrl = client.mxcUrlToHttp(
|
||||
/*mxcUrl=*/ "mxc://example.org/abc123", // the MXC URI to download/thumbnail, typically from an event or profile
|
||||
/*width=*/ undefined, // part of the thumbnail API. Use as required.
|
||||
/*height=*/ undefined, // part of the thumbnail API. Use as required.
|
||||
/*resizeMethod=*/ undefined, // part of the thumbnail API. Use as required.
|
||||
/*allowDirectLinks=*/ false, // should generally be left `false`.
|
||||
/*allowRedirects=*/ true, // implied supported with authentication
|
||||
/*useAuthentication=*/ true, // the flag we're after in this example
|
||||
);
|
||||
const img = await fetch(downloadUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${client.getAccessToken()}`,
|
||||
},
|
||||
});
|
||||
// Do something with `img`.
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> In future the js-sdk will _only_ return authentication-required URLs, mandating population of the `Authorization` header.
|
||||
|
||||
## What does this SDK do?
|
||||
What does this SDK do?
|
||||
----------------------
|
||||
|
||||
This SDK provides a full object model around the Matrix Client-Server API and emits
|
||||
events for incoming data and state changes. Aside from wrapping the HTTP API, it:
|
||||
- Handles syncing (via `/initialSync` and `/events`)
|
||||
- Handles the generation of "friendly" room and member names.
|
||||
- Handles historical `RoomMember` information (e.g. display names).
|
||||
- Manages room member state across multiple events (e.g. it handles typing, power
|
||||
levels and membership changes).
|
||||
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
|
||||
which can be listened to for things like name changes, new messages, membership
|
||||
changes, presence changes, and more.
|
||||
- Handle "local echo" of messages sent using the SDK. This means that messages
|
||||
that have just been sent will appear in the timeline as 'sending', until it
|
||||
completes. This is beneficial because it prevents there being a gap between
|
||||
hitting the send button and having the "remote echo" arrive.
|
||||
- Mark messages which failed to send as not sent.
|
||||
- Automatically retry requests to send messages due to network errors.
|
||||
- Automatically retry requests to send messages due to rate limiting errors.
|
||||
- Handle queueing of messages.
|
||||
- Handles pagination.
|
||||
- Handle assigning push actions for events.
|
||||
- Handles room initial sync on accepting invites.
|
||||
- Handles WebRTC calling.
|
||||
|
||||
- Handles syncing (via `/sync`)
|
||||
- Handles the generation of "friendly" room and member names.
|
||||
- Handles historical `RoomMember` information (e.g. display names).
|
||||
- Manages room member state across multiple events (e.g. it handles typing, power
|
||||
levels and membership changes).
|
||||
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
|
||||
which can be listened to for things like name changes, new messages, membership
|
||||
changes, presence changes, and more.
|
||||
- Handle "local echo" of messages sent using the SDK. This means that messages
|
||||
that have just been sent will appear in the timeline as 'sending', until it
|
||||
completes. This is beneficial because it prevents there being a gap between
|
||||
hitting the send button and having the "remote echo" arrive.
|
||||
- Mark messages which failed to send as not sent.
|
||||
- Automatically retry requests to send messages due to network errors.
|
||||
- Automatically retry requests to send messages due to rate limiting errors.
|
||||
- Handle queueing of messages.
|
||||
- Handles pagination.
|
||||
- Handle assigning push actions for events.
|
||||
- Handles room initial sync on accepting invites.
|
||||
- Handles WebRTC calling.
|
||||
Later versions of the SDK will:
|
||||
- Expose a `RoomSummary` which would be suitable for a recents page.
|
||||
- Provide different pluggable storage layers (e.g. local storage, database-backed)
|
||||
|
||||
# Usage
|
||||
Usage
|
||||
=====
|
||||
|
||||
## Supported platforms
|
||||
|
||||
`matrix-js-sdk` can be used in either Node.js applications (ensure you have the latest LTS version of Node.js installed),
|
||||
or in browser applications, via a bundler such as Webpack or Vite.
|
||||
Conventions
|
||||
-----------
|
||||
|
||||
You can also use the sdk with [Deno](https://deno.land/) (`import npm:matrix-js-sdk`) but its not officially supported.
|
||||
### Emitted events
|
||||
|
||||
## Emitted events
|
||||
|
||||
The SDK raises notifications to the application using
|
||||
[`EventEmitter`s](https://nodejs.org/api/events.html#class-eventemitter). The `MatrixClient` itself
|
||||
implements `EventEmitter`, as do many of the high-level abstractions such as `Room` and `RoomMember`.
|
||||
The SDK will emit events using an ``EventEmitter``. It also
|
||||
emits object models (e.g. ``Rooms``, ``RoomMembers``) when they
|
||||
are updated.
|
||||
|
||||
```javascript
|
||||
// Listen for low-level MatrixEvents
|
||||
client.on(ClientEvent.Event, function (event) {
|
||||
// Listen for low-level MatrixEvents
|
||||
client.on("event", function(event) {
|
||||
console.log(event.getType());
|
||||
});
|
||||
});
|
||||
|
||||
// Listen for typing changes
|
||||
client.on(RoomMemberEvent.Typing, function (event, member) {
|
||||
// Listen for typing changes
|
||||
client.on("RoomMember.typing", function(event, member) {
|
||||
if (member.typing) {
|
||||
console.log(member.name + " is typing...");
|
||||
} else {
|
||||
console.log(member.name + " stopped typing.");
|
||||
console.log(member.name + " is typing...");
|
||||
}
|
||||
});
|
||||
else {
|
||||
console.log(member.name + " stopped typing.");
|
||||
}
|
||||
});
|
||||
|
||||
// start the client to setup the connection to the server
|
||||
client.startClient();
|
||||
// start the client to setup the connection to the server
|
||||
client.startClient();
|
||||
```
|
||||
|
||||
## Entry points
|
||||
### Promises and Callbacks
|
||||
|
||||
As well as the primary entry point (`matrix-js-sdk`), there are several other entry points which may be useful:
|
||||
Most of the methods in the SDK are asynchronous: they do not directly return a
|
||||
result, but instead return a [Promise](http://documentup.com/kriskowal/q/)
|
||||
which will be fulfilled in the future.
|
||||
|
||||
| Entry point | Description |
|
||||
| ------------------------------ | --------------------------------------------------------------------------------------------------- |
|
||||
| `matrix-js-sdk` | Primary entry point. High-level functionality, and lots of historical clutter in need of a cleanup. |
|
||||
| `matrix-js-sdk/lib/crypto-api` | Cryptography functionality. |
|
||||
| `matrix-js-sdk/lib/types` | Low-level types, reflecting data structures defined in the Matrix spec. |
|
||||
| `matrix-js-sdk/lib/testing` | Test utilities, which may be useful in test code but should not be used in production code. |
|
||||
| `matrix-js-sdk/lib/utils/*.js` | A set of modules exporting standalone functions (and their types). |
|
||||
|
||||
## Examples
|
||||
|
||||
This section provides some useful code snippets which demonstrate the
|
||||
core functionality of the SDK. These examples assume the SDK is set up like this:
|
||||
The typical usage is something like:
|
||||
|
||||
```javascript
|
||||
import * as sdk from "matrix-js-sdk";
|
||||
const myUserId = "@example:localhost";
|
||||
const myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
|
||||
const matrixClient = sdk.createClient({
|
||||
baseUrl: "http://localhost:8008",
|
||||
accessToken: myAccessToken,
|
||||
userId: myUserId,
|
||||
});
|
||||
matrixClient.someMethod(arg1, arg2).done(function(result) {
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
Alternatively, if you have a Node.js-style ``callback(err, result)`` function,
|
||||
you can pass the result of the promise into it with something like:
|
||||
|
||||
```javascript
|
||||
matrixClient.someMethod(arg1, arg2).nodeify(callback);
|
||||
```
|
||||
|
||||
The main thing to note is that it is an error to discard the result of a
|
||||
promise-returning function, as that will cause exceptions to go unobserved. If
|
||||
you have nothing better to do with the result, just call ``.done()`` on it. See
|
||||
http://documentup.com/kriskowal/q/#the-end for more information.
|
||||
|
||||
Methods which return a promise show this in their documentation.
|
||||
|
||||
Many methods in the SDK support *both* Node.js-style callbacks *and* Promises,
|
||||
via an optional ``callback`` argument. The callback support is now deprecated:
|
||||
new methods do not include a ``callback`` argument, and in the future it may be
|
||||
removed from existing methods.
|
||||
|
||||
Examples
|
||||
--------
|
||||
This section provides some useful code snippets which demonstrate the
|
||||
core functionality of the SDK. These examples assume the SDK is setup like this:
|
||||
|
||||
```javascript
|
||||
var sdk = require("matrix-js-sdk");
|
||||
var myUserId = "@example:localhost";
|
||||
var myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
|
||||
var matrixClient = sdk.createClient({
|
||||
baseUrl: "http://localhost:8008",
|
||||
accessToken: myAccessToken,
|
||||
userId: myUserId
|
||||
});
|
||||
```
|
||||
|
||||
### Automatically join rooms when invited
|
||||
|
||||
```javascript
|
||||
matrixClient.on(RoomEvent.MyMembership, function (room, membership, prevMembership) {
|
||||
if (membership === KnownMembership.Invite) {
|
||||
matrixClient.joinRoom(room.roomId).then(function () {
|
||||
console.log("Auto-joined %s", room.roomId);
|
||||
});
|
||||
}
|
||||
});
|
||||
matrixClient.on("RoomMember.membership", function(event, member) {
|
||||
if (member.membership === "invite" && member.userId === myUserId) {
|
||||
matrixClient.joinRoom(member.roomId).done(function() {
|
||||
console.log("Auto-joined %s", member.roomId);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.startClient();
|
||||
matrixClient.startClient();
|
||||
```
|
||||
|
||||
### Print out messages for all rooms
|
||||
|
||||
```javascript
|
||||
matrixClient.on(RoomEvent.Timeline, function (event, room, toStartOfTimeline) {
|
||||
if (toStartOfTimeline) {
|
||||
return; // don't print paginated results
|
||||
}
|
||||
if (event.getType() !== "m.room.message") {
|
||||
return; // only print messages
|
||||
}
|
||||
console.log(
|
||||
// the room name will update with m.room.name events automatically
|
||||
"(%s) %s :: %s",
|
||||
room.name,
|
||||
event.getSender(),
|
||||
event.getContent().body,
|
||||
);
|
||||
});
|
||||
matrixClient.on("Room.timeline", function(event, room, toStartOfTimeline) {
|
||||
if (toStartOfTimeline) {
|
||||
return; // don't print paginated results
|
||||
}
|
||||
if (event.getType() !== "m.room.message") {
|
||||
return; // only print messages
|
||||
}
|
||||
console.log(
|
||||
// the room name will update with m.room.name events automatically
|
||||
"(%s) %s :: %s", room.name, event.getSender(), event.getContent().body
|
||||
);
|
||||
});
|
||||
|
||||
matrixClient.startClient();
|
||||
matrixClient.startClient();
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
(My Room) @megan:localhost :: Hello world
|
||||
(My Room) @megan:localhost :: how are you?
|
||||
@@ -274,24 +243,27 @@ Output:
|
||||
### Print out membership lists whenever they are changed
|
||||
|
||||
```javascript
|
||||
matrixClient.on(RoomStateEvent.Members, function (event, state, member) {
|
||||
const room = matrixClient.getRoom(state.roomId);
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
const memberList = state.getMembers();
|
||||
console.log(room.name);
|
||||
console.log(Array(room.name.length + 1).join("=")); // underline
|
||||
for (var i = 0; i < memberList.length; i++) {
|
||||
console.log("(%s) %s", memberList[i].membership, memberList[i].name);
|
||||
}
|
||||
});
|
||||
matrixClient.on("RoomState.members", function(event, state, member) {
|
||||
var room = matrixClient.getRoom(state.roomId);
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
var memberList = state.getMembers();
|
||||
console.log(room.name);
|
||||
console.log(Array(room.name.length + 1).join("=")); // underline
|
||||
for (var i = 0; i < memberList.length; i++) {
|
||||
console.log(
|
||||
"(%s) %s",
|
||||
memberList[i].membership,
|
||||
memberList[i].name
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.startClient();
|
||||
matrixClient.startClient();
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
My Room
|
||||
=======
|
||||
@@ -301,177 +273,92 @@ Output:
|
||||
(invite) @charlie:localhost
|
||||
```
|
||||
|
||||
# API Reference
|
||||
API Reference
|
||||
=============
|
||||
|
||||
A hosted reference can be found at
|
||||
http://matrix-org.github.io/matrix-js-sdk/index.html
|
||||
|
||||
This SDK uses [Typedoc](https://typedoc.org/guides/doccomments) doc comments. You can manually build and
|
||||
This SDK uses JSDoc3 style comments. You can manually build and
|
||||
host the API reference from the source files like this:
|
||||
|
||||
```
|
||||
$ pnpm gendoc
|
||||
$ cd docs
|
||||
$ python -m http.server 8005
|
||||
$ npm run gendoc
|
||||
$ cd .jsdoc
|
||||
$ python -m SimpleHTTPServer 8005
|
||||
```
|
||||
|
||||
Then visit `http://localhost:8005` to see the API docs.
|
||||
Then visit ``http://localhost:8005`` to see the API docs.
|
||||
|
||||
# End-to-end encryption support
|
||||
End-to-end encryption support
|
||||
=============================
|
||||
|
||||
`matrix-js-sdk`'s end-to-end encryption support is based on the [WebAssembly bindings](https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm) of the Rust [matrix-sdk-crypto](https://github.com/matrix-org/matrix-rust-sdk/tree/main/crates/matrix-sdk-crypto) library.
|
||||
The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
|
||||
[libolm](http://matrix.org/git/olm). It is left up to the application to make
|
||||
libolm available, via the ``Olm`` global.
|
||||
|
||||
## Initialization
|
||||
It is also necessry to call ``matrixClient.initCrypto()`` after creating a new
|
||||
``MatrixClient`` (but **before** calling ``matrixClient.startClient()``) to
|
||||
initialise the crypto layer.
|
||||
|
||||
To initialize the end-to-end encryption support in the matrix client:
|
||||
If the ``Olm`` global is not available, the SDK will show a warning, as shown
|
||||
below; ``initCrypto()`` will also fail.
|
||||
|
||||
```javascript
|
||||
// Create a new matrix client
|
||||
const matrixClient = sdk.createClient({
|
||||
baseUrl: "http://localhost:8008",
|
||||
accessToken: myAccessToken,
|
||||
userId: myUserId,
|
||||
});
|
||||
|
||||
// Initialize to enable end-to-end encryption support.
|
||||
await matrixClient.initRustCrypto();
|
||||
```
|
||||
Unable to load crypto module: crypto will be disabled: Error: global.Olm is not defined
|
||||
```
|
||||
|
||||
Note that by default it will attempt to use the Indexed DB provided by the browser as a crypto store. If running outside the browser, you will need to pass [an options object](https://matrix-org.github.io/matrix-js-sdk/classes/matrix.MatrixClient.html#initrustcrypto) which includes `useIndexedDB: false`, to use an ephemeral in-memory store instead. Note that without a persistent store, you'll need to create a new device on the server side (with [`MatrixClient.loginRequest`](https://matrix-org.github.io/matrix-js-sdk/classes/matrix.MatrixClient.html#loginrequest)) each time your application starts.
|
||||
If the crypto layer is not (successfully) initialised, the SDK will continue to
|
||||
work for unencrypted rooms, but it will not support the E2E parts of the Matrix
|
||||
specification.
|
||||
|
||||
After calling `initRustCrypto`, you can obtain a reference to the [`CryptoApi`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html) interface, which is the main entry point for end-to-end encryption, by calling [`MatrixClient.getCrypto`](https://matrix-org.github.io/matrix-js-sdk/classes/matrix.MatrixClient.html#getCrypto).
|
||||
To provide the Olm library in a browser application:
|
||||
|
||||
**WARNING**: the cryptography stack is not thread-safe. Having multiple `MatrixClient` instances connected to the same Indexed DB will cause data corruption and decryption failures. The application layer is responsible for ensuring that only one `MatrixClient` issue is instantiated at a time.
|
||||
* download the transpiled libolm (from https://matrix.org/packages/npm/olm/).
|
||||
* load ``olm.js`` as a ``<script>`` *before* ``browser-matrix.js``.
|
||||
|
||||
To provide the Olm library in a node.js application:
|
||||
|
||||
## Secret storage
|
||||
* ``npm install https://matrix.org/packages/npm/olm/olm-3.0.0.tgz``
|
||||
(replace the URL with the latest version you want to use from
|
||||
https://matrix.org/packages/npm/olm/)
|
||||
* ``global.Olm = require('olm');`` *before* loading ``matrix-js-sdk``.
|
||||
|
||||
You should normally set up [secret storage](https://spec.matrix.org/v1.12/client-server-api/#secret-storage) before using the end-to-end encryption. To do this, call [`CryptoApi.bootstrapSecretStorage`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html#bootstrapSecretStorage).
|
||||
`bootstrapSecretStorage` can be called unconditionally: it will only set up the secret storage if it is not already set up (unless you use the `setupNewSecretStorage` parameter).
|
||||
If you want to package Olm as dependency for your node.js application, you
|
||||
can use ``npm install https://matrix.org/packages/npm/olm/olm-3.0.0.tgz
|
||||
--save-optional`` (if your application also works without e2e crypto enabled)
|
||||
or ``--save`` (if it doesn't) to do so.
|
||||
|
||||
```javascript
|
||||
const matrixClient = sdk.createClient({
|
||||
...,
|
||||
cryptoCallbacks: {
|
||||
getSecretStorageKey: async (keys) => {
|
||||
// This function should prompt the user to enter their secret storage key.
|
||||
return mySecretStorageKeys;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
matrixClient.getCrypto().bootstrapSecretStorage({
|
||||
// This function will be called if a new secret storage key (aka recovery key) is needed.
|
||||
// You should prompt the user to save the key somewhere, because they will need it to unlock secret storage in future.
|
||||
createSecretStorageKey: async () => {
|
||||
return mySecretStorageKey;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The example above will create a new secret storage key if secret storage was not previously set up.
|
||||
The secret storage data will be encrypted using the secret storage key returned in [`createSecretStorageKey`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CreateSecretStorageOpts.html#createSecretStorageKey).
|
||||
|
||||
We recommend that you prompt the user to re-enter this key when [`CryptoCallbacks.getSecretStorageKey`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoCallbacks.html#getSecretStorageKey) is called (when the secret storage access is needed).
|
||||
|
||||
## Set up cross-signing
|
||||
|
||||
To set up cross-signing to verify devices and other users, call
|
||||
[`CryptoApi.bootstrapCrossSigning`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html#bootstrapCrossSigning):
|
||||
|
||||
```javascript
|
||||
matrixClient.getCrypto().bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (makeRequest) => {
|
||||
return makeRequest(authDict);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The [`authUploadDeviceSigningKeys`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.BootstrapCrossSigningOpts.html#authUploadDeviceSigningKeys)
|
||||
callback is required in order to upload newly-generated public cross-signing keys to the server.
|
||||
|
||||
## Key backup
|
||||
|
||||
If the user doesn't already have a [key backup](https://spec.matrix.org/v1.12/client-server-api/#server-side-key-backups) you should create one:
|
||||
|
||||
```javascript
|
||||
// Check if we have a key backup.
|
||||
// If checkKeyBackupAndEnable returns null, there is no key backup.
|
||||
const hasKeyBackup = (await matrixClient.getCrypto().checkKeyBackupAndEnable()) !== null;
|
||||
|
||||
// Create the key backup
|
||||
await matrixClient.getCrypto().resetKeyBackup();
|
||||
```
|
||||
|
||||
## Verify a new device
|
||||
|
||||
Once the cross-signing is set up on one of your devices, you can verify another device with two methods:
|
||||
|
||||
1. Use `CryptoApi.bootstrapCrossSigning`.
|
||||
|
||||
`bootstrapCrossSigning` will call the [CryptoCallbacks.getSecretStorageKey](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoCallbacks.html#getSecretStorageKey) callback. The device is verified with the private cross-signing keys fetched from the secret storage.
|
||||
|
||||
2. Request an interactive verification against existing devices, by calling [CryptoApi.requestOwnUserVerification](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html#requestOwnUserVerification).
|
||||
|
||||
## Migrating from the legacy crypto stack to Rust crypto
|
||||
|
||||
If your application previously used the legacy crypto stack, (i.e, it called `MatrixClient.initLegacyCrypto()`), you will
|
||||
need to migrate existing devices to the Rust crypto stack.
|
||||
|
||||
This migration happens automatically when you call `initRustCrypto()` instead of `initLegacyCrypto()`,
|
||||
but you need to provide the legacy [`cryptoStore`](https://matrix-org.github.io/matrix-js-sdk/interfaces/matrix.ICreateClientOpts.html#cryptoStore) and [`pickleKey`](https://matrix-org.github.io/matrix-js-sdk/interfaces/matrix.ICreateClientOpts.html#pickleKey) to [`createClient`](https://matrix-org.github.io/matrix-js-sdk/functions/matrix.createClient.html):
|
||||
|
||||
```javascript
|
||||
// You should provide the legacy crypto store and the pickle key to the matrix client in order to migrate the data.
|
||||
const matrixClient = sdk.createClient({
|
||||
cryptoStore: myCryptoStore,
|
||||
pickleKey: myPickleKey,
|
||||
baseUrl: "http://localhost:8008",
|
||||
accessToken: myAccessToken,
|
||||
userId: myUserId,
|
||||
});
|
||||
|
||||
// The migration will be done automatically when you call `initRustCrypto`.
|
||||
await matrixClient.initRustCrypto();
|
||||
```
|
||||
|
||||
To follow the migration progress, you can listen to the [`CryptoEvent.LegacyCryptoStoreMigrationProgress`](https://matrix-org.github.io/matrix-js-sdk/enums/crypto_api.CryptoEvent.html#LegacyCryptoStoreMigrationProgress) event:
|
||||
|
||||
```javascript
|
||||
// When progress === total === -1, the migration is finished.
|
||||
matrixClient.on(CryptoEvent.LegacyCryptoStoreMigrationProgress, (progress, total) => {
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
The Rust crypto stack is not supported in a lot of deprecated methods of [`MatrixClient`](https://matrix-org.github.io/matrix-js-sdk/classes/matrix.MatrixClient.html). If you use them, you should migrate to the [`CryptoApi`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html). Also, the legacy `MatrixClient.crypto` object is not available any more: you should use `MatrixClient.getCrypto()` instead.
|
||||
|
||||
# Contributing
|
||||
|
||||
_This section is for people who want to modify the SDK. If you just
|
||||
want to use this SDK, skip this section._
|
||||
Contributing
|
||||
============
|
||||
*This section is for people who want to modify the SDK. If you just
|
||||
want to use this SDK, skip this section.*
|
||||
|
||||
First, you need to pull in the right build tools:
|
||||
|
||||
```
|
||||
$ pnpm install
|
||||
$ npm install
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To build a browser version from scratch when developing:
|
||||
Building
|
||||
--------
|
||||
|
||||
To build a browser version from scratch when developing::
|
||||
```
|
||||
$ pnpm build
|
||||
$ npm run build
|
||||
```
|
||||
|
||||
To run tests:
|
||||
|
||||
To constantly do builds when files are modified (using ``watchify``)::
|
||||
```
|
||||
$ pnpm test
|
||||
$ npm run watch
|
||||
```
|
||||
|
||||
To run tests (Jasmine)::
|
||||
```
|
||||
$ npm test
|
||||
```
|
||||
|
||||
To run linting:
|
||||
|
||||
```
|
||||
$ pnpm lint
|
||||
$ npm run lint
|
||||
```
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
module.exports = {
|
||||
sourceMaps: true,
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
targets: {
|
||||
esmodules: true,
|
||||
},
|
||||
modules: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
"@babel/preset-typescript",
|
||||
{
|
||||
rewriteImportExtensions: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
plugins: [
|
||||
["@babel/plugin-proposal-decorators", { version: "2023-11" }],
|
||||
"@babel/plugin-transform-numeric-separator",
|
||||
"@babel/plugin-transform-class-properties",
|
||||
"@babel/plugin-transform-object-rest-spread",
|
||||
"@babel/plugin-syntax-dynamic-import",
|
||||
"@babel/plugin-transform-runtime",
|
||||
[
|
||||
"search-and-replace",
|
||||
{
|
||||
// Since rewriteImportExtensions doesn't work on dynamic imports (yet), we need to manually replace
|
||||
// the dynamic rust-crypto import.
|
||||
// (see https://github.com/babel/babel/issues/16750)
|
||||
rules:
|
||||
process.env.NODE_ENV !== "test"
|
||||
? [
|
||||
{
|
||||
search: "./rust-crypto/index.ts",
|
||||
replace: "./rust-crypto/index.js",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
var matrixcs = require("./lib/matrix");
|
||||
const request = require('browser-request');
|
||||
const queryString = require('qs');
|
||||
|
||||
matrixcs.request(function(opts, fn) {
|
||||
// We manually fix the query string for browser-request because
|
||||
// it doesn't correctly handle cases like ?via=one&via=two. Instead
|
||||
// we mimic `request`'s query string interface to make it all work
|
||||
// as expected.
|
||||
// browser-request will happily take the constructed string as the
|
||||
// query string without trying to modify it further.
|
||||
opts.qs = queryString.stringify(opts.qs || {}, opts.qsStringifyOptions);
|
||||
return request(opts, fn);
|
||||
});
|
||||
|
||||
// just *accessing* indexedDB throws an exception in firefox with
|
||||
// indexeddb disabled.
|
||||
var indexedDB;
|
||||
try {
|
||||
indexedDB = global.indexedDB;
|
||||
} catch(e) {}
|
||||
|
||||
// if our browser (appears to) support indexeddb, use an indexeddb crypto store.
|
||||
if (indexedDB) {
|
||||
matrixcs.setCryptoStoreFactory(
|
||||
function() {
|
||||
return new matrixcs.IndexedDBCryptoStore(
|
||||
indexedDB, "matrix-js-sdk:crypto"
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = matrixcs; // keep export for browserify package deps
|
||||
global.matrixcs = matrixcs;
|
||||
-284
@@ -1,284 +0,0 @@
|
||||
## Guiding principles
|
||||
|
||||
1. We want the lint rules to feel natural for most team members. No one should have to think too much
|
||||
about the linter.
|
||||
2. We want to stay relatively close to [industry standards](https://google.github.io/styleguide/tsguide.html)
|
||||
to make onboarding easier.
|
||||
3. We describe what good code looks like rather than point out bad examples. We do this to avoid
|
||||
excessively punishing people for writing code which fails the linter.
|
||||
4. When something isn't covered by the style guide, we come up with a reasonable rule rather than
|
||||
claim that it "passes the linter". We update the style guide and linter accordingly.
|
||||
5. While we aim to improve readability, understanding, and other aspects of the code, we deliberately
|
||||
do not let solely our personal preferences drive decisions.
|
||||
6. We aim to have an understandable guide.
|
||||
|
||||
## Coding practices
|
||||
|
||||
1. Lint rules enforce decisions made by this guide. The lint rules and this guide are kept in
|
||||
perfect sync.
|
||||
2. Commit messages are descriptive for the changes. When the project supports squash merging,
|
||||
only the squashed commit needs to have a descriptive message.
|
||||
3. When there is disagreement with a code style approved by the linter, a PR is opened against
|
||||
the lint rules rather than making exceptions on the responsible code PR.
|
||||
4. Rules which are intentionally broken (via eslint-ignore, @ts-ignore, etc) have a comment
|
||||
included in the immediate vicinity for why. Determination of whether this is valid applies at
|
||||
code review time.
|
||||
5. When editing a file, nearby code is updated to meet the modern standards. "Nearby" is subjective,
|
||||
but should be whatever is reasonable at review time. Such an example might be to update the
|
||||
class's code style, but not the file's.
|
||||
1. These changes should be minor enough to include in the same commit without affecting a code
|
||||
reviewer's job.
|
||||
|
||||
## All code
|
||||
|
||||
Unless otherwise specified, the following applies to all code:
|
||||
|
||||
1. Files must be formatted with Prettier.
|
||||
2. 120 character limit per line. Match existing code in the file if it is using a lower guide.
|
||||
3. A tab/indentation is 4 spaces.
|
||||
4. Newlines are Unix.
|
||||
5. A file has a single empty line at the end.
|
||||
6. Lines are trimmed of all excess whitespace, including blank lines.
|
||||
7. Long lines are broken up for readability.
|
||||
|
||||
## TypeScript / JavaScript
|
||||
|
||||
1. Write TypeScript. Turn JavaScript into TypeScript when working in the area.
|
||||
2. Use [TSDoc](https://tsdoc.org/) to document your code. See [Comments](#comments) below.
|
||||
3. Use named exports.
|
||||
4. Use semicolons for block/line termination.
|
||||
1. Except when defining interfaces, classes, and non-arrow functions specifically.
|
||||
5. When a statement's body is a single line, it must be written without curly braces, so long as the body is placed on
|
||||
the same line as the statement.
|
||||
|
||||
```typescript
|
||||
if (x) doThing();
|
||||
```
|
||||
|
||||
6. Blocks for `if`, `for`, `switch` and so on must have a space surrounding the condition, but not
|
||||
within the condition.
|
||||
|
||||
```typescript
|
||||
if (x) {
|
||||
doThing();
|
||||
}
|
||||
```
|
||||
|
||||
7. lowerCamelCase is used for function and variable naming.
|
||||
8. UpperCamelCase is used for general naming.
|
||||
9. Interface names should not be marked with an uppercase `I`.
|
||||
10. One variable declaration per line.
|
||||
11. If a variable is not receiving a value on declaration, its type must be defined.
|
||||
|
||||
```typescript
|
||||
let errorMessage: string;
|
||||
```
|
||||
|
||||
12. Objects can use shorthand declarations, including mixing of types.
|
||||
|
||||
```typescript
|
||||
{
|
||||
room,
|
||||
prop: this.prop,
|
||||
}
|
||||
// ... or ...
|
||||
{ room, prop: this.prop }
|
||||
```
|
||||
|
||||
13. Object keys should always be non-strings when possible.
|
||||
|
||||
```typescript
|
||||
{
|
||||
property: "value",
|
||||
"m.unavoidable": true,
|
||||
[EventType.RoomMessage]: true,
|
||||
}
|
||||
```
|
||||
|
||||
14. If a variable's type should be boolean, make sure it really is one.
|
||||
|
||||
```typescript
|
||||
const isRealUser = !!userId && ...; // good
|
||||
const isRealUser = Boolean(userId) && Boolean(userName); // also good
|
||||
const isRealUser = Boolean(userId) && isReal; // also good (where isReal is another boolean variable)
|
||||
const isRealUser = Boolean(userId && userName); // also fine
|
||||
const isRealUser = Boolean(userId || userName); // good: same as &&
|
||||
const isRealUser = userId && ...; // bad: isRealUser is userId's type, not a boolean
|
||||
|
||||
if (userId) // fine: userId is evaluated for truthiness, not stored as a boolean
|
||||
```
|
||||
|
||||
15. Use `switch` statements when checking against more than a few enum-like values.
|
||||
16. Use `const` for constants, `let` for mutability.
|
||||
17. Describe types exhaustively (ensure noImplictAny would pass).
|
||||
1. Notable exceptions are arrow functions used as parameters, when a void return type is
|
||||
obvious, and when declaring and assigning a variable in the same line.
|
||||
18. Declare member visibility (public/private/protected).
|
||||
19. Private members are private and not prefixed unless required for naming conflicts.
|
||||
1. Convention is to use an underscore or the word "internal" to denote conflicted member names.
|
||||
2. "Conflicted" typically refers to a getter which wants the same name as the underlying variable.
|
||||
20. Prefer readonly members over getters backed by a variable, unless an internal setter is required.
|
||||
21. Prefer Interfaces for object definitions, and types for parameter-value-only declarations.
|
||||
1. Note that an explicit type is optional if not expected to be used outside of the function call,
|
||||
unlike in this example:
|
||||
|
||||
```typescript
|
||||
interface MyObject {
|
||||
hasString: boolean;
|
||||
}
|
||||
|
||||
type Options = MyObject | string;
|
||||
|
||||
function doThing(arg: Options) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
22. Variables/properties which are `public static` should also be `readonly` when possible.
|
||||
23. Interface and type properties are terminated with semicolons, not commas.
|
||||
24. Prefer arrow formatting when declaring functions for interfaces/types:
|
||||
|
||||
```typescript
|
||||
interface Test {
|
||||
myCallback: (arg: string) => Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
25. Prefer a type definition over an inline type. For example, define an interface.
|
||||
26. Always prefer to add types or declare a type over the use of `any`. Prefer inferred types
|
||||
when they are not `any`.
|
||||
1. When using `any`, a comment explaining why must be present.
|
||||
27. `import` should be used instead of `require`, as `require` does not have types.
|
||||
28. Export only what can be reused.
|
||||
29. Prefer a type like `X | null` instead of truly optional parameters.
|
||||
1. A notable exception is when the likelihood of a bug is minimal, such as when a function
|
||||
takes an argument that is more often not required than required. An example where the
|
||||
`?` operator is inappropriate is when taking a room ID: typically the caller should
|
||||
supply the room ID if it knows it, otherwise deliberately acknowledge that it doesn't
|
||||
have one with `null`.
|
||||
|
||||
```typescript
|
||||
function doThingWithRoom(
|
||||
thing: string,
|
||||
room: string | null, // require the caller to specify
|
||||
) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
30. There should be approximately one interface, class, or enum per file unless the file is named
|
||||
"types.ts", "global.d.ts", or ends with "-types.ts".
|
||||
1. The file name should match the interface, class, or enum name.
|
||||
31. Bulk functions can be declared in a single file, though named as "foo-utils.ts" or "utils/foo.ts".
|
||||
32. Imports are grouped by external module imports first, then by internal imports.
|
||||
33. File ordering is not strict, but should generally follow this sequence:
|
||||
1. Licence header
|
||||
2. Imports
|
||||
3. Constants
|
||||
4. Enums
|
||||
5. Interfaces
|
||||
6. Functions
|
||||
7. Classes
|
||||
1. Public/protected/private static properties
|
||||
2. Public/protected/private properties
|
||||
3. Constructors
|
||||
4. Public/protected/private getters & setters
|
||||
5. Protected and abstract functions
|
||||
6. Public/private functions
|
||||
7. Public/protected/private static functions
|
||||
34. Variable names should be noticeably unique from their types. For example, "str: string" instead
|
||||
of "string: string".
|
||||
35. Use double quotes to enclose strings. You may use single quotes if the string contains double quotes.
|
||||
|
||||
```typescript
|
||||
const example1 = "simple string";
|
||||
const example2 = 'string containing "double quotes"';
|
||||
```
|
||||
|
||||
36. Prefer async-await to promise-chaining
|
||||
|
||||
```typescript
|
||||
async function () {
|
||||
const result = await anotherAsyncFunction();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
37. Avoid functions whose fundamental behaviour varies with different parameter types.
|
||||
Multiple return types are fine, but if the function's behaviour is going to change significantly,
|
||||
have two separate functions. For example, `SDKConfig.get()` with a string param which returns the
|
||||
type according to the param given is ok, but `SDKConfig.get()` with no args returning the whole
|
||||
config object would not be: this should just be a separate function.
|
||||
|
||||
## Tests
|
||||
|
||||
1. Tests must be written in TypeScript.
|
||||
2. Mocks are declared below imports, but above everything else.
|
||||
3. Use the following convention template:
|
||||
|
||||
```typescript
|
||||
// Describe the class, component, or file name.
|
||||
describe("FooComponent", () => {
|
||||
// all test inspecific variables go here
|
||||
|
||||
beforeEach(() => {
|
||||
// exclude if not used.
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// exclude if not used.
|
||||
});
|
||||
|
||||
// Use "it should..." terminology
|
||||
it("should call the correct API", async () => {
|
||||
// test-specific variables go here
|
||||
// function calls/state changes go here
|
||||
// expectations go here
|
||||
});
|
||||
});
|
||||
|
||||
// If the file being tested is a utility class:
|
||||
describe("foo-utils", () => {
|
||||
describe("firstUtilFunction", () => {
|
||||
it("should...", async () => {
|
||||
// ...
|
||||
});
|
||||
});
|
||||
|
||||
describe("secondUtilFunction", () => {
|
||||
it("should...", async () => {
|
||||
// ...
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Comments
|
||||
|
||||
1. As a general principle: be liberal with comments. This applies to all files: stylesheets as well as
|
||||
JavaScript/TypeScript.
|
||||
|
||||
Good comments not only help future readers understand and maintain the code; they can also encourage good design
|
||||
by clearly setting out how different parts of the codebase interact where that would otherwise be implicit and
|
||||
subject to interpretation.
|
||||
|
||||
2. Aim to document all types, methods, class properties, functions, etc, with [TSDoc](https://tsdoc.org/) doc comments.
|
||||
This is _especially_ important for public interfaces in `matrix-js-sdk`, but is good practice in general.
|
||||
|
||||
Even very simple interfaces can often benefit from a doc-comment, both as a matter of consistency, and because simple
|
||||
interfaces have a habit of becoming more complex over time.
|
||||
|
||||
3. Inside a function, there is no need to comment every line, but consider:
|
||||
- before a particular multiline section of code within the function, give an overview of what it does,
|
||||
to make it easier for a reader to follow the flow through the function as a whole.
|
||||
- if it is anything less than obvious, explain _why_ we are doing a particular operation, with particular emphasis
|
||||
on how this function interacts with other parts of the codebase.
|
||||
|
||||
4. When making changes to existing code, authors are expected to read existing comments and make any necessary changes
|
||||
to ensure they remain accurate.
|
||||
|
||||
5. Reviewers are encouraged to consider whether more comments would be useful, and to ask the author to add them.
|
||||
|
||||
It is natural for an author to feel that the code they have just written is "obvious" and that comments would be
|
||||
redundant, whereas in reality it would take some time for reader unfamiliar with the code to understand it. A
|
||||
reviewer is well-placed to make a more objective judgement.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,8 +0,0 @@
|
||||
# Summary
|
||||
|
||||
- [Introduction](../README.md)
|
||||
|
||||
# Deep dive
|
||||
|
||||
- [Storage notes](storage-notes.md)
|
||||
- [Unverified devices](warning-on-unverified-devices.md)
|
||||
@@ -1,70 +0,0 @@
|
||||
# Browser Storage Notes
|
||||
|
||||
## Overview
|
||||
|
||||
Browsers examined: Firefox 67, Chrome 75
|
||||
|
||||
The examination below applies to the default, non-persistent storage policy.
|
||||
|
||||
## Quota Measurement
|
||||
|
||||
Browsers appear to enforce and measure the quota in terms of space on disk, not
|
||||
data stored, so you may be able to store more data than the simple sum of all
|
||||
input data depending on how compressible your data is.
|
||||
|
||||
## Quota Limit
|
||||
|
||||
Specs and documentation suggest we should consistently receive
|
||||
`QuotaExceededError` when we're near space limits, but the reality is a bit
|
||||
blurrier.
|
||||
|
||||
When we are low on disk space overall or near the group limit / origin quota:
|
||||
|
||||
- Chrome
|
||||
- Log database may fail to start with AbortError
|
||||
- IndexedDB fails to start for crypto: AbortError in connect from
|
||||
indexeddb-store-worker
|
||||
- When near the quota, QuotaExceededError is used more consistently
|
||||
- Firefox
|
||||
- The first error will be QuotaExceededError
|
||||
- Future write attempts will fail with various errors when space is low,
|
||||
including nonsense like "InvalidStateError: A mutation operation was
|
||||
attempted on a database that did not allow mutations."
|
||||
- Once you start getting errors, the DB is effectively wedged in read-only
|
||||
mode
|
||||
- Can revive access if you reopen the DB
|
||||
|
||||
## Cache Eviction
|
||||
|
||||
While the Storage Standard says all storage for an origin group should be
|
||||
limited by a single quota, in practice, browsers appear to handle `localStorage`
|
||||
separately from the others, so it has a separate quota limit and isn't evicted
|
||||
when low on space.
|
||||
|
||||
- Chrome, Firefox
|
||||
- IndexedDB for origin deleted
|
||||
- Local Storage remains in place
|
||||
|
||||
## Persistent Storage
|
||||
|
||||
Storage Standard offers a `navigator.storage.persist` API that can be used to
|
||||
request persistent storage that won't be deleted by the browser because of low
|
||||
space.
|
||||
|
||||
- Chrome
|
||||
- Chrome 75 seems to grant this without any prompt based on [interaction
|
||||
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
|
||||
- Firefox
|
||||
- Firefox 67 shows a prompt to grant
|
||||
- Reverting persistent seems to require revoking permission _and_ clearing
|
||||
site data
|
||||
|
||||
## Storage Estimation
|
||||
|
||||
Storage Standard offers a `navigator.storage.estimate` API to get some clue of
|
||||
how much space remains.
|
||||
|
||||
- Chrome, Firefox
|
||||
- Can run this at any time to request an estimate of space remaining
|
||||
- Firefox
|
||||
- Returns `0` for `usage` if a site is persisted
|
||||
@@ -1,29 +0,0 @@
|
||||
Random notes from Matthew on the two possible approaches for warning users about unexpected
|
||||
unverified devices popping up in their rooms....
|
||||
|
||||
# Original idea...
|
||||
|
||||
Warn when an existing user adds an unknown device to a room.
|
||||
|
||||
Warn when a user joins the room with unverified or unknown devices.
|
||||
|
||||
Warn when you initial sync if the room has any unverified devices in it.
|
||||
^ this is good enough if we're doing local storage.
|
||||
OR, better:
|
||||
Warn when you initial sync if the room has any new undefined devices since you were last there.
|
||||
=> This means persisting the rooms that devices are in, across initial syncs.
|
||||
|
||||
# Updated idea...
|
||||
|
||||
Warn when the user tries to send a message:
|
||||
|
||||
- If the room has unverified devices which the user has not yet been told about in the context of this room
|
||||
...or in the context of this user? currently all verification is per-user, not per-room.
|
||||
...this should be good enough.
|
||||
|
||||
- so track whether we have warned the user or not about unverified devices - blocked, unverified, verified, unverified_warned.
|
||||
throw an error when trying to encrypt if there are pure unverified devices there
|
||||
app will have to search for the devices which are pure unverified to warn about them - have to do this from MembersList anyway?
|
||||
- or megolm could warn which devices are causing the problems.
|
||||
|
||||
Why do we wait to establish outbound sessions? It just makes a horrible pause when we first try to send a message... but could otherwise unnecessarily consume resources?
|
||||
@@ -0,0 +1,31 @@
|
||||
Random notes from Matthew on the two possible approaches for warning users about unexpected
|
||||
unverified devices popping up in their rooms....
|
||||
|
||||
Original idea...
|
||||
================
|
||||
|
||||
Warn when an existing user adds an unknown device to a room.
|
||||
|
||||
Warn when a user joins the room with unverified or unknown devices.
|
||||
|
||||
Warn when you initial sync if the room has any unverified devices in it.
|
||||
^ this is good enough if we're doing local storage.
|
||||
OR, better:
|
||||
Warn when you initial sync if the room has any new undefined devices since you were last there.
|
||||
=> This means persisting the rooms that devices are in, across initial syncs.
|
||||
|
||||
|
||||
Updated idea...
|
||||
===============
|
||||
|
||||
Warn when the user tries to send a message:
|
||||
- If the room has unverified devices which the user has not yet been told about in the context of this room
|
||||
...or in the context of this user? currently all verification is per-user, not per-room.
|
||||
...this should be good enough.
|
||||
|
||||
- so track whether we have warned the user or not about unverified devices - blocked, unverified, verified, unverified_warned.
|
||||
throw an error when trying to encrypt if there are pure unverified devices there
|
||||
app will have to search for the devices which are pure unverified to warn about them - have to do this from MembersList anyway?
|
||||
- or megolm could warn which devices are causing the problems.
|
||||
|
||||
Why do we wait to establish outbound sessions? It just makes a horrible pause when we first try to send a message... but could otherwise unnecessarily consume resources?
|
||||
@@ -0,0 +1,9 @@
|
||||
To try it out, **you must build the SDK first** and then host this folder:
|
||||
|
||||
```
|
||||
$ npm run build
|
||||
$ cd examples/browser
|
||||
$ python -m SimpleHTTPServer 8003
|
||||
```
|
||||
|
||||
Then visit ``http://localhost:8003``.
|
||||
@@ -0,0 +1,14 @@
|
||||
"use strict";
|
||||
console.log("Loading browser sdk");
|
||||
|
||||
var client = matrixcs.createClient("http://matrix.org");
|
||||
client.publicRooms(function (err, data) {
|
||||
if (err) {
|
||||
console.error("err %s", JSON.stringify(err));
|
||||
return;
|
||||
}
|
||||
console.log("data %s [...]", JSON.stringify(data).substring(0, 100));
|
||||
console.log("Congratulations! The SDK is working on the browser!");
|
||||
var result = document.getElementById("result");
|
||||
result.innerHTML = "<p>The SDK appears to be working correctly.</p>";
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Test</title>
|
||||
<script src="lib/matrix.js"></script>
|
||||
<script src="browserTest.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
Sanity Testing (check the console) : This example is here to make sure that
|
||||
the SDK works inside a browser. It simply does a GET /publicRooms on
|
||||
matrix.org
|
||||
<br/>
|
||||
You should see a message confirming that the SDK works below:
|
||||
<br/>
|
||||
<div id="result"></div>
|
||||
</body>
|
||||
</html>
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../../dist/browser-matrix.js
|
||||
@@ -1,5 +1,6 @@
|
||||
This is a functional terminal app which allows you to see the room list for a user, join rooms, send messages and view room membership lists.
|
||||
|
||||
|
||||
To try it out, you will need to edit `app.js` to configure it for your `homeserver`, `access_token` and `user_id`. Then run:
|
||||
|
||||
```
|
||||
@@ -23,7 +24,7 @@ Room list index commands:
|
||||
Room commands:
|
||||
'/exit' Return to the room list index.
|
||||
'/members' Show the room member list.
|
||||
|
||||
|
||||
$ /enter 2
|
||||
|
||||
[2015-06-12 15:14:54] Megan2 <<< herro
|
||||
|
||||
+166
-152
@@ -1,17 +1,13 @@
|
||||
import clc from "cli-color";
|
||||
import fs from "fs";
|
||||
import readline from "readline";
|
||||
import sdk, { ClientEvent, EventType, MsgType, RoomEvent } from "matrix-js-sdk";
|
||||
import { KnownMembership } from "matrix-js-sdk/lib/@types/membership.js";
|
||||
"use strict";
|
||||
|
||||
var myHomeServer = "http://localhost:8008";
|
||||
var myUserId = "@example:localhost";
|
||||
var myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
|
||||
|
||||
var sdk = require("matrix-js-sdk");
|
||||
var clc = require("cli-color");
|
||||
var matrixClient = sdk.createClient({
|
||||
baseUrl: myHomeServer,
|
||||
baseUrl: "http://localhost:8008",
|
||||
accessToken: myAccessToken,
|
||||
userId: myUserId,
|
||||
userId: myUserId
|
||||
});
|
||||
|
||||
// Data structures
|
||||
@@ -20,14 +16,15 @@ var viewingRoom = null;
|
||||
var numMessagesToShow = 20;
|
||||
|
||||
// Reading from stdin
|
||||
var CLEAR_CONSOLE = "\x1B[2J";
|
||||
var CLEAR_CONSOLE = '\x1B[2J';
|
||||
var readline = require("readline");
|
||||
var rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
completer: completer,
|
||||
completer: completer
|
||||
});
|
||||
rl.setPrompt("$ ");
|
||||
rl.on("line", function (line) {
|
||||
rl.on('line', function(line) {
|
||||
if (line.trim().length === 0) {
|
||||
rl.prompt();
|
||||
return;
|
||||
@@ -42,11 +39,14 @@ rl.on("line", function (line) {
|
||||
if (line === "/exit") {
|
||||
viewingRoom = null;
|
||||
printRoomList();
|
||||
} else if (line === "/members") {
|
||||
}
|
||||
else if (line === "/members") {
|
||||
printMemberList(viewingRoom);
|
||||
} else if (line === "/roominfo") {
|
||||
}
|
||||
else if (line === "/roominfo") {
|
||||
printRoomInfo(viewingRoom);
|
||||
} else if (line === "/resend") {
|
||||
}
|
||||
else if (line === "/resend") {
|
||||
// get the oldest not sent event.
|
||||
var notSentEvent;
|
||||
for (var i = 0; i < viewingRoom.timeline.length; i++) {
|
||||
@@ -56,99 +56,97 @@ rl.on("line", function (line) {
|
||||
}
|
||||
}
|
||||
if (notSentEvent) {
|
||||
matrixClient.resendEvent(notSentEvent, viewingRoom).then(
|
||||
function () {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
},
|
||||
function (err) {
|
||||
printMessages();
|
||||
print("/resend Error: %s", err);
|
||||
rl.prompt();
|
||||
},
|
||||
);
|
||||
matrixClient.resendEvent(notSentEvent, viewingRoom).done(function() {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
}, function(err) {
|
||||
printMessages();
|
||||
print("/resend Error: %s", err);
|
||||
rl.prompt();
|
||||
});
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
}
|
||||
} else if (line.indexOf("/more ") === 0) {
|
||||
}
|
||||
else if (line.indexOf("/more ") === 0) {
|
||||
var amount = parseInt(line.split(" ")[1]) || 20;
|
||||
matrixClient.scrollback(viewingRoom, amount).then(
|
||||
function (room) {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
},
|
||||
function (err) {
|
||||
print("/more Error: %s", err);
|
||||
},
|
||||
);
|
||||
} else if (line.indexOf("/invite ") === 0) {
|
||||
var userId = line.split(" ")[1].trim();
|
||||
matrixClient.invite(viewingRoom.roomId, userId).then(
|
||||
function () {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
},
|
||||
function (err) {
|
||||
print("/invite Error: %s", err);
|
||||
},
|
||||
);
|
||||
} else if (line.indexOf("/file ") === 0) {
|
||||
var filename = line.split(" ")[1].trim();
|
||||
let buffer = fs.readFileSync("./your_file_name");
|
||||
matrixClient.uploadContent(new Blob([buffer])).then(function (response) {
|
||||
matrixClient.sendMessage(viewingRoom.roomId, {
|
||||
msgtype: MsgType.File,
|
||||
body: filename,
|
||||
url: response.content_uri,
|
||||
});
|
||||
matrixClient.scrollback(viewingRoom, amount).done(function(room) {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
}, function(err) {
|
||||
print("/more Error: %s", err);
|
||||
});
|
||||
} else {
|
||||
matrixClient.sendTextMessage(viewingRoom.roomId, line).finally(function () {
|
||||
}
|
||||
else if (line.indexOf("/invite ") === 0) {
|
||||
var userId = line.split(" ")[1].trim();
|
||||
matrixClient.invite(viewingRoom.roomId, userId).done(function() {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
}, function(err) {
|
||||
print("/invite Error: %s", err);
|
||||
});
|
||||
}
|
||||
else if (line.indexOf("/file ") === 0) {
|
||||
var filename = line.split(" ")[1].trim();
|
||||
var stream = fs.createReadStream(filename);
|
||||
matrixClient.uploadContent({
|
||||
stream: stream,
|
||||
name: filename
|
||||
}).done(function(url) {
|
||||
var content = {
|
||||
msgtype: "m.file",
|
||||
body: filename,
|
||||
url: JSON.parse(url).content_uri
|
||||
};
|
||||
matrixClient.sendMessage(viewingRoom.roomId, content);
|
||||
});
|
||||
}
|
||||
else {
|
||||
matrixClient.sendTextMessage(viewingRoom.roomId, line).finally(function() {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
});
|
||||
// print local echo immediately
|
||||
printMessages();
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
if (line.indexOf("/join ") === 0) {
|
||||
var roomIndex = line.split(" ")[1];
|
||||
viewingRoom = roomList[roomIndex];
|
||||
if (viewingRoom.getMember(myUserId).membership === KnownMembership.Invite) {
|
||||
if (viewingRoom.getMember(myUserId).membership === "invite") {
|
||||
// join the room first
|
||||
matrixClient.joinRoom(viewingRoom.roomId).then(
|
||||
function (room) {
|
||||
setRoomList();
|
||||
viewingRoom = room;
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
},
|
||||
function (err) {
|
||||
print("/join Error: %s", err);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
matrixClient.joinRoom(viewingRoom.roomId).done(function(room) {
|
||||
setRoomList();
|
||||
viewingRoom = room;
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
}, function(err) {
|
||||
print("/join Error: %s", err);
|
||||
});
|
||||
}
|
||||
else {
|
||||
printMessages();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
rl.prompt();
|
||||
});
|
||||
// ==== END User input
|
||||
|
||||
// show the room list after syncing.
|
||||
matrixClient.on(ClientEvent.Sync, function (state, prevState, data) {
|
||||
matrixClient.on("sync", function(state, prevState, data) {
|
||||
switch (state) {
|
||||
case "PREPARED":
|
||||
setRoomList();
|
||||
printRoomList();
|
||||
printHelp();
|
||||
rl.prompt();
|
||||
break;
|
||||
}
|
||||
setRoomList();
|
||||
printRoomList();
|
||||
printHelp();
|
||||
rl.prompt();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.on(ClientEvent.Room, function () {
|
||||
matrixClient.on("Room", function() {
|
||||
setRoomList();
|
||||
if (!viewingRoom) {
|
||||
printRoomList();
|
||||
@@ -157,11 +155,11 @@ matrixClient.on(ClientEvent.Room, function () {
|
||||
});
|
||||
|
||||
// print incoming messages.
|
||||
matrixClient.on(RoomEvent.Timeline, function (event, room, toStartOfTimeline) {
|
||||
matrixClient.on("Room.timeline", function(event, room, toStartOfTimeline) {
|
||||
if (toStartOfTimeline) {
|
||||
return; // don't print paginated results
|
||||
}
|
||||
if (!viewingRoom || viewingRoom.roomId !== room?.roomId) {
|
||||
if (!viewingRoom || viewingRoom.roomId !== room.roomId) {
|
||||
return; // not viewing a room or viewing the wrong room.
|
||||
}
|
||||
printLine(event);
|
||||
@@ -169,19 +167,20 @@ matrixClient.on(RoomEvent.Timeline, function (event, room, toStartOfTimeline) {
|
||||
|
||||
function setRoomList() {
|
||||
roomList = matrixClient.getRooms();
|
||||
roomList.sort(function (a, b) {
|
||||
roomList.sort(function(a,b) {
|
||||
// < 0 = a comes first (lower index) - we want high indexes = newer
|
||||
var aMsg = a.timeline[a.timeline.length - 1];
|
||||
var aMsg = a.timeline[a.timeline.length-1];
|
||||
if (!aMsg) {
|
||||
return -1;
|
||||
}
|
||||
var bMsg = b.timeline[b.timeline.length - 1];
|
||||
var bMsg = b.timeline[b.timeline.length-1];
|
||||
if (!bMsg) {
|
||||
return 1;
|
||||
}
|
||||
if (aMsg.getTs() > bMsg.getTs()) {
|
||||
return 1;
|
||||
} else if (aMsg.getTs() < bMsg.getTs()) {
|
||||
}
|
||||
else if (aMsg.getTs() < bMsg.getTs()) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
@@ -192,15 +191,16 @@ function printRoomList() {
|
||||
print(CLEAR_CONSOLE);
|
||||
print("Room List:");
|
||||
var fmts = {
|
||||
invite: clc.cyanBright,
|
||||
leave: clc.blackBright,
|
||||
"invite": clc.cyanBright,
|
||||
"leave": clc.blackBright
|
||||
};
|
||||
for (var i = 0; i < roomList.length; i++) {
|
||||
var msg = roomList[i].timeline[roomList[i].timeline.length - 1];
|
||||
var msg = roomList[i].timeline[roomList[i].timeline.length-1];
|
||||
var dateStr = "---";
|
||||
var fmt;
|
||||
if (msg) {
|
||||
dateStr = new Date(msg.getTs()).toISOString().replace(/T/, " ").replace(/\..+/, "");
|
||||
dateStr = new Date(msg.getTs()).toISOString().replace(
|
||||
/T/, ' ').replace(/\..+/, '');
|
||||
}
|
||||
var myMembership = roomList[i].getMyMembership();
|
||||
if (myMembership) {
|
||||
@@ -209,10 +209,9 @@ function printRoomList() {
|
||||
var roomName = fixWidth(roomList[i].name, 25);
|
||||
print(
|
||||
"[%s] %s (%s members) %s",
|
||||
i,
|
||||
fmt ? fmt(roomName) : roomName,
|
||||
i, fmt ? fmt(roomName) : roomName,
|
||||
roomList[i].getJoinedMembers().length,
|
||||
dateStr,
|
||||
dateStr
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -233,12 +232,12 @@ function printHelp() {
|
||||
}
|
||||
|
||||
function completer(line) {
|
||||
var completions = ["/help", "/join ", "/exit", "/members", "/more ", "/resend", "/invite"];
|
||||
var hits = completions.filter(function (c) {
|
||||
return c.indexOf(line) == 0;
|
||||
});
|
||||
var completions = [
|
||||
"/help", "/join ", "/exit", "/members", "/more ", "/resend", "/invite"
|
||||
];
|
||||
var hits = completions.filter(function(c) { return c.indexOf(line) == 0 });
|
||||
// show all completions if none found
|
||||
return [hits.length ? hits : completions, line];
|
||||
return [hits.length ? hits : completions, line]
|
||||
}
|
||||
|
||||
function printMessages() {
|
||||
@@ -255,14 +254,14 @@ function printMessages() {
|
||||
|
||||
function printMemberList(room) {
|
||||
var fmts = {
|
||||
join: clc.green,
|
||||
ban: clc.red,
|
||||
invite: clc.blue,
|
||||
leave: clc.blackBright,
|
||||
"join": clc.green,
|
||||
"ban": clc.red,
|
||||
"invite": clc.blue,
|
||||
"leave": clc.blackBright
|
||||
};
|
||||
var members = room.currentState.getMembers();
|
||||
// sorted based on name.
|
||||
members.sort(function (a, b) {
|
||||
members.sort(function(a, b) {
|
||||
if (a.name > b.name) {
|
||||
return -1;
|
||||
}
|
||||
@@ -271,58 +270,61 @@ function printMemberList(room) {
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
print('Membership list for room "%s"', room.name);
|
||||
print("Membership list for room \"%s\"", room.name);
|
||||
print(new Array(room.name.length + 28).join("-"));
|
||||
room.currentState.getMembers().forEach(function (member) {
|
||||
room.currentState.getMembers().forEach(function(member) {
|
||||
if (!member.membership) {
|
||||
return;
|
||||
}
|
||||
var fmt =
|
||||
fmts[member.membership] ||
|
||||
function (a) {
|
||||
return a;
|
||||
};
|
||||
var membershipWithPadding = member.membership + new Array(10 - member.membership.length).join(" ");
|
||||
var fmt = fmts[member.membership] || function(a){return a;};
|
||||
var membershipWithPadding = (
|
||||
member.membership + new Array(10 - member.membership.length).join(" ")
|
||||
);
|
||||
print(
|
||||
"%s" + fmt(" :: ") + "%s" + fmt(" (") + "%s" + fmt(")"),
|
||||
membershipWithPadding,
|
||||
member.name,
|
||||
member.userId === myUserId ? "Me" : member.userId,
|
||||
fmt,
|
||||
"%s"+fmt(" :: ")+"%s"+fmt(" (")+"%s"+fmt(")"),
|
||||
membershipWithPadding, member.name,
|
||||
(member.userId === myUserId ? "Me" : member.userId),
|
||||
fmt
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function printRoomInfo(room) {
|
||||
var eventMap = room.currentState.events;
|
||||
var eventDict = room.currentState.events;
|
||||
var eTypeHeader = " Event Type(state_key) ";
|
||||
var sendHeader = " Sender ";
|
||||
// pad content to 100
|
||||
var restCount = 100 - "Content".length - " | ".length - " | ".length - eTypeHeader.length - sendHeader.length;
|
||||
var padSide = new Array(Math.floor(restCount / 2)).join(" ");
|
||||
var restCount = (
|
||||
100 - "Content".length - " | ".length - " | ".length -
|
||||
eTypeHeader.length - sendHeader.length
|
||||
);
|
||||
var padSide = new Array(Math.floor(restCount/2)).join(" ");
|
||||
var contentHeader = padSide + "Content" + padSide;
|
||||
print(eTypeHeader + sendHeader + contentHeader);
|
||||
print(eTypeHeader+sendHeader+contentHeader);
|
||||
print(new Array(100).join("-"));
|
||||
eventMap.keys().forEach(function (eventType) {
|
||||
if (eventType === EventType.RoomMember) {
|
||||
return;
|
||||
} // use /members instead.
|
||||
var eventEventMap = eventMap.get(eventType);
|
||||
eventEventMap.keys().forEach(function (stateKey) {
|
||||
var typeAndKey = eventType + (stateKey.length > 0 ? "(" + stateKey + ")" : "");
|
||||
Object.keys(eventDict).forEach(function(eventType) {
|
||||
if (eventType === "m.room.member") { return; } // use /members instead.
|
||||
Object.keys(eventDict[eventType]).forEach(function(stateKey) {
|
||||
var typeAndKey = eventType + (
|
||||
stateKey.length > 0 ? "("+stateKey+")" : ""
|
||||
);
|
||||
var typeStr = fixWidth(typeAndKey, eTypeHeader.length);
|
||||
var event = eventEventMap.get(stateKey);
|
||||
var event = eventDict[eventType][stateKey];
|
||||
var sendStr = fixWidth(event.getSender(), sendHeader.length);
|
||||
var contentStr = fixWidth(JSON.stringify(event.getContent()), contentHeader.length);
|
||||
print(typeStr + " | " + sendStr + " | " + contentStr);
|
||||
var contentStr = fixWidth(
|
||||
JSON.stringify(event.getContent()), contentHeader.length
|
||||
);
|
||||
print(typeStr+" | "+sendStr+" | "+contentStr);
|
||||
});
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function printLine(event) {
|
||||
var fmt;
|
||||
var name = event.sender ? event.sender.name : event.getSender();
|
||||
var time = new Date(event.getTs()).toISOString().replace(/T/, " ").replace(/\..+/, "");
|
||||
var time = new Date(
|
||||
event.getTs()
|
||||
).toISOString().replace(/T/, ' ').replace(/\..+/, '');
|
||||
var separator = "<<<";
|
||||
if (event.getSender() === myUserId) {
|
||||
name = "Me";
|
||||
@@ -330,7 +332,8 @@ function printLine(event) {
|
||||
if (event.status === sdk.EventStatus.SENDING) {
|
||||
separator = "...";
|
||||
fmt = clc.xterm(8);
|
||||
} else if (event.status === sdk.EventStatus.NOT_SENT) {
|
||||
}
|
||||
else if (event.status === sdk.EventStatus.NOT_SENT) {
|
||||
separator = " x ";
|
||||
fmt = clc.redBright;
|
||||
}
|
||||
@@ -339,58 +342,69 @@ function printLine(event) {
|
||||
|
||||
var maxNameWidth = 15;
|
||||
if (name.length > maxNameWidth) {
|
||||
name = name.slice(0, maxNameWidth - 1) + "\u2026";
|
||||
name = name.substr(0, maxNameWidth-1) + "\u2026";
|
||||
}
|
||||
|
||||
if (event.getType() === EventType.RoomMessage) {
|
||||
if (event.getType() === "m.room.message") {
|
||||
body = event.getContent().body;
|
||||
} else if (event.isState()) {
|
||||
}
|
||||
else if (event.isState()) {
|
||||
var stateName = event.getType();
|
||||
if (event.getStateKey().length > 0) {
|
||||
stateName += " (" + event.getStateKey() + ")";
|
||||
stateName += " ("+event.getStateKey()+")";
|
||||
}
|
||||
body = "[State: " + stateName + " updated to: " + JSON.stringify(event.getContent()) + "]";
|
||||
body = (
|
||||
"[State: "+stateName+" updated to: "+JSON.stringify(event.getContent())+"]"
|
||||
);
|
||||
separator = "---";
|
||||
fmt = clc.xterm(249).italic;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
// random message event
|
||||
body = "[Message: " + event.getType() + " Content: " + JSON.stringify(event.getContent()) + "]";
|
||||
body = (
|
||||
"[Message: "+event.getType()+" Content: "+JSON.stringify(event.getContent())+"]"
|
||||
);
|
||||
separator = "---";
|
||||
fmt = clc.xterm(249).italic;
|
||||
}
|
||||
if (fmt) {
|
||||
print("[%s] %s %s %s", time, name, separator, body, fmt);
|
||||
} else {
|
||||
print(
|
||||
"[%s] %s %s %s", time, name, separator, body, fmt
|
||||
);
|
||||
}
|
||||
else {
|
||||
print("[%s] %s %s %s", time, name, separator, body);
|
||||
}
|
||||
}
|
||||
|
||||
function print(str, formatter) {
|
||||
if (typeof arguments[arguments.length - 1] === "function") {
|
||||
if (typeof arguments[arguments.length-1] === "function") {
|
||||
// last arg is the formatter so get rid of it and use it on each
|
||||
// param passed in but not the template string.
|
||||
var newArgs = [];
|
||||
var i = 0;
|
||||
for (i = 0; i < arguments.length - 1; i++) {
|
||||
for (i=0; i<arguments.length-1; i++) {
|
||||
newArgs.push(arguments[i]);
|
||||
}
|
||||
var fmt = arguments[arguments.length - 1];
|
||||
for (i = 0; i < newArgs.length; i++) {
|
||||
var fmt = arguments[arguments.length-1];
|
||||
for (i=0; i<newArgs.length; i++) {
|
||||
newArgs[i] = fmt(newArgs[i]);
|
||||
}
|
||||
console.log.apply(console.log, newArgs);
|
||||
} else {
|
||||
console.log.apply(console.log, [...arguments]);
|
||||
}
|
||||
else {
|
||||
console.log.apply(console.log, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
function fixWidth(str, len) {
|
||||
if (str.length > len) {
|
||||
return str.substring(0, len - 2) + "\u2026";
|
||||
} else if (str.length < len) {
|
||||
return str.substr(0, len-2) + "\u2026";
|
||||
}
|
||||
else if (str.length < len) {
|
||||
return str + new Array(len - str.length).join(" ");
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
matrixClient.startClient({ initialSyncLimit: numMessagesToShow });
|
||||
matrixClient.startClient(numMessagesToShow); // messages for each room.
|
||||
|
||||
+12
-18
@@ -1,20 +1,14 @@
|
||||
{
|
||||
"name": "example-app",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"author": "",
|
||||
"license": "Apache 2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"cli-color": "^1.0.0",
|
||||
"matrix-js-sdk": "^34.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cli-color": "^2.0.6",
|
||||
"typescript": "^5.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
"name": "example-app",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"preinstall": "npm install ../.."
|
||||
},
|
||||
"author": "",
|
||||
"license": "Apache 2.0",
|
||||
"dependencies": {
|
||||
"cli-color": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"noImplicitAny": false,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["app.js"]
|
||||
}
|
||||
@@ -6,4 +6,4 @@ To try it out, **you must build the SDK first** and then host this folder:
|
||||
$ python -m SimpleHTTPServer 8003
|
||||
```
|
||||
|
||||
Then visit `http://localhost:8003`.
|
||||
Then visit ``http://localhost:8003``.
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
"use strict";
|
||||
console.log("Loading browser sdk");
|
||||
const BASE_URL = "https://matrix.org";
|
||||
const TOKEN = "accesstokengoeshere";
|
||||
const USER_ID = "@username:localhost";
|
||||
const ROOM_ID = "!room:id";
|
||||
const DEVICE_ID = "some_device_id";
|
||||
var BASE_URL = "https://matrix.org";
|
||||
var TOKEN = "accesstokengoeshere";
|
||||
var USER_ID = "@username:localhost";
|
||||
var ROOM_ID = "!room:id";
|
||||
|
||||
const client = matrixcs.createClient({
|
||||
|
||||
var client = matrixcs.createClient({
|
||||
baseUrl: BASE_URL,
|
||||
accessToken: TOKEN,
|
||||
userId: USER_ID,
|
||||
deviceId: DEVICE_ID,
|
||||
userId: USER_ID
|
||||
});
|
||||
let call;
|
||||
var call;
|
||||
|
||||
function disableButtons(place, answer, hangup) {
|
||||
document.getElementById("hangup").disabled = hangup;
|
||||
@@ -20,82 +20,65 @@ function disableButtons(place, answer, hangup) {
|
||||
}
|
||||
|
||||
function addListeners(call) {
|
||||
let lastError = "";
|
||||
call.on("hangup", function () {
|
||||
var lastError = "";
|
||||
call.on("hangup", function() {
|
||||
disableButtons(false, true, true);
|
||||
document.getElementById("result").innerHTML = "<p>Call ended. Last error: " + lastError + "</p>";
|
||||
document.getElementById("result").innerHTML = (
|
||||
"<p>Call ended. Last error: "+lastError+"</p>"
|
||||
);
|
||||
});
|
||||
call.on("error", function (err) {
|
||||
call.on("error", function(err) {
|
||||
lastError = err.message;
|
||||
call.hangup();
|
||||
disableButtons(false, true, true);
|
||||
});
|
||||
call.on("feeds_changed", function (feeds) {
|
||||
const localFeed = feeds.find((feed) => feed.isLocal());
|
||||
const remoteFeed = feeds.find((feed) => !feed.isLocal());
|
||||
|
||||
const remoteElement = document.getElementById("remote");
|
||||
const localElement = document.getElementById("local");
|
||||
|
||||
if (remoteFeed) {
|
||||
remoteElement.srcObject = remoteFeed.stream;
|
||||
remoteElement.play();
|
||||
}
|
||||
if (localFeed) {
|
||||
localElement.muted = true;
|
||||
localElement.srcObject = localFeed.stream;
|
||||
localElement.play();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.onload = function () {
|
||||
window.onload = function() {
|
||||
document.getElementById("result").innerHTML = "<p>Please wait. Syncing...</p>";
|
||||
document.getElementById("config").innerHTML =
|
||||
"<p>" +
|
||||
"Homeserver: <code>" +
|
||||
BASE_URL +
|
||||
"</code><br/>" +
|
||||
"Room: <code>" +
|
||||
ROOM_ID +
|
||||
"</code><br/>" +
|
||||
"User: <code>" +
|
||||
USER_ID +
|
||||
"</code><br/>" +
|
||||
document.getElementById("config").innerHTML = "<p>" +
|
||||
"Homeserver: <code>"+BASE_URL+"</code><br/>"+
|
||||
"Room: <code>"+ROOM_ID+"</code><br/>"+
|
||||
"User: <code>"+USER_ID+"</code><br/>"+
|
||||
"</p>";
|
||||
disableButtons(true, true, true);
|
||||
};
|
||||
|
||||
client.on("sync", function (state, prevState, data) {
|
||||
client.on("sync", function(state, prevState, data) {
|
||||
switch (state) {
|
||||
case "PREPARED":
|
||||
syncComplete();
|
||||
break;
|
||||
}
|
||||
syncComplete();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
function syncComplete() {
|
||||
document.getElementById("result").innerHTML = "<p>Ready for calls.</p>";
|
||||
disableButtons(false, true, true);
|
||||
|
||||
document.getElementById("call").onclick = function () {
|
||||
document.getElementById("call").onclick = function() {
|
||||
console.log("Placing call...");
|
||||
call = matrixcs.createNewMatrixCall(client, ROOM_ID);
|
||||
call = matrixcs.createNewMatrixCall(
|
||||
client, ROOM_ID
|
||||
);
|
||||
console.log("Call => %s", call);
|
||||
addListeners(call);
|
||||
call.placeVideoCall();
|
||||
call.placeVideoCall(
|
||||
document.getElementById("remote"),
|
||||
document.getElementById("local")
|
||||
);
|
||||
document.getElementById("result").innerHTML = "<p>Placed call.</p>";
|
||||
disableButtons(true, true, false);
|
||||
};
|
||||
|
||||
document.getElementById("hangup").onclick = function () {
|
||||
document.getElementById("hangup").onclick = function() {
|
||||
console.log("Hanging up call...");
|
||||
console.log("Call => %s", call);
|
||||
call.hangup();
|
||||
document.getElementById("result").innerHTML = "<p>Hungup call.</p>";
|
||||
};
|
||||
|
||||
document.getElementById("answer").onclick = function () {
|
||||
document.getElementById("answer").onclick = function() {
|
||||
console.log("Answering call...");
|
||||
console.log("Call => %s", call);
|
||||
call.answer();
|
||||
@@ -103,7 +86,7 @@ function syncComplete() {
|
||||
document.getElementById("result").innerHTML = "<p>Answered call.</p>";
|
||||
};
|
||||
|
||||
client.on("Call.incoming", function (c) {
|
||||
client.on("Call.incoming", function(c) {
|
||||
console.log("Call ringing");
|
||||
disableButtons(true, false, false);
|
||||
document.getElementById("result").innerHTML = "<p>Incoming call...</p>";
|
||||
|
||||
+23
-29
@@ -1,32 +1,26 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>VoIP Test</title>
|
||||
<script src="lib/matrix.js"></script>
|
||||
<script src="browserTest.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
You can place and receive calls with this example. Make sure to edit the constants in
|
||||
<code>browserTest.js</code> first.
|
||||
<div id="config"></div>
|
||||
<div id="result"></div>
|
||||
<button id="call">Place Call</button>
|
||||
<button id="answer">Answer Call</button>
|
||||
<button id="hangup">Hangup Call</button>
|
||||
<div id="videoBackground" class="video-background">
|
||||
<video class="video-element" id="local"></video>
|
||||
<video class="video-element" id="remote"></video>
|
||||
<head>
|
||||
<title>VoIP Test</title>
|
||||
<script src="lib/matrix.js"></script>
|
||||
<script src="browserTest.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
You can place and receive calls with this example. Make sure to edit the
|
||||
constants in <code>browserTest.js</code> first.
|
||||
<div id="config"></div>
|
||||
<div id="result"></div>
|
||||
<button id="call">Place Call</button>
|
||||
<button id="answer">Answer Call</button>
|
||||
<button id="hangup">Hangup Call</button>
|
||||
<div id="videoBackground">
|
||||
<div id="videoContainer">
|
||||
<video id="remote"></video>
|
||||
</div>
|
||||
</body>
|
||||
</div>
|
||||
<div id="videoBackground">
|
||||
<div id="videoContainer">
|
||||
<video id="local"></video>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style>
|
||||
.video-background {
|
||||
height: 500px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.video-element {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,4 +21,4 @@ export PATH="$rootdir/node_modules/.bin:$PATH"
|
||||
|
||||
# now run our checks
|
||||
cd "$tmpdir"
|
||||
pnpm lint
|
||||
npm run lint
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
var matrixcs = require("./lib/matrix");
|
||||
matrixcs.request(require("request"));
|
||||
module.exports = matrixcs;
|
||||
|
||||
var utils = require("./lib/utils");
|
||||
utils.runPolyfills();
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash -l
|
||||
|
||||
set -x
|
||||
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||||
|
||||
nvm use 10 || exit $?
|
||||
npm install || exit $?
|
||||
|
||||
RC=0
|
||||
|
||||
function fail {
|
||||
echo $@ >&2
|
||||
RC=1
|
||||
}
|
||||
|
||||
# don't use last time's test reports
|
||||
rm -rf reports coverage || exit $?
|
||||
|
||||
npm test || fail "npm test finished with return code $?"
|
||||
|
||||
npm run -s lint -- -f checkstyle > eslint.xml ||
|
||||
fail "eslint finished with return code $?"
|
||||
|
||||
# delete the old tarball, if it exists
|
||||
rm -f matrix-js-sdk-*.tgz
|
||||
|
||||
npm pack ||
|
||||
fail "npm pack finished with return code $?"
|
||||
|
||||
npm run gendoc || fail "JSDoc failed with code $?"
|
||||
|
||||
exit $RC
|
||||
@@ -1,42 +0,0 @@
|
||||
import { KnipConfig } from "knip";
|
||||
|
||||
// Specify this as knip loads config files which may conditionally add reporters, e.g. `vitest-sonar-reporter'
|
||||
process.env.GITHUB_ACTIONS = "1";
|
||||
|
||||
export default {
|
||||
entry: [
|
||||
"src/index.ts",
|
||||
"src/types.ts",
|
||||
"src/browser-index.ts",
|
||||
"src/indexeddb-worker.ts",
|
||||
"src/crypto-api/index.ts",
|
||||
"src/testing.ts",
|
||||
"src/matrix.ts",
|
||||
"src/utils.ts", // not really an entrypoint but we have deprecated `defer` there
|
||||
"scripts/**",
|
||||
"spec/**",
|
||||
// XXX: these should be re-exported by one of the supported exports
|
||||
"src/matrixrtc/index.ts",
|
||||
"src/sliding-sync.ts",
|
||||
"src/webrtc/groupCall.ts",
|
||||
"src/webrtc/stats/media/mediaTrackStats.ts",
|
||||
"src/rendezvous/RendezvousChannel.ts",
|
||||
],
|
||||
project: ["**/*.{js,ts}"],
|
||||
ignore: ["examples/**"],
|
||||
ignoreDependencies: [
|
||||
// Required for `action-validator`
|
||||
"@action-validator/*",
|
||||
// Used for git pre-commit hooks
|
||||
"husky",
|
||||
// Used in script which only runs in environment with `@octokit/rest` installed
|
||||
"@octokit/rest",
|
||||
],
|
||||
ignoreBinaries: [
|
||||
// Used when available by reusable workflow `.github/workflows/release-make.yml`
|
||||
"dist",
|
||||
],
|
||||
ignoreExportsUsedInFile: true,
|
||||
includeEntryExports: false,
|
||||
exclude: ["enumMembers"],
|
||||
} satisfies KnipConfig;
|
||||
Generated
+6772
File diff suppressed because it is too large
Load Diff
+93
-136
@@ -1,138 +1,95 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "41.3.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "pnpm build",
|
||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel --delete-dir-on-start src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"build": "pnpm build:compile && pnpm build:types",
|
||||
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
|
||||
"build:compile": "babel --delete-dir-on-start -d lib --verbose --extensions \".ts,.js\" src",
|
||||
"gendoc": "typedoc",
|
||||
"lint": "pnpm lint:types && pnpm lint:js && pnpm lint:workflows",
|
||||
"lint:js": "eslint --max-warnings 0 src spec && prettier --check .",
|
||||
"lint:js-fix": "prettier --log-level=warn --write . && eslint --fix src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'",
|
||||
"lint:knip": "knip",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest watch",
|
||||
"coverage": "pnpm test --coverage"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/matrix-org/matrix-js-sdk.git"
|
||||
},
|
||||
"keywords": [
|
||||
"matrix-org"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "./lib/index.js",
|
||||
"browser": "./lib/browser-index.js",
|
||||
"typings": "./lib/index.d.ts",
|
||||
"author": "matrix.org",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
"lib",
|
||||
"src",
|
||||
"git-revision.txt",
|
||||
"CHANGELOG.md",
|
||||
"CONTRIBUTING.rst",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"package.json",
|
||||
"release.sh"
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^18.1.0",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^6.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"loglevel": "^1.9.2",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.16.1",
|
||||
"oidc-client-ts": "^3.0.1",
|
||||
"p-retry": "8",
|
||||
"sdp-transform": "^3.0.0",
|
||||
"unhomoglyph": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@action-validator/cli": "^0.6.0",
|
||||
"@action-validator/core": "^0.6.0",
|
||||
"@babel/cli": "^7.12.10",
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/eslint-parser": "^7.12.10",
|
||||
"@babel/eslint-plugin": "^7.12.10",
|
||||
"@babel/plugin-proposal-decorators": "^7.25.9",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-class-properties": "^7.12.1",
|
||||
"@babel/plugin-transform-numeric-separator": "^7.12.7",
|
||||
"@babel/plugin-transform-object-rest-spread": "^7.12.1",
|
||||
"@babel/plugin-transform-runtime": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@fetch-mock/vitest": "^0.2.18",
|
||||
"@matrix-org/olm": "3.2.15",
|
||||
"@peculiar/webcrypto": "^1.4.5",
|
||||
"@stylistic/eslint-plugin": "^5.0.0",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/node": "22",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vitest/coverage-v8": "^4.0.17",
|
||||
"@vitest/eslint-plugin": "^1.6.6",
|
||||
"@vitest/ui": "^4.0.17",
|
||||
"babel-plugin-search-and-replace": "^1.1.1",
|
||||
"debug": "^4.3.4",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-import-resolver-typescript": "^4.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsdoc": "^62.0.0",
|
||||
"eslint-plugin-matrix-org": "^3.0.0",
|
||||
"eslint-plugin-n": "^14.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.5.0",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"fake-indexeddb": "^5.0.2",
|
||||
"fetch-mock": "^12.6.0",
|
||||
"happy-dom": "^20.1.0",
|
||||
"husky": "^9.0.0",
|
||||
"knip": "^6.0.0",
|
||||
"lint-staged": "^16.0.0",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"prettier": "3.8.3",
|
||||
"typedoc": "^0.28.1",
|
||||
"typedoc-plugin-coverage": "^4.0.0",
|
||||
"typedoc-plugin-mdn-links": "^5.0.0",
|
||||
"typedoc-plugin-missing-exports": "^4.0.0",
|
||||
"typescript": "^6.0.0",
|
||||
"vitest": "^4.0.17",
|
||||
"vitest-sonar-reporter": "^3.0.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"allowedVersions": {
|
||||
"eslint": "8"
|
||||
}
|
||||
},
|
||||
"allowedDeprecatedVersions": {
|
||||
"eslint": "8"
|
||||
},
|
||||
"overrides": {
|
||||
"expect": "30.3.0",
|
||||
"flatted@<=3.4.1": "^3.4.2",
|
||||
"picomatch@>=4.0.0 <4.0.4": "^4.0.4",
|
||||
"yaml@>=2.0.0 <2.8.3": "^2.8.3",
|
||||
"vite": "8.0.8"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "0.14.3-rc.1",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test:build": "babel -s -d specbuild spec",
|
||||
"test:run": "istanbul cover --report text --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" node_modules/mocha/bin/_mocha -- --recursive specbuild --colors --reporter mocha-jenkins-reporter --reporter-options junit_report_path=reports/test-results.xml",
|
||||
"test:watch": "mocha --watch --compilers js:babel-core/register --recursive spec --colors",
|
||||
"test": "npm run test:build && npm run test:run",
|
||||
"check": "npm run test:build && _mocha --recursive specbuild --colors",
|
||||
"gendoc": "babel --no-babelrc -d .jsdocbuild src && jsdoc -r .jsdocbuild -P package.json -R README.md -d .jsdoc",
|
||||
"start": "npm run start:init && npm run start:watch",
|
||||
"start:watch": "babel -s -w --skip-initial-build -d lib src",
|
||||
"start:init": "babel -s -d lib src",
|
||||
"clean": "rimraf lib dist",
|
||||
"build": "babel -s -d lib src && rimraf dist && mkdir dist && browserify -d browser-index.js | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js && uglifyjs -c -m -o dist/browser-matrix.min.js --source-map dist/browser-matrix.min.js.map --in-source-map dist/browser-matrix.js.map dist/browser-matrix.js",
|
||||
"dist": "npm run build",
|
||||
"watch": "watchify -d browser-index.js -o 'exorcist dist/browser-matrix.js.map > dist/browser-matrix.js' -v",
|
||||
"lint": "eslint --max-warnings 101 src spec",
|
||||
"prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/matrix-org/matrix-js-sdk"
|
||||
},
|
||||
"keywords": [
|
||||
"matrix-org"
|
||||
],
|
||||
"browser": "browser-index.js",
|
||||
"author": "matrix.org",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
".babelrc",
|
||||
".eslintrc.js",
|
||||
"spec/.eslintrc.js",
|
||||
"CHANGELOG.md",
|
||||
"CONTRIBUTING.rst",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"RELEASING.md",
|
||||
"examples",
|
||||
"git-hooks",
|
||||
"git-revision.txt",
|
||||
"index.js",
|
||||
"browser-index.js",
|
||||
"jenkins.sh",
|
||||
"lib",
|
||||
"package.json",
|
||||
"release.sh",
|
||||
"spec",
|
||||
"src"
|
||||
],
|
||||
"dependencies": {
|
||||
"another-json": "^0.2.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"base-x": "3.0.4",
|
||||
"bluebird": "^3.5.0",
|
||||
"browser-request": "^0.3.3",
|
||||
"bs58": "^4.0.1",
|
||||
"content-type": "^1.0.2",
|
||||
"loglevel": "1.6.1",
|
||||
"qs": "^6.5.2",
|
||||
"request": "^2.88.0",
|
||||
"unhomoglyph": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.18.0",
|
||||
"babel-plugin-transform-async-to-bluebird": "^1.1.1",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-es2015": "^6.18.0",
|
||||
"browserify": "^16.2.3",
|
||||
"browserify-shim": "^3.8.13",
|
||||
"eslint": "^5.12.0",
|
||||
"eslint-config-google": "^0.7.1",
|
||||
"exorcist": "^0.4.0",
|
||||
"expect": "^1.20.2",
|
||||
"istanbul": "^0.4.5",
|
||||
"jsdoc": "^3.5.5",
|
||||
"lolex": "^1.5.2",
|
||||
"matrix-mock-request": "^1.2.2",
|
||||
"mocha": "^5.2.0",
|
||||
"mocha-jenkins-reporter": "^0.4.0",
|
||||
"rimraf": "^2.5.4",
|
||||
"source-map-support": "^0.4.11",
|
||||
"sourceify": "^0.1.0",
|
||||
"uglify-js": "^2.8.26",
|
||||
"watchify": "^3.11.0"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"sourceify"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Generated
-8353
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
nodeLinker: hoisted
|
||||
Executable
+316
@@ -0,0 +1,316 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Script to perform a release of matrix-js-sdk.
|
||||
#
|
||||
# Requires:
|
||||
# github-changelog-generator; install via:
|
||||
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
|
||||
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
|
||||
# hub; install via brew (OSX) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
|
||||
|
||||
set -e
|
||||
|
||||
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
|
||||
if [[ `command -v hub` ]] && [[ `hub --version` =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
|
||||
HUB_VERSION_MAJOR=${BASH_REMATCH[1]}
|
||||
HUB_VERSION_MINOR=${BASH_REMATCH[2]}
|
||||
if [[ $HUB_VERSION_MAJOR -lt 2 ]] || [[ $HUB_VERSION_MAJOR -eq 2 && $HUB_VERSION_MINOR -lt 5 ]]; then
|
||||
echo "hub version 2.5 is required, you have $HUB_VERSION_MAJOR.$HUB_VERSION_MINOR installed"
|
||||
exit
|
||||
fi
|
||||
else
|
||||
echo "hub is required: please install it"
|
||||
exit
|
||||
fi
|
||||
|
||||
USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z"
|
||||
|
||||
help() {
|
||||
cat <<EOF
|
||||
$USAGE
|
||||
|
||||
-c changelog_file: specify name of file containing changelog
|
||||
-x: skip updating the changelog
|
||||
-z: skip generating the jsdoc
|
||||
EOF
|
||||
}
|
||||
|
||||
ret=0
|
||||
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
|
||||
if [ "$ret" -eq 0 ]; then
|
||||
echo "package.json contains develop dependencies. Refusing to release."
|
||||
exit
|
||||
fi
|
||||
|
||||
if ! git diff-index --quiet --cached HEAD; then
|
||||
echo "this git checkout has staged (uncommitted) changes. Refusing to release."
|
||||
exit
|
||||
fi
|
||||
|
||||
if ! git diff-files --quiet; then
|
||||
echo "this git checkout has uncommitted changes. Refusing to release."
|
||||
exit
|
||||
fi
|
||||
|
||||
skip_changelog=
|
||||
skip_jsdoc=
|
||||
changelog_file="CHANGELOG.md"
|
||||
expected_npm_user="matrixdotorg"
|
||||
while getopts hc:u:xz f; do
|
||||
case $f in
|
||||
h)
|
||||
help
|
||||
exit 0
|
||||
;;
|
||||
c)
|
||||
changelog_file="$OPTARG"
|
||||
;;
|
||||
x)
|
||||
skip_changelog=1
|
||||
;;
|
||||
z)
|
||||
skip_jsdoc=1
|
||||
;;
|
||||
u)
|
||||
expected_npm_user="$OPTARG"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift `expr $OPTIND - 1`
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "Usage: $USAGE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
# update_changelog doesn't have a --version flag
|
||||
update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit)
|
||||
fi
|
||||
|
||||
actual_npm_user=`npm whoami`;
|
||||
if [ $expected_npm_user != $actual_npm_user ]; then
|
||||
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ignore leading v on release
|
||||
release="${1#v}"
|
||||
tag="v${release}"
|
||||
rel_branch="release-$tag"
|
||||
|
||||
prerelease=0
|
||||
# We check if this build is a prerelease by looking to
|
||||
# see if the version has a hyphen in it. Crude,
|
||||
# but semver doesn't support postreleases so anything
|
||||
# with a hyphen is a prerelease.
|
||||
echo $release | grep -q '-' && prerelease=1
|
||||
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
echo Making a PRE-RELEASE
|
||||
fi
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
if ! command -v update_changelog >/dev/null 2>&1; then
|
||||
echo "release.sh requires github-changelog-generator. Try:" >&2
|
||||
echo " pip install git+https://github.com/matrix-org/github-changelog-generator.git" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# we might already be on the release branch, in which case, yay
|
||||
# If we're on any branch starting with 'release', we don't create
|
||||
# a separate release branch (this allows us to use the same
|
||||
# release branch for releases and release candidates).
|
||||
curbranch=$(git symbolic-ref --short HEAD)
|
||||
if [[ "$curbranch" != release* ]]; then
|
||||
echo "Creating release branch"
|
||||
git checkout -b "$rel_branch"
|
||||
else
|
||||
echo "Using current branch ($curbranch) for release"
|
||||
rel_branch=$curbranch
|
||||
fi
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
echo "Generating changelog"
|
||||
update_changelog -f "$changelog_file" "$release"
|
||||
read -p "Edit $changelog_file manually, or press enter to continue " REPLY
|
||||
|
||||
if [ -n "$(git ls-files --modified $changelog_file)" ]; then
|
||||
echo "Committing updated changelog"
|
||||
git commit "$changelog_file" -m "Prepare changelog for $tag"
|
||||
fi
|
||||
fi
|
||||
latest_changes=`mktemp`
|
||||
cat "${changelog_file}" | `dirname $0`/scripts/changelog_head.py > "${latest_changes}"
|
||||
|
||||
set -x
|
||||
|
||||
# Bump package.json and build the dist
|
||||
echo "npm version"
|
||||
# npm version will automatically commit its modification
|
||||
# and make a release tag. We don't want it to create the tag
|
||||
# because it can only sign with the default key, but we can
|
||||
# only turn off both of these behaviours, so we have to
|
||||
# manually commit the result.
|
||||
npm version --no-git-tag-version "$release"
|
||||
git commit package.json -m "$tag"
|
||||
|
||||
|
||||
# figure out if we should be signing this release
|
||||
signing_id=
|
||||
if [ -f release_config.yaml ]; then
|
||||
signing_id=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']"`
|
||||
fi
|
||||
|
||||
|
||||
# If there is a 'dist' script in the package.json,
|
||||
# run it in a separate checkout of the project, then
|
||||
# upload any files in the 'dist' directory as release
|
||||
# assets.
|
||||
# We make a completely separate checkout to be sure
|
||||
# we're using released versions of the dependencies
|
||||
# (rather than whatever we're pulling in from npm link)
|
||||
assets=''
|
||||
dodist=0
|
||||
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
|
||||
if [ $dodist -eq 0 ]; then
|
||||
projdir=`pwd`
|
||||
builddir=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'`
|
||||
echo "Building distribution copy in $builddir"
|
||||
pushd "$builddir"
|
||||
git clone "$projdir" .
|
||||
git checkout "$rel_branch"
|
||||
npm install
|
||||
# We haven't tagged yet, so tell the dist script what version
|
||||
# it's building
|
||||
DIST_VERSION="$tag" npm run dist
|
||||
|
||||
popd
|
||||
|
||||
for i in "$builddir"/dist/*; do
|
||||
assets="$assets -a $i"
|
||||
if [ -n "$signing_id" ]
|
||||
then
|
||||
gpg -u "$signing_id" --armor --output "$i".asc --detach-sig "$i"
|
||||
assets="$assets -a $i.asc"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -n "$signing_id" ]; then
|
||||
# make a signed tag
|
||||
# gnupg seems to fail to get the right tty device unless we set it here
|
||||
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=`tty` git tag -u "$signing_id" -F "${latest_changes}" "$tag"
|
||||
else
|
||||
git tag -a -F "${latest_changes}" "$tag"
|
||||
fi
|
||||
|
||||
# push the tag and the release branch
|
||||
git push origin "$rel_branch" "$tag"
|
||||
|
||||
if [ -n "$signing_id" ]; then
|
||||
# make a signature for the source tarball.
|
||||
#
|
||||
# github will make us a tarball from the tag - we want to create a
|
||||
# signature for it, which means that first of all we need to check that
|
||||
# it's correct.
|
||||
#
|
||||
# we can't deterministically build exactly the same tarball, due to
|
||||
# differences in gzip implementation - but we *can* build the same tar - so
|
||||
# the easiest way to check the validity of the tarball from git is to unzip
|
||||
# it and compare it with our own idea of what the tar should look like.
|
||||
|
||||
# the name of the sig file we want to create
|
||||
source_sigfile="${tag}-src.tar.gz.asc"
|
||||
|
||||
tarfile="$tag.tar.gz"
|
||||
gh_project_url=$(git remote get-url origin |
|
||||
sed -e 's#^git@github\.com:#https://github.com/#' \
|
||||
-e 's#^git\+ssh://git@github\.com/#https://github.com/#' \
|
||||
-e 's/\.git$//')
|
||||
project_name="${gh_project_url##*/}"
|
||||
curl -L "${gh_project_url}/archive/${tarfile}" -o "${tarfile}"
|
||||
|
||||
# unzip it and compare it with the tar we would generate
|
||||
if ! cmp --silent <(gunzip -c $tarfile) \
|
||||
<(git archive --format tar --prefix="${project_name}-${release}/" "$tag"); then
|
||||
|
||||
# we don't bail out here, because really it's more likely that our comparison
|
||||
# screwed up and it's super annoying to abort the script at this point.
|
||||
cat >&2 <<EOF
|
||||
!!!!!!!!!!!!!!!!!
|
||||
!!!! WARNING !!!!
|
||||
|
||||
Mismatch between our own tarfile and that generated by github: not signing
|
||||
source tarball.
|
||||
|
||||
To resolve, determine if $tarfile is correct, and if so sign it with gpg and
|
||||
attach it to the release as $source_sigfile.
|
||||
|
||||
!!!!!!!!!!!!!!!!!
|
||||
EOF
|
||||
else
|
||||
gpg -u "$signing_id" --armor --output "$source_sigfile" --detach-sig "$tarfile"
|
||||
assets="$assets -a $source_sigfile"
|
||||
fi
|
||||
fi
|
||||
|
||||
hubflags=''
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
hubflags='-p'
|
||||
fi
|
||||
|
||||
release_text=`mktemp`
|
||||
echo "$tag" > "${release_text}"
|
||||
echo >> "${release_text}"
|
||||
cat "${latest_changes}" >> "${release_text}"
|
||||
hub release create $hubflags $assets -F "${release_text}" "$tag"
|
||||
|
||||
if [ $dodist -eq 0 ]; then
|
||||
rm -rf "$builddir"
|
||||
fi
|
||||
rm "${release_text}"
|
||||
rm "${latest_changes}"
|
||||
|
||||
# publish to npmjs
|
||||
npm publish
|
||||
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
echo "generating jsdocs"
|
||||
npm run gendoc
|
||||
|
||||
echo "copying jsdocs to gh-pages branch"
|
||||
git checkout gh-pages
|
||||
git pull
|
||||
cp -a ".jsdoc/matrix-js-sdk/$release" .
|
||||
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
|
||||
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' \
|
||||
$release index.html
|
||||
git add "$release"
|
||||
git commit --no-verify -m "Add jsdoc for $release" index.html "$release"
|
||||
fi
|
||||
|
||||
# if it is a pre-release, leave it on the release branch for now.
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
git checkout "$rel_branch"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# merge release branch to master
|
||||
echo "updating master branch"
|
||||
git checkout master
|
||||
git pull
|
||||
git merge "$rel_branch"
|
||||
|
||||
# push master and docs (if generated) to github
|
||||
git push origin master
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
git push origin gh-pages
|
||||
fi
|
||||
|
||||
# finally, merge master back onto develop
|
||||
git checkout develop
|
||||
git pull
|
||||
git merge master
|
||||
git push origin develop
|
||||
@@ -15,4 +15,4 @@ for line in sys.stdin:
|
||||
break
|
||||
found_first_header = True
|
||||
elif not re.match(r"^=+$", line) and len(line) > 0:
|
||||
print(line)
|
||||
print line
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
async function listReleases(github, owner, repo) {
|
||||
const response = await github.rest.repos.listReleases({
|
||||
owner,
|
||||
repo,
|
||||
per_page: 100,
|
||||
});
|
||||
// Filters out draft releases
|
||||
return response.data.filter((release) => !release.draft);
|
||||
}
|
||||
|
||||
// Dependency can be a tuple of dependency, from version, to version, in which case a list of releases in that range (to inclusive) will be returned
|
||||
// Or it can be a string in the form accepted by `getRelease`
|
||||
async function getReleases(github, dependency) {
|
||||
if (Array.isArray(dependency)) {
|
||||
const [dep, fromVersion, toVersion] = dependency;
|
||||
const upstreamPackageJson = getDependencyPackageJson(dep);
|
||||
const [owner, repo] = upstreamPackageJson.repository.url.split("/").slice(-2);
|
||||
|
||||
const unfilteredReleases = await listReleases(github, owner, repo);
|
||||
// Only include non-draft & non-prerelease releases, unless the to-release is a pre-release, include that one
|
||||
const releases = unfilteredReleases.filter(
|
||||
(release) => !release.prerelease || release.tag_name === `v${toVersion}`,
|
||||
);
|
||||
|
||||
const fromVersionIndex = releases.findIndex((release) => release.tag_name === `v${fromVersion}`);
|
||||
const toVersionIndex = releases.findIndex((release) => release.tag_name === `v${toVersion}`);
|
||||
|
||||
return releases.slice(toVersionIndex, fromVersionIndex);
|
||||
}
|
||||
|
||||
return [await getRelease(github, dependency)];
|
||||
}
|
||||
|
||||
// Dependency can be the name of an entry in package.json, in which case the owner, repo & version will be looked up in its own package.json
|
||||
// Or it can be a string in the form owner/repo@tag - in this case the tag is used exactly to find the release
|
||||
// Or it can be a string in the form owner/repo~tag - in this case the latest tag in the same major.minor.patch set is used to find the release
|
||||
async function getRelease(github, dependency) {
|
||||
let owner;
|
||||
let repo;
|
||||
let tag;
|
||||
|
||||
if (dependency.includes("/")) {
|
||||
let rest;
|
||||
[owner, rest] = dependency.split("/");
|
||||
|
||||
if (dependency.includes("@")) {
|
||||
[repo, tag] = rest.split("@");
|
||||
} else if (dependency.includes("~")) {
|
||||
[repo, tag] = rest.split("~");
|
||||
|
||||
if (tag.includes("-rc.")) {
|
||||
// If the tag is an RC, find the latest matching RC in the set
|
||||
try {
|
||||
const releases = await listReleases(github, owner, repo);
|
||||
const baseVersion = tag.split("-rc.")[0];
|
||||
const release = releases.find((release) => release.tag_name.startsWith(baseVersion));
|
||||
if (release) return release;
|
||||
} catch (e) {
|
||||
// Fall back to getReleaseByTag
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const upstreamPackageJson = getDependencyPackageJson(dependency);
|
||||
[owner, repo] = upstreamPackageJson.repository.url.split("/").slice(-2);
|
||||
tag = `v${upstreamPackageJson.version}`;
|
||||
}
|
||||
|
||||
const response = await github.rest.repos.getReleaseByTag({
|
||||
owner,
|
||||
repo,
|
||||
tag,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
function getDependencyPackageJson(dependency) {
|
||||
return JSON.parse(fs.readFileSync(`./node_modules/${dependency}/package.json`, "utf8"));
|
||||
}
|
||||
|
||||
const HEADING_PREFIX = "## ";
|
||||
|
||||
const categories = [
|
||||
"🔒 SECURITY FIXES",
|
||||
"🚨 BREAKING CHANGESd",
|
||||
"🦖 Deprecations",
|
||||
"✨ Features",
|
||||
"🐛 Bug Fixes",
|
||||
"🧰 Maintenance",
|
||||
];
|
||||
|
||||
const parseReleaseNotes = (body, sections) => {
|
||||
let heading = null;
|
||||
for (const line of body.split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith(HEADING_PREFIX)) {
|
||||
heading = trimmed.slice(HEADING_PREFIX.length);
|
||||
if (!categories.includes(heading)) heading = null;
|
||||
continue;
|
||||
}
|
||||
if (heading && trimmed) {
|
||||
sections[heading].push(trimmed);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const main = async ({ github, releaseId, dependencies }) => {
|
||||
const { GITHUB_REPOSITORY } = process.env;
|
||||
const [owner, repo] = GITHUB_REPOSITORY.split("/");
|
||||
|
||||
const { data: release } = await github.rest.repos.getRelease({
|
||||
owner,
|
||||
repo,
|
||||
release_id: releaseId,
|
||||
});
|
||||
|
||||
const sections = Object.fromEntries(categories.map((cat) => [cat, []]));
|
||||
parseReleaseNotes(release.body, sections);
|
||||
for (const dependency of dependencies) {
|
||||
const releases = await getReleases(github, dependency);
|
||||
for (const release of releases) {
|
||||
parseReleaseNotes(release.body, sections);
|
||||
}
|
||||
}
|
||||
|
||||
const intro = release.body.split(HEADING_PREFIX, 2)[0].trim();
|
||||
|
||||
let output = "";
|
||||
if (intro) {
|
||||
output = intro + "\n\n";
|
||||
}
|
||||
|
||||
for (const section in sections) {
|
||||
const lines = sections[section];
|
||||
if (!lines.length) continue;
|
||||
output += HEADING_PREFIX + section + "\n\n";
|
||||
output += lines.join("\n");
|
||||
output += "\n\n";
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
// This is just for testing locally
|
||||
// Needs environment variables GITHUB_TOKEN & GITHUB_REPOSITORY
|
||||
if (require.main === module) {
|
||||
const { Octokit } = require("@octokit/rest");
|
||||
const github = new Octokit({ auth: process.env.GITHUB_TOKEN });
|
||||
if (process.argv.length < 4) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Usage: node merge-release-notes.js owner/repo:release_id npm-package-name ...");
|
||||
process.exit(1);
|
||||
}
|
||||
const [releaseId, ...dependencies] = process.argv.slice(2);
|
||||
main({ github, releaseId, dependencies }).then((output) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(output);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = main;
|
||||
@@ -1,16 +0,0 @@
|
||||
sonar.projectKey=matrix-js-sdk
|
||||
sonar.organization=matrix-org
|
||||
|
||||
# Encoding of the source code. Default is default system encoding
|
||||
#sonar.sourceEncoding=UTF-8
|
||||
|
||||
sonar.sources=src
|
||||
sonar.tests=spec
|
||||
sonar.exclusions=docs,examples,git-hooks
|
||||
|
||||
sonar.typescript.tsconfigPath=./tsconfig.json
|
||||
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||
sonar.coverage.exclusions=spec/**/*
|
||||
sonar.testExecutionReportPaths=coverage/sonar-report.xml
|
||||
|
||||
sonar.lang.patterns.ts=**/*.ts,**/*.tsx
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
mocha: true,
|
||||
},
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019, 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -17,32 +16,31 @@ limitations under the License.
|
||||
|
||||
/**
|
||||
* A mock implementation of the webstorage api
|
||||
* @constructor
|
||||
*/
|
||||
export class MockStorageApi {
|
||||
public data: Record<string, string> = {};
|
||||
public keys: string[] = [];
|
||||
public length = 0;
|
||||
function MockStorageApi() {
|
||||
this.data = {};
|
||||
this.keys = [];
|
||||
this.length = 0;
|
||||
}
|
||||
|
||||
public setItem(k: string, v: string): void {
|
||||
MockStorageApi.prototype = {
|
||||
setItem: function(k, v) {
|
||||
this.data[k] = v;
|
||||
this.recalc();
|
||||
}
|
||||
|
||||
public getItem(k: string): string | null {
|
||||
this._recalc();
|
||||
},
|
||||
getItem: function(k) {
|
||||
return this.data[k] || null;
|
||||
}
|
||||
|
||||
public removeItem(k: string): void {
|
||||
},
|
||||
removeItem: function(k) {
|
||||
delete this.data[k];
|
||||
this.recalc();
|
||||
}
|
||||
|
||||
public key(index: number): string {
|
||||
this._recalc();
|
||||
},
|
||||
key: function(index) {
|
||||
return this.keys[index];
|
||||
}
|
||||
|
||||
private recalc(): void {
|
||||
const keys: string[] = [];
|
||||
},
|
||||
_recalc: function() {
|
||||
const keys = [];
|
||||
for (const k in this.data) {
|
||||
if (!this.data.hasOwnProperty(k)) {
|
||||
continue;
|
||||
@@ -51,5 +49,8 @@ export class MockStorageApi {
|
||||
}
|
||||
this.keys = keys;
|
||||
this.length = keys.length;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/** */
|
||||
module.exports = MockStorageApi;
|
||||
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import './olm-loader';
|
||||
|
||||
import sdk from '..';
|
||||
import testUtils from './test-utils';
|
||||
import MockHttpBackend from 'matrix-mock-request';
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
import LocalStorageCryptoStore from '../lib/crypto/store/localStorage-crypto-store';
|
||||
|
||||
/**
|
||||
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
|
||||
*
|
||||
* @constructor
|
||||
* @param {string} userId
|
||||
* @param {string} deviceId
|
||||
* @param {string} accessToken
|
||||
*
|
||||
* @param {WebStorage=} sessionStoreBackend a web storage object to use for the
|
||||
* session store. If undefined, we will create a MockStorageApi.
|
||||
*/
|
||||
export default function TestClient(
|
||||
userId, deviceId, accessToken, sessionStoreBackend,
|
||||
) {
|
||||
this.userId = userId;
|
||||
this.deviceId = deviceId;
|
||||
|
||||
if (sessionStoreBackend === undefined) {
|
||||
sessionStoreBackend = new testUtils.MockStorageApi();
|
||||
}
|
||||
const sessionStore = new sdk.WebStorageSessionStore(sessionStoreBackend);
|
||||
|
||||
// expose this so the tests can get to it
|
||||
this.cryptoStore = new LocalStorageCryptoStore(sessionStoreBackend);
|
||||
|
||||
this.httpBackend = new MockHttpBackend();
|
||||
this.client = sdk.createClient({
|
||||
baseUrl: "http://" + userId + ".test.server",
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
deviceId: deviceId,
|
||||
sessionStore: sessionStore,
|
||||
cryptoStore: this.cryptoStore,
|
||||
request: this.httpBackend.requestFn,
|
||||
});
|
||||
|
||||
this.deviceKeys = null;
|
||||
this.oneTimeKeys = {};
|
||||
}
|
||||
|
||||
TestClient.prototype.toString = function() {
|
||||
return 'TestClient[' + this.userId + ']';
|
||||
};
|
||||
|
||||
/**
|
||||
* start the client, and wait for it to initialise.
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
TestClient.prototype.start = function() {
|
||||
console.log(this + ': starting');
|
||||
this.httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
this.expectDeviceKeyUpload();
|
||||
|
||||
// we let the client do a very basic initial sync, which it needs before
|
||||
// it will upload one-time keys.
|
||||
this.httpBackend.when("GET", "/sync").respond(200, { next_batch: 1 });
|
||||
|
||||
this.client.startClient({
|
||||
// set this so that we can get hold of failed events
|
||||
pendingEventOrdering: 'detached',
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
this.httpBackend.flushAllExpected(),
|
||||
testUtils.syncPromise(this.client),
|
||||
]).then(() => {
|
||||
console.log(this + ': started');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* stop the client
|
||||
* @return {Promise} Resolves once the mock http backend has finished all pending flushes
|
||||
*/
|
||||
TestClient.prototype.stop = function() {
|
||||
this.client.stopClient();
|
||||
return this.httpBackend.stop();
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up expectations that the client will upload device keys.
|
||||
*/
|
||||
TestClient.prototype.expectDeviceKeyUpload = function() {
|
||||
const self = this;
|
||||
this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) {
|
||||
expect(content.one_time_keys).toBe(undefined);
|
||||
expect(content.device_keys).toBeTruthy();
|
||||
|
||||
console.log(self + ': received device keys');
|
||||
// we expect this to happen before any one-time keys are uploaded.
|
||||
expect(Object.keys(self.oneTimeKeys).length).toEqual(0);
|
||||
|
||||
self.deviceKeys = content.device_keys;
|
||||
return {one_time_key_counts: {signed_curve25519: 0}};
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* If one-time keys have already been uploaded, return them. Otherwise,
|
||||
* set up an expectation that the keys will be uploaded, and wait for
|
||||
* that to happen.
|
||||
*
|
||||
* @returns {Promise} for the one-time keys
|
||||
*/
|
||||
TestClient.prototype.awaitOneTimeKeyUpload = function() {
|
||||
if (Object.keys(this.oneTimeKeys).length != 0) {
|
||||
// already got one-time keys
|
||||
return Promise.resolve(this.oneTimeKeys);
|
||||
}
|
||||
|
||||
this.httpBackend.when("POST", "/keys/upload")
|
||||
.respond(200, (path, content) => {
|
||||
expect(content.device_keys).toBe(undefined);
|
||||
expect(content.one_time_keys).toBe(undefined);
|
||||
return {one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys).length,
|
||||
}};
|
||||
});
|
||||
|
||||
this.httpBackend.when("POST", "/keys/upload")
|
||||
.respond(200, (path, content) => {
|
||||
expect(content.device_keys).toBe(undefined);
|
||||
expect(content.one_time_keys).toBeTruthy();
|
||||
expect(content.one_time_keys).toNotEqual({});
|
||||
console.log('%s: received %i one-time keys', this,
|
||||
Object.keys(content.one_time_keys).length);
|
||||
this.oneTimeKeys = content.one_time_keys;
|
||||
return {one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys).length,
|
||||
}};
|
||||
});
|
||||
|
||||
// this can take ages
|
||||
return this.httpBackend.flush('/keys/upload', 2, 1000).then((flushed) => {
|
||||
expect(flushed).toEqual(2);
|
||||
return this.oneTimeKeys;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up expectations that the client will query device keys.
|
||||
*
|
||||
* We check that the query contains each of the users in `response`.
|
||||
*
|
||||
* @param {Object} response response to the query.
|
||||
*/
|
||||
TestClient.prototype.expectKeyQuery = function(response) {
|
||||
this.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, (path, content) => {
|
||||
Object.keys(response.device_keys).forEach((userId) => {
|
||||
expect(content.device_keys[userId]).toEqual({});
|
||||
});
|
||||
return response;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* get the uploaded curve25519 device key
|
||||
*
|
||||
* @return {string} base64 device key
|
||||
*/
|
||||
TestClient.prototype.getDeviceKey = function() {
|
||||
const keyId = 'curve25519:' + this.deviceId;
|
||||
return this.deviceKeys.keys[keyId];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* get the uploaded ed25519 device key
|
||||
*
|
||||
* @return {string} base64 device key
|
||||
*/
|
||||
TestClient.prototype.getSigningKey = function() {
|
||||
const keyId = 'ed25519:' + this.deviceId;
|
||||
return this.deviceKeys.keys[keyId];
|
||||
};
|
||||
|
||||
/**
|
||||
* flush a single /sync request, and wait for the syncing event
|
||||
*
|
||||
* @returns {Promise} promise which completes once the sync has been flushed
|
||||
*/
|
||||
TestClient.prototype.flushSync = function() {
|
||||
console.log(`${this}: flushSync`);
|
||||
return Promise.all([
|
||||
this.httpBackend.flush('/sync', 1),
|
||||
testUtils.syncPromise(this.client),
|
||||
]).then(() => {
|
||||
console.log(`${this}: flushSync completed`);
|
||||
});
|
||||
};
|
||||
@@ -1,258 +0,0 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018-2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// `expect` is allowed in helper functions which are called within `test`/`it` blocks
|
||||
/* eslint-disable @vitest/no-standalone-expect */
|
||||
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import type { IDeviceKeys, IOneTimeKey } from "../src/@types/crypto";
|
||||
import type { IE2EKeyReceiver } from "./test-utils/E2EKeyReceiver";
|
||||
import { logger } from "../src/logger";
|
||||
import { syncPromise } from "./test-utils/test-utils";
|
||||
import { createClient, type IStartClientOpts } from "../src/matrix";
|
||||
import {
|
||||
type ICreateClientOpts,
|
||||
type IDownloadKeyResult,
|
||||
type MatrixClient,
|
||||
PendingEventOrdering,
|
||||
} from "../src/client";
|
||||
import { type IKeysUploadResponse, type IUploadKeysRequest } from "../src/client";
|
||||
import { type ISyncResponder } from "./test-utils/SyncResponder";
|
||||
|
||||
/**
|
||||
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
|
||||
*
|
||||
* @deprecated Avoid using this; it is tied too tightly to matrix-mock-request and is generally inconvenient to use.
|
||||
* Instead, construct a MatrixClient manually, use fetch-mock to intercept the HTTP requests, and
|
||||
* use things like {@link E2EKeyReceiver} and {@link SyncResponder} to manage the requests.
|
||||
*/
|
||||
export class TestClient implements IE2EKeyReceiver, ISyncResponder {
|
||||
public readonly httpBackend: MockHttpBackend;
|
||||
public readonly client: MatrixClient;
|
||||
public deviceKeys?: IDeviceKeys | null;
|
||||
public oneTimeKeys?: Record<string, IOneTimeKey>;
|
||||
|
||||
constructor(
|
||||
public readonly userId?: string,
|
||||
public readonly deviceId?: string,
|
||||
accessToken?: string,
|
||||
sessionStoreBackend?: Storage,
|
||||
options?: Partial<ICreateClientOpts>,
|
||||
) {
|
||||
this.httpBackend = new MockHttpBackend();
|
||||
|
||||
const fullOptions: ICreateClientOpts = {
|
||||
baseUrl: "http://" + userId?.slice(1).replace(":", ".") + ".test.server",
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
deviceId: deviceId,
|
||||
fetchFn: this.httpBackend.fetchFn as typeof globalThis.fetch,
|
||||
...options,
|
||||
};
|
||||
this.client = createClient(fullOptions);
|
||||
|
||||
this.deviceKeys = null;
|
||||
this.oneTimeKeys = {};
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return "TestClient[" + this.userId + "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* start the client, and wait for it to initialise.
|
||||
*/
|
||||
public start(opts: IStartClientOpts = {}): Promise<void> {
|
||||
logger.log(this + ": starting");
|
||||
this.httpBackend.when("GET", "/versions").respond(200, {
|
||||
// we have tests that rely on support for lazy-loading members
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
this.httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
this.expectDeviceKeyUpload();
|
||||
|
||||
// we let the client do a very basic initial sync, which it needs before
|
||||
// it will upload one-time keys.
|
||||
this.httpBackend.when("GET", "/sync").respond(200, { next_batch: 1 });
|
||||
|
||||
this.client.startClient({
|
||||
// set this so that we can get hold of failed events
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
|
||||
...opts,
|
||||
});
|
||||
|
||||
return Promise.all([this.httpBackend.flushAllExpected(), syncPromise(this.client)]).then(() => {
|
||||
logger.log(this + ": started");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* stop the client
|
||||
* @returns Promise which resolves once the mock http backend has finished all pending flushes
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
this.client.stopClient();
|
||||
await this.httpBackend.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up expectations that the client will upload device keys (and possibly one-time keys)
|
||||
*/
|
||||
public expectDeviceKeyUpload() {
|
||||
this.httpBackend
|
||||
.when("POST", "/keys/upload")
|
||||
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content) => {
|
||||
expect(content.device_keys).toBeTruthy();
|
||||
|
||||
logger.log(this + ": received device keys");
|
||||
// we expect this to happen before any one-time keys are uploaded.
|
||||
expect(Object.keys(this.oneTimeKeys!).length).toEqual(0);
|
||||
|
||||
this.deviceKeys = content.device_keys;
|
||||
|
||||
// the first batch of one-time keys may be uploaded at the same time.
|
||||
if (content.one_time_keys) {
|
||||
logger.log(`${this}: received ${Object.keys(content.one_time_keys).length} one-time keys`);
|
||||
this.oneTimeKeys = content.one_time_keys;
|
||||
}
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* If one-time keys have already been uploaded, return them. Otherwise,
|
||||
* set up an expectation that the keys will be uploaded, and wait for
|
||||
* that to happen.
|
||||
*
|
||||
* @returns Promise for the one-time keys
|
||||
*/
|
||||
public awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>> {
|
||||
if (Object.keys(this.oneTimeKeys!).length != 0) {
|
||||
// already got one-time keys
|
||||
return Promise.resolve(this.oneTimeKeys!);
|
||||
}
|
||||
|
||||
this.httpBackend
|
||||
.when("POST", "/keys/upload")
|
||||
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content: IUploadKeysRequest) => {
|
||||
expect(content.device_keys).toBe(undefined);
|
||||
expect(content.one_time_keys).toBe(undefined);
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
this.httpBackend
|
||||
.when("POST", "/keys/upload")
|
||||
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content: IUploadKeysRequest) => {
|
||||
expect(content.device_keys).toBe(undefined);
|
||||
expect(content.one_time_keys).toBeTruthy();
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
logger.log("%s: received %i one-time keys", this, Object.keys(content.one_time_keys!).length);
|
||||
this.oneTimeKeys = content.one_time_keys;
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// this can take ages
|
||||
return this.httpBackend.flush("/keys/upload", 2, 1000).then((flushed) => {
|
||||
expect(flushed).toEqual(2);
|
||||
return this.oneTimeKeys!;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up expectations that the client will query device keys.
|
||||
*
|
||||
* We check that the query contains each of the users in `response`.
|
||||
*
|
||||
* @param response - response to the query.
|
||||
*/
|
||||
public expectKeyQuery(response: IDownloadKeyResult) {
|
||||
this.httpBackend.when("POST", "/keys/query").respond<IDownloadKeyResult>(200, (_path, content) => {
|
||||
Object.keys(response.device_keys).forEach((userId) => {
|
||||
expect((content.device_keys! as Record<string, any>)[userId]).toEqual([]);
|
||||
});
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* get the uploaded curve25519 device key
|
||||
*
|
||||
* @returns base64 device key
|
||||
*/
|
||||
public getDeviceKey(): string {
|
||||
const keyId = "curve25519:" + this.deviceId;
|
||||
return this.deviceKeys!.keys[keyId];
|
||||
}
|
||||
|
||||
/**
|
||||
* get the uploaded ed25519 device key
|
||||
*
|
||||
* @returns base64 device key
|
||||
*/
|
||||
public getSigningKey(): string {
|
||||
const keyId = "ed25519:" + this.deviceId;
|
||||
return this.deviceKeys!.keys[keyId];
|
||||
}
|
||||
|
||||
/** Next time we see a sync request (or immediately, if there is one waiting), send the given response
|
||||
*
|
||||
* Calling this will register a response for `/sync`, and then, in the background, flush a single `/sync` request.
|
||||
* Try calling {@link syncPromise} to wait for the sync to complete.
|
||||
*
|
||||
* @param response - response to /sync request
|
||||
*/
|
||||
public sendOrQueueSyncResponse(syncResponse: object): void {
|
||||
this.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
this.httpBackend.flush("/sync", 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* flush a single /sync request, and wait for the syncing event
|
||||
*
|
||||
* @deprecated: prefer to use {@link #sendOrQueueSyncResponse} followed by {@link syncPromise}.
|
||||
*/
|
||||
public flushSync(): Promise<void> {
|
||||
logger.log(`${this}: flushSync`);
|
||||
return Promise.all([this.httpBackend.flush("/sync", 1), syncPromise(this.client)]).then(() => {
|
||||
logger.log(`${this}: flushSync completed`);
|
||||
});
|
||||
}
|
||||
|
||||
public isFallbackICEServerAllowed(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public getUserId(): string {
|
||||
return this.userId!;
|
||||
}
|
||||
}
|
||||
@@ -1,485 +0,0 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import debug from "debug";
|
||||
|
||||
import { syncPromise } from "../../test-utils/test-utils";
|
||||
import { type AuthDict, createClient, DebugLogger, type MatrixClient } from "../../../src";
|
||||
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
|
||||
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
|
||||
import { type CryptoCallbacks, CrossSigningKey } from "../../../src/crypto-api";
|
||||
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
|
||||
import { type ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import {
|
||||
MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
||||
SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
||||
SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64,
|
||||
SIGNED_CROSS_SIGNING_KEYS_DATA,
|
||||
SIGNED_TEST_DEVICE_DATA,
|
||||
USER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
||||
} from "../../test-utils/test-data";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator";
|
||||
import { CryptoEvent } from "../../../src/crypto-api";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
const TEST_USER_ID = "@alice:localhost";
|
||||
const TEST_DEVICE_ID = "xzcvb";
|
||||
|
||||
/**
|
||||
* Integration tests for cross-signing functionality.
|
||||
*
|
||||
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
|
||||
* to provide the most effective integration tests possible.
|
||||
*/
|
||||
describe("cross-signing", () => {
|
||||
let aliceClient: MatrixClient;
|
||||
|
||||
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
|
||||
let syncResponder: ISyncResponder;
|
||||
|
||||
/** an object which intercepts `/keys/query` requests on the test homeserver */
|
||||
let e2eKeyResponder: E2EKeyResponder;
|
||||
|
||||
// Encryption key used to encrypt cross signing keys
|
||||
const encryptionKey = new Uint8Array(32);
|
||||
|
||||
/**
|
||||
* Create the {@link CryptoCallbacks}
|
||||
*/
|
||||
function createCryptoCallbacks(): CryptoCallbacks {
|
||||
return {
|
||||
getSecretStorageKey: (keys, name) => {
|
||||
return Promise.resolve<[string, Uint8Array<ArrayBuffer>]>(["key_id", encryptionKey]);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(
|
||||
async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
|
||||
const homeserverUrl = "https://alice-server.com";
|
||||
aliceClient = createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
userId: TEST_USER_ID,
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: TEST_DEVICE_ID,
|
||||
cryptoCallbacks: createCryptoCallbacks(),
|
||||
logger: new DebugLogger(debug(`matrix-js-sdk:cross-signing`)),
|
||||
});
|
||||
|
||||
syncResponder = new SyncResponder(homeserverUrl);
|
||||
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
/** an object which intercepts `/keys/upload` requests on the test homeserver */
|
||||
new E2EKeyReceiver(homeserverUrl);
|
||||
|
||||
// Silence warnings from the backup manager
|
||||
fetchMock.getOnce(new URL("/_matrix/client/v3/room_keys/version", homeserverUrl).toString(), {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND" },
|
||||
});
|
||||
|
||||
await aliceClient.initRustCrypto();
|
||||
},
|
||||
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
|
||||
10000,
|
||||
);
|
||||
|
||||
afterEach(async () => {
|
||||
aliceClient.stopClient();
|
||||
});
|
||||
|
||||
/**
|
||||
* Create cross-signing keys and publish the keys
|
||||
*
|
||||
* @param authDict - The parameters to as the `auth` dict in the key upload request.
|
||||
* @see https://spec.matrix.org/v1.6/client-server-api/#authentication-types
|
||||
*/
|
||||
async function bootstrapCrossSigning(authDict: AuthDict): Promise<void> {
|
||||
await aliceClient.getCrypto()?.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: (makeRequest) => makeRequest(authDict).then(() => undefined),
|
||||
});
|
||||
}
|
||||
|
||||
describe("bootstrapCrossSigning (before initialsync completes)", () => {
|
||||
it("publishes keys if none were yet published", async () => {
|
||||
mockSetupCrossSigningRequests();
|
||||
|
||||
// provide a UIA callback, so that the cross-signing keys are uploaded
|
||||
const authDict = { type: "test" };
|
||||
await bootstrapCrossSigning(authDict);
|
||||
|
||||
// check that the cross-signing keys have been uploaded
|
||||
expect(fetchMock.callHistory.called("upload-cross-signing-keys")).toBeTruthy();
|
||||
const keysOpts = fetchMock.callHistory.lastCall("upload-cross-signing-keys")!.options;
|
||||
const keysBody = JSON.parse(keysOpts!.body as string);
|
||||
expect(keysBody.auth).toEqual(authDict); // check uia dict was passed
|
||||
// there should be a key of each type
|
||||
// master key is signed by the device
|
||||
expect(keysBody).toHaveProperty(["master_key", "signatures", TEST_USER_ID, `ed25519:${TEST_DEVICE_ID}`]);
|
||||
const masterKeyId = Object.keys(keysBody.master_key.keys)[0];
|
||||
// ssk and usk are signed by the master key
|
||||
expect(keysBody).toHaveProperty(["self_signing_key", "signatures", TEST_USER_ID, masterKeyId]);
|
||||
expect(keysBody).toHaveProperty(["user_signing_key", "signatures", TEST_USER_ID, masterKeyId]);
|
||||
const sskId = Object.keys(keysBody.self_signing_key.keys)[0];
|
||||
|
||||
// check the publish call
|
||||
expect(fetchMock.callHistory.called("upload-sigs")).toBeTruthy();
|
||||
const sigsOpts = fetchMock.callHistory.lastCall("upload-sigs")!.options;
|
||||
const body = JSON.parse(sigsOpts!.body as string);
|
||||
// there should be a signature for our device, by our self-signing key.
|
||||
expect(body).toHaveProperty([TEST_USER_ID, TEST_DEVICE_ID, "signatures", TEST_USER_ID, sskId]);
|
||||
});
|
||||
|
||||
it("get cross signing keys from secret storage and import them", async () => {
|
||||
// Return public cross signing keys
|
||||
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||
|
||||
mockInitialApiRequests(aliceClient.getHomeserverUrl());
|
||||
|
||||
// Encrypt the private keys and return them in the /sync response as if they are in Secret Storage
|
||||
const masterKey = await encryptAESSecretStorageItem(
|
||||
MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
||||
encryptionKey,
|
||||
"m.cross_signing.master",
|
||||
);
|
||||
const selfSigningKey = await encryptAESSecretStorageItem(
|
||||
SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
||||
encryptionKey,
|
||||
"m.cross_signing.self_signing",
|
||||
);
|
||||
const userSigningKey = await encryptAESSecretStorageItem(
|
||||
USER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
||||
encryptionKey,
|
||||
"m.cross_signing.user_signing",
|
||||
);
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: 1,
|
||||
account_data: {
|
||||
events: [
|
||||
{
|
||||
type: "m.cross_signing.master",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: masterKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "m.cross_signing.self_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: selfSigningKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "m.cross_signing.user_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: userSigningKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "m.secret_storage.key.key_id",
|
||||
content: {
|
||||
key: "key_id",
|
||||
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await aliceClient.startClient();
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// we expect the UserTrustStatusChanged event to be fired after the cross signing keys import
|
||||
const userTrustStatusChangedPromise = new Promise<string>((resolve) =>
|
||||
aliceClient.on(CryptoEvent.UserTrustStatusChanged, resolve),
|
||||
);
|
||||
|
||||
const authDict = { type: "test" };
|
||||
await bootstrapCrossSigning(authDict);
|
||||
|
||||
// Check if the UserTrustStatusChanged event was fired
|
||||
expect(await userTrustStatusChangedPromise).toBe(aliceClient.getUserId());
|
||||
|
||||
// Expect the signature to be uploaded
|
||||
expect(fetchMock.callHistory.called("upload-sigs")).toBeTruthy();
|
||||
const sigsOpts = fetchMock.callHistory.lastCall("upload-sigs")!.options;
|
||||
const body = JSON.parse(sigsOpts!.body as string);
|
||||
// the device should have a signature with the public self cross signing keys.
|
||||
expect(body).toHaveProperty([
|
||||
TEST_USER_ID,
|
||||
TEST_DEVICE_ID,
|
||||
"signatures",
|
||||
TEST_USER_ID,
|
||||
`ed25519:${SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64}`,
|
||||
]);
|
||||
});
|
||||
|
||||
it("can bootstrapCrossSigning twice", async () => {
|
||||
mockSetupCrossSigningRequests();
|
||||
|
||||
const authDict = { type: "test" };
|
||||
await bootstrapCrossSigning(authDict);
|
||||
|
||||
// a second call should do nothing except GET requests
|
||||
fetchMock.mockClear();
|
||||
await bootstrapCrossSigning(authDict);
|
||||
expect(fetchMock).toHaveFetchedTimes(0, "unmatched");
|
||||
});
|
||||
|
||||
it("will upload existing cross-signing keys to an established secret storage", async () => {
|
||||
// This rather obscure codepath covers the case that:
|
||||
// - 4S is set up and working
|
||||
// - our device has private cross-signing keys, but has not published them to 4S
|
||||
//
|
||||
// To arrange that, we call `bootstrapCrossSigning` on our main device, and then (pretend to) set up 4S from
|
||||
// a *different* device. Then, when we call `bootstrapCrossSigning` again, it should do the honours.
|
||||
|
||||
const accountDataAccumulator = new AccountDataAccumulator(syncResponder);
|
||||
accountDataAccumulator.interceptGetAccountData();
|
||||
|
||||
const authDict = { type: "test" };
|
||||
await bootstrapCrossSigning(authDict);
|
||||
|
||||
// Pretend that another device has uploaded a 4S key
|
||||
accountDataAccumulator.accountDataEvents.set("m.secret_storage.default_key", { key: "key_id" });
|
||||
accountDataAccumulator.accountDataEvents.set("m.secret_storage.key.key_id", {
|
||||
key: "keykeykey",
|
||||
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
});
|
||||
|
||||
// Prepare for the cross-signing keys
|
||||
const p = accountDataAccumulator.waitForAccountData("m.cross_signing.master");
|
||||
|
||||
await bootstrapCrossSigning(authDict);
|
||||
await p;
|
||||
|
||||
// The cross-signing keys should have been uploaded
|
||||
expect(accountDataAccumulator.accountDataEvents.has("m.cross_signing.master")).toBeTruthy();
|
||||
expect(accountDataAccumulator.accountDataEvents.has("m.cross_signing.self_signing")).toBeTruthy();
|
||||
expect(accountDataAccumulator.accountDataEvents.has("m.cross_signing.user_signing")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCrossSigningStatus()", () => {
|
||||
it("should return correct values without bootstrapping cross-signing", async () => {
|
||||
mockSetupCrossSigningRequests();
|
||||
|
||||
const crossSigningStatus = await aliceClient.getCrypto()!.getCrossSigningStatus();
|
||||
|
||||
// Expect the cross signing keys to be unavailable
|
||||
expect(crossSigningStatus).toStrictEqual({
|
||||
publicKeysOnDevice: false,
|
||||
privateKeysInSecretStorage: false,
|
||||
privateKeysCachedLocally: { masterKey: false, userSigningKey: false, selfSigningKey: false },
|
||||
});
|
||||
});
|
||||
|
||||
it("should return correct values after bootstrapping cross-signing", async () => {
|
||||
mockSetupCrossSigningRequests();
|
||||
|
||||
// provide a UIA callback, so that the cross-signing keys are uploaded
|
||||
const authDict = { type: "test" };
|
||||
await bootstrapCrossSigning(authDict);
|
||||
|
||||
const crossSigningStatus = await aliceClient.getCrypto()!.getCrossSigningStatus();
|
||||
|
||||
// Expect the cross signing keys to be available
|
||||
expect(crossSigningStatus).toStrictEqual({
|
||||
publicKeysOnDevice: true,
|
||||
privateKeysInSecretStorage: false,
|
||||
privateKeysCachedLocally: { masterKey: true, userSigningKey: true, selfSigningKey: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isCrossSigningReady()", () => {
|
||||
it("should return false if cross-signing is not bootstrapped", async () => {
|
||||
mockSetupCrossSigningRequests();
|
||||
|
||||
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
|
||||
|
||||
expect(isCrossSigningReady).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return true after bootstrapping cross-signing", async () => {
|
||||
mockSetupCrossSigningRequests();
|
||||
await bootstrapCrossSigning({ type: "test" });
|
||||
|
||||
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
|
||||
|
||||
expect(isCrossSigningReady).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return false if identity is not trusted, even if the secrets are in 4S", async () => {
|
||||
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||
|
||||
// Complete initial sync, to get the 4S account_data events stored
|
||||
mockInitialApiRequests(aliceClient.getHomeserverUrl());
|
||||
|
||||
// For this test we need to have a well-formed 4S setup.
|
||||
const mockSecretInfo = {
|
||||
encrypted: {
|
||||
// Don't care about the actual values here, just need to be present for validation
|
||||
KeyId: {
|
||||
iv: "IVIVIVIVIVIVIV",
|
||||
ciphertext: "CIPHERTEXTB64",
|
||||
mac: "MACMACMAC",
|
||||
},
|
||||
},
|
||||
};
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: 1,
|
||||
account_data: {
|
||||
events: [
|
||||
{
|
||||
type: "m.secret_storage.key.KeyId",
|
||||
content: {
|
||||
algorithm: "m.secret_storage.v1.aes-hmac-sha2",
|
||||
// iv and mac not relevant for this test
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "m.secret_storage.default_key",
|
||||
content: {
|
||||
key: "KeyId",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "m.cross_signing.master",
|
||||
content: mockSecretInfo,
|
||||
},
|
||||
{
|
||||
type: "m.cross_signing.user_signing",
|
||||
content: mockSecretInfo,
|
||||
},
|
||||
{
|
||||
type: "m.cross_signing.self_signing",
|
||||
content: mockSecretInfo,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await aliceClient.startClient();
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// Sanity: ensure that the secrets are in 4S
|
||||
const status = await aliceClient.getCrypto()!.getCrossSigningStatus();
|
||||
expect(status.privateKeysInSecretStorage).toBeTruthy();
|
||||
|
||||
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
|
||||
|
||||
expect(isCrossSigningReady).toBeFalsy();
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
describe("getCrossSigningKeyId", () => {
|
||||
/**
|
||||
* Intercept /keys/device_signing/upload request and return the cross signing keys
|
||||
* https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3keysdevice_signingupload
|
||||
*
|
||||
* @returns the cross signing keys
|
||||
*/
|
||||
function awaitCrossSigningKeysUpload() {
|
||||
return new Promise<any>((resolve) => {
|
||||
fetchMock.modifyRoute("upload-cross-signing-keys", {
|
||||
response: (callLog) => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
resolve(content);
|
||||
return {};
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it("should return the cross signing key id for each cross signing key", async () => {
|
||||
mockSetupCrossSigningRequests();
|
||||
|
||||
// Intercept cross signing keys upload
|
||||
const crossSigningKeysPromise = awaitCrossSigningKeysUpload();
|
||||
|
||||
// provide a UIA callback, so that the cross-signing keys are uploaded
|
||||
const authDict = { type: "test" };
|
||||
await bootstrapCrossSigning(authDict);
|
||||
// Get the cross signing keys
|
||||
const crossSigningKeys = await crossSigningKeysPromise;
|
||||
|
||||
const getPubKey = (crossSigningKey: any) => Object.values(crossSigningKey!.keys)[0];
|
||||
|
||||
const masterKeyId = await aliceClient.getCrypto()!.getCrossSigningKeyId();
|
||||
expect(masterKeyId).toBe(getPubKey(crossSigningKeys.master_key));
|
||||
|
||||
const selfSigningKeyId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.SelfSigning);
|
||||
expect(selfSigningKeyId).toBe(getPubKey(crossSigningKeys.self_signing_key));
|
||||
|
||||
const userSigningKeyId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.UserSigning);
|
||||
expect(userSigningKeyId).toBe(getPubKey(crossSigningKeys.user_signing_key));
|
||||
});
|
||||
});
|
||||
|
||||
describe("crossSignDevice", () => {
|
||||
beforeEach(async () => {
|
||||
// make sure that there is another device which we can sign
|
||||
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
|
||||
|
||||
// Complete initialsync, to get the outgoing requests going
|
||||
mockInitialApiRequests(aliceClient.getHomeserverUrl());
|
||||
syncResponder.sendOrQueueSyncResponse({ next_batch: 1 });
|
||||
await aliceClient.startClient();
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([aliceClient.getSafeUserId()]);
|
||||
expect(devices.get(aliceClient.getSafeUserId())!.has(testData.TEST_DEVICE_ID)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("fails for an unknown device", async () => {
|
||||
await expect(aliceClient.getCrypto()!.crossSignDevice("unknown")).rejects.toThrow("Unknown device");
|
||||
});
|
||||
|
||||
it("cross-signs the device", async () => {
|
||||
mockSetupCrossSigningRequests();
|
||||
await aliceClient.getCrypto()!.bootstrapCrossSigning({});
|
||||
|
||||
fetchMock.mockClear();
|
||||
await aliceClient.getCrypto()!.crossSignDevice(testData.TEST_DEVICE_ID);
|
||||
|
||||
// check that a sig for the device was uploaded
|
||||
const calls = fetchMock.callHistory.calls("upload-sigs");
|
||||
expect(calls.length).toEqual(1);
|
||||
const body = JSON.parse(calls[0].options!.body as string);
|
||||
const deviceSig = body[aliceClient.getSafeUserId()][testData.TEST_DEVICE_ID];
|
||||
expect(deviceSig).toHaveProperty("signatures");
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,239 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import { type CallLog } from "fetch-mock";
|
||||
import debug from "debug";
|
||||
|
||||
import { ClientEvent, createClient, DebugLogger, type MatrixClient, MatrixEvent } from "../../../src";
|
||||
import { CryptoEvent } from "../../../src/crypto-api/index";
|
||||
import { type RustCrypto } from "../../../src/rust-crypto/rust-crypto";
|
||||
import { type AddSecretStorageKeyOpts } from "../../../src/secret-storage";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { emitPromise, EventCounter } from "../../test-utils/test-utils";
|
||||
|
||||
describe("Device dehydration", () => {
|
||||
it("should rehydrate and dehydrate a device", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
cryptoCallbacks: {
|
||||
getSecretStorageKey: async (keys: any, name: string) => {
|
||||
return [[...Object.keys(keys.keys)][0], new Uint8Array(32)];
|
||||
},
|
||||
},
|
||||
logger: new DebugLogger(debug(`matrix-js-sdk:dehydration`)),
|
||||
});
|
||||
|
||||
await initializeSecretStorage(matrixClient, "@alice:localhost", "http://test.server");
|
||||
|
||||
const creationEventCounter = new EventCounter(matrixClient, CryptoEvent.DehydratedDeviceCreated);
|
||||
const dehydrationKeyCachedEventCounter = new EventCounter(matrixClient, CryptoEvent.DehydrationKeyCached);
|
||||
const rehydrationStartedCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationStarted);
|
||||
const rehydrationCompletedCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationCompleted);
|
||||
const rehydrationProgressCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationProgress);
|
||||
|
||||
// count the number of times the dehydration key gets set
|
||||
let setDehydrationCount = 0;
|
||||
matrixClient.on(ClientEvent.AccountData, (event: MatrixEvent) => {
|
||||
if (event.getType() === "org.matrix.msc3814") {
|
||||
setDehydrationCount++;
|
||||
}
|
||||
});
|
||||
|
||||
const crypto = matrixClient.getCrypto()!;
|
||||
|
||||
// start dehydration -- we start with no dehydrated device, and we
|
||||
// store the dehydrated device that we create
|
||||
fetchMock.get(
|
||||
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
|
||||
{
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "Not found",
|
||||
},
|
||||
},
|
||||
{ name: "get-dehydrated-device" },
|
||||
);
|
||||
let dehydratedDeviceBody: any;
|
||||
let dehydrationCount = 0;
|
||||
let resolveDehydrationPromise: () => void;
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
|
||||
(callLog) => {
|
||||
dehydratedDeviceBody = JSON.parse(callLog.options.body as string);
|
||||
dehydrationCount++;
|
||||
if (resolveDehydrationPromise) {
|
||||
resolveDehydrationPromise();
|
||||
}
|
||||
return {};
|
||||
},
|
||||
{ name: "put-dehydrated-device" },
|
||||
);
|
||||
await crypto.startDehydration();
|
||||
|
||||
expect(dehydrationCount).toEqual(1);
|
||||
expect(creationEventCounter.counter).toEqual(1);
|
||||
expect(dehydrationKeyCachedEventCounter.counter).toEqual(1);
|
||||
|
||||
// a week later, we should have created another dehydrated device
|
||||
const dehydrationPromise = new Promise<void>((resolve, reject) => {
|
||||
resolveDehydrationPromise = resolve;
|
||||
});
|
||||
vi.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
||||
await dehydrationPromise;
|
||||
|
||||
expect(dehydrationKeyCachedEventCounter.counter).toEqual(1);
|
||||
expect(dehydrationCount).toEqual(2);
|
||||
expect(creationEventCounter.counter).toEqual(2);
|
||||
|
||||
// restart dehydration -- rehydrate the device that we created above,
|
||||
// and create a new dehydrated device. We also set `createNewKey`, so
|
||||
// a new dehydration key will be set
|
||||
fetchMock.modifyRoute("get-dehydrated-device", {
|
||||
response: {
|
||||
device_id: dehydratedDeviceBody.device_id,
|
||||
device_data: dehydratedDeviceBody.device_data,
|
||||
},
|
||||
});
|
||||
const eventsResponse = vi.fn((callLog: CallLog) => {
|
||||
// rehydrating should make two calls to the /events endpoint.
|
||||
// The first time will return a single event, and the second
|
||||
// time will return no events (which will signal to the
|
||||
// rehydration function that it can stop)
|
||||
const body = JSON.parse(callLog.options.body as string);
|
||||
const nextBatch = body.next_batch ?? "0";
|
||||
const events = nextBatch === "0" ? [{ sender: "@alice:localhost", type: "m.dummy", content: {} }] : [];
|
||||
return {
|
||||
events,
|
||||
next_batch: nextBatch + "1",
|
||||
};
|
||||
});
|
||||
fetchMock.post(
|
||||
`path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device/${encodeURIComponent(dehydratedDeviceBody.device_id)}/events`,
|
||||
eventsResponse,
|
||||
);
|
||||
await crypto.startDehydration(true);
|
||||
expect(dehydrationCount).toEqual(3);
|
||||
|
||||
expect(setDehydrationCount).toEqual(2);
|
||||
expect(eventsResponse.mock.calls).toHaveLength(2);
|
||||
|
||||
expect(rehydrationStartedCounter.counter).toEqual(1);
|
||||
expect(rehydrationCompletedCounter.counter).toEqual(1);
|
||||
expect(creationEventCounter.counter).toEqual(3);
|
||||
expect(rehydrationProgressCounter.counter).toEqual(1);
|
||||
expect(dehydrationKeyCachedEventCounter.counter).toEqual(2);
|
||||
|
||||
// test that if we get an error when we try to rotate, it emits an event
|
||||
fetchMock.modifyRoute("put-dehydrated-device", {
|
||||
response: {
|
||||
status: 500,
|
||||
body: {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Unknown error",
|
||||
},
|
||||
},
|
||||
});
|
||||
const rotationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.DehydratedDeviceRotationError);
|
||||
vi.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
||||
await rotationErrorEventPromise;
|
||||
|
||||
// Restart dehydration, but return an error for GET /dehydrated_device so that rehydration fails.
|
||||
fetchMock.modifyRoute("get-dehydrated-device", {
|
||||
response: {
|
||||
status: 500,
|
||||
body: {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Unknown error",
|
||||
},
|
||||
},
|
||||
});
|
||||
fetchMock.modifyRoute("put-dehydrated-device", { response: { body: {} } });
|
||||
const rehydrationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.RehydrationError);
|
||||
await crypto.startDehydration(true);
|
||||
await rehydrationErrorEventPromise;
|
||||
|
||||
matrixClient.stopClient();
|
||||
});
|
||||
});
|
||||
|
||||
/** create a new secret storage and cross-signing keys */
|
||||
async function initializeSecretStorage(
|
||||
matrixClient: MatrixClient,
|
||||
userId: string,
|
||||
homeserverUrl: string,
|
||||
): Promise<void> {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "Not found",
|
||||
},
|
||||
});
|
||||
const e2eKeyReceiver = new E2EKeyReceiver(homeserverUrl);
|
||||
const e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
e2eKeyResponder.addKeyReceiver(userId, e2eKeyReceiver);
|
||||
const accountData: Map<string, object> = new Map();
|
||||
fetchMock.get("glob:http://*/_matrix/client/v3/user/*/account_data/*", (callLog) => {
|
||||
const name = callLog.url.split("/").pop()!;
|
||||
const value = accountData.get(name);
|
||||
if (value) {
|
||||
return value;
|
||||
} else {
|
||||
return {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "Not found",
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
fetchMock.put("glob:http://*/_matrix/client/v3/user/*/account_data/*", (callLog) => {
|
||||
const name = callLog.url.split("/").pop()!;
|
||||
const value = JSON.parse(callLog.options.body as string);
|
||||
accountData.set(name, value);
|
||||
matrixClient.emit(ClientEvent.AccountData, new MatrixEvent({ type: name, content: value }));
|
||||
return {};
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
const crypto = matrixClient.getCrypto()! as RustCrypto;
|
||||
// we need to process a sync so that the OlmMachine will upload keys
|
||||
await crypto.preprocessToDeviceMessages([]);
|
||||
await crypto.onSyncCompleted({});
|
||||
|
||||
// create initial secret storage
|
||||
async function createSecretStorageKey() {
|
||||
return {
|
||||
keyInfo: {} as AddSecretStorageKeyOpts,
|
||||
privateKey: new Uint8Array(32),
|
||||
};
|
||||
}
|
||||
await matrixClient.getCrypto()!.bootstrapCrossSigning({ setupNewCrossSigning: true });
|
||||
await matrixClient.getCrypto()!.bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
setupNewSecretStorage: true,
|
||||
setupNewKeyBackup: false,
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,547 +0,0 @@
|
||||
/*
|
||||
Copyright 2016-2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Olm from "@matrix-org/olm";
|
||||
import anotherjson from "another-json";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import { type RouteResponse } from "fetch-mock";
|
||||
|
||||
import {
|
||||
type IContent,
|
||||
type IDeviceKeys,
|
||||
type IDownloadKeyResult,
|
||||
type IEvent,
|
||||
type Keys,
|
||||
type MatrixClient,
|
||||
type SigningKeys,
|
||||
} from "../../../src";
|
||||
import { type IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { type ISyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { syncPromise } from "../../test-utils/test-utils";
|
||||
import { type KeyBackupInfo } from "../../../src/crypto-api";
|
||||
import { logger } from "../../../src/logger";
|
||||
|
||||
/**
|
||||
* @module
|
||||
*
|
||||
* A set of utilities for creating Olm accounts and sessions, and encrypting/decrypting with Olm/Megolm.
|
||||
*/
|
||||
|
||||
/** Create an Olm Account object */
|
||||
export async function createOlmAccount(): Promise<Olm.Account> {
|
||||
await Olm.init();
|
||||
const testOlmAccount = new Olm.Account();
|
||||
testOlmAccount.create();
|
||||
return testOlmAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the device keys for the test Olm Account
|
||||
*
|
||||
* @param olmAccount - Test olm account
|
||||
* @param userId - The user ID to present the keys as belonging to
|
||||
*/
|
||||
export function getTestOlmAccountKeys(olmAccount: Olm.Account, userId: string, deviceId: string): IDeviceKeys {
|
||||
const testE2eKeys = JSON.parse(olmAccount.identity_keys());
|
||||
const testDeviceKeys: IDeviceKeys = {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: deviceId,
|
||||
keys: {
|
||||
[`curve25519:${deviceId}`]: testE2eKeys.curve25519,
|
||||
[`ed25519:${deviceId}`]: testE2eKeys.ed25519,
|
||||
},
|
||||
user_id: userId,
|
||||
};
|
||||
|
||||
const j = anotherjson.stringify(testDeviceKeys);
|
||||
const sig = olmAccount.sign(j);
|
||||
testDeviceKeys.signatures = { [userId]: { [`ed25519:${deviceId}`]: sig } };
|
||||
return testDeviceKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap cross signing for the given Olm account.
|
||||
*
|
||||
* Will generate the cross signing keys and sign them with the master key, and returns the `IDownloadKeyResult`
|
||||
* that can be directly fed into a test e2eKeyResponder.
|
||||
*
|
||||
* The cross-signing keys are randomly generated, similar to how the olm account keys are generated. There may not
|
||||
* be any value in using static vectors, as the device keys change at every test run.
|
||||
*
|
||||
* If some `KeyBackupInfo` are provided, the `auth_data` of each backup info will be signed with the
|
||||
* master key, meaning the backups will be then trusted after verification.
|
||||
*
|
||||
* @param olmAccount - The Olm account object to use for signing the device keys.
|
||||
* @param userId - The user ID to associate with the device keys.
|
||||
* @param deviceId - The device ID to associate with the device keys.
|
||||
* @param keyBackupInfo - Optional key backup infos to sign with the master key.
|
||||
* @returns A valid keys/query response that can be fed into a test e2eKeyResponder.
|
||||
*/
|
||||
export function bootstrapCrossSigningTestOlmAccount(
|
||||
olmAccount: Olm.Account,
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
keyBackupInfo: KeyBackupInfo[] = [],
|
||||
): Partial<IDownloadKeyResult> {
|
||||
const olmAliceMSK = new Olm.PkSigning();
|
||||
const masterPrivkey = olmAliceMSK.generate_seed();
|
||||
const masterPubkey = olmAliceMSK.init_with_seed(masterPrivkey);
|
||||
|
||||
const olmAliceUSK = new Olm.PkSigning();
|
||||
const userPrivkey = olmAliceUSK.generate_seed();
|
||||
const userPubkey = olmAliceUSK.init_with_seed(userPrivkey);
|
||||
|
||||
const olmAliceSSK = new Olm.PkSigning();
|
||||
const sskPrivkey = olmAliceSSK.generate_seed();
|
||||
const sskPubkey = olmAliceSSK.init_with_seed(sskPrivkey);
|
||||
|
||||
const mskInfo: Keys = {
|
||||
user_id: userId,
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
["ed25519:" + masterPubkey]: masterPubkey,
|
||||
},
|
||||
};
|
||||
|
||||
const sskInfo: Partial<SigningKeys> = {
|
||||
user_id: userId,
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
["ed25519:" + sskPubkey]: sskPubkey,
|
||||
},
|
||||
};
|
||||
// sign the ssk with the msk
|
||||
const sskSig = olmAliceMSK.sign(anotherjson.stringify(sskInfo));
|
||||
sskInfo.signatures = {
|
||||
[userId]: {
|
||||
["ed25519:" + masterPubkey]: sskSig,
|
||||
},
|
||||
};
|
||||
|
||||
const uskInfo: Partial<SigningKeys> = {
|
||||
user_id: userId,
|
||||
usage: ["user_signing"],
|
||||
keys: {
|
||||
["ed25519:" + userPubkey]: userPubkey,
|
||||
},
|
||||
};
|
||||
|
||||
// sign the usk with the msk
|
||||
const uskSig = olmAliceMSK.sign(anotherjson.stringify(uskInfo));
|
||||
uskInfo.signatures = {
|
||||
[userId]: {
|
||||
["ed25519:" + masterPubkey]: uskSig,
|
||||
},
|
||||
};
|
||||
|
||||
// get the device keys and sign them with the ssk (the device is then cross signed)
|
||||
const deviceKeys = getTestOlmAccountKeys(olmAccount, userId, deviceId);
|
||||
|
||||
const copy = Object.assign({}, deviceKeys);
|
||||
delete copy.signatures;
|
||||
const crossSignature = olmAliceSSK.sign(anotherjson.stringify(copy));
|
||||
|
||||
// add the signature
|
||||
deviceKeys.signatures![userId]["ed25519:" + sskPubkey] = crossSignature;
|
||||
|
||||
// if we have some key backup info, sign them with the msk
|
||||
keyBackupInfo.forEach((info) => {
|
||||
const unsignedAuthData = Object.assign({}, info.auth_data);
|
||||
delete unsignedAuthData.signatures;
|
||||
const backupSignature = olmAliceMSK.sign(anotherjson.stringify(unsignedAuthData));
|
||||
|
||||
info.auth_data.signatures = {
|
||||
[userId]: {
|
||||
["ed25519:" + masterPubkey]: backupSignature,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// clean the olm resources as we don't need them anymore
|
||||
olmAliceMSK.free();
|
||||
olmAliceSSK.free();
|
||||
olmAliceUSK.free();
|
||||
|
||||
return {
|
||||
master_keys: { [userId]: mskInfo },
|
||||
user_signing_keys: { [userId]: uskInfo as SigningKeys },
|
||||
self_signing_keys: { [userId]: sskInfo as SigningKeys },
|
||||
device_keys: { [userId]: { [deviceId]: deviceKeys } },
|
||||
};
|
||||
}
|
||||
|
||||
/** start an Olm session with a given recipient */
|
||||
export async function createOlmSession(
|
||||
olmAccount: Olm.Account,
|
||||
recipientTestClient: IE2EKeyReceiver,
|
||||
): Promise<Olm.Session> {
|
||||
const keys = await recipientTestClient.awaitOneTimeKeyUpload();
|
||||
const otkId = Object.keys(keys)[0];
|
||||
const otk = keys[otkId];
|
||||
|
||||
const session = new Olm.Session();
|
||||
session.create_outbound(olmAccount, recipientTestClient.getDeviceKey(), otk.key);
|
||||
return session;
|
||||
}
|
||||
|
||||
// IToDeviceEvent isn't exported by src/sync-accumulator.ts
|
||||
export interface ToDeviceEvent {
|
||||
content: IContent;
|
||||
sender: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
/** encrypt an event with an existing olm session */
|
||||
export function encryptOlmEvent(opts: {
|
||||
/** the sender's user id */
|
||||
sender?: string;
|
||||
/** the sender's curve25519 key */
|
||||
senderKey: string;
|
||||
/** the sender's ed25519 key */
|
||||
senderSigningKey: string;
|
||||
/** the olm session to use for encryption */
|
||||
p2pSession: Olm.Session;
|
||||
/** the recipient's user id */
|
||||
recipient: string;
|
||||
/** the recipient's curve25519 key */
|
||||
recipientCurve25519Key: string;
|
||||
/** the recipient's ed25519 key */
|
||||
recipientEd25519Key: string;
|
||||
/** the payload of the message */
|
||||
plaincontent?: object;
|
||||
/** the event type of the payload */
|
||||
plaintype?: string;
|
||||
}): ToDeviceEvent {
|
||||
expect(opts.senderKey).toBeTruthy();
|
||||
expect(opts.p2pSession).toBeTruthy();
|
||||
expect(opts.recipient).toBeTruthy();
|
||||
|
||||
const plaintext = {
|
||||
content: opts.plaincontent || {},
|
||||
recipient: opts.recipient,
|
||||
recipient_keys: {
|
||||
ed25519: opts.recipientEd25519Key,
|
||||
},
|
||||
keys: {
|
||||
ed25519: opts.senderSigningKey,
|
||||
},
|
||||
sender: opts.sender || "@bob:xyz",
|
||||
type: opts.plaintype || "m.test",
|
||||
};
|
||||
|
||||
return {
|
||||
content: {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
ciphertext: {
|
||||
[opts.recipientCurve25519Key]: opts.p2pSession.encrypt(JSON.stringify(plaintext)),
|
||||
},
|
||||
sender_key: opts.senderKey,
|
||||
},
|
||||
sender: opts.sender || "@bob:xyz",
|
||||
type: "m.room.encrypted",
|
||||
};
|
||||
}
|
||||
|
||||
// encrypt an event with megolm
|
||||
export function encryptMegolmEvent(opts: {
|
||||
senderKey: string;
|
||||
groupSession: Olm.OutboundGroupSession;
|
||||
plaintext?: Partial<IEvent>;
|
||||
room_id?: string;
|
||||
}): IEvent {
|
||||
expect(opts.senderKey).toBeTruthy();
|
||||
expect(opts.groupSession).toBeTruthy();
|
||||
|
||||
const plaintext = opts.plaintext || {};
|
||||
if (!plaintext.content) {
|
||||
plaintext.content = {
|
||||
body: "42",
|
||||
msgtype: "m.text",
|
||||
};
|
||||
}
|
||||
if (!plaintext.type) {
|
||||
plaintext.type = "m.room.message";
|
||||
}
|
||||
if (!plaintext.room_id) {
|
||||
expect(opts.room_id).toBeTruthy();
|
||||
plaintext.room_id = opts.room_id;
|
||||
}
|
||||
return encryptMegolmEventRawPlainText({
|
||||
senderKey: opts.senderKey,
|
||||
groupSession: opts.groupSession,
|
||||
plaintext,
|
||||
});
|
||||
}
|
||||
|
||||
export function encryptMegolmEventRawPlainText(opts: {
|
||||
senderKey: string;
|
||||
groupSession: Olm.OutboundGroupSession;
|
||||
plaintext: Partial<IEvent>;
|
||||
origin_server_ts?: number;
|
||||
}): IEvent {
|
||||
return {
|
||||
event_id: "$test_megolm_event_" + Math.random(),
|
||||
sender: opts.plaintext.sender ?? "@not_the_real_sender:example.com",
|
||||
origin_server_ts: opts.plaintext.origin_server_ts ?? 1672944778000,
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
ciphertext: opts.groupSession.encrypt(JSON.stringify(opts.plaintext)),
|
||||
device_id: "testDevice",
|
||||
sender_key: opts.senderKey,
|
||||
session_id: opts.groupSession.session_id(),
|
||||
},
|
||||
type: "m.room.encrypted",
|
||||
unsigned: {},
|
||||
state_key: opts.plaintext.hasOwnProperty("state_key")
|
||||
? `${opts.plaintext.type}:${opts.plaintext.state_key}`
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** build an encrypted room_key event to share a group session, using an existing olm session */
|
||||
export function encryptGroupSessionKey(opts: {
|
||||
/** recipient's user id */
|
||||
recipient: string;
|
||||
/** the recipient's curve25519 key */
|
||||
recipientCurve25519Key: string;
|
||||
/** the recipient's ed25519 key */
|
||||
recipientEd25519Key: string;
|
||||
/** sender's olm account */
|
||||
olmAccount: Olm.Account;
|
||||
/** sender's olm session with the recipient */
|
||||
p2pSession: Olm.Session;
|
||||
groupSession: Olm.OutboundGroupSession;
|
||||
room_id?: string;
|
||||
}): ToDeviceEvent {
|
||||
const senderKeys = JSON.parse(opts.olmAccount.identity_keys());
|
||||
return encryptOlmEvent({
|
||||
senderKey: senderKeys.curve25519,
|
||||
senderSigningKey: senderKeys.ed25519,
|
||||
recipient: opts.recipient,
|
||||
recipientCurve25519Key: opts.recipientCurve25519Key,
|
||||
recipientEd25519Key: opts.recipientEd25519Key,
|
||||
p2pSession: opts.p2pSession,
|
||||
plaincontent: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: opts.room_id,
|
||||
session_id: opts.groupSession.session_id(),
|
||||
session_key: opts.groupSession.session_key(),
|
||||
},
|
||||
plaintype: "m.room_key",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test utility to correctly encrypt a secret send event to a test device using the provided p2p session.
|
||||
*
|
||||
* @param opts - the options for the secret send event
|
||||
* @returns the to-device event, ready to be returned in a sync response for the test device.
|
||||
*/
|
||||
export function encryptSecretSend(opts: {
|
||||
/** the sender's user id */
|
||||
sender: string;
|
||||
/** recipient's user id */
|
||||
recipient: string;
|
||||
/** the recipient's curve25519 key */
|
||||
recipientCurve25519Key: string;
|
||||
/** the recipient's ed25519 key */
|
||||
recipientEd25519Key: string;
|
||||
/** sender's olm account */
|
||||
olmAccount: Olm.Account;
|
||||
/** sender's olm session with the recipient */
|
||||
p2pSession: Olm.Session;
|
||||
/** The requestId of the secret request that this secret send is replying. */
|
||||
requestId: string;
|
||||
/** The secret value */
|
||||
secret: string;
|
||||
}): ToDeviceEvent {
|
||||
const senderKeys = JSON.parse(opts.olmAccount.identity_keys());
|
||||
return encryptOlmEvent({
|
||||
sender: opts.sender,
|
||||
senderKey: senderKeys.curve25519,
|
||||
senderSigningKey: senderKeys.ed25519,
|
||||
recipient: opts.recipient,
|
||||
recipientCurve25519Key: opts.recipientCurve25519Key,
|
||||
recipientEd25519Key: opts.recipientEd25519Key,
|
||||
p2pSession: opts.p2pSession,
|
||||
plaincontent: {
|
||||
request_id: opts.requestId,
|
||||
secret: opts.secret,
|
||||
},
|
||||
plaintype: "m.secret.send",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish an Olm Session with the test user
|
||||
*
|
||||
* Waits for the test user to upload their keys, then sends a /sync response with a to-device message which will
|
||||
* establish an Olm session.
|
||||
*
|
||||
* @param testClient - the MatrixClient under test, which we expect to upload account keys, and to make a
|
||||
* /sync request which we will respond to.
|
||||
* @param keyReceiver - an IE2EKeyReceiver which will intercept the /keys/upload request from the client under test
|
||||
* @param syncResponder - an ISyncResponder which will intercept /sync requests from the client under test
|
||||
* @param peerOlmAccount: an OlmAccount which will be used to initiate the Olm session.
|
||||
*/
|
||||
export async function establishOlmSession(
|
||||
testClient: MatrixClient,
|
||||
keyReceiver: IE2EKeyReceiver,
|
||||
syncResponder: ISyncResponder,
|
||||
peerOlmAccount: Olm.Account,
|
||||
): Promise<Olm.Session> {
|
||||
const peerE2EKeys = JSON.parse(peerOlmAccount.identity_keys());
|
||||
const p2pSession = await createOlmSession(peerOlmAccount, keyReceiver);
|
||||
const olmEvent = encryptOlmEvent({
|
||||
senderKey: peerE2EKeys.curve25519,
|
||||
senderSigningKey: peerE2EKeys.ed25519,
|
||||
recipient: testClient.getUserId()!,
|
||||
recipientCurve25519Key: keyReceiver.getDeviceKey(),
|
||||
recipientEd25519Key: keyReceiver.getSigningKey(),
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: 1,
|
||||
to_device: { events: [olmEvent] },
|
||||
});
|
||||
await syncPromise(testClient);
|
||||
return p2pSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect that the client shares keys with the given recipient
|
||||
*
|
||||
* Waits for an HTTP request to send the encrypted m.room_key to-device message; decrypts it and uses it
|
||||
* to establish an Olm InboundGroupSession.
|
||||
*
|
||||
* @param recipientUserID - the user id of the expected recipient
|
||||
*
|
||||
* @param recipientOlmAccount - Olm.Account for the recipient
|
||||
*
|
||||
* @param recipientOlmSession - an Olm.Session for the recipient, which must already have exchanged pre-key
|
||||
* messages with the sender. Alternatively, null, in which case we will expect a pre-key message.
|
||||
*
|
||||
* @returns the established inbound group session
|
||||
*/
|
||||
export async function expectSendRoomKey(
|
||||
recipientUserID: string,
|
||||
recipientOlmAccount: Olm.Account,
|
||||
recipientOlmSession: Olm.Session | null = null,
|
||||
): Promise<Olm.InboundGroupSession> {
|
||||
const testRecipientKey = JSON.parse(recipientOlmAccount.identity_keys())["curve25519"];
|
||||
|
||||
function onSendRoomKey(content: any): Olm.InboundGroupSession {
|
||||
const m = content.messages[recipientUserID].DEVICE_ID;
|
||||
const ct = m.ciphertext[testRecipientKey];
|
||||
|
||||
if (!recipientOlmSession) {
|
||||
expect(ct.type).toEqual(0); // pre-key message
|
||||
recipientOlmSession = new Olm.Session();
|
||||
recipientOlmSession.create_inbound(recipientOlmAccount, ct.body);
|
||||
} else {
|
||||
expect(ct.type).toEqual(1); // regular message
|
||||
}
|
||||
|
||||
const decrypted = JSON.parse(recipientOlmSession.decrypt(ct.type, ct.body));
|
||||
expect(decrypted.type).toEqual("m.room_key");
|
||||
const inboundGroupSession = new Olm.InboundGroupSession();
|
||||
inboundGroupSession.create(decrypted.content.session_key);
|
||||
return inboundGroupSession;
|
||||
}
|
||||
return await new Promise<Olm.InboundGroupSession>((resolve) => {
|
||||
fetchMock.putOnce(new RegExp("/sendToDevice/m.room.encrypted/"), (callLog): RouteResponse => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
resolve(onSendRoomKey(content));
|
||||
return {};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the event received on rooms/{roomId}/send/m.room.encrypted endpoint.
|
||||
* See https://spec.matrix.org/latest/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid
|
||||
* @returns the content of the encrypted event
|
||||
*/
|
||||
export function expectEncryptedSendMessageEvent() {
|
||||
return new Promise<IContent>((resolve) => {
|
||||
fetchMock.putOnce(new RegExp("/send/m.room.encrypted/"), (callLog) => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
resolve(content);
|
||||
return { event_id: "$event_id" };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the event received on rooms/{roomId}/state/m.room.encrypted/{stateKey} endpoint.
|
||||
* See https://spec.matrix.org/latest/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey
|
||||
* @returns the content of the encrypted event
|
||||
*/
|
||||
function expectEncryptedSendStateEvent() {
|
||||
return new Promise<IContent>((resolve) => {
|
||||
fetchMock.putOnce(new RegExp("/state/m.room.encrypted/"), (callLog) => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
resolve(content);
|
||||
return { event_id: "$event_id" };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect that the client sends an encrypted message event
|
||||
*
|
||||
* Waits for an HTTP request to send an encrypted message in the test room.
|
||||
*
|
||||
* @param inboundGroupSessionPromise - a promise for an Olm InboundGroupSession, which will
|
||||
* be used to decrypt the event. We will wait for this to resolve once the HTTP request has been processed.
|
||||
*
|
||||
* @returns The content of the successfully-decrypted event
|
||||
*/
|
||||
export async function expectSendMegolmMessageEvent(
|
||||
inboundGroupSessionPromise: Promise<Olm.InboundGroupSession>,
|
||||
): Promise<Partial<IEvent>> {
|
||||
const encryptedMessageContent = await expectEncryptedSendMessageEvent();
|
||||
|
||||
// In some of the tests, the room key is sent *after* the actual event, so we may need to wait for it now.
|
||||
const inboundGroupSession = await inboundGroupSessionPromise;
|
||||
|
||||
const r: any = inboundGroupSession.decrypt(encryptedMessageContent!.ciphertext);
|
||||
logger.log("Decrypted received megolm message", r);
|
||||
return JSON.parse(r.plaintext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect that the client sends an encrypted state event
|
||||
*
|
||||
* Waits for an HTTP request to send an encrypted state event in the test room.
|
||||
*
|
||||
* @param inboundGroupSessionPromise - a promise for an Olm InboundGroupSession, which will
|
||||
* be used to decrypt the event. We will wait for this to resolve once the HTTP request has been processed.
|
||||
*
|
||||
* @returns The content of the successfully-decrypted state event
|
||||
*/
|
||||
export async function expectSendMegolmStateEvent(
|
||||
inboundGroupSessionPromise: Promise<Olm.InboundGroupSession>,
|
||||
): Promise<Partial<IEvent>> {
|
||||
const encryptedStateContent = await expectEncryptedSendStateEvent();
|
||||
|
||||
// In some of the tests, the room key is sent *after* the actual event, so we may need to wait for it now.
|
||||
const inboundGroupSession = await inboundGroupSessionPromise;
|
||||
|
||||
const r: any = inboundGroupSession.decrypt(encryptedStateContent!.ciphertext);
|
||||
logger.log("Decrypted received megolm state event", r);
|
||||
return JSON.parse(r.plaintext);
|
||||
}
|
||||
@@ -1,511 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import { createClient, IndexedDBCryptoStore } from "../../../src";
|
||||
import { populateStore } from "../../test-utils/test_indexeddb_cryptostore_dump";
|
||||
import { MSK_NOT_CACHED_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/no_cached_msk_dump";
|
||||
import { IDENTITY_NOT_TRUSTED_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/unverified";
|
||||
import { FULL_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/full_account";
|
||||
import { EMPTY_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/empty_account";
|
||||
import { CryptoEvent } from "../../../src/crypto-api";
|
||||
|
||||
vi.setConfig({ testTimeout: 15000 });
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
describe("MatrixClient.initRustCrypto", () => {
|
||||
it("should raise if userId or deviceId is unknown", async () => {
|
||||
const unknownUserClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
await expect(() => unknownUserClient.initRustCrypto()).rejects.toThrow("unknown userId");
|
||||
|
||||
const unknownDeviceClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:test",
|
||||
});
|
||||
await expect(() => unknownDeviceClient.initRustCrypto()).rejects.toThrow("unknown deviceId");
|
||||
});
|
||||
|
||||
it("should create the indexed db", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
|
||||
// No databases.
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
// should have an indexed db now
|
||||
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
|
||||
expect(databaseNames).toEqual(expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto"]));
|
||||
});
|
||||
|
||||
it("should create the indexed db with a custom prefix", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
|
||||
// No databases.
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
|
||||
await matrixClient.initRustCrypto({ cryptoDatabasePrefix: "my-prefix" });
|
||||
|
||||
// should have an indexed db now
|
||||
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
|
||||
expect(databaseNames).toEqual(expect.arrayContaining(["my-prefix::matrix-sdk-crypto"]));
|
||||
});
|
||||
|
||||
it("should create the meta db if given a storageKey", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
|
||||
// No databases.
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
|
||||
await matrixClient.initRustCrypto({ storageKey: new Uint8Array(32) });
|
||||
|
||||
// should have two indexed dbs now
|
||||
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
|
||||
expect(databaseNames).toEqual(
|
||||
expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto", "matrix-js-sdk::matrix-sdk-crypto-meta"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should create the meta db if given a storagePassword", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
|
||||
// No databases.
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
|
||||
await matrixClient.initRustCrypto({ storagePassword: "the cow is on the moon" });
|
||||
|
||||
// should have two indexed dbs now
|
||||
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
|
||||
expect(databaseNames).toEqual(
|
||||
expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto", "matrix-js-sdk::matrix-sdk-crypto-meta"]),
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should ignore a second call", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
await matrixClient.initRustCrypto();
|
||||
});
|
||||
|
||||
describe("Libolm Migration", () => {
|
||||
it("should migrate from libolm", async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", FULL_ACCOUNT_DATASET.backupResponse);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", FULL_ACCOUNT_DATASET.keyQueryResponse);
|
||||
|
||||
const testStoreName = "test-store";
|
||||
await populateStore(testStoreName, FULL_ACCOUNT_DATASET.dumpPath);
|
||||
const cryptoStore = new IndexedDBCryptoStore(indexedDB, testStoreName);
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: FULL_ACCOUNT_DATASET.userId,
|
||||
deviceId: FULL_ACCOUNT_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: FULL_ACCOUNT_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
const progressListener = vi.fn();
|
||||
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const verificationStatus = await matrixClient
|
||||
.getCrypto()!
|
||||
.getDeviceVerificationStatus(FULL_ACCOUNT_DATASET.userId, FULL_ACCOUNT_DATASET.deviceId);
|
||||
|
||||
// Check that the current device and identity trust is migrated correctly just after migration
|
||||
expect(verificationStatus).toBeDefined();
|
||||
expect(verificationStatus!.crossSigningVerified).toEqual(true);
|
||||
expect(verificationStatus!.signedByOwner).toEqual(true);
|
||||
|
||||
// Do some basic checks on the imported data
|
||||
const deviceKeys = await matrixClient.getCrypto()!.getOwnDeviceKeys();
|
||||
expect(deviceKeys.curve25519).toEqual("LKv0bKbc0EC4h0jknbemv3QalEkeYvuNeUXVRgVVTTU");
|
||||
expect(deviceKeys.ed25519).toEqual("qK70DEqIXq7T+UU3v/al47Ab4JkMEBLpNrTBMbS5rrw");
|
||||
|
||||
expect(await matrixClient.getCrypto()!.getActiveSessionBackupVersion()).toEqual("7");
|
||||
|
||||
expect(await matrixClient.getCrypto()!.isEncryptionEnabledInRoom("!CWLUCoEWXSFyTCOtfL:matrix.org")).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
// check the progress callback
|
||||
expect(progressListener.mock.calls.length).toBeGreaterThan(50);
|
||||
|
||||
// The first call should have progress == 0
|
||||
const [firstProgress, totalSteps] = progressListener.mock.calls[0];
|
||||
expect(totalSteps).toBeGreaterThan(3000);
|
||||
expect(firstProgress).toEqual(0);
|
||||
|
||||
for (let i = 1; i < progressListener.mock.calls.length - 1; i++) {
|
||||
const [progress, total] = progressListener.mock.calls[i];
|
||||
expect(total).toEqual(totalSteps);
|
||||
expect(progress).toBeGreaterThan(progressListener.mock.calls[i - 1][0]);
|
||||
expect(progress).toBeLessThanOrEqual(totalSteps);
|
||||
}
|
||||
|
||||
// The final call should have progress == total == -1
|
||||
expect(progressListener).toHaveBeenLastCalledWith(-1, -1);
|
||||
}, 60000);
|
||||
|
||||
describe("Private key backup migration", () => {
|
||||
it("should not migrate the backup private key if backup has changed", async () => {
|
||||
// Here we have a new backup server side, and the migrated account has the previous backup key.
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.newBackupResponse);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
|
||||
|
||||
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
|
||||
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: MSK_NOT_CACHED_DATASET.userId,
|
||||
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
|
||||
expect(privateBackupKey).toBeNull();
|
||||
});
|
||||
|
||||
it("should not migrate the backup private key if backup has unknown algorithm", async () => {
|
||||
// Here we have a new backup server side, and the migrated account has the previous backup key.
|
||||
const backupResponse = {
|
||||
...MSK_NOT_CACHED_DATASET.backupResponse,
|
||||
algorithm: "m.megolm_backup.v8",
|
||||
};
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupResponse);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
|
||||
|
||||
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
|
||||
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: MSK_NOT_CACHED_DATASET.userId,
|
||||
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
|
||||
expect(privateBackupKey).toBeNull();
|
||||
});
|
||||
|
||||
it("should not migrate the backup private key if the backup has been deleted", async () => {
|
||||
// The old backup has been deleted server side.
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No backup found",
|
||||
},
|
||||
});
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
|
||||
|
||||
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
|
||||
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: MSK_NOT_CACHED_DATASET.userId,
|
||||
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
|
||||
expect(privateBackupKey).toBeNull();
|
||||
});
|
||||
|
||||
it("should migrate the backup private key if the backup matches", async () => {
|
||||
// The old backup has been deleted server side.
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.backupResponse);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
|
||||
|
||||
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
|
||||
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: MSK_NOT_CACHED_DATASET.userId,
|
||||
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
|
||||
expect(privateBackupKey).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not migrate if account data is missing", async () => {
|
||||
// See https://github.com/element-hq/element-web/issues/27447
|
||||
|
||||
// Given we have an almost-empty legacy account in the database
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "No backup found" },
|
||||
});
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", EMPTY_ACCOUNT_DATASET.keyQueryResponse);
|
||||
|
||||
const testStoreName = "test-store";
|
||||
await populateStore(testStoreName, EMPTY_ACCOUNT_DATASET.dumpPath);
|
||||
const cryptoStore = new IndexedDBCryptoStore(indexedDB, testStoreName);
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: EMPTY_ACCOUNT_DATASET.userId,
|
||||
deviceId: EMPTY_ACCOUNT_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: EMPTY_ACCOUNT_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
// When we start Rust crypto, potentially triggering an upgrade
|
||||
const progressListener = vi.fn();
|
||||
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
// Then no error occurs, and no upgrade happens
|
||||
expect(progressListener.mock.calls.length).toBe(0);
|
||||
}, 60000);
|
||||
|
||||
describe("Legacy trust migration", () => {
|
||||
async function populateAndStartLegacyCryptoStore(dumpPath: string): Promise<IndexedDBCryptoStore> {
|
||||
const testStoreName = "test-store";
|
||||
await populateStore(testStoreName, dumpPath);
|
||||
const cryptoStore = new IndexedDBCryptoStore(indexedDB, testStoreName);
|
||||
await cryptoStore.startup();
|
||||
return cryptoStore;
|
||||
}
|
||||
|
||||
it("should not revert to untrusted if legacy was trusted but msk not in cache, big account", async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No backup found",
|
||||
},
|
||||
});
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", FULL_ACCOUNT_DATASET.keyQueryResponse);
|
||||
|
||||
const cryptoStore = await populateAndStartLegacyCryptoStore(FULL_ACCOUNT_DATASET.dumpPath);
|
||||
|
||||
// Remove the master key from the cache
|
||||
await cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
|
||||
const objectStore = txn.objectStore("account");
|
||||
objectStore.delete(`ssss_cache:master`);
|
||||
});
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: FULL_ACCOUNT_DATASET.userId,
|
||||
deviceId: FULL_ACCOUNT_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: FULL_ACCOUNT_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const verificationStatus = await matrixClient
|
||||
.getCrypto()!
|
||||
.getUserVerificationStatus(FULL_ACCOUNT_DATASET.userId);
|
||||
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(true);
|
||||
}, 60000);
|
||||
|
||||
it("should not revert to untrusted if legacy was trusted but msk not in cache", async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.backupResponse);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
|
||||
|
||||
const cryptoStore = await populateAndStartLegacyCryptoStore(MSK_NOT_CACHED_DATASET.dumpPath);
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: MSK_NOT_CACHED_DATASET.userId,
|
||||
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const verificationStatus = await matrixClient
|
||||
.getCrypto()!
|
||||
.getUserVerificationStatus("@migration:localhost");
|
||||
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(true);
|
||||
});
|
||||
|
||||
it("should not migrate local trust if key has changed", async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.backupResponse);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.rotatedKeyQueryResponse);
|
||||
|
||||
const cryptoStore = await populateAndStartLegacyCryptoStore(MSK_NOT_CACHED_DATASET.dumpPath);
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: MSK_NOT_CACHED_DATASET.userId,
|
||||
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const verificationStatus = await matrixClient
|
||||
.getCrypto()!
|
||||
.getUserVerificationStatus("@migration:localhost");
|
||||
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||
});
|
||||
|
||||
it("should not migrate local trust if was not trusted in legacy", async () => {
|
||||
// Just 404 here for the test
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No backup found",
|
||||
},
|
||||
});
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", IDENTITY_NOT_TRUSTED_DATASET.keyQueryResponse);
|
||||
|
||||
const cryptoStore = await populateAndStartLegacyCryptoStore(IDENTITY_NOT_TRUSTED_DATASET.dumpPath);
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: IDENTITY_NOT_TRUSTED_DATASET.userId,
|
||||
deviceId: IDENTITY_NOT_TRUSTED_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: IDENTITY_NOT_TRUSTED_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const verificationStatus = await matrixClient
|
||||
.getCrypto()!
|
||||
.getUserVerificationStatus("@untrusted:localhost");
|
||||
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("MatrixClient.clearStores", () => {
|
||||
it("should clear the indexeddbs", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto({ storagePassword: "testKey" });
|
||||
expect(await indexedDB.databases()).toHaveLength(2);
|
||||
await matrixClient.stopClient();
|
||||
|
||||
await matrixClient.clearStores();
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should not fail in environments without indexedDB", async () => {
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = undefined!;
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
|
||||
await matrixClient.stopClient();
|
||||
|
||||
await matrixClient.clearStores();
|
||||
// No error thrown in clearStores
|
||||
});
|
||||
|
||||
it("should clear the indexeddbs with a custom prefix", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
|
||||
// No databases.
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
|
||||
await matrixClient.initRustCrypto({ cryptoDatabasePrefix: "my-prefix" });
|
||||
expect(await indexedDB.databases()).toHaveLength(1);
|
||||
await matrixClient.stopClient();
|
||||
|
||||
await matrixClient.clearStores({ cryptoDatabasePrefix: "my-prefix" });
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -1,221 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import anotherjson from "another-json";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import "fake-indexeddb/auto";
|
||||
import Olm from "@matrix-org/olm";
|
||||
|
||||
import * as testUtils from "../../test-utils/test-utils";
|
||||
import { getSyncResponse, syncPromise } from "../../test-utils/test-utils";
|
||||
import { TEST_ROOM_ID as ROOM_ID } from "../../test-utils/test-data";
|
||||
import { logger } from "../../../src/logger";
|
||||
import {
|
||||
createClient,
|
||||
HistoryVisibility,
|
||||
PendingEventOrdering,
|
||||
type IStartClientOpts,
|
||||
type MatrixClient,
|
||||
} from "../../../src/matrix";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { type ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import {
|
||||
createOlmAccount,
|
||||
createOlmSession,
|
||||
encryptGroupSessionKey,
|
||||
encryptMegolmEvent,
|
||||
getTestOlmAccountKeys,
|
||||
expectSendRoomKey,
|
||||
expectSendMegolmStateEvent,
|
||||
} from "./olm-utils";
|
||||
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
|
||||
|
||||
describe("Encrypted State Events", () => {
|
||||
let testOlmAccount = {} as unknown as Olm.Account;
|
||||
let testSenderKey = "";
|
||||
|
||||
/** the MatrixClient under test */
|
||||
let aliceClient: MatrixClient;
|
||||
|
||||
/** an object which intercepts `/keys/upload` requests from {@link #aliceClient} to catch the uploaded keys */
|
||||
let keyReceiver: E2EKeyReceiver;
|
||||
|
||||
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
|
||||
let syncResponder: ISyncResponder;
|
||||
|
||||
async function startClientAndAwaitFirstSync(opts: IStartClientOpts = {}): Promise<void> {
|
||||
logger.log(aliceClient.getUserId() + ": starting");
|
||||
|
||||
mockInitialApiRequests(aliceClient.getHomeserverUrl());
|
||||
|
||||
// we let the client do a very basic initial sync, which it needs before
|
||||
// it will upload one-time keys.
|
||||
syncResponder.sendOrQueueSyncResponse({ next_batch: 1 });
|
||||
|
||||
aliceClient.startClient({
|
||||
// set this so that we can get hold of failed events
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
...opts,
|
||||
});
|
||||
|
||||
await syncPromise(aliceClient);
|
||||
logger.log(aliceClient.getUserId() + ": started");
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
fetchMock.catch(404);
|
||||
|
||||
const homeserverUrl = "https://alice-server.com";
|
||||
aliceClient = createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
userId: "@alice:localhost",
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: "xzcvb",
|
||||
logger: logger.getChild("aliceClient"),
|
||||
enableEncryptedStateEvents: true,
|
||||
});
|
||||
|
||||
keyReceiver = new E2EKeyReceiver(homeserverUrl);
|
||||
syncResponder = new SyncResponder(homeserverUrl);
|
||||
|
||||
await aliceClient.initRustCrypto();
|
||||
|
||||
// create a test olm device which we will use to communicate with alice. We use libolm to implement this.
|
||||
testOlmAccount = await createOlmAccount();
|
||||
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
|
||||
testSenderKey = testE2eKeys.curve25519;
|
||||
}, 10000);
|
||||
|
||||
afterEach(async () => {
|
||||
aliceClient.stopClient();
|
||||
});
|
||||
|
||||
function expectAliceKeyQuery(response: any) {
|
||||
fetchMock.postOnce(new RegExp("/keys/query"), (callLog) => response);
|
||||
}
|
||||
|
||||
function expectAliceKeyClaim(response: any) {
|
||||
fetchMock.postOnce(new RegExp("/keys/claim"), response);
|
||||
}
|
||||
|
||||
function getTestKeysClaimResponse(userId: string) {
|
||||
testOlmAccount.generate_one_time_keys(1);
|
||||
const testOneTimeKeys = JSON.parse(testOlmAccount.one_time_keys());
|
||||
testOlmAccount.mark_keys_as_published();
|
||||
|
||||
const keyId = Object.keys(testOneTimeKeys.curve25519)[0];
|
||||
const oneTimeKey: string = testOneTimeKeys.curve25519[keyId];
|
||||
const unsignedKeyResult = { key: oneTimeKey };
|
||||
const j = anotherjson.stringify(unsignedKeyResult);
|
||||
const sig = testOlmAccount.sign(j);
|
||||
const keyResult = {
|
||||
...unsignedKeyResult,
|
||||
signatures: { [userId]: { "ed25519:DEVICE_ID": sig } },
|
||||
};
|
||||
|
||||
return {
|
||||
one_time_keys: { [userId]: { DEVICE_ID: { ["signed_curve25519:" + keyId]: keyResult } } },
|
||||
failures: {},
|
||||
};
|
||||
}
|
||||
|
||||
it("Should receive an encrypted state event", async () => {
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
recipient: aliceClient.getUserId()!,
|
||||
recipientCurve25519Key: keyReceiver.getDeviceKey(),
|
||||
recipientEd25519Key: keyReceiver.getSigningKey(),
|
||||
olmAccount: testOlmAccount,
|
||||
p2pSession: p2pSession,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// encrypt a state event with the group session
|
||||
const eventEncrypted = encryptMegolmEvent({
|
||||
senderKey: testSenderKey,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
plaintext: {
|
||||
type: "m.room.topic",
|
||||
state_key: "",
|
||||
content: {
|
||||
topic: "Secret!",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Alice gets both the events in a single sync
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
to_device: {
|
||||
events: [roomKeyEncrypted],
|
||||
},
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: { timeline: { events: [eventEncrypted] } },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const room = aliceClient.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.isEncrypted()).toBe(true);
|
||||
|
||||
// it probably won't be decrypted yet, because it takes a while to process the olm keys
|
||||
const decryptedEvent = await testUtils.awaitDecryption(event, { waitOnDecryptionFailure: true });
|
||||
expect(decryptedEvent.getContent().topic).toEqual("Secret!");
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("Should send an encrypted state event", async () => {
|
||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
|
||||
|
||||
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
||||
keyResponder.addDeviceKeys(testDeviceKeys);
|
||||
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
// Alice shares a room with Bob
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"], HistoryVisibility.Joined, ROOM_ID, true));
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// ... and claim one of Bob's OTKs ...
|
||||
expectAliceKeyClaim(getTestKeysClaimResponse("@bob:xyz"));
|
||||
|
||||
// ... and send an m.room.topic message
|
||||
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount);
|
||||
|
||||
// Finally, send the message, and expect to get an `m.room.encrypted` event that we can decrypt.
|
||||
await Promise.all([
|
||||
aliceClient.setRoomTopic(ROOM_ID, "Secret!"),
|
||||
expectSendMegolmStateEvent(inboundGroupSessionPromise),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,268 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import Olm from "@matrix-org/olm";
|
||||
|
||||
import { getSyncResponse, syncPromise } from "../../test-utils/test-utils";
|
||||
import {
|
||||
ClientEvent,
|
||||
createClient,
|
||||
type IToDeviceEvent,
|
||||
type MatrixClient,
|
||||
type MatrixEvent,
|
||||
type ReceivedToDeviceMessage,
|
||||
} from "../../../src";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { encryptOlmEvent, establishOlmSession, getTestOlmAccountKeys } from "./olm-utils.ts";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
/**
|
||||
* Integration tests for to-device messages functionality.
|
||||
*
|
||||
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
|
||||
* to provide the most effective integration tests possible.
|
||||
*/
|
||||
describe("to-device-messages", () => {
|
||||
let aliceClient: MatrixClient;
|
||||
|
||||
/** an object which intercepts `/keys/query` requests on the test homeserver */
|
||||
let e2eKeyResponder: E2EKeyResponder;
|
||||
let e2eKeyReceiver: E2EKeyReceiver;
|
||||
let syncResponder: SyncResponder;
|
||||
|
||||
beforeEach(
|
||||
async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
|
||||
const homeserverUrl = "https://server.com";
|
||||
aliceClient = createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
userId: testData.TEST_USER_ID,
|
||||
accessToken: "akjgkrgjsalice",
|
||||
deviceId: testData.TEST_DEVICE_ID,
|
||||
});
|
||||
|
||||
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
e2eKeyReceiver = new E2EKeyReceiver(homeserverUrl);
|
||||
syncResponder = new SyncResponder(homeserverUrl);
|
||||
|
||||
// add bob as known user
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse([testData.BOB_TEST_USER_ID]));
|
||||
|
||||
// Silence warnings from the backup manager
|
||||
fetchMock.getOnce(new URL("/_matrix/client/v3/room_keys/version", homeserverUrl).toString(), {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND" },
|
||||
});
|
||||
|
||||
fetchMock.get(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {});
|
||||
fetchMock.get(new URL("/_matrix/client/versions/", homeserverUrl).toString(), {});
|
||||
fetchMock.post(
|
||||
new URL(
|
||||
`/_matrix/client/v3/user/${encodeURIComponent(testData.TEST_USER_ID)}/filter`,
|
||||
homeserverUrl,
|
||||
).toString(),
|
||||
{ filter_id: "fid" },
|
||||
);
|
||||
|
||||
await aliceClient.initRustCrypto();
|
||||
},
|
||||
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
|
||||
10000,
|
||||
);
|
||||
|
||||
afterEach(async () => {
|
||||
aliceClient.stopClient();
|
||||
});
|
||||
|
||||
describe("encryptToDeviceMessages", () => {
|
||||
it("returns empty batch for device that is not known", async () => {
|
||||
await aliceClient.startClient();
|
||||
|
||||
const toDeviceBatch = await aliceClient
|
||||
.getCrypto()
|
||||
?.encryptToDeviceMessages(
|
||||
"m.test.event",
|
||||
[{ userId: testData.BOB_TEST_USER_ID, deviceId: testData.BOB_TEST_DEVICE_ID }],
|
||||
{
|
||||
some: "content",
|
||||
},
|
||||
);
|
||||
|
||||
expect(toDeviceBatch).toBeDefined();
|
||||
const { batch, eventType } = toDeviceBatch!;
|
||||
expect(eventType).toBe("m.room.encrypted");
|
||||
expect(batch.length).toBe(0);
|
||||
});
|
||||
|
||||
it("returns encrypted batch for known device", async () => {
|
||||
await aliceClient.startClient();
|
||||
e2eKeyResponder.addDeviceKeys(testData.BOB_SIGNED_TEST_DEVICE_DATA);
|
||||
fetchMock.post("express:/_matrix/client/v3/keys/claim", () => ({
|
||||
one_time_keys: testData.BOB_ONE_TIME_KEYS,
|
||||
}));
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const toDeviceBatch = await aliceClient
|
||||
.getCrypto()
|
||||
?.encryptToDeviceMessages(
|
||||
"m.test.event",
|
||||
[{ userId: testData.BOB_TEST_USER_ID, deviceId: testData.BOB_TEST_DEVICE_ID }],
|
||||
{
|
||||
some: "content",
|
||||
},
|
||||
);
|
||||
|
||||
expect(toDeviceBatch?.batch.length).toBe(1);
|
||||
expect(toDeviceBatch?.eventType).toBe("m.room.encrypted");
|
||||
const { deviceId, payload, userId } = toDeviceBatch!.batch[0];
|
||||
expect(deviceId).toBe(testData.BOB_TEST_DEVICE_ID);
|
||||
expect(userId).toBe(testData.BOB_TEST_USER_ID);
|
||||
expect(payload.algorithm).toBe("m.olm.v1.curve25519-aes-sha2");
|
||||
expect(payload.sender_key).toEqual(expect.any(String));
|
||||
expect(payload.ciphertext).toEqual(
|
||||
expect.objectContaining({
|
||||
[testData.BOB_SIGNED_TEST_DEVICE_DATA.keys[`curve25519:${testData.BOB_TEST_DEVICE_ID}`]]: {
|
||||
body: expect.any(String),
|
||||
type: 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// for future: check that bob's device can decrypt the ciphertext?
|
||||
});
|
||||
});
|
||||
|
||||
describe("receive to-device-messages", () => {
|
||||
it("Should receive decrypted to-device message via ClientEvent", async () => {
|
||||
// create a test olm device which we will use to communicate with alice. We use libolm to implement this.
|
||||
await Olm.init();
|
||||
const testOlmAccount = new Olm.Account();
|
||||
testOlmAccount.create();
|
||||
|
||||
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
||||
e2eKeyResponder.addDeviceKeys(testDeviceKeys);
|
||||
|
||||
await aliceClient.startClient();
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"]));
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const p2pSession = await establishOlmSession(aliceClient, e2eKeyReceiver, syncResponder, testOlmAccount);
|
||||
|
||||
const toDeviceEvent = encryptOlmEvent({
|
||||
sender: "@bob:xyz",
|
||||
senderKey: testDeviceKeys.keys[`curve25519:DEVICE_ID`],
|
||||
senderSigningKey: testDeviceKeys.keys[`ed25519:DEVICE_ID`],
|
||||
p2pSession: p2pSession,
|
||||
recipient: aliceClient.getUserId()!,
|
||||
recipientCurve25519Key: e2eKeyReceiver.getDeviceKey(),
|
||||
recipientEd25519Key: e2eKeyReceiver.getSigningKey(),
|
||||
plaincontent: {
|
||||
body: "foo",
|
||||
},
|
||||
plaintype: "m.test.type",
|
||||
});
|
||||
|
||||
const processedToDeviceResolver: PromiseWithResolvers<ReceivedToDeviceMessage> = Promise.withResolvers();
|
||||
|
||||
aliceClient.on(ClientEvent.ReceivedToDeviceMessage, (payload) => {
|
||||
processedToDeviceResolver.resolve(payload);
|
||||
});
|
||||
|
||||
const oldToDeviceResolver: PromiseWithResolvers<MatrixEvent> = Promise.withResolvers();
|
||||
|
||||
aliceClient.on(ClientEvent.ToDeviceEvent, (event) => {
|
||||
oldToDeviceResolver.resolve(event);
|
||||
});
|
||||
|
||||
expect(toDeviceEvent.type).toBe("m.room.encrypted");
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [toDeviceEvent] } });
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const { message, encryptionInfo } = await processedToDeviceResolver.promise;
|
||||
|
||||
expect(message.type).toBe("m.test.type");
|
||||
expect(message.content["body"]).toBe("foo");
|
||||
|
||||
expect(encryptionInfo).not.toBeNull();
|
||||
expect(encryptionInfo!.senderVerified).toBe(false);
|
||||
expect(encryptionInfo!.sender).toBe("@bob:xyz");
|
||||
expect(encryptionInfo!.senderDevice).toBe("DEVICE_ID");
|
||||
|
||||
const oldFormat = await oldToDeviceResolver.promise;
|
||||
expect(oldFormat.isEncrypted()).toBe(true);
|
||||
expect(oldFormat.getType()).toBe("m.test.type");
|
||||
expect(oldFormat.getContent()["body"]).toBe("foo");
|
||||
});
|
||||
|
||||
it("Should receive clear to-device message via ClientEvent", async () => {
|
||||
await aliceClient.startClient();
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const toDeviceEvent: IToDeviceEvent = {
|
||||
sender: "@bob:xyz",
|
||||
type: "m.test.type",
|
||||
content: {
|
||||
body: "foo",
|
||||
},
|
||||
};
|
||||
|
||||
const processedToDeviceResolver: PromiseWithResolvers<ReceivedToDeviceMessage> = Promise.withResolvers();
|
||||
|
||||
aliceClient.on(ClientEvent.ReceivedToDeviceMessage, (payload) => {
|
||||
processedToDeviceResolver.resolve(payload);
|
||||
});
|
||||
|
||||
const oldToDeviceResolver: PromiseWithResolvers<MatrixEvent> = Promise.withResolvers();
|
||||
|
||||
aliceClient.on(ClientEvent.ToDeviceEvent, (event) => {
|
||||
oldToDeviceResolver.resolve(event);
|
||||
});
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [toDeviceEvent] } });
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const { message, encryptionInfo } = await processedToDeviceResolver.promise;
|
||||
|
||||
expect(message.type).toBe("m.test.type");
|
||||
expect(message.content["body"]).toBe("foo");
|
||||
|
||||
// When the message is not encrypted, we don't have the encryptionInfo.
|
||||
expect(encryptionInfo).toBeNull();
|
||||
|
||||
const oldFormat = await oldToDeviceResolver.promise;
|
||||
expect(oldFormat.isEncrypted()).toBe(false);
|
||||
expect(oldFormat.getType()).toBe("m.test.type");
|
||||
expect(oldFormat.getContent()["body"]).toBe("foo");
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,400 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import TestClient from '../TestClient';
|
||||
import testUtils from '../test-utils';
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
/**
|
||||
* get a /sync response which contains a single e2e room (ROOM_ID), with the
|
||||
* members given
|
||||
*
|
||||
* @param {string[]} roomMembers
|
||||
*
|
||||
* @return {object} sync response
|
||||
*/
|
||||
function getSyncResponse(roomMembers) {
|
||||
const stateEvents = [
|
||||
testUtils.mkEvent({
|
||||
type: 'm.room.encryption',
|
||||
skey: '',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
Array.prototype.push.apply(
|
||||
stateEvents,
|
||||
roomMembers.map(
|
||||
(m) => testUtils.mkMembership({
|
||||
mship: 'join',
|
||||
sender: m,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: {
|
||||
state: {
|
||||
events: stateEvents,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return syncResponse;
|
||||
}
|
||||
|
||||
|
||||
describe("DeviceList management:", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('not running deviceList tests: Olm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
let sessionStoreBackend;
|
||||
let aliceTestClient;
|
||||
|
||||
async function createTestClient() {
|
||||
const testClient = new TestClient(
|
||||
"@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend,
|
||||
);
|
||||
await testClient.client.initCrypto();
|
||||
return testClient;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
|
||||
// we create our own sessionStoreBackend so that we can use it for
|
||||
// another TestClient.
|
||||
sessionStoreBackend = new testUtils.MockStorageApi();
|
||||
|
||||
aliceTestClient = await createTestClient();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
return aliceTestClient.stop();
|
||||
});
|
||||
|
||||
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
|
||||
return aliceTestClient.start().then(function() {
|
||||
const syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
console.log("Forcing alice to download our device keys");
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
},
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.downloadKeys(['@bob:xyz']),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1),
|
||||
]);
|
||||
}).then(function() {
|
||||
console.log("Telling alice to send a megolm message");
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
).respond(200, {
|
||||
event_id: '$event_id',
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
|
||||
// the crypto stuff can take a while, so give the requests a whole second.
|
||||
aliceTestClient.httpBackend.flushAllExpected({
|
||||
timeout: 1000,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("We should not get confused by out-of-order device query responses",
|
||||
() => {
|
||||
// https://github.com/vector-im/riot-web/issues/3126
|
||||
return aliceTestClient.start().then(() => {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(() => {
|
||||
// to make sure the initial device queries are flushed out, we
|
||||
// attempt to send a message.
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
'@chris:abc': {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
aliceTestClient.httpBackend.when('PUT', '/send/').respond(
|
||||
200, {event_id: '$event1'});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
|
||||
() => aliceTestClient.httpBackend.flush('/send/', 1),
|
||||
),
|
||||
aliceTestClient.client._crypto._deviceList.saveIfDirty(),
|
||||
]);
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
expect(data.syncToken).toEqual(1);
|
||||
});
|
||||
|
||||
// invalidate bob's and chris's device lists in separate syncs
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
|
||||
next_batch: '2',
|
||||
device_lists: {
|
||||
changed: ['@bob:xyz'],
|
||||
},
|
||||
});
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
|
||||
next_batch: '3',
|
||||
device_lists: {
|
||||
changed: ['@chris:abc'],
|
||||
},
|
||||
});
|
||||
// flush both syncs
|
||||
return aliceTestClient.flushSync().then(() => {
|
||||
return aliceTestClient.flushSync();
|
||||
});
|
||||
}).then(() => {
|
||||
// check that we don't yet have a request for chris's devices.
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query', {
|
||||
device_keys: {
|
||||
'@chris:abc': {},
|
||||
},
|
||||
token: '3',
|
||||
}).respond(200, {
|
||||
device_keys: {'@chris:abc': {}},
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(0);
|
||||
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
if (bobStat != 1 && bobStat != 2) {
|
||||
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
|
||||
bobStat);
|
||||
}
|
||||
const chrisStat = data.trackingStatus['@chris:abc'];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error(
|
||||
'Unexpected status for chris: wanted 1 or 2, got ' + chrisStat,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// now add an expectation for a query for bob's devices, and let
|
||||
// it complete.
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query', {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
},
|
||||
token: '2',
|
||||
}).respond(200, {
|
||||
device_keys: {'@bob:xyz': {}},
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||
}).then(() => {
|
||||
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
expect(bobStat).toEqual(3);
|
||||
const chrisStat = data.trackingStatus['@chris:abc'];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error(
|
||||
'Unexpected status for chris: wanted 1 or 2, got ' + bobStat,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// now let the query for chris's devices complete.
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@chris:abc']);
|
||||
}).then(() => {
|
||||
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
const chrisStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
expect(bobStat).toEqual(3);
|
||||
expect(chrisStat).toEqual(3);
|
||||
expect(data.syncToken).toEqual(3);
|
||||
});
|
||||
});
|
||||
}).timeout(3000);
|
||||
|
||||
// https://github.com/vector-im/riot-web/issues/4983
|
||||
describe("Alice should know she has stale device lists", () => {
|
||||
beforeEach(async function() {
|
||||
await aliceTestClient.start();
|
||||
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse(['@bob:xyz']));
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
},
|
||||
},
|
||||
);
|
||||
await aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
expect(bobStat).toBeGreaterThan(
|
||||
0, "Alice should be tracking bob's device list",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("when Bob leaves", async function() {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, {
|
||||
next_batch: 2,
|
||||
device_lists: {
|
||||
left: ['@bob:xyz'],
|
||||
},
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: 'leave',
|
||||
sender: '@bob:xyz',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
expect(bobStat).toEqual(
|
||||
0, "Alice should have marked bob's device list as untracked",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("when Alice leaves", async function() {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, {
|
||||
next_batch: 2,
|
||||
device_lists: {
|
||||
left: ['@bob:xyz'],
|
||||
},
|
||||
rooms: {
|
||||
leave: {
|
||||
[ROOM_ID]: {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: 'leave',
|
||||
sender: '@bob:xyz',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
expect(bobStat).toEqual(
|
||||
0, "Alice should have marked bob's device list as untracked",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("when Bob leaves whilst Alice is offline", async function() {
|
||||
aliceTestClient.stop();
|
||||
|
||||
const anotherTestClient = await createTestClient();
|
||||
|
||||
try {
|
||||
await anotherTestClient.start();
|
||||
anotherTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse([]));
|
||||
await anotherTestClient.flushSync();
|
||||
await anotherTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
|
||||
anotherTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
expect(bobStat).toEqual(
|
||||
0, "Alice should have marked bob's device list as untracked",
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
anotherTestClient.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,755 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/* This file consists of a set of integration tests which try to simulate
|
||||
* communication via an Olm-encrypted room between two users, Alice and Bob.
|
||||
*
|
||||
* Note that megolm (group) conversation is not tested here.
|
||||
*
|
||||
* See also `megolm.spec.js`.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import '../olm-loader';
|
||||
|
||||
import expect from 'expect';
|
||||
const sdk = require("../..");
|
||||
import Promise from 'bluebird';
|
||||
const utils = require("../../lib/utils");
|
||||
const testUtils = require("../test-utils");
|
||||
const TestClient = require('../TestClient').default;
|
||||
|
||||
let aliTestClient;
|
||||
const roomId = "!room:localhost";
|
||||
const aliUserId = "@ali:localhost";
|
||||
const aliDeviceId = "zxcvb";
|
||||
const aliAccessToken = "aseukfgwef";
|
||||
let bobTestClient;
|
||||
const bobUserId = "@bob:localhost";
|
||||
const bobDeviceId = "bvcxz";
|
||||
const bobAccessToken = "fewgfkuesa";
|
||||
let aliMessages;
|
||||
let bobMessages;
|
||||
|
||||
function bobUploadsDeviceKeys() {
|
||||
bobTestClient.expectDeviceKeyUpload();
|
||||
return Promise.all([
|
||||
bobTestClient.client.uploadKeys(),
|
||||
bobTestClient.httpBackend.flush(),
|
||||
]).then(() => {
|
||||
expect(Object.keys(bobTestClient.deviceKeys).length).toNotEqual(0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that ali will query bobs keys; then flush the http request.
|
||||
*
|
||||
* @return {promise} resolves once the http request has completed.
|
||||
*/
|
||||
function expectAliQueryKeys() {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(bobTestClient.deviceKeys).toBeTruthy();
|
||||
|
||||
const bobKeys = {};
|
||||
bobKeys[bobDeviceId] = bobTestClient.deviceKeys;
|
||||
aliTestClient.httpBackend.when("POST", "/keys/query")
|
||||
.respond(200, function(path, content) {
|
||||
expect(content.device_keys[bobUserId]).toEqual({});
|
||||
const result = {};
|
||||
result[bobUserId] = bobKeys;
|
||||
return {device_keys: result};
|
||||
});
|
||||
return aliTestClient.httpBackend.flush("/keys/query", 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that bob will query alis keys; then flush the http request.
|
||||
*
|
||||
* @return {promise} which resolves once the http request has completed.
|
||||
*/
|
||||
function expectBobQueryKeys() {
|
||||
// can't query keys before ali has uploaded them
|
||||
expect(aliTestClient.deviceKeys).toBeTruthy();
|
||||
|
||||
const aliKeys = {};
|
||||
aliKeys[aliDeviceId] = aliTestClient.deviceKeys;
|
||||
console.log("query result will be", aliKeys);
|
||||
|
||||
bobTestClient.httpBackend.when(
|
||||
"POST", "/keys/query",
|
||||
).respond(200, function(path, content) {
|
||||
expect(content.device_keys[aliUserId]).toEqual({});
|
||||
const result = {};
|
||||
result[aliUserId] = aliKeys;
|
||||
return {device_keys: result};
|
||||
});
|
||||
return bobTestClient.httpBackend.flush("/keys/query", 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that ali will claim one of bob's keys; then flush the http request.
|
||||
*
|
||||
* @return {promise} resolves once the http request has completed.
|
||||
*/
|
||||
function expectAliClaimKeys() {
|
||||
return bobTestClient.awaitOneTimeKeyUpload().then((keys) => {
|
||||
aliTestClient.httpBackend.when(
|
||||
"POST", "/keys/claim",
|
||||
).respond(200, function(path, content) {
|
||||
const claimType = content.one_time_keys[bobUserId][bobDeviceId];
|
||||
expect(claimType).toEqual("signed_curve25519");
|
||||
let keyId = null;
|
||||
for (keyId in keys) {
|
||||
if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) {
|
||||
if (keyId.indexOf(claimType + ":") === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = {};
|
||||
result[bobUserId] = {};
|
||||
result[bobUserId][bobDeviceId] = {};
|
||||
result[bobUserId][bobDeviceId][keyId] = keys[keyId];
|
||||
return {one_time_keys: result};
|
||||
});
|
||||
}).then(() => {
|
||||
// it can take a while to process the key query, so give it some extra
|
||||
// time, and make sure the claim actually happens rather than ploughing on
|
||||
// confusingly.
|
||||
return aliTestClient.httpBackend.flush("/keys/claim", 1, 500).then((r) => {
|
||||
expect(r).toEqual(1, "Ali did not claim Bob's keys");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function aliDownloadsKeys() {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(bobTestClient.getSigningKey()).toBeTruthy();
|
||||
|
||||
const p1 = aliTestClient.client.downloadKeys([bobUserId]).then(function() {
|
||||
return aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
||||
}).then((devices) => {
|
||||
expect(devices.length).toEqual(1);
|
||||
expect(devices[0].deviceId).toEqual("bvcxz");
|
||||
});
|
||||
const p2 = expectAliQueryKeys();
|
||||
|
||||
// check that the localStorage is updated as we expect (not sure this is
|
||||
// an integration test, but meh)
|
||||
return Promise.all([p1, p2]).then(() => {
|
||||
return aliTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const devices = data.devices[bobUserId];
|
||||
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys);
|
||||
expect(devices[bobDeviceId].verified).
|
||||
toBe(0); // DeviceVerification.UNVERIFIED
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function aliEnablesEncryption() {
|
||||
return aliTestClient.client.setRoomEncryption(roomId, {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
}).then(function() {
|
||||
expect(aliTestClient.client.isRoomEncrypted(roomId)).toBeTruthy();
|
||||
});
|
||||
}
|
||||
|
||||
function bobEnablesEncryption() {
|
||||
return bobTestClient.client.setRoomEncryption(roomId, {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
}).then(function() {
|
||||
expect(bobTestClient.client.isRoomEncrypted(roomId)).toBeTruthy();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ali sends a message, first claiming e2e keys. Set the expectations and
|
||||
* check the results.
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
function aliSendsFirstMessage() {
|
||||
return Promise.all([
|
||||
sendMessage(aliTestClient.client),
|
||||
expectAliQueryKeys()
|
||||
.then(expectAliClaimKeys)
|
||||
.then(expectAliSendMessageRequest),
|
||||
]).spread(function(_, ciphertext) {
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ali sends a message without first claiming e2e keys. Set the expectations
|
||||
* and check the results.
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
function aliSendsMessage() {
|
||||
return Promise.all([
|
||||
sendMessage(aliTestClient.client),
|
||||
expectAliSendMessageRequest(),
|
||||
]).spread(function(_, ciphertext) {
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bob sends a message, first querying (but not claiming) e2e keys. Set the
|
||||
* expectations and check the results.
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Ali's device.
|
||||
*/
|
||||
function bobSendsReplyMessage() {
|
||||
return Promise.all([
|
||||
sendMessage(bobTestClient.client),
|
||||
expectBobQueryKeys()
|
||||
.then(expectBobSendMessageRequest),
|
||||
]).spread(function(_, ciphertext) {
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that Ali will send a message, and flush the request
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
function expectAliSendMessageRequest() {
|
||||
return expectSendMessageRequest(aliTestClient.httpBackend).then(function(content) {
|
||||
aliMessages.push(content);
|
||||
expect(utils.keys(content.ciphertext)).toEqual([bobTestClient.getDeviceKey()]);
|
||||
const ciphertext = content.ciphertext[bobTestClient.getDeviceKey()];
|
||||
expect(ciphertext).toBeTruthy();
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that Bob will send a message, and flush the request
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
function expectBobSendMessageRequest() {
|
||||
return expectSendMessageRequest(bobTestClient.httpBackend).then(function(content) {
|
||||
bobMessages.push(content);
|
||||
const aliKeyId = "curve25519:" + aliDeviceId;
|
||||
const aliDeviceCurve25519Key = aliTestClient.deviceKeys.keys[aliKeyId];
|
||||
expect(utils.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
|
||||
const ciphertext = content.ciphertext[aliDeviceCurve25519Key];
|
||||
expect(ciphertext).toBeTruthy();
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
function sendMessage(client) {
|
||||
return client.sendMessage(
|
||||
roomId, {msgtype: "m.text", body: "Hello, World"},
|
||||
);
|
||||
}
|
||||
|
||||
function expectSendMessageRequest(httpBackend) {
|
||||
const path = "/send/m.room.encrypted/";
|
||||
const deferred = Promise.defer();
|
||||
httpBackend.when("PUT", path).respond(200, function(path, content) {
|
||||
deferred.resolve(content);
|
||||
return {
|
||||
event_id: "asdfgh",
|
||||
};
|
||||
});
|
||||
|
||||
// it can take a while to process the key query
|
||||
return httpBackend.flush(path, 1).then(() => deferred.promise);
|
||||
}
|
||||
|
||||
function aliRecvMessage() {
|
||||
const message = bobMessages.shift();
|
||||
return recvMessage(
|
||||
aliTestClient.httpBackend, aliTestClient.client, bobUserId, message,
|
||||
);
|
||||
}
|
||||
|
||||
function bobRecvMessage() {
|
||||
const message = aliMessages.shift();
|
||||
return recvMessage(
|
||||
bobTestClient.httpBackend, bobTestClient.client, aliUserId, message,
|
||||
);
|
||||
}
|
||||
|
||||
function recvMessage(httpBackend, client, sender, message) {
|
||||
const syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: message,
|
||||
sender: sender,
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
const eventPromise = new Promise((resolve, reject) => {
|
||||
const onEvent = function(event) {
|
||||
// ignore the m.room.member events
|
||||
if (event.getType() == "m.room.member") {
|
||||
return;
|
||||
}
|
||||
console.log(client.credentials.userId + " received event",
|
||||
event);
|
||||
|
||||
client.removeListener("event", onEvent);
|
||||
resolve(event);
|
||||
};
|
||||
client.on("event", onEvent);
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
|
||||
return eventPromise.then((event) => {
|
||||
expect(event.isEncrypted()).toBeTruthy();
|
||||
|
||||
// it may still be being decrypted
|
||||
return testUtils.awaitDecryption(event);
|
||||
}).then((event) => {
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent()).toEqual({
|
||||
msgtype: "m.text",
|
||||
body: "Hello, World",
|
||||
});
|
||||
expect(event.isEncrypted()).toBeTruthy();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send an initial sync response to the client (which just includes the member
|
||||
* list for our test room).
|
||||
*
|
||||
* @param {TestClient} testClient
|
||||
* @returns {Promise} which resolves when the sync has been flushed.
|
||||
*/
|
||||
function firstSync(testClient) {
|
||||
// send a sync response including our test room.
|
||||
const syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: { },
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
state: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: "join",
|
||||
user: aliUserId,
|
||||
}),
|
||||
testUtils.mkMembership({
|
||||
mship: "join",
|
||||
user: bobUserId,
|
||||
}),
|
||||
],
|
||||
},
|
||||
timeline: {
|
||||
events: [],
|
||||
},
|
||||
};
|
||||
|
||||
testClient.httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
return testClient.flushSync();
|
||||
}
|
||||
|
||||
|
||||
describe("MatrixClient crypto", function() {
|
||||
if (!sdk.CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
|
||||
aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken);
|
||||
await aliTestClient.client.initCrypto();
|
||||
|
||||
bobTestClient = new TestClient(bobUserId, bobDeviceId, bobAccessToken);
|
||||
await bobTestClient.client.initCrypto();
|
||||
|
||||
aliMessages = [];
|
||||
bobMessages = [];
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
aliTestClient.httpBackend.verifyNoOutstandingExpectation();
|
||||
bobTestClient.httpBackend.verifyNoOutstandingExpectation();
|
||||
|
||||
return Promise.all([aliTestClient.stop(), bobTestClient.stop()]);
|
||||
});
|
||||
|
||||
it("Bob uploads device keys", function() {
|
||||
return Promise.resolve()
|
||||
.then(bobUploadsDeviceKeys);
|
||||
});
|
||||
|
||||
it("Ali downloads Bobs device keys", function(done) {
|
||||
Promise.resolve()
|
||||
.then(bobUploadsDeviceKeys)
|
||||
.then(aliDownloadsKeys)
|
||||
.nodeify(done);
|
||||
});
|
||||
|
||||
it("Ali gets keys with an invalid signature", function(done) {
|
||||
Promise.resolve()
|
||||
.then(bobUploadsDeviceKeys)
|
||||
.then(function() {
|
||||
// tamper bob's keys
|
||||
const bobDeviceKeys = bobTestClient.deviceKeys;
|
||||
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy();
|
||||
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
|
||||
|
||||
return Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId]),
|
||||
expectAliQueryKeys(),
|
||||
]);
|
||||
}).then(function() {
|
||||
return aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
||||
}).then((devices) => {
|
||||
// should get an empty list
|
||||
expect(devices).toEqual([]);
|
||||
})
|
||||
.nodeify(done);
|
||||
});
|
||||
|
||||
it("Ali gets keys with an incorrect userId", function(done) {
|
||||
const eveUserId = "@eve:localhost";
|
||||
|
||||
const bobDeviceKeys = {
|
||||
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||
device_id: 'bvcxz',
|
||||
keys: {
|
||||
'ed25519:bvcxz': 'pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q',
|
||||
'curve25519:bvcxz': '7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ',
|
||||
},
|
||||
user_id: '@eve:localhost',
|
||||
signatures: {
|
||||
'@eve:localhost': {
|
||||
'ed25519:bvcxz': 'CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG' +
|
||||
'0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const bobKeys = {};
|
||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||
aliTestClient.httpBackend.when(
|
||||
"POST", "/keys/query",
|
||||
).respond(200, function(path, content) {
|
||||
const result = {};
|
||||
result[bobUserId] = bobKeys;
|
||||
return {device_keys: result};
|
||||
});
|
||||
|
||||
Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId, eveUserId]),
|
||||
aliTestClient.httpBackend.flush("/keys/query", 1),
|
||||
]).then(function() {
|
||||
return Promise.all([
|
||||
aliTestClient.client.getStoredDevicesForUser(bobUserId),
|
||||
aliTestClient.client.getStoredDevicesForUser(eveUserId),
|
||||
]);
|
||||
}).spread((bobDevices, eveDevices) => {
|
||||
// should get an empty list
|
||||
expect(bobDevices).toEqual([]);
|
||||
expect(eveDevices).toEqual([]);
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
it("Ali gets keys with an incorrect deviceId", function(done) {
|
||||
const bobDeviceKeys = {
|
||||
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||
device_id: 'bad_device',
|
||||
keys: {
|
||||
'ed25519:bad_device': 'e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0',
|
||||
'curve25519:bad_device': 'YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc',
|
||||
},
|
||||
user_id: '@bob:localhost',
|
||||
signatures: {
|
||||
'@bob:localhost': {
|
||||
'ed25519:bad_device': 'fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A' +
|
||||
'me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const bobKeys = {};
|
||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||
aliTestClient.httpBackend.when(
|
||||
"POST", "/keys/query",
|
||||
).respond(200, function(path, content) {
|
||||
const result = {};
|
||||
result[bobUserId] = bobKeys;
|
||||
return {device_keys: result};
|
||||
});
|
||||
|
||||
Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId]),
|
||||
aliTestClient.httpBackend.flush("/keys/query", 1),
|
||||
]).then(function() {
|
||||
return aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
||||
}).then((devices) => {
|
||||
// should get an empty list
|
||||
expect(devices).toEqual([]);
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
|
||||
it("Bob starts his client and uploads device keys and one-time keys", function() {
|
||||
return Promise.resolve()
|
||||
.then(() => bobTestClient.start())
|
||||
.then(() => bobTestClient.awaitOneTimeKeyUpload())
|
||||
.then((keys) => {
|
||||
expect(Object.keys(keys).length).toEqual(5);
|
||||
expect(Object.keys(bobTestClient.deviceKeys).length).toNotEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("Ali sends a message", function(done) {
|
||||
Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsFirstMessage)
|
||||
.nodeify(done);
|
||||
});
|
||||
|
||||
it("Bob receives a message", function() {
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsFirstMessage)
|
||||
.then(bobRecvMessage);
|
||||
});
|
||||
|
||||
it("Bob receives a message with a bogus sender", function() {
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsFirstMessage)
|
||||
.then(function() {
|
||||
const message = aliMessages.shift();
|
||||
const syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: message,
|
||||
sender: "@bogus:sender",
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
bobTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
const eventPromise = new Promise((resolve, reject) => {
|
||||
const onEvent = function(event) {
|
||||
console.log(bobUserId + " received event",
|
||||
event);
|
||||
resolve(event);
|
||||
};
|
||||
bobTestClient.client.once("event", onEvent);
|
||||
});
|
||||
|
||||
bobTestClient.httpBackend.flush();
|
||||
return eventPromise;
|
||||
}).then((event) => {
|
||||
expect(event.isEncrypted()).toBeTruthy();
|
||||
|
||||
// it may still be being decrypted
|
||||
return testUtils.awaitDecryption(event);
|
||||
}).then((event) => {
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent().msgtype).toEqual("m.bad.encrypted");
|
||||
});
|
||||
});
|
||||
|
||||
it("Ali blocks Bob's device", function(done) {
|
||||
Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliDownloadsKeys)
|
||||
.then(function() {
|
||||
aliTestClient.client.setDeviceBlocked(bobUserId, bobDeviceId, true);
|
||||
const p1 = sendMessage(aliTestClient.client);
|
||||
const p2 = expectSendMessageRequest(aliTestClient.httpBackend)
|
||||
.then(function(sentContent) {
|
||||
// no unblocked devices, so the ciphertext should be empty
|
||||
expect(sentContent.ciphertext).toEqual({});
|
||||
});
|
||||
return Promise.all([p1, p2]);
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
it("Bob receives two pre-key messages", function(done) {
|
||||
Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsFirstMessage)
|
||||
.then(bobRecvMessage)
|
||||
.then(aliSendsMessage)
|
||||
.then(bobRecvMessage)
|
||||
.nodeify(done);
|
||||
});
|
||||
|
||||
it("Bob replies to the message", function() {
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
.then(() => firstSync(bobTestClient))
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsFirstMessage)
|
||||
.then(bobRecvMessage)
|
||||
.then(bobEnablesEncryption)
|
||||
.then(bobSendsReplyMessage).then(function(ciphertext) {
|
||||
expect(ciphertext.type).toEqual(1);
|
||||
}).then(aliRecvMessage);
|
||||
});
|
||||
|
||||
it("Ali does a key query when encryption is enabled", function() {
|
||||
// enabling encryption in the room should make alice download devices
|
||||
// for both members.
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
.then(() => {
|
||||
const syncData = {
|
||||
next_batch: '2',
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
state: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: 'm.room.encryption',
|
||||
skey: '',
|
||||
content: {
|
||||
algorithm: 'm.olm.v1.curve25519-aes-sha2',
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
aliTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, syncData);
|
||||
return aliTestClient.httpBackend.flush('/sync', 1);
|
||||
}).then(() => {
|
||||
aliTestClient.expectKeyQuery({
|
||||
device_keys: {
|
||||
[aliUserId]: {},
|
||||
[bobUserId]: {},
|
||||
},
|
||||
});
|
||||
return aliTestClient.httpBackend.flushAllExpected();
|
||||
});
|
||||
});
|
||||
|
||||
it("Upload new oneTimeKeys based on a /sync request - no count-asking", function() {
|
||||
// Send a response which causes a key upload
|
||||
const httpBackend = aliTestClient.httpBackend;
|
||||
const syncDataEmpty = {
|
||||
next_batch: "a",
|
||||
device_one_time_keys_count: {
|
||||
signed_curve25519: 0,
|
||||
},
|
||||
};
|
||||
|
||||
// enqueue expectations:
|
||||
// * Sync with empty one_time_keys => upload keys
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
console.log(aliTestClient + ': starting');
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
aliTestClient.expectDeviceKeyUpload();
|
||||
|
||||
// we let the client do a very basic initial sync, which it needs before
|
||||
// it will upload one-time keys.
|
||||
httpBackend.when("GET", "/sync").respond(200, syncDataEmpty);
|
||||
|
||||
aliTestClient.client.startClient({});
|
||||
|
||||
return httpBackend.flushAllExpected().then(() => {
|
||||
console.log(aliTestClient + ': started');
|
||||
});
|
||||
})
|
||||
.then(() => httpBackend.when("POST", "/keys/upload")
|
||||
.respond(200, (path, content) => {
|
||||
expect(content.one_time_keys).toBeTruthy();
|
||||
expect(content.one_time_keys).toNotEqual({});
|
||||
expect(Object.keys(content.one_time_keys).length)
|
||||
.toBeGreaterThanOrEqualTo(1);
|
||||
console.log('received %i one-time keys',
|
||||
Object.keys(content.one_time_keys).length);
|
||||
// cancel futher calls by telling the client
|
||||
// we have more than we need
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: 70,
|
||||
},
|
||||
};
|
||||
}))
|
||||
.then(() => httpBackend.flushAllExpected());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,321 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
describe("MatrixClient events", function() {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let client;
|
||||
let httpBackend;
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: selfUserId,
|
||||
accessToken: selfAccessToken,
|
||||
});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
describe("emissions", function() {
|
||||
const SYNC_DATA = {
|
||||
next_batch: "s_5_3",
|
||||
presence: {
|
||||
events: [
|
||||
utils.mkPresence({
|
||||
user: "@foo:bar", name: "Foo Bar", presence: "online",
|
||||
}),
|
||||
],
|
||||
},
|
||||
rooms: {
|
||||
join: {
|
||||
"!erufh:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar", user: "@foo:bar", msg: "hmmm",
|
||||
}),
|
||||
],
|
||||
prev_batch: "s",
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: "!erufh:bar", mship: "join", user: "@foo:bar",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: "!erufh:bar",
|
||||
user: "@foo:bar",
|
||||
content: {
|
||||
creator: "@foo:bar",
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const NEXT_SYNC_DATA = {
|
||||
next_batch: "e_6_7",
|
||||
rooms: {
|
||||
join: {
|
||||
"!erufh:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar", user: "@foo:bar",
|
||||
msg: "ello ello",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar", user: "@foo:bar", msg: ":D",
|
||||
}),
|
||||
],
|
||||
},
|
||||
ephemeral: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.typing", room: "!erufh:bar", content: {
|
||||
user_ids: ["@foo:bar"],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it("should emit events from both the first and subsequent /sync calls",
|
||||
function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
let expectedEvents = [];
|
||||
expectedEvents = expectedEvents.concat(
|
||||
SYNC_DATA.presence.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
|
||||
);
|
||||
|
||||
client.on("event", function(event) {
|
||||
let found = false;
|
||||
for (let i = 0; i < expectedEvents.length; i++) {
|
||||
if (expectedEvents[i].event_id === event.getId()) {
|
||||
expectedEvents.splice(i, 1);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(found).toBe(
|
||||
true, "Unexpected 'event' emitted: " + event.getType(),
|
||||
);
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
|
||||
return Promise.all([
|
||||
// wait for two SYNCING events
|
||||
utils.syncPromise(client).then(() => {
|
||||
return utils.syncPromise(client);
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]).then(() => {
|
||||
expect(expectedEvents.length).toEqual(
|
||||
0, "Failed to see all events from /sync calls",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit User events", function(done) {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
let fired = false;
|
||||
client.on("User.presence", function(event, user) {
|
||||
fired = true;
|
||||
expect(user).toBeTruthy();
|
||||
expect(event).toBeTruthy();
|
||||
if (!user || !event) {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
|
||||
expect(user.presence).toEqual(
|
||||
SYNC_DATA.presence.events[0].content.presence,
|
||||
);
|
||||
});
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flushAllExpected().done(function() {
|
||||
expect(fired).toBe(true, "User.presence didn't fire.");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Room events", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
let roomInvokeCount = 0;
|
||||
let roomNameInvokeCount = 0;
|
||||
let timelineFireCount = 0;
|
||||
client.on("Room", function(room) {
|
||||
roomInvokeCount++;
|
||||
expect(room.roomId).toEqual("!erufh:bar");
|
||||
});
|
||||
client.on("Room.timeline", function(event, room) {
|
||||
timelineFireCount++;
|
||||
expect(room.roomId).toEqual("!erufh:bar");
|
||||
});
|
||||
client.on("Room.name", function(room) {
|
||||
roomNameInvokeCount++;
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 2),
|
||||
]).then(function() {
|
||||
expect(roomInvokeCount).toEqual(
|
||||
1, "Room fired wrong number of times.",
|
||||
);
|
||||
expect(roomNameInvokeCount).toEqual(
|
||||
1, "Room.name fired wrong number of times.",
|
||||
);
|
||||
expect(timelineFireCount).toEqual(
|
||||
3, "Room.timeline fired the wrong number of times",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit RoomState events", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
const roomStateEventTypes = [
|
||||
"m.room.member", "m.room.create",
|
||||
];
|
||||
let eventsInvokeCount = 0;
|
||||
let membersInvokeCount = 0;
|
||||
let newMemberInvokeCount = 0;
|
||||
client.on("RoomState.events", function(event, state) {
|
||||
eventsInvokeCount++;
|
||||
const index = roomStateEventTypes.indexOf(event.getType());
|
||||
expect(index).toNotEqual(
|
||||
-1, "Unexpected room state event type: " + event.getType(),
|
||||
);
|
||||
if (index >= 0) {
|
||||
roomStateEventTypes.splice(index, 1);
|
||||
}
|
||||
});
|
||||
client.on("RoomState.members", function(event, state, member) {
|
||||
membersInvokeCount++;
|
||||
expect(member.roomId).toEqual("!erufh:bar");
|
||||
expect(member.userId).toEqual("@foo:bar");
|
||||
expect(member.membership).toEqual("join");
|
||||
});
|
||||
client.on("RoomState.newMember", function(event, state, member) {
|
||||
newMemberInvokeCount++;
|
||||
expect(member.roomId).toEqual("!erufh:bar");
|
||||
expect(member.userId).toEqual("@foo:bar");
|
||||
expect(member.membership).toBeFalsy();
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 2),
|
||||
]).then(function() {
|
||||
expect(membersInvokeCount).toEqual(
|
||||
1, "RoomState.members fired wrong number of times",
|
||||
);
|
||||
expect(newMemberInvokeCount).toEqual(
|
||||
1, "RoomState.newMember fired wrong number of times",
|
||||
);
|
||||
expect(eventsInvokeCount).toEqual(
|
||||
2, "RoomState.events fired wrong number of times",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit RoomMember events", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
let typingInvokeCount = 0;
|
||||
let powerLevelInvokeCount = 0;
|
||||
let nameInvokeCount = 0;
|
||||
let membershipInvokeCount = 0;
|
||||
client.on("RoomMember.name", function(event, member) {
|
||||
nameInvokeCount++;
|
||||
});
|
||||
client.on("RoomMember.typing", function(event, member) {
|
||||
typingInvokeCount++;
|
||||
expect(member.typing).toBe(true);
|
||||
});
|
||||
client.on("RoomMember.powerLevel", function(event, member) {
|
||||
powerLevelInvokeCount++;
|
||||
});
|
||||
client.on("RoomMember.membership", function(event, member) {
|
||||
membershipInvokeCount++;
|
||||
expect(member.membership).toEqual("join");
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 2),
|
||||
]).then(function() {
|
||||
expect(typingInvokeCount).toEqual(
|
||||
1, "RoomMember.typing fired wrong number of times",
|
||||
);
|
||||
expect(powerLevelInvokeCount).toEqual(
|
||||
0, "RoomMember.powerLevel fired wrong number of times",
|
||||
);
|
||||
expect(nameInvokeCount).toEqual(
|
||||
0, "RoomMember.name fired wrong number of times",
|
||||
);
|
||||
expect(membershipInvokeCount).toEqual(
|
||||
1, "RoomMember.membership fired wrong number of times",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() {
|
||||
httpBackend.when("GET", "/sync").respond(401, { errcode: 'M_UNKNOWN_TOKEN' });
|
||||
|
||||
let sessionLoggedOutCount = 0;
|
||||
client.on("Session.logged_out", function(event, member) {
|
||||
sessionLoggedOutCount++;
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
|
||||
return httpBackend.flushAllExpected().then(function() {
|
||||
expect(sessionLoggedOutCount).toEqual(
|
||||
1, "Session.logged_out fired wrong number of times",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,360 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type HttpBackend from "matrix-mock-request";
|
||||
import {
|
||||
ClientEvent,
|
||||
HttpApiEvent,
|
||||
type IEvent,
|
||||
type MatrixClient,
|
||||
RoomEvent,
|
||||
RoomMemberEvent,
|
||||
RoomStateEvent,
|
||||
UserEvent,
|
||||
} from "../../src";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
describe("MatrixClient events", function () {
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: HttpBackend | undefined;
|
||||
|
||||
const setupTests = (): [MatrixClient, HttpBackend] => {
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
const client = testClient.client;
|
||||
const httpBackend = testClient.httpBackend;
|
||||
httpBackend!.when("GET", "/versions").respond(200, {});
|
||||
httpBackend!.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
|
||||
return [client!, httpBackend];
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
[client!, httpBackend] = setupTests();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
httpBackend?.verifyNoOutstandingExpectation();
|
||||
client?.stopClient();
|
||||
return httpBackend?.stop();
|
||||
});
|
||||
|
||||
describe("emissions", function () {
|
||||
const SYNC_DATA = {
|
||||
next_batch: "s_5_3",
|
||||
presence: {
|
||||
events: [
|
||||
utils.mkPresence({
|
||||
user: "@foo:bar",
|
||||
name: "Foo Bar",
|
||||
presence: "online",
|
||||
}),
|
||||
],
|
||||
},
|
||||
rooms: {
|
||||
join: {
|
||||
"!erufh:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar",
|
||||
user: "@foo:bar",
|
||||
msg: "hmmm",
|
||||
}),
|
||||
],
|
||||
prev_batch: "s",
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: "!erufh:bar",
|
||||
mship: KnownMembership.Join,
|
||||
user: "@foo:bar",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create",
|
||||
room: "!erufh:bar",
|
||||
user: "@foo:bar",
|
||||
content: {},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const NEXT_SYNC_DATA = {
|
||||
next_batch: "e_6_7",
|
||||
rooms: {
|
||||
join: {
|
||||
"!erufh:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar",
|
||||
user: "@foo:bar",
|
||||
msg: "ello ello",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar",
|
||||
user: "@foo:bar",
|
||||
msg: ":D",
|
||||
}),
|
||||
],
|
||||
},
|
||||
ephemeral: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.typing",
|
||||
room: "!erufh:bar",
|
||||
content: {
|
||||
user_ids: ["@foo:bar"],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it("should emit events from both the first and subsequent /sync calls", function () {
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
let expectedEvents: Partial<IEvent>[] = [];
|
||||
expectedEvents = expectedEvents.concat(
|
||||
SYNC_DATA.presence.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
|
||||
);
|
||||
|
||||
client!.on(ClientEvent.Event, function (event) {
|
||||
let found = false;
|
||||
for (let i = 0; i < expectedEvents.length; i++) {
|
||||
if (expectedEvents[i].event_id === event.getId()) {
|
||||
expectedEvents.splice(i, 1);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(found).toBe(true);
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
// wait for two SYNCING events
|
||||
utils.syncPromise(client!).then(() => {
|
||||
return utils.syncPromise(client!);
|
||||
}),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]).then(() => {
|
||||
expect(expectedEvents.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit User events", async () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
let fired = false;
|
||||
client!.on(UserEvent.Presence, function (event, user) {
|
||||
fired = true;
|
||||
expect(user).toBeTruthy();
|
||||
expect(event).toBeTruthy();
|
||||
if (!user || !event) {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
|
||||
expect(user.presence).toEqual(SYNC_DATA.presence.events[0]?.content?.presence);
|
||||
});
|
||||
client!.startClient();
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(fired).toBe(true);
|
||||
});
|
||||
|
||||
it("should emit User events when presence data is absent in first sync", async () => {
|
||||
const MODIFIED_SYNC_DATA: any = structuredClone(SYNC_DATA);
|
||||
delete MODIFIED_SYNC_DATA["presence"];
|
||||
const MODIFIED_NEXT_SYNC_DATA: any = structuredClone(NEXT_SYNC_DATA);
|
||||
MODIFIED_NEXT_SYNC_DATA.presence = {
|
||||
events: [
|
||||
utils.mkPresence({
|
||||
user: "@foo:bar",
|
||||
name: "Foo Bar",
|
||||
presence: "online",
|
||||
}),
|
||||
],
|
||||
};
|
||||
httpBackend!.when("GET", "/sync").respond(200, MODIFIED_SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, MODIFIED_NEXT_SYNC_DATA);
|
||||
let fired = false;
|
||||
client!.on(UserEvent.Presence, function (event, user) {
|
||||
fired = true;
|
||||
expect(user).toBeTruthy();
|
||||
expect(event).toBeTruthy();
|
||||
if (!user || !event) {
|
||||
return;
|
||||
}
|
||||
expect(event.event).toEqual(MODIFIED_NEXT_SYNC_DATA.presence.events[0]);
|
||||
expect(user.presence).toEqual(MODIFIED_NEXT_SYNC_DATA.presence.events[0]?.content?.presence);
|
||||
});
|
||||
client!.startClient();
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(fired).toBe(true);
|
||||
});
|
||||
|
||||
it("should emit Room events", function () {
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
let roomInvokeCount = 0;
|
||||
let roomNameInvokeCount = 0;
|
||||
let timelineFireCount = 0;
|
||||
client!.on(ClientEvent.Room, function (room) {
|
||||
roomInvokeCount++;
|
||||
expect(room.roomId).toEqual("!erufh:bar");
|
||||
});
|
||||
client!.on(RoomEvent.Timeline, function (event, room) {
|
||||
timelineFireCount++;
|
||||
expect(room?.roomId).toEqual("!erufh:bar");
|
||||
});
|
||||
client!.on(RoomEvent.Name, function (room) {
|
||||
roomNameInvokeCount++;
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 2)]).then(function () {
|
||||
expect(roomInvokeCount).toEqual(1);
|
||||
expect(roomNameInvokeCount).toEqual(1);
|
||||
expect(timelineFireCount).toEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit RoomState events", function () {
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
const roomStateEventTypes = ["m.room.member", "m.room.create"];
|
||||
let eventsInvokeCount = 0;
|
||||
let membersInvokeCount = 0;
|
||||
let newMemberInvokeCount = 0;
|
||||
client!.on(RoomStateEvent.Events, function (event, state) {
|
||||
eventsInvokeCount++;
|
||||
const index = roomStateEventTypes.indexOf(event.getType());
|
||||
expect(index).not.toEqual(-1);
|
||||
if (index >= 0) {
|
||||
roomStateEventTypes.splice(index, 1);
|
||||
}
|
||||
});
|
||||
client!.on(RoomStateEvent.Members, function (event, state, member) {
|
||||
membersInvokeCount++;
|
||||
expect(member.roomId).toEqual("!erufh:bar");
|
||||
expect(member.userId).toEqual("@foo:bar");
|
||||
expect(member.membership).toEqual(KnownMembership.Join);
|
||||
});
|
||||
client!.on(RoomStateEvent.NewMember, function (event, state, member) {
|
||||
newMemberInvokeCount++;
|
||||
expect(member.roomId).toEqual("!erufh:bar");
|
||||
expect(member.userId).toEqual("@foo:bar");
|
||||
expect(member.membership).toBeFalsy();
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 2)]).then(function () {
|
||||
expect(membersInvokeCount).toEqual(1);
|
||||
expect(newMemberInvokeCount).toEqual(1);
|
||||
expect(eventsInvokeCount).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit RoomMember events", function () {
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
let typingInvokeCount = 0;
|
||||
let powerLevelInvokeCount = 0;
|
||||
let nameInvokeCount = 0;
|
||||
let membershipInvokeCount = 0;
|
||||
client!.on(RoomMemberEvent.Name, function (event, member) {
|
||||
nameInvokeCount++;
|
||||
});
|
||||
client!.on(RoomMemberEvent.Typing, function (event, member) {
|
||||
typingInvokeCount++;
|
||||
expect(member.typing).toBe(true);
|
||||
});
|
||||
client!.on(RoomMemberEvent.PowerLevel, function (event, member) {
|
||||
powerLevelInvokeCount++;
|
||||
});
|
||||
client!.on(RoomMemberEvent.Membership, function (event, member) {
|
||||
membershipInvokeCount++;
|
||||
expect(member.membership).toEqual(KnownMembership.Join);
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 2)]).then(function () {
|
||||
expect(typingInvokeCount).toEqual(1);
|
||||
expect(powerLevelInvokeCount).toEqual(0);
|
||||
expect(nameInvokeCount).toEqual(0);
|
||||
expect(membershipInvokeCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function () {
|
||||
const error = { errcode: "M_UNKNOWN_TOKEN" };
|
||||
httpBackend!.when("GET", "/sync").respond(401, error);
|
||||
|
||||
let sessionLoggedOutCount = 0;
|
||||
client!.on(HttpApiEvent.SessionLoggedOut, function (errObj) {
|
||||
sessionLoggedOutCount++;
|
||||
expect(errObj.data).toEqual(error);
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
|
||||
return httpBackend!.flushAllExpected().then(function () {
|
||||
expect(sessionLoggedOutCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function () {
|
||||
const error = { errcode: "M_UNKNOWN_TOKEN", soft_logout: true };
|
||||
httpBackend!.when("GET", "/sync").respond(401, error);
|
||||
|
||||
let sessionLoggedOutCount = 0;
|
||||
client!.on(HttpApiEvent.SessionLoggedOut, function (errObj) {
|
||||
sessionLoggedOutCount++;
|
||||
expect(errObj.data).toEqual(error);
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
|
||||
return httpBackend!.flushAllExpected().then(function () {
|
||||
expect(sessionLoggedOutCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,769 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
import Promise from 'bluebird';
|
||||
const sdk = require("../..");
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const utils = require("../test-utils");
|
||||
const EventTimeline = sdk.EventTimeline;
|
||||
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
const userId = "@alice:localhost";
|
||||
const userName = "Alice";
|
||||
const accessToken = "aseukfgwef";
|
||||
const roomId = "!foo:bar";
|
||||
const otherUserId = "@bob:localhost";
|
||||
|
||||
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userId, name: userName,
|
||||
});
|
||||
|
||||
const ROOM_NAME_EVENT = utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: otherUserId,
|
||||
content: {
|
||||
name: "Old room name",
|
||||
},
|
||||
});
|
||||
|
||||
const INITIAL_SYNC_DATA = {
|
||||
next_batch: "s_5_3",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": { // roomId
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: otherUserId, msg: "hello",
|
||||
}),
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
ROOM_NAME_EVENT,
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "join",
|
||||
user: otherUserId, name: "Bob",
|
||||
}),
|
||||
USER_MEMBERSHIP_EVENT,
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomId, user: userId,
|
||||
content: {
|
||||
creator: userId,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const EVENTS = [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "we",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "could",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "be",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "heroes",
|
||||
}),
|
||||
];
|
||||
|
||||
// start the client, and wait for it to initialise
|
||||
function startClient(httpBackend, client) {
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA);
|
||||
|
||||
client.startClient();
|
||||
|
||||
// set up a promise which will resolve once the client is initialised
|
||||
const deferred = Promise.defer();
|
||||
client.on("sync", function(state) {
|
||||
console.log("sync", state);
|
||||
if (state != "SYNCING") {
|
||||
return;
|
||||
}
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
deferred.promise,
|
||||
]);
|
||||
}
|
||||
|
||||
describe("getEventTimeline support", function() {
|
||||
let httpBackend;
|
||||
let client;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
if (client) {
|
||||
client.stopClient();
|
||||
}
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
it("timeline support must be enabled to work", function(done) {
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
});
|
||||
|
||||
startClient(httpBackend, client,
|
||||
).then(function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
expect(function() {
|
||||
client.getEventTimeline(timelineSet, "event");
|
||||
}).toThrow();
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
it("timeline support works when enabled", function() {
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
timelineSupport: true,
|
||||
});
|
||||
|
||||
return startClient(httpBackend, client).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
expect(function() {
|
||||
client.getEventTimeline(timelineSet, "event");
|
||||
}).toNotThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("scrollback should be able to scroll back to before a gappy /sync",
|
||||
function(done) {
|
||||
// need a client with timelineSupport disabled to make this work
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
});
|
||||
let room;
|
||||
|
||||
startClient(httpBackend, client,
|
||||
).then(function() {
|
||||
room = client.getRoom(roomId);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[0],
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_5",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[1],
|
||||
],
|
||||
limited: true,
|
||||
prev_batch: "f_1_2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 2),
|
||||
]);
|
||||
}).then(function() {
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
expect(room.timeline[0].event).toEqual(EVENTS[1]);
|
||||
|
||||
httpBackend.when("GET", "/messages").respond(200, {
|
||||
chunk: [EVENTS[0]],
|
||||
start: "pagin_start",
|
||||
end: "pagin_end",
|
||||
});
|
||||
httpBackend.flush("/messages", 1);
|
||||
return client.scrollback(room);
|
||||
}).then(function() {
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
expect(room.timeline[0].event).toEqual(EVENTS[0]);
|
||||
expect(room.timeline[1].event).toEqual(EVENTS[1]);
|
||||
expect(room.oldState.paginationToken).toEqual("pagin_end");
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("MatrixClient event timelines", function() {
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
timelineSupport: true,
|
||||
});
|
||||
|
||||
return startClient(httpBackend, client);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
describe("getEventTimeline", function() {
|
||||
it("should create a new timeline for new events", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1%3Abar")
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token",
|
||||
events_before: [EVENTS[1], EVENTS[0]],
|
||||
event: EVENTS[2],
|
||||
events_after: [EVENTS[3]],
|
||||
state: [
|
||||
ROOM_NAME_EVENT,
|
||||
USER_MEMBERSHIP_EVENT,
|
||||
],
|
||||
end: "end_token",
|
||||
};
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
client.getEventTimeline(timelineSet, "event1:bar").then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(4);
|
||||
for (let i = 0; i < 4; i++) {
|
||||
expect(tl.getEvents()[i].event).toEqual(EVENTS[i]);
|
||||
expect(tl.getEvents()[i].sender.name).toEqual(userName);
|
||||
}
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("start_token");
|
||||
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
|
||||
.toEqual("end_token");
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return existing timeline for known events", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[0],
|
||||
],
|
||||
prev_batch: "f_1_2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync"),
|
||||
utils.syncPromise(client),
|
||||
]).then(function() {
|
||||
return client.getEventTimeline(timelineSet, EVENTS[0].event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[0]);
|
||||
expect(tl.getEvents()[1].sender.name).toEqual(userName);
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("f_1_1");
|
||||
// expect(tl.getPaginationToken(EventTimeline.FORWARDS))
|
||||
// .toEqual("s_5_4");
|
||||
});
|
||||
});
|
||||
|
||||
it("should update timelines where they overlap a previous /sync", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[3],
|
||||
],
|
||||
prev_batch: "f_1_2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[2].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token",
|
||||
events_before: [EVENTS[1]],
|
||||
event: EVENTS[2],
|
||||
events_after: [EVENTS[3]],
|
||||
end: "end_token",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
const deferred = Promise.defer();
|
||||
client.on("sync", function() {
|
||||
client.getEventTimeline(timelineSet, EVENTS[2].event_id,
|
||||
).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(4);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
|
||||
expect(tl.getEvents()[3].event).toEqual(EVENTS[3]);
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("start_token");
|
||||
// expect(tl.getPaginationToken(EventTimeline.FORWARDS))
|
||||
// .toEqual("s_5_4");
|
||||
}).done(() => deferred.resolve(),
|
||||
(e) => deferred.reject(e));
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
deferred.promise,
|
||||
]);
|
||||
});
|
||||
|
||||
it("should join timelines where they overlap a previous /context",
|
||||
function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
|
||||
// with context which joins them all up.
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[0].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token0",
|
||||
events_before: [],
|
||||
event: EVENTS[0],
|
||||
events_after: [],
|
||||
end: "end_token0",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[2].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token2",
|
||||
events_before: [],
|
||||
event: EVENTS[2],
|
||||
events_after: [],
|
||||
end: "end_token2",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[3].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token3",
|
||||
events_before: [],
|
||||
event: EVENTS[3],
|
||||
events_after: [],
|
||||
end: "end_token3",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[1].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token4",
|
||||
events_before: [EVENTS[0]],
|
||||
event: EVENTS[1],
|
||||
events_after: [EVENTS[2], EVENTS[3]],
|
||||
end: "end_token4",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
let tl0;
|
||||
let tl3;
|
||||
return Promise.all([
|
||||
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
|
||||
).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
tl0 = tl;
|
||||
return client.getEventTimeline(timelineSet, EVENTS[2].event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
return client.getEventTimeline(timelineSet, EVENTS[3].event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
tl3 = tl;
|
||||
return client.getEventTimeline(timelineSet, EVENTS[1].event_id);
|
||||
}).then(function(tl) {
|
||||
// we expect it to get merged in with event 2
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
|
||||
expect(tl.getNeighbouringTimeline(EventTimeline.BACKWARDS))
|
||||
.toBe(tl0);
|
||||
expect(tl.getNeighbouringTimeline(EventTimeline.FORWARDS))
|
||||
.toBe(tl3);
|
||||
expect(tl0.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("start_token0");
|
||||
expect(tl0.getPaginationToken(EventTimeline.FORWARDS))
|
||||
.toBe(null);
|
||||
expect(tl3.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toBe(null);
|
||||
expect(tl3.getPaginationToken(EventTimeline.FORWARDS))
|
||||
.toEqual("end_token3");
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should fail gracefully if there is no event field", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
|
||||
// with context which joins them all up.
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1")
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token",
|
||||
events_before: [],
|
||||
events_after: [],
|
||||
end: "end_token",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
client.getEventTimeline(timelineSet, "event1",
|
||||
).then(function(tl) {
|
||||
// could do with a fail()
|
||||
expect(true).toBeFalsy();
|
||||
}, function(e) {
|
||||
expect(String(e)).toMatch(/'event'/);
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("paginateEventTimeline", function() {
|
||||
it("should allow you to paginate backwards", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[0].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token0",
|
||||
events_before: [],
|
||||
event: EVENTS[0],
|
||||
events_after: [],
|
||||
end: "end_token0",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
|
||||
.check(function(req) {
|
||||
const params = req.queryParams;
|
||||
expect(params.dir).toEqual("b");
|
||||
expect(params.from).toEqual("start_token0");
|
||||
expect(params.limit).toEqual(30);
|
||||
}).respond(200, function() {
|
||||
return {
|
||||
chunk: [EVENTS[1], EVENTS[2]],
|
||||
end: "start_token1",
|
||||
};
|
||||
});
|
||||
|
||||
let tl;
|
||||
return Promise.all([
|
||||
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
|
||||
).then(function(tl0) {
|
||||
tl = tl0;
|
||||
return client.paginateEventTimeline(tl, {backwards: true});
|
||||
}).then(function(success) {
|
||||
expect(success).toBeTruthy();
|
||||
expect(tl.getEvents().length).toEqual(3);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[2]);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
|
||||
expect(tl.getEvents()[2].event).toEqual(EVENTS[0]);
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("start_token1");
|
||||
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
|
||||
.toEqual("end_token0");
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
it("should allow you to paginate forwards", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[0].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token0",
|
||||
events_before: [],
|
||||
event: EVENTS[0],
|
||||
events_after: [],
|
||||
end: "end_token0",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
|
||||
.check(function(req) {
|
||||
const params = req.queryParams;
|
||||
expect(params.dir).toEqual("f");
|
||||
expect(params.from).toEqual("end_token0");
|
||||
expect(params.limit).toEqual(20);
|
||||
}).respond(200, function() {
|
||||
return {
|
||||
chunk: [EVENTS[1], EVENTS[2]],
|
||||
end: "end_token1",
|
||||
};
|
||||
});
|
||||
|
||||
let tl;
|
||||
return Promise.all([
|
||||
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
|
||||
).then(function(tl0) {
|
||||
tl = tl0;
|
||||
return client.paginateEventTimeline(
|
||||
tl, {backwards: false, limit: 20});
|
||||
}).then(function(success) {
|
||||
expect(success).toBeTruthy();
|
||||
expect(tl.getEvents().length).toEqual(3);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[0]);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
|
||||
expect(tl.getEvents()[2].event).toEqual(EVENTS[2]);
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("start_token0");
|
||||
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
|
||||
.toEqual("end_token1");
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("event timeline for sent events", function() {
|
||||
const TXN_ID = "txn1";
|
||||
const event = utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "a body",
|
||||
});
|
||||
event.unsigned = {transaction_id: TXN_ID};
|
||||
|
||||
beforeEach(function() {
|
||||
// set up handlers for both the message send, and the
|
||||
// /sync
|
||||
httpBackend.when("PUT", "/send/m.room.message/" + TXN_ID)
|
||||
.respond(200, {
|
||||
event_id: event.event_id,
|
||||
});
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
event,
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should work when /send returns before /sync", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
return Promise.all([
|
||||
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
|
||||
expect(res.event_id).toEqual(event.event_id);
|
||||
return client.getEventTimeline(timelineSet, event.event_id);
|
||||
}).then(function(tl) {
|
||||
// 2 because the initial sync contained an event
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
|
||||
|
||||
// now let the sync complete, and check it again
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]);
|
||||
}).then(function() {
|
||||
return client.getEventTimeline(timelineSet, event.event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].event).toEqual(event);
|
||||
}),
|
||||
|
||||
httpBackend.flush("/send/m.room.message/" + TXN_ID, 1),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should work when /send returns after /sync", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
return Promise.all([
|
||||
// initiate the send, and set up checks to be done when it completes
|
||||
// - but note that it won't complete until after the /sync does, below.
|
||||
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
|
||||
console.log("sendTextMessage completed");
|
||||
expect(res.event_id).toEqual(event.event_id);
|
||||
return client.getEventTimeline(timelineSet, event.event_id);
|
||||
}).then(function(tl) {
|
||||
console.log("getEventTimeline completed (2)");
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
|
||||
}),
|
||||
|
||||
Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(function() {
|
||||
return client.getEventTimeline(timelineSet, event.event_id);
|
||||
}).then(function(tl) {
|
||||
console.log("getEventTimeline completed (1)");
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].event).toEqual(event);
|
||||
|
||||
// now let the send complete.
|
||||
return httpBackend.flush("/send/m.room.message/" + TXN_ID, 1);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should handle gappy syncs after redactions", function(done) {
|
||||
// https://github.com/vector-im/vector-web/issues/1389
|
||||
|
||||
// a state event, followed by a redaction thereof
|
||||
const event = utils.mkMembership({
|
||||
room: roomId, mship: "join", user: otherUserId,
|
||||
});
|
||||
const redaction = utils.mkEvent({
|
||||
type: "m.room.redaction",
|
||||
room_id: roomId,
|
||||
sender: otherUserId,
|
||||
content: {},
|
||||
});
|
||||
redaction.redacts = event.event_id;
|
||||
|
||||
const syncData = {
|
||||
next_batch: "batch1",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
event,
|
||||
redaction,
|
||||
],
|
||||
limited: false,
|
||||
},
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client),
|
||||
]).then(function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const tl = room.getLiveTimeline();
|
||||
expect(tl.getEvents().length).toEqual(3);
|
||||
expect(tl.getEvents()[1].isRedacted()).toBe(true);
|
||||
|
||||
const sync2 = {
|
||||
next_batch: "batch2",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
sync2.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: otherUserId, msg: "world",
|
||||
}),
|
||||
],
|
||||
limited: true,
|
||||
prev_batch: "newerTok",
|
||||
},
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, sync2);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client),
|
||||
]);
|
||||
}).then(function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const tl = room.getLiveTimeline();
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,419 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const publicGlobals = require("../../lib/matrix");
|
||||
const Room = publicGlobals.Room;
|
||||
const MatrixInMemoryStore = publicGlobals.MatrixInMemoryStore;
|
||||
const Filter = publicGlobals.Filter;
|
||||
const utils = require("../test-utils");
|
||||
const MockStorageApi = require("../MockStorageApi");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("MatrixClient", function() {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
let store = null;
|
||||
let sessionStore = null;
|
||||
const userId = "@alice:localhost";
|
||||
const accessToken = "aseukfgwef";
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
store = new MatrixInMemoryStore();
|
||||
|
||||
const mockStorage = new MockStorageApi();
|
||||
sessionStore = new sdk.WebStorageSessionStore(mockStorage);
|
||||
|
||||
sdk.request(httpBackend.requestFn);
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
deviceId: "aliceDevice",
|
||||
accessToken: accessToken,
|
||||
store: store,
|
||||
sessionStore: sessionStore,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
describe("uploadContent", function() {
|
||||
const buf = new Buffer('hello world');
|
||||
it("should upload the file", function(done) {
|
||||
httpBackend.when(
|
||||
"POST", "/_matrix/media/v1/upload",
|
||||
).check(function(req) {
|
||||
expect(req.rawData).toEqual(buf);
|
||||
expect(req.queryParams.filename).toEqual("hi.txt");
|
||||
if (!(req.queryParams.access_token == accessToken ||
|
||||
req.headers["Authorization"] == "Bearer " + accessToken)) {
|
||||
expect(true).toBe(false);
|
||||
}
|
||||
expect(req.headers["Content-Type"]).toEqual("text/plain");
|
||||
expect(req.opts.json).toBeFalsy();
|
||||
expect(req.opts.timeout).toBe(undefined);
|
||||
}).respond(200, "content", true);
|
||||
|
||||
const prom = client.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
type: "text/plain",
|
||||
});
|
||||
|
||||
expect(prom).toBeTruthy();
|
||||
|
||||
const uploads = client.getCurrentUploads();
|
||||
expect(uploads.length).toEqual(1);
|
||||
expect(uploads[0].promise).toBe(prom);
|
||||
expect(uploads[0].loaded).toEqual(0);
|
||||
|
||||
prom.then(function(response) {
|
||||
// for backwards compatibility, we return the raw JSON
|
||||
expect(response).toEqual("content");
|
||||
|
||||
const uploads = client.getCurrentUploads();
|
||||
expect(uploads.length).toEqual(0);
|
||||
}).nodeify(done);
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
|
||||
it("should parse the response if rawResponse=false", function(done) {
|
||||
httpBackend.when(
|
||||
"POST", "/_matrix/media/v1/upload",
|
||||
).check(function(req) {
|
||||
expect(req.opts.json).toBeFalsy();
|
||||
}).respond(200, { "content_uri": "uri" });
|
||||
|
||||
client.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
type: "text/plain",
|
||||
}, {
|
||||
rawResponse: false,
|
||||
}).then(function(response) {
|
||||
expect(response.content_uri).toEqual("uri");
|
||||
}).nodeify(done);
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
|
||||
it("should parse errors into a MatrixError", function(done) {
|
||||
httpBackend.when(
|
||||
"POST", "/_matrix/media/v1/upload",
|
||||
).check(function(req) {
|
||||
expect(req.rawData).toEqual(buf);
|
||||
expect(req.opts.json).toBeFalsy();
|
||||
}).respond(400, {
|
||||
"errcode": "M_SNAFU",
|
||||
"error": "broken",
|
||||
});
|
||||
|
||||
client.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
type: "text/plain",
|
||||
}).then(function(response) {
|
||||
throw Error("request not failed");
|
||||
}, function(error) {
|
||||
expect(error.httpStatus).toEqual(400);
|
||||
expect(error.errcode).toEqual("M_SNAFU");
|
||||
expect(error.message).toEqual("broken");
|
||||
}).nodeify(done);
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
|
||||
it("should return a promise which can be cancelled", function(done) {
|
||||
const prom = client.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
type: "text/plain",
|
||||
});
|
||||
|
||||
const uploads = client.getCurrentUploads();
|
||||
expect(uploads.length).toEqual(1);
|
||||
expect(uploads[0].promise).toBe(prom);
|
||||
expect(uploads[0].loaded).toEqual(0);
|
||||
|
||||
prom.then(function(response) {
|
||||
throw Error("request not aborted");
|
||||
}, function(error) {
|
||||
expect(error).toEqual("aborted");
|
||||
|
||||
const uploads = client.getCurrentUploads();
|
||||
expect(uploads.length).toEqual(0);
|
||||
}).nodeify(done);
|
||||
|
||||
const r = client.cancelUpload(prom);
|
||||
expect(r).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("joinRoom", function() {
|
||||
it("should no-op if you've already joined a room", function() {
|
||||
const roomId = "!foo:bar";
|
||||
const room = new Room(roomId, userId);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userId, room: roomId, mship: "join", event: true,
|
||||
}),
|
||||
]);
|
||||
store.storeRoom(room);
|
||||
client.joinRoom(roomId);
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFilter", function() {
|
||||
const filterId = "f1lt3r1d";
|
||||
|
||||
it("should return a filter from the store if allowCached", function(done) {
|
||||
const filter = Filter.fromJson(userId, filterId, {
|
||||
event_format: "client",
|
||||
});
|
||||
store.storeFilter(filter);
|
||||
client.getFilter(userId, filterId, true).done(function(gotFilter) {
|
||||
expect(gotFilter).toEqual(filter);
|
||||
done();
|
||||
});
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
});
|
||||
|
||||
it("should do an HTTP request if !allowCached even if one exists",
|
||||
function(done) {
|
||||
const httpFilterDefinition = {
|
||||
event_format: "federation",
|
||||
};
|
||||
|
||||
httpBackend.when(
|
||||
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId,
|
||||
).respond(200, httpFilterDefinition);
|
||||
|
||||
const storeFilter = Filter.fromJson(userId, filterId, {
|
||||
event_format: "client",
|
||||
});
|
||||
store.storeFilter(storeFilter);
|
||||
client.getFilter(userId, filterId, false).done(function(gotFilter) {
|
||||
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
|
||||
done();
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
|
||||
it("should do an HTTP request if nothing is in the cache and then store it",
|
||||
function(done) {
|
||||
const httpFilterDefinition = {
|
||||
event_format: "federation",
|
||||
};
|
||||
expect(store.getFilter(userId, filterId)).toBe(null);
|
||||
|
||||
httpBackend.when(
|
||||
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId,
|
||||
).respond(200, httpFilterDefinition);
|
||||
client.getFilter(userId, filterId, true).done(function(gotFilter) {
|
||||
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
|
||||
expect(store.getFilter(userId, filterId)).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createFilter", function() {
|
||||
const filterId = "f1llllllerid";
|
||||
|
||||
it("should do an HTTP request and then store the filter", function(done) {
|
||||
expect(store.getFilter(userId, filterId)).toBe(null);
|
||||
|
||||
const filterDefinition = {
|
||||
event_format: "client",
|
||||
};
|
||||
|
||||
httpBackend.when(
|
||||
"POST", "/user/" + encodeURIComponent(userId) + "/filter",
|
||||
).check(function(req) {
|
||||
expect(req.data).toEqual(filterDefinition);
|
||||
}).respond(200, {
|
||||
filter_id: filterId,
|
||||
});
|
||||
|
||||
client.createFilter(filterDefinition).done(function(gotFilter) {
|
||||
expect(gotFilter.getDefinition()).toEqual(filterDefinition);
|
||||
expect(store.getFilter(userId, filterId)).toEqual(gotFilter);
|
||||
done();
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
});
|
||||
|
||||
describe("searching", function() {
|
||||
const response = {
|
||||
search_categories: {
|
||||
room_events: {
|
||||
count: 24,
|
||||
results: {
|
||||
"$flibble:localhost": {
|
||||
rank: 0.1,
|
||||
result: {
|
||||
type: "m.room.message",
|
||||
user_id: "@alice:localhost",
|
||||
room_id: "!feuiwhf:localhost",
|
||||
content: {
|
||||
body: "a result",
|
||||
msgtype: "m.text",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it("searchMessageText should perform a /search for room_events", function(done) {
|
||||
client.searchMessageText({
|
||||
query: "monkeys",
|
||||
});
|
||||
httpBackend.when("POST", "/search").check(function(req) {
|
||||
expect(req.data).toEqual({
|
||||
search_categories: {
|
||||
room_events: {
|
||||
search_term: "monkeys",
|
||||
},
|
||||
},
|
||||
});
|
||||
}).respond(200, response);
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("downloadKeys", function() {
|
||||
if (!sdk.CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
return client.initCrypto();
|
||||
});
|
||||
|
||||
it("should do an HTTP request and then store the keys", function(done) {
|
||||
const ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78";
|
||||
// ed25519key = client.getDeviceEd25519Key();
|
||||
const borisKeys = {
|
||||
dev1: {
|
||||
algorithms: ["1"],
|
||||
device_id: "dev1",
|
||||
keys: { "ed25519:dev1": ed25519key },
|
||||
signatures: {
|
||||
boris: {
|
||||
"ed25519:dev1":
|
||||
"RAhmbNDq1efK3hCpBzZDsKoGSsrHUxb25NW5/WbEV9R" +
|
||||
"JVwLdP032mg5QsKt/pBDUGtggBcnk43n3nBWlA88WAw",
|
||||
},
|
||||
},
|
||||
unsigned: { "abc": "def" },
|
||||
user_id: "boris",
|
||||
},
|
||||
};
|
||||
const chazKeys = {
|
||||
dev2: {
|
||||
algorithms: ["2"],
|
||||
device_id: "dev2",
|
||||
keys: { "ed25519:dev2": ed25519key },
|
||||
signatures: {
|
||||
chaz: {
|
||||
"ed25519:dev2":
|
||||
"FwslH/Q7EYSb7swDJbNB5PSzcbEO1xRRBF1riuijqvL" +
|
||||
"EkrK9/XVN8jl4h7thGuRITQ01siBQnNmMK9t45QfcCQ",
|
||||
},
|
||||
},
|
||||
unsigned: { "ghi": "def" },
|
||||
user_id: "chaz",
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
function sign(o) {
|
||||
var anotherjson = require('another-json');
|
||||
var b = JSON.parse(JSON.stringify(o));
|
||||
delete(b.signatures);
|
||||
delete(b.unsigned);
|
||||
return client._crypto._olmDevice.sign(anotherjson.stringify(b));
|
||||
};
|
||||
|
||||
console.log("Ed25519: " + ed25519key);
|
||||
console.log("boris:", sign(borisKeys.dev1));
|
||||
console.log("chaz:", sign(chazKeys.dev2));
|
||||
*/
|
||||
|
||||
httpBackend.when("POST", "/keys/query").check(function(req) {
|
||||
expect(req.data).toEqual({device_keys: {
|
||||
'boris': {},
|
||||
'chaz': {},
|
||||
}});
|
||||
}).respond(200, {
|
||||
device_keys: {
|
||||
boris: borisKeys,
|
||||
chaz: chazKeys,
|
||||
},
|
||||
});
|
||||
|
||||
client.downloadKeys(["boris", "chaz"]).then(function(res) {
|
||||
assertObjectContains(res.boris.dev1, {
|
||||
verified: 0, // DeviceVerification.UNVERIFIED
|
||||
keys: { "ed25519:dev1": ed25519key },
|
||||
algorithms: ["1"],
|
||||
unsigned: { "abc": "def" },
|
||||
});
|
||||
|
||||
assertObjectContains(res.chaz.dev2, {
|
||||
verified: 0, // DeviceVerification.UNVERIFIED
|
||||
keys: { "ed25519:dev2": ed25519key },
|
||||
algorithms: ["2"],
|
||||
unsigned: { "ghi": "def" },
|
||||
});
|
||||
}).nodeify(done);
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteDevice", function() {
|
||||
const auth = {a: 1};
|
||||
it("should pass through an auth dict", function(done) {
|
||||
httpBackend.when(
|
||||
"DELETE", "/_matrix/client/unstable/devices/my_device",
|
||||
).check(function(req) {
|
||||
expect(req.data).toEqual({auth: auth});
|
||||
}).respond(200);
|
||||
|
||||
client.deleteDevice(
|
||||
"my_device", auth,
|
||||
).nodeify(done);
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function assertObjectContains(obj, expected) {
|
||||
for (const k in expected) {
|
||||
if (expected.hasOwnProperty(k)) {
|
||||
expect(obj[k]).toEqual(expected[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user