Compare commits
485 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b6369cc2bd | |||
| e6e079f487 | |||
| 83a1e07380 | |||
| 7f3123ed65 | |||
| 5a88a6c62a | |||
| 12cecbdcf1 | |||
| c17deb0806 | |||
| 31c4f6c16b | |||
| 22271d22f8 | |||
| 9d3ac66cf8 | |||
| a4ad4ed2cf | |||
| 7fd55a61bf | |||
| 847766c114 | |||
| c8c39052a7 | |||
| 6592b2c205 | |||
| fc91153be4 | |||
| 5511a6ef8c | |||
| 19e02e894f | |||
| c54d61e158 | |||
| 44da9040f4 | |||
| 995f5bf7d7 | |||
| ad16b26247 | |||
| aaf3702c66 | |||
| 74147b9943 | |||
| 815370c5f9 | |||
| a01d8e3174 | |||
| 007b7dd242 | |||
| 77d6def1cc | |||
| b318a77ece | |||
| 1a90259326 | |||
| 3702ac56f4 | |||
| af4811b327 | |||
| f46ecf970c | |||
| dd98d7eb2c | |||
| f3dc1c4ca2 | |||
| 305b83f8ea | |||
| acc488da64 | |||
| 7217f83db9 | |||
| 37ea905faa | |||
| 78de55b835 | |||
| cb410f463a | |||
| 72f9d5e6f9 | |||
| c389de98f3 | |||
| 20745dc9ac | |||
| 9410902049 | |||
| a6badbb7fa | |||
| 0b65b199e3 | |||
| c1138bc085 | |||
| c0f7df8c3b | |||
| e085609572 | |||
| 0a4f86a79e | |||
| 5d6ff6c7f9 | |||
| ffcdfe166e | |||
| e1aa7d335b | |||
| 29643e745c | |||
| 54622ce424 | |||
| ca2ae24d46 | |||
| d4601d9910 | |||
| 2ced5e1aa4 | |||
| 45e19e51c1 | |||
| 3f1c3392d4 | |||
| f86f67f5a5 | |||
| 4dcf54f448 | |||
| d43e664594 | |||
| 0e322848f9 | |||
| b454318684 | |||
| 692f1d49b9 | |||
| b40cf75c9d | |||
| ba6a001d67 | |||
| d0c71ec516 | |||
| 67f343d6f0 | |||
| a7f0ba97cd | |||
| 54d11e1745 | |||
| 14744fd4dc | |||
| 9b0919350c | |||
| b53ad2c081 | |||
| 6222d238e4 | |||
| c6ee258789 | |||
| a584324a0d | |||
| 059b07cfa0 | |||
| b628cabe58 | |||
| b7d925f5ec | |||
| 1c901e3137 | |||
| bd4589fcc4 | |||
| 0fbd0b3685 | |||
| 1646ea05bc | |||
| 885ec1fc73 | |||
| df2b65f111 | |||
| f09853ccb1 | |||
| 6c543382e6 | |||
| 52932f59ab | |||
| 4f63ff21ea | |||
| c8dc71eb69 | |||
| 2dda837db6 | |||
| fff4cdab7c | |||
| 4f00566b9f | |||
| c1a3b95073 | |||
| 38adbaf923 | |||
| 9459a95134 | |||
| 433b7afd71 | |||
| 777cf1f135 | |||
| 8235b65d71 | |||
| 4a33e584b0 | |||
| 6ee185e93f | |||
| 5df9705bae | |||
| 76458d3a40 | |||
| 27bb79a29a | |||
| 82d942dcc5 | |||
| ce6d0e2cb1 | |||
| db49cd8d13 | |||
| 42b08eca57 | |||
| a92c148f15 | |||
| 6cd60e32dc | |||
| b6633ad4b0 | |||
| e4dd7bcc87 | |||
| b6e97fcecb | |||
| dee2b60c3d | |||
| a07fe44565 | |||
| 0a35f2e2c7 | |||
| 32d535c2b1 | |||
| 7fb313c17c | |||
| d8f6449422 | |||
| c043e36f50 | |||
| e6524239bd | |||
| daed4b9dcc | |||
| 135d2da143 | |||
| fef53be5b4 | |||
| 7ec726e10b | |||
| 476f6f78b1 | |||
| cb8123dec7 | |||
| 6729c7d421 | |||
| 7ddd198df8 | |||
| 81681f4090 | |||
| 94072a096d | |||
| b9cccf9109 | |||
| f0d4ef7f99 | |||
| 849e3d67c2 | |||
| 4c6e1e5c21 | |||
| 9ff6b357fc | |||
| 77ef8558bd | |||
| d979302e9b | |||
| dbdaa1540a | |||
| 13c751c060 | |||
| 87115d181d | |||
| 5679c86ca6 | |||
| 4cd50e4871 | |||
| 0f1012278a | |||
| 0d211dfbad | |||
| 384116c8f5 | |||
| c374ba2367 | |||
| 450ff00c3e | |||
| b4ab7fc0b3 | |||
| 35f697a04b | |||
| 193d8a429a | |||
| 8cd5aac128 | |||
| eddd0cafe8 | |||
| 5a0787349d | |||
| c57c8978cf | |||
| dfe535bc07 | |||
| 3c33c422e6 | |||
| d521f97411 | |||
| c0a5299704 | |||
| ce3b72c850 | |||
| 935517746a | |||
| e48d919cd4 | |||
| ab39ee37d6 | |||
| af6f9d49f4 | |||
| a2981efac3 | |||
| 4625ed73cf | |||
| 6f7a72d69e | |||
| 2a0ffe1223 | |||
| 72a6ec0dd3 | |||
| 72b89fde6e | |||
| c400dd4ff8 | |||
| f41b7706e4 | |||
| de694459be | |||
| 6fc9827b10 | |||
| f52c5eb667 | |||
| c05cb3ad2b | |||
| 586a313c8d | |||
| c605310b87 | |||
| 41cee6f1cc | |||
| 3e1e99f8e5 | |||
| 276849f068 | |||
| 37118991f5 | |||
| 00629e6dc9 | |||
| 02f6a09bcf | |||
| 36a6117ee2 | |||
| aebe26db96 | |||
| 60e175a0e0 | |||
| d950cda05c | |||
| 83c848093f | |||
| fa6f70f708 | |||
| 98d119d6e1 | |||
| aca51fd8a3 | |||
| c78631bdee | |||
| 0d6a93b5f6 | |||
| 40ecfa7932 | |||
| d656b848f8 | |||
| 0981652de4 | |||
| db32420d16 | |||
| d5b82e343a | |||
| 965f4fb13b | |||
| 9e1b126854 | |||
| c527f85fb1 | |||
| 4a294c9dd3 | |||
| be94f5ea93 | |||
| 5f9369abee | |||
| e7a7ec0673 | |||
| 92cd84fc0c | |||
| 45e56f8cc3 | |||
| e95947dc73 | |||
| 448a5c9a77 | |||
| 9589a97952 | |||
| 2566c40e96 | |||
| 099cac0162 | |||
| e4cf5b26ee | |||
| c698317f3f | |||
| e8f682f452 | |||
| 020743141b | |||
| 5f5a9b1a43 | |||
| 3334c01191 | |||
| 0b8de251bf | |||
| 88ce017333 | |||
| 471f174889 | |||
| c0dacb5037 | |||
| 2cc51e0db7 | |||
| 22c5999fed | |||
| b711781f16 | |||
| 8ba2d257ae | |||
| 9e2e144530 | |||
| 38a6949e5d | |||
| e876482e62 | |||
| 544b1c6742 | |||
| 984dd26a13 | |||
| bdb91b3806 | |||
| 9a15094374 | |||
| e980c88901 | |||
| 6ea2885796 | |||
| ca5ac79927 | |||
| f9672cf307 | |||
| e7493fd417 | |||
| f553854730 | |||
| c89bbf4bf5 | |||
| ebcb26f1b3 | |||
| 5b4263bf55 | |||
| df9ffdc408 | |||
| 70449ea003 | |||
| 9192b876d2 | |||
| 04d0d61a0e | |||
| 404f8e130e | |||
| b97b862fb6 | |||
| 5e766978b8 | |||
| 34ef7bc64a | |||
| 18e2052af2 | |||
| aa0d3bd1f5 | |||
| 942a28ddf6 | |||
| 87791cd391 | |||
| 38e54ae7f2 | |||
| acef1d7dd0 | |||
| da615fd512 | |||
| f4f05550ef | |||
| f475251ddd | |||
| 83f61c96f3 | |||
| 85a6a552b5 | |||
| 9702e8a5fa | |||
| d82c041b99 | |||
| 8d9cd0fcb3 | |||
| 96ba061732 | |||
| ee4cbd1ec9 | |||
| 2a0dc39eec | |||
| 6e25b13312 | |||
| 94c5e37570 | |||
| 09fee4a2d9 | |||
| 49994ac4fc | |||
| e68cabc70e | |||
| c819ac634f | |||
| 17f5ab4191 | |||
| e270f075a4 | |||
| 0ef6c2e35f | |||
| 7a249e3ef5 | |||
| 353d6bab47 | |||
| 7f21f569d5 | |||
| fa5eae70dd | |||
| 3db056ad3e | |||
| a2a127d9a4 | |||
| d12bccd211 | |||
| d8e597ccdf | |||
| c801690e28 | |||
| b4fe00a3a8 | |||
| d42e2fe2c0 | |||
| 4a4465b9fc | |||
| 1a78301adb | |||
| bbf7020755 | |||
| 592fb0cf10 | |||
| 015eb5d5c4 | |||
| 42fef0e7aa | |||
| 28f3169a28 | |||
| d8285aad00 | |||
| eeacf8c22c | |||
| ee995cb39b | |||
| 7529af43e4 | |||
| 3fac6d7180 | |||
| 487bfc88ef | |||
| c91617a799 | |||
| 87bf115967 | |||
| 18bb5c3079 | |||
| f3f9e41787 | |||
| 7993dd7630 | |||
| bef557976b | |||
| 549f9b7e29 | |||
| 06d9d6207c | |||
| e336aceaba | |||
| fcc4b71f06 | |||
| d1a62eddfc | |||
| ffbd10a7b8 | |||
| d0e37ee323 | |||
| 96ef535ebb | |||
| 0683133d5b | |||
| 64c3ac55a4 | |||
| 5f06df8a87 | |||
| 3291846714 | |||
| 139904f297 | |||
| c2fe2ab270 | |||
| 4e26f29032 | |||
| 31391121dc | |||
| 7d48a8394d | |||
| 28da62c01c | |||
| e880cece93 | |||
| 97e8fcea75 | |||
| f28cb48fe1 | |||
| a2e255c2c9 | |||
| 74c5a20371 | |||
| 4b87907b92 | |||
| f76f708c96 | |||
| 17f7dc5463 | |||
| b253ad9e81 | |||
| c1f56ba3c4 | |||
| 7998817f7e | |||
| bdc12a2544 | |||
| 5a92597abd | |||
| 6f695c1b82 | |||
| d99428f2c1 | |||
| 4c9648a23b | |||
| 8c5f88c4a7 | |||
| 923e9c4ada | |||
| 13d62e71b6 | |||
| 32aca09f47 | |||
| 067ac62271 | |||
| 841e6e999d | |||
| a48546f60d | |||
| 2f09e9641c | |||
| f46355e7c0 | |||
| 53397ee0d1 | |||
| 5a83635ef5 | |||
| 56c0c9be4d | |||
| 24406d2411 | |||
| aeeed6ecd7 | |||
| 9f3f9990ef | |||
| 119ce2e46f | |||
| fc8a867e8e | |||
| b4d8c0b603 | |||
| 3b0d1b2696 | |||
| 5110e0b91e | |||
| 305de54106 | |||
| 0555f9db1c | |||
| 159e825877 | |||
| 8131b3900d | |||
| 431d7a0933 | |||
| e9b52e23d2 | |||
| 0148ad0766 | |||
| 213f1134b6 | |||
| 50e6a8f6b1 | |||
| 4a82e1bf05 | |||
| 843973c4da | |||
| debeb66d6f | |||
| 015d0f9135 | |||
| 5c8e7f2be0 | |||
| 411b5f111c | |||
| 2d231c0ae2 | |||
| ec37eb8b6f | |||
| 1cdcebb5db | |||
| a0f6eea363 | |||
| 18b1a44df7 | |||
| 4b6b1599a2 | |||
| a582b19435 | |||
| 4a8c3d273f | |||
| 8dc608d917 | |||
| 7ef38ed1b2 | |||
| 593f62c1c4 | |||
| 04d674b8c7 | |||
| 27eb88f4a1 | |||
| 1409a4f814 | |||
| 8232896c85 | |||
| e2ed80ffa0 | |||
| 8ac3841a2f | |||
| ba57736bf6 | |||
| 8be4ca909e | |||
| 0d964523a9 | |||
| bb504bc001 | |||
| 326aec9f9e | |||
| 688327dab5 | |||
| 3f4522ba88 | |||
| 625983a2b2 | |||
| 137fd2bd40 | |||
| 1e65bfd316 | |||
| 5da072712d | |||
| 529d61b5f4 | |||
| 5111ca622a | |||
| f627507b86 | |||
| aee4459201 | |||
| 1a824750dd | |||
| 73cb5e1ee9 | |||
| 96bde1f706 | |||
| 5251dcf67f | |||
| ce0b0ea182 | |||
| 7a142e9102 | |||
| f85aa44f28 | |||
| efbf252e22 | |||
| d873f14b6d | |||
| cf1ba12232 | |||
| df208e4de8 | |||
| d8ef7f9f63 | |||
| 2515ba31a0 | |||
| 715c4577d0 | |||
| a2f23900c9 | |||
| e9e65cf484 | |||
| 205c80ea28 | |||
| 678023717b | |||
| b535969845 | |||
| 027bc6bfc9 | |||
| 71ca424712 | |||
| 3280394bf9 | |||
| fc07530434 | |||
| f592d4dbc5 | |||
| 96f48929ac | |||
| 454da84f6e | |||
| 89bda6c2e5 | |||
| ac70dcfc91 | |||
| 9c7cb3cbea | |||
| d8d7bd548f | |||
| 55ef57ead8 | |||
| 9996afed03 | |||
| 61a80a11c9 | |||
| 6a8e8ed0a6 | |||
| 5895ce32fa | |||
| fe0a268991 | |||
| 7f189b0abd | |||
| 6e07c9e900 | |||
| bbeea51a36 | |||
| 151b54ed65 | |||
| 18986cb33a | |||
| aef5d73de4 | |||
| e4fc1f3628 | |||
| 8b1c173659 | |||
| f0916f14d1 | |||
| a291f5ab05 | |||
| 2d7e07f4ed | |||
| 2427f75f98 | |||
| d25fb71eba | |||
| c81b9d2fd9 | |||
| fb3ca90bc9 | |||
| eb2a47623f | |||
| f18d8ead08 | |||
| 2da14bd6e9 | |||
| 1dbb776e12 | |||
| 07b2c57064 | |||
| 7021f70a66 | |||
| 503e954671 | |||
| 2add1fcbcb | |||
| 4fe115b2c4 | |||
| 60e168806d | |||
| 03dfab1282 | |||
| 19302ea4fb | |||
| d5aaed67ba | |||
| 8fe6afd9ab | |||
| 782fbb115f | |||
| 3971bf34ed | |||
| 6dac6e53f7 | |||
| 7ec84e92a0 | |||
| 154e5c45a6 | |||
| 2cd5c813ac | |||
| 1c5101aa1a | |||
| 76f11bee9e | |||
| 91f409e8f4 |
@@ -85,5 +85,14 @@ module.exports = {
|
||||
// We use a `logger` intermediary module
|
||||
"no-console": "error",
|
||||
},
|
||||
}, {
|
||||
files: [
|
||||
"spec/**/*.ts",
|
||||
],
|
||||
rules: {
|
||||
// We don't need super strict typing in test utilities
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-member-accessibility": "off",
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
* @matrix-org/element-web
|
||||
|
||||
/src/webrtc @matrix-org/element-call-reviewers
|
||||
/spec/*/webrtc @matrix-org/element-call-reviewers
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
name: Deploy documentation PR preview
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Static Analysis" ]
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
netlify:
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
|
||||
- name: 📥 Download artifact
|
||||
uses: dawidd6/action-download-artifact@e6e25ac3a2b93187502a8be1ef9e9603afc34925 # v2.24.2
|
||||
with:
|
||||
workflow: static_analysis.yml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: docs
|
||||
path: docs
|
||||
|
||||
- name: 📤 Deploy to Netlify
|
||||
uses: matrix-org/netlify-pr-preview@v1
|
||||
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
|
||||
@@ -24,10 +24,7 @@ jobs:
|
||||
|
||||
- name: 📋 Copy to temp
|
||||
run: |
|
||||
tag="${{ github.ref_name }}"
|
||||
version="${tag#v}"
|
||||
echo "VERSION=$version" >> $GITHUB_ENV
|
||||
cp -a "./.jsdoc/matrix-js-sdk/$version" $RUNNER_TEMP
|
||||
cp -a "./_docs" "$RUNNER_TEMP/"
|
||||
|
||||
- name: 🧮 Checkout gh-pages
|
||||
uses: actions/checkout@v3
|
||||
@@ -36,7 +33,10 @@ jobs:
|
||||
|
||||
- name: 🔪 Prepare
|
||||
run: |
|
||||
cp -a "$RUNNER_TEMP/$VERSION" .
|
||||
tag="${{ github.ref_name }}"
|
||||
VERSION="${tag#v}"
|
||||
[ ! -e "$VERSION" ] || rm -r $VERSION
|
||||
cp -r $RUNNER_TEMP/docs/ $VERSION
|
||||
|
||||
# Add the new directory to the index if it isn't there already
|
||||
if ! grep -q ">Version $VERSION</a>" index.html; then
|
||||
|
||||
@@ -5,6 +5,11 @@ on:
|
||||
secrets:
|
||||
SONAR_TOKEN:
|
||||
required: true
|
||||
inputs:
|
||||
extra_args:
|
||||
type: string
|
||||
required: false
|
||||
description: "Extra args to pass to SonarCloud"
|
||||
jobs:
|
||||
sonarqube:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -22,7 +27,7 @@ jobs:
|
||||
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
id: sonarcloud
|
||||
uses: matrix-org/sonarcloud-workflow-action@v2.2
|
||||
uses: matrix-org/sonarcloud-workflow-action@v2.3
|
||||
with:
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
@@ -33,8 +38,8 @@ jobs:
|
||||
coverage_run_id: ${{ github.event.workflow_run.id }}
|
||||
coverage_workflow_name: tests.yml
|
||||
coverage_extract_path: coverage
|
||||
|
||||
|
||||
extra_args: ${{ inputs.extra_args }}
|
||||
|
||||
- uses: Sibz/github-status-action@v1
|
||||
if: always()
|
||||
with:
|
||||
|
||||
@@ -8,8 +8,36 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
# This is a workaround for https://github.com/SonarSource/SonarJS/issues/578
|
||||
prepare:
|
||||
name: Prepare
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
reportPaths: ${{ steps.extra_args.outputs.reportPaths }}
|
||||
testExecutionReportPaths: ${{ steps.extra_args.outputs.testExecutionReportPaths }}
|
||||
steps:
|
||||
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
|
||||
- name: 📥 Download artifact
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
with:
|
||||
workflow: tests.yaml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: coverage
|
||||
path: coverage
|
||||
|
||||
- id: extra_args
|
||||
run: |
|
||||
coverage=$(find coverage -type f -name '*lcov.info' | tr '\n' ',' | sed 's/,$//g')
|
||||
echo "reportPaths=$coverage" >> $GITHUB_OUTPUT
|
||||
reports=$(find coverage -type f -name 'jest-sonar-report*.xml' | tr '\n' ',' | sed 's/,$//g')
|
||||
echo "testExecutionReportPaths=$reports" >> $GITHUB_OUTPUT
|
||||
|
||||
sonarqube:
|
||||
name: 🩻 SonarQube
|
||||
needs: prepare
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
|
||||
secrets:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
with:
|
||||
extra_args: -Dsonar.javascript.lcov.reportPaths=${{ needs.prepare.outputs.reportPaths }} -Dsonar.testExecutionReportPaths=${{ needs.prepare.outputs.testExecutionReportPaths }}
|
||||
|
||||
@@ -64,6 +64,14 @@ jobs:
|
||||
|
||||
- name: Generate Docs
|
||||
run: "yarn run gendoc"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: docs
|
||||
path: _docs
|
||||
# We'll only use this in a workflow_run, then we're done with it
|
||||
retention-days: 1
|
||||
|
||||
tsc-strict:
|
||||
name: Typescript Strict Error Checker
|
||||
@@ -94,7 +102,7 @@ jobs:
|
||||
use-check: false
|
||||
check-fail-mode: added
|
||||
output-behaviour: annotate
|
||||
ts-extra-args: '--strict'
|
||||
ts-extra-args: '--noImplicitAny'
|
||||
files-changed: ${{ steps.files.outputs.files_updated }}
|
||||
files-added: ${{ steps.files.outputs.files_created }}
|
||||
files-deleted: ${{ steps.files.outputs.files_deleted }}
|
||||
|
||||
@@ -8,25 +8,49 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
jest:
|
||||
name: Jest
|
||||
name: 'Jest [${{ matrix.specs }}] (Node ${{ matrix.node }})'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
matrix:
|
||||
specs: [browserify, integ, unit]
|
||||
node: [16, 18, latest]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Yarn cache
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: "yarn install"
|
||||
|
||||
- name: Build
|
||||
if: matrix.specs == 'browserify'
|
||||
run: "yarn build"
|
||||
|
||||
- name: Get number of CPU cores
|
||||
id: cpu-cores
|
||||
uses: SimenB/github-actions-cpu-cores@v1
|
||||
|
||||
- name: Run tests with coverage and metrics
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
run: |
|
||||
yarn coverage --ci --reporters github-actions '--reporters=<rootDir>/spec/slowReporter.js' --max-workers ${{ steps.cpu-cores.outputs.count }} ./spec/${{ matrix.specs }}
|
||||
mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
|
||||
env:
|
||||
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: "yarn coverage --ci --reporters github-actions"
|
||||
if: github.ref != 'refs/heads/develop'
|
||||
run: |
|
||||
yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }} ./spec/${{ matrix.specs }}
|
||||
mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
|
||||
env:
|
||||
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/.jsdocbuild
|
||||
/.jsdoc
|
||||
/_docs
|
||||
.DS_Store
|
||||
|
||||
node_modules
|
||||
/.npmrc
|
||||
|
||||
@@ -1,3 +1,87 @@
|
||||
Changes in [22.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v22.0.0) (2022-12-06)
|
||||
==================================================================================================
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Enable users to join group calls from multiple devices ([\#2902](https://github.com/matrix-org/matrix-js-sdk/pull/2902)).
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Deprecate a function containing a typo ([\#2904](https://github.com/matrix-org/matrix-js-sdk/pull/2904)).
|
||||
|
||||
## ✨ Features
|
||||
* sliding sync: add receipts extension ([\#2912](https://github.com/matrix-org/matrix-js-sdk/pull/2912)).
|
||||
* Define a spec support policy for the js-sdk ([\#2882](https://github.com/matrix-org/matrix-js-sdk/pull/2882)).
|
||||
* Further improvements to e2ee logging ([\#2900](https://github.com/matrix-org/matrix-js-sdk/pull/2900)).
|
||||
* sliding sync: add support for typing extension ([\#2893](https://github.com/matrix-org/matrix-js-sdk/pull/2893)).
|
||||
* Improve logging on Olm session errors ([\#2885](https://github.com/matrix-org/matrix-js-sdk/pull/2885)).
|
||||
* Improve logging of e2ee messages ([\#2884](https://github.com/matrix-org/matrix-js-sdk/pull/2884)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix 3pid invite acceptance not working due to mxid being sent in body ([\#2907](https://github.com/matrix-org/matrix-js-sdk/pull/2907)). Fixes vector-im/element-web#23823.
|
||||
* Don't hang up calls that haven't started yet ([\#2898](https://github.com/matrix-org/matrix-js-sdk/pull/2898)).
|
||||
* Read receipt accumulation for threads ([\#2881](https://github.com/matrix-org/matrix-js-sdk/pull/2881)).
|
||||
* Make GroupCall work better with widgets ([\#2935](https://github.com/matrix-org/matrix-js-sdk/pull/2935)).
|
||||
* Fix highlight notifications increasing when total notification is zero ([\#2937](https://github.com/matrix-org/matrix-js-sdk/pull/2937)). Fixes vector-im/element-web#23885.
|
||||
* Fix synthesizeReceipt ([\#2916](https://github.com/matrix-org/matrix-js-sdk/pull/2916)). Fixes vector-im/element-web#23827 vector-im/element-web#23754 and vector-im/element-web#23847.
|
||||
|
||||
Changes in [21.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.2.0) (2022-11-22)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Make calls go back to 'connecting' state when media lost ([\#2880](https://github.com/matrix-org/matrix-js-sdk/pull/2880)).
|
||||
* Add ability to send unthreaded receipt ([\#2878](https://github.com/matrix-org/matrix-js-sdk/pull/2878)).
|
||||
* Add way to abort search requests ([\#2877](https://github.com/matrix-org/matrix-js-sdk/pull/2877)).
|
||||
* sliding sync: add custom room subscriptions support ([\#2834](https://github.com/matrix-org/matrix-js-sdk/pull/2834)).
|
||||
* webrtc: add advanced audio settings ([\#2434](https://github.com/matrix-org/matrix-js-sdk/pull/2434)). Contributed by @MrAnno.
|
||||
* Add support for group calls using MSC3401 ([\#2553](https://github.com/matrix-org/matrix-js-sdk/pull/2553)).
|
||||
* Make the js-sdk conform to tsc --strict ([\#2835](https://github.com/matrix-org/matrix-js-sdk/pull/2835)). Fixes #2112 #2116 and #2124.
|
||||
* Let leave requests outlive the window ([\#2815](https://github.com/matrix-org/matrix-js-sdk/pull/2815)). Fixes vector-im/element-call#639.
|
||||
* Add event and message capabilities to RoomWidgetClient ([\#2797](https://github.com/matrix-org/matrix-js-sdk/pull/2797)).
|
||||
* Misc fixes for group call widgets ([\#2657](https://github.com/matrix-org/matrix-js-sdk/pull/2657)).
|
||||
* Support nested Matrix clients via the widget API ([\#2473](https://github.com/matrix-org/matrix-js-sdk/pull/2473)).
|
||||
* Set max average bitrate on PTT calls ([\#2499](https://github.com/matrix-org/matrix-js-sdk/pull/2499)). Fixes vector-im/element-call#440.
|
||||
* Add config option for e2e group call signalling ([\#2492](https://github.com/matrix-org/matrix-js-sdk/pull/2492)).
|
||||
* Enable DTX on audio tracks in calls ([\#2482](https://github.com/matrix-org/matrix-js-sdk/pull/2482)).
|
||||
* Don't ignore call member events with a distant future expiration date ([\#2466](https://github.com/matrix-org/matrix-js-sdk/pull/2466)).
|
||||
* Expire call member state events after 1 hour ([\#2446](https://github.com/matrix-org/matrix-js-sdk/pull/2446)).
|
||||
* Emit unknown device errors for group call participants without e2e ([\#2447](https://github.com/matrix-org/matrix-js-sdk/pull/2447)).
|
||||
* Mute disconnected peers in PTT mode ([\#2421](https://github.com/matrix-org/matrix-js-sdk/pull/2421)).
|
||||
* Add support for sending encrypted to-device events with OLM ([\#2322](https://github.com/matrix-org/matrix-js-sdk/pull/2322)). Contributed by @robertlong.
|
||||
* Support for PTT group call mode ([\#2338](https://github.com/matrix-org/matrix-js-sdk/pull/2338)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix registration add phone number not working ([\#2876](https://github.com/matrix-org/matrix-js-sdk/pull/2876)). Contributed by @bagvand.
|
||||
* Use an underride rule for Element Call notifications ([\#2873](https://github.com/matrix-org/matrix-js-sdk/pull/2873)). Fixes vector-im/element-web#23691.
|
||||
* Fixes unwanted highlight notifications with encrypted threads ([\#2862](https://github.com/matrix-org/matrix-js-sdk/pull/2862)).
|
||||
* Extra insurance that we don't mix events in the wrong timelines - v2 ([\#2856](https://github.com/matrix-org/matrix-js-sdk/pull/2856)). Contributed by @MadLittleMods.
|
||||
* Hide pending events in thread timelines ([\#2843](https://github.com/matrix-org/matrix-js-sdk/pull/2843)). Fixes vector-im/element-web#23684.
|
||||
* Fix pagination token tracking for mixed room timelines ([\#2855](https://github.com/matrix-org/matrix-js-sdk/pull/2855)). Fixes vector-im/element-web#23695.
|
||||
* Extra insurance that we don't mix events in the wrong timelines ([\#2848](https://github.com/matrix-org/matrix-js-sdk/pull/2848)). Contributed by @MadLittleMods.
|
||||
* Do not freeze state in `initialiseState()` ([\#2846](https://github.com/matrix-org/matrix-js-sdk/pull/2846)).
|
||||
* Don't remove our own member for a split second when entering a call ([\#2844](https://github.com/matrix-org/matrix-js-sdk/pull/2844)).
|
||||
* Resolve races between `initLocalCallFeed` and `leave` ([\#2826](https://github.com/matrix-org/matrix-js-sdk/pull/2826)).
|
||||
* Add throwOnFail to groupCall.setScreensharingEnabled ([\#2787](https://github.com/matrix-org/matrix-js-sdk/pull/2787)).
|
||||
* Fix connectivity regressions ([\#2780](https://github.com/matrix-org/matrix-js-sdk/pull/2780)).
|
||||
* Fix screenshare failing after several attempts ([\#2771](https://github.com/matrix-org/matrix-js-sdk/pull/2771)). Fixes vector-im/element-call#625.
|
||||
* Don't block muting/unmuting on network requests ([\#2754](https://github.com/matrix-org/matrix-js-sdk/pull/2754)). Fixes vector-im/element-call#592.
|
||||
* Fix ICE restarts ([\#2702](https://github.com/matrix-org/matrix-js-sdk/pull/2702)).
|
||||
* Target widget actions at a specific room ([\#2670](https://github.com/matrix-org/matrix-js-sdk/pull/2670)).
|
||||
* Add tests for ice candidate sending ([\#2674](https://github.com/matrix-org/matrix-js-sdk/pull/2674)).
|
||||
* Prevent exception when muting ([\#2667](https://github.com/matrix-org/matrix-js-sdk/pull/2667)). Fixes vector-im/element-call#578.
|
||||
* Fix race in creating calls ([\#2662](https://github.com/matrix-org/matrix-js-sdk/pull/2662)).
|
||||
* Add client.waitUntilRoomReadyForGroupCalls() ([\#2641](https://github.com/matrix-org/matrix-js-sdk/pull/2641)).
|
||||
* Wait for client to start syncing before making group calls ([\#2632](https://github.com/matrix-org/matrix-js-sdk/pull/2632)). Fixes #2589.
|
||||
* Add GroupCallEventHandlerEvent.Room ([\#2631](https://github.com/matrix-org/matrix-js-sdk/pull/2631)).
|
||||
* Add missing events from reemitter to GroupCall ([\#2527](https://github.com/matrix-org/matrix-js-sdk/pull/2527)). Contributed by @toger5.
|
||||
* Prevent double mute status changed events ([\#2502](https://github.com/matrix-org/matrix-js-sdk/pull/2502)).
|
||||
* Don't mute the remote side immediately in PTT calls ([\#2487](https://github.com/matrix-org/matrix-js-sdk/pull/2487)). Fixes vector-im/element-call#425.
|
||||
* Fix some MatrixCall leaks and use a shared AudioContext ([\#2484](https://github.com/matrix-org/matrix-js-sdk/pull/2484)). Fixes vector-im/element-call#412.
|
||||
* Don't block muting on determining whether the device exists ([\#2461](https://github.com/matrix-org/matrix-js-sdk/pull/2461)).
|
||||
* Only clone streams on Safari ([\#2450](https://github.com/matrix-org/matrix-js-sdk/pull/2450)). Fixes vector-im/element-call#267.
|
||||
* Set PTT mode on call correctly ([\#2445](https://github.com/matrix-org/matrix-js-sdk/pull/2445)). Fixes vector-im/element-call#382.
|
||||
* Wait for mute event to send in PTT mode ([\#2401](https://github.com/matrix-org/matrix-js-sdk/pull/2401)).
|
||||
* Handle other members having no e2e keys ([\#2383](https://github.com/matrix-org/matrix-js-sdk/pull/2383)). Fixes vector-im/element-call#338.
|
||||
* Fix races when muting/unmuting ([\#2370](https://github.com/matrix-org/matrix-js-sdk/pull/2370)).
|
||||
|
||||
Changes in [21.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.1.0) (2022-11-08)
|
||||
==================================================================================================
|
||||
|
||||
|
||||
@@ -9,8 +9,14 @@
|
||||
Matrix Javascript SDK
|
||||
=====================
|
||||
|
||||
This is the [Matrix](https://matrix.org) Client-Server r0 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.
|
||||
|
||||
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
|
||||
==========
|
||||
@@ -295,12 +301,12 @@ API Reference
|
||||
A hosted reference can be found at
|
||||
http://matrix-org.github.io/matrix-js-sdk/index.html
|
||||
|
||||
This SDK uses JSDoc3 style comments. You can manually build and
|
||||
This SDK uses [Typedoc](https://typedoc.org/guides/doccomments) doc comments. You can manually build and
|
||||
host the API reference from the source files like this:
|
||||
|
||||
```
|
||||
$ yarn gendoc
|
||||
$ cd .jsdoc
|
||||
$ cd _docs
|
||||
$ python -m http.server 8005
|
||||
```
|
||||
|
||||
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"tags": {
|
||||
"allowUnknownTags": true
|
||||
},
|
||||
"plugins": [
|
||||
"node_modules/better-docs/category",
|
||||
"node_modules/better-docs/typescript"
|
||||
],
|
||||
"source": {
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"includePattern": ".(ts|js)$"
|
||||
},
|
||||
"opts": {
|
||||
"encoding": "utf8",
|
||||
"destination": ".jsdoc",
|
||||
"readme": "README.md",
|
||||
"recurse": true,
|
||||
"verbose": true,
|
||||
"template": "node_modules/docdash"
|
||||
},
|
||||
"docdash": {
|
||||
"static": true,
|
||||
"private": false,
|
||||
"search": true,
|
||||
"collapse": true,
|
||||
"typedefs": true
|
||||
}
|
||||
}
|
||||
+20
-14
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "21.1.0",
|
||||
"version": "22.0.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
@@ -16,7 +16,7 @@
|
||||
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
|
||||
"build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
|
||||
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
|
||||
"gendoc": "jsdoc -c jsdoc.json -P package.json",
|
||||
"gendoc": "typedoc",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 0 src spec",
|
||||
"lint:js-fix": "eslint --fix src spec",
|
||||
@@ -54,13 +54,16 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"loglevel": "^1.7.1",
|
||||
"matrix-events-sdk": "^0.0.1-beta.7",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.0.0",
|
||||
"p-retry": "4",
|
||||
"qs": "^6.9.6",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"unhomoglyph": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -76,12 +79,13 @@
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@casualbot/jest-sonar-reporter": "^2.2.5",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/domexception": "^4.0.0",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/node": "16",
|
||||
"@types/node": "18",
|
||||
"@typescript-eslint/eslint-plugin": "^5.6.0",
|
||||
"@typescript-eslint/parser": "^5.6.0",
|
||||
"allchange": "^1.0.6",
|
||||
@@ -89,25 +93,26 @@
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^2.4.0-beta.9",
|
||||
"browserify": "^17.0.0",
|
||||
"docdash": "^1.2.0",
|
||||
"docdash": "^2.0.0",
|
||||
"domexception": "^4.0.0",
|
||||
"eslint": "8.25.0",
|
||||
"eslint": "8.28.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-matrix-org": "^0.7.0",
|
||||
"eslint-plugin-unicorn": "^44.0.2",
|
||||
"eslint-plugin-matrix-org": "^0.8.0",
|
||||
"eslint-plugin-unicorn": "^45.0.0",
|
||||
"exorcist": "^2.0.0",
|
||||
"fake-indexeddb": "^4.0.0",
|
||||
"jest": "^29.0.0",
|
||||
"jest-environment-jsdom": "^29.0.0",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-mock": "^29.0.0",
|
||||
"jest-sonar-reporter": "^2.0.0",
|
||||
"jsdoc": "^3.6.6",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"terser": "^5.5.1",
|
||||
"tsify": "^5.0.2",
|
||||
"typedoc": "^0.23.20",
|
||||
"typedoc-plugin-missing-exports": "^1.0.0",
|
||||
"typescript": "^4.5.3"
|
||||
},
|
||||
"jest": {
|
||||
@@ -125,11 +130,12 @@
|
||||
"text-summary",
|
||||
"lcov"
|
||||
],
|
||||
"testResultsProcessor": "jest-sonar-reporter"
|
||||
"testResultsProcessor": "@casualbot/jest-sonar-reporter"
|
||||
},
|
||||
"jestSonar": {
|
||||
"reportPath": "coverage",
|
||||
"sonar56x": true
|
||||
"@casualbot/jest-sonar-reporter": {
|
||||
"outputDirectory": "coverage",
|
||||
"outputName": "jest-sonar-report.xml",
|
||||
"relativePaths": true
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
}
|
||||
|
||||
@@ -11,6 +11,6 @@ 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/test-report.xml
|
||||
sonar.testExecutionReportPaths=coverage/jest-sonar-report.xml
|
||||
|
||||
sonar.lang.patterns.ts=**/*.ts,**/*.tsx
|
||||
|
||||
@@ -14,6 +14,21 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
|
||||
import type { MatrixClient, ClientEvent } from "../../src";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace NodeJS {
|
||||
interface Global {
|
||||
matrixcs: {
|
||||
MatrixClient: typeof MatrixClient;
|
||||
ClientEvent: typeof ClientEvent;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stub for browser-matrix browserify tests
|
||||
// @ts-ignore
|
||||
global.XMLHttpRequest = jest.fn();
|
||||
@@ -23,3 +38,9 @@ afterAll(() => {
|
||||
// @ts-ignore
|
||||
global.XMLHttpRequest = undefined;
|
||||
});
|
||||
|
||||
// Akin to spec/setupTests.ts - but that won't affect the browser-matrix bundle
|
||||
global.matrixcs = {
|
||||
...global.matrixcs,
|
||||
timeoutSignal: () => new AbortController().signal,
|
||||
};
|
||||
|
||||
@@ -16,27 +16,14 @@ limitations under the License.
|
||||
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import "./setupTests";
|
||||
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
|
||||
import type { MatrixClient, ClientEvent } from "../../src";
|
||||
import "./setupTests";// uses browser-matrix instead of the src
|
||||
import type { MatrixClient } from "../../src";
|
||||
|
||||
const USER_ID = "@user:test.server";
|
||||
const DEVICE_ID = "device_id";
|
||||
const ACCESS_TOKEN = "access_token";
|
||||
const ROOM_ID = "!room_id:server.test";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace NodeJS {
|
||||
interface Global {
|
||||
matrixcs: {
|
||||
MatrixClient: typeof MatrixClient;
|
||||
ClientEvent: typeof ClientEvent;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("Browserify Test", function() {
|
||||
let client: MatrixClient;
|
||||
let httpBackend: HttpBackend;
|
||||
|
||||
@@ -18,12 +18,14 @@ import * as utils from "../test-utils/test-utils";
|
||||
import {
|
||||
ClientEvent,
|
||||
Direction,
|
||||
EventStatus,
|
||||
EventTimeline,
|
||||
EventTimelineSet,
|
||||
Filter,
|
||||
IEvent,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
PendingEventOrdering,
|
||||
Room,
|
||||
} from "../../src/matrix";
|
||||
import { logger } from "../../src/logger";
|
||||
@@ -1162,6 +1164,25 @@ describe("MatrixClient event timelines", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA);
|
||||
await flushHttp(room.fetchRoomThreads());
|
||||
});
|
||||
|
||||
it("should prevent displaying pending events", async function() {
|
||||
const room = new Room("room123", client, "john", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
const timelineSets = await room!.createThreadsTimelineSets();
|
||||
expect(timelineSets).not.toBeNull();
|
||||
|
||||
const event = utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "a body", event: true,
|
||||
});
|
||||
event.status = EventStatus.SENDING;
|
||||
room.addPendingEvent(event, "txn");
|
||||
|
||||
const [allThreads, myThreads] = timelineSets!;
|
||||
expect(allThreads.getPendingEvents()).toHaveLength(0);
|
||||
expect(myThreads.getPendingEvents()).toHaveLength(0);
|
||||
expect(room.getPendingEvents()).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("without server compatibility", function() {
|
||||
|
||||
@@ -173,7 +173,9 @@ describe("MatrixClient", function() {
|
||||
signatures: {},
|
||||
};
|
||||
|
||||
httpBackend!.when("POST", inviteSignUrl).respond(200, signature);
|
||||
httpBackend!.when("POST", inviteSignUrl).check(request => {
|
||||
expect(request.queryParams?.mxid).toEqual(client!.getUserId());
|
||||
}).respond(200, signature);
|
||||
httpBackend!.when("POST", "/join/" + encodeURIComponent(roomId)).check(request => {
|
||||
expect(request.data.third_party_signed).toEqual(signature);
|
||||
}).respond(200, { room_id: roomId });
|
||||
|
||||
@@ -707,17 +707,13 @@ describe("MatrixClient syncing", () => {
|
||||
awaitSyncEvent(2),
|
||||
]).then(() => {
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
const stateAtStart = room.getLiveTimeline().getState(
|
||||
EventTimeline.BACKWARDS,
|
||||
);
|
||||
const stateAtStart = room.getLiveTimeline().getState(EventTimeline.BACKWARDS)!;
|
||||
const startRoomNameEvent = stateAtStart.getStateEvents('m.room.name', '');
|
||||
expect(startRoomNameEvent.getContent().name).toEqual('Old room name');
|
||||
expect(startRoomNameEvent!.getContent().name).toEqual('Old room name');
|
||||
|
||||
const stateAtEnd = room.getLiveTimeline().getState(
|
||||
EventTimeline.FORWARDS,
|
||||
);
|
||||
const stateAtEnd = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
||||
const endRoomNameEvent = stateAtEnd.getStateEvents('m.room.name', '');
|
||||
expect(endRoomNameEvent.getContent().name).toEqual('A new room name');
|
||||
expect(endRoomNameEvent!.getContent().name).toEqual('A new room name');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1603,7 +1599,7 @@ describe("MatrixClient syncing", () => {
|
||||
expect(room.roomId).toBe(roomOne);
|
||||
expect(room.getMyMembership()).toBe("leave");
|
||||
expect(room.name).toBe("Room Name");
|
||||
expect(room.currentState.getStateEvents("m.room.name", "").getId()).toBe("$eventId");
|
||||
expect(room.currentState.getStateEvents("m.room.name", "")?.getId()).toBe("$eventId");
|
||||
expect(room.timeline[0].getContent().body).toBe("Message 1");
|
||||
expect(room.timeline[1].getContent().body).toBe("Message 2");
|
||||
client?.stopPeeking();
|
||||
|
||||
@@ -542,6 +542,7 @@ describe("SlidingSyncSdk", () => {
|
||||
|
||||
describe("ExtensionE2EE", () => {
|
||||
let ext: Extension;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupClient({
|
||||
withCrypto: true,
|
||||
@@ -551,18 +552,21 @@ describe("SlidingSyncSdk", () => {
|
||||
await hasSynced;
|
||||
ext = findExtension("e2ee");
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// needed else we do some async operations in the background which can cause Jest to whine:
|
||||
// "Cannot log after tests are done. Did you forget to wait for something async in your test?"
|
||||
// Attempted to log "Saving device tracking data null"."
|
||||
client!.crypto!.stop();
|
||||
});
|
||||
|
||||
it("gets enabled on the initial request only", () => {
|
||||
expect(ext.onRequest(true)).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(ext.onRequest(false)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("can update device lists", () => {
|
||||
ext.onResponse({
|
||||
device_lists: {
|
||||
@@ -572,6 +576,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
// TODO: more assertions?
|
||||
});
|
||||
|
||||
it("can update OTK counts", () => {
|
||||
client!.crypto!.updateOneTimeKeyCount = jest.fn();
|
||||
ext.onResponse({
|
||||
@@ -588,6 +593,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it("can update fallback keys", () => {
|
||||
ext.onResponse({
|
||||
device_unused_fallback_key_types: ["signed_curve25519"],
|
||||
@@ -599,8 +605,10 @@ describe("SlidingSyncSdk", () => {
|
||||
expect(client!.crypto!.getNeedsNewFallback()).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ExtensionAccountData", () => {
|
||||
let ext: Extension;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk!.sync();
|
||||
@@ -608,12 +616,14 @@ describe("SlidingSyncSdk", () => {
|
||||
await hasSynced;
|
||||
ext = findExtension("account_data");
|
||||
});
|
||||
|
||||
it("gets enabled on the initial request only", () => {
|
||||
expect(ext.onRequest(true)).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(ext.onRequest(false)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("processes global account data", async () => {
|
||||
const globalType = "global_test";
|
||||
const globalContent = {
|
||||
@@ -633,6 +643,7 @@ describe("SlidingSyncSdk", () => {
|
||||
expect(globalData).toBeDefined();
|
||||
expect(globalData.getContent()).toEqual(globalContent);
|
||||
});
|
||||
|
||||
it("processes rooms account data", async () => {
|
||||
const roomId = "!room:id";
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
@@ -667,6 +678,7 @@ describe("SlidingSyncSdk", () => {
|
||||
expect(event).toBeDefined();
|
||||
expect(event.getContent()).toEqual(roomContent);
|
||||
});
|
||||
|
||||
it("doesn't crash for unknown room account data", async () => {
|
||||
const unknownRoomId = "!unknown:id";
|
||||
const roomType = "tester";
|
||||
@@ -686,6 +698,7 @@ describe("SlidingSyncSdk", () => {
|
||||
expect(room).toBeNull();
|
||||
expect(client!.getAccountData(roomType)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("can update push rules via account data", async () => {
|
||||
const roomId = "!foo:bar";
|
||||
const pushRulesContent: IPushRules = {
|
||||
@@ -718,8 +731,10 @@ describe("SlidingSyncSdk", () => {
|
||||
expect(pushRule).toEqual(pushRulesContent.global[PushRuleKind.RoomSpecific]![0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ExtensionToDevice", () => {
|
||||
let ext: Extension;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk!.sync();
|
||||
@@ -727,12 +742,14 @@ describe("SlidingSyncSdk", () => {
|
||||
await hasSynced;
|
||||
ext = findExtension("to_device");
|
||||
});
|
||||
|
||||
it("gets enabled with a limit on the initial request only", () => {
|
||||
const reqJson: any = ext.onRequest(true);
|
||||
expect(reqJson.enabled).toEqual(true);
|
||||
expect(reqJson.limit).toBeGreaterThan(0);
|
||||
expect(reqJson.since).toBeUndefined();
|
||||
});
|
||||
|
||||
it("updates the since value", async () => {
|
||||
ext.onResponse({
|
||||
next_batch: "12345",
|
||||
@@ -742,12 +759,14 @@ describe("SlidingSyncSdk", () => {
|
||||
since: "12345",
|
||||
});
|
||||
});
|
||||
|
||||
it("can handle missing fields", async () => {
|
||||
ext.onResponse({
|
||||
next_batch: "23456",
|
||||
// no events array
|
||||
});
|
||||
});
|
||||
|
||||
it("emits to-device events on the client", async () => {
|
||||
const toDeviceType = "custom_test";
|
||||
const toDeviceContent = {
|
||||
@@ -770,6 +789,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
|
||||
it("can cancel key verification requests", async () => {
|
||||
const seen: Record<string, boolean> = {};
|
||||
client!.on(ClientEvent.ToDeviceEvent, (ev) => {
|
||||
@@ -809,4 +829,189 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ExtensionTyping", () => {
|
||||
let ext: Extension;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
ext = findExtension("typing");
|
||||
});
|
||||
|
||||
it("gets enabled on the initial request only", () => {
|
||||
expect(ext.onRequest(true)).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(ext.onRequest(false)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("processes typing notifications", async () => {
|
||||
const roomId = "!room:id";
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
name: "Room with typing",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
|
||||
],
|
||||
initial: true,
|
||||
});
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(false);
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
[roomId]: {
|
||||
type: EventType.Typing,
|
||||
content: {
|
||||
user_ids: [selfUserId],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(true);
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
[roomId]: {
|
||||
type: EventType.Typing,
|
||||
content: {
|
||||
user_ids: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(false);
|
||||
});
|
||||
|
||||
it("gracefully handles missing rooms and members when typing", async () => {
|
||||
const roomId = "!room:id";
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
name: "Room with typing",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
|
||||
],
|
||||
initial: true,
|
||||
});
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(false);
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
[roomId]: {
|
||||
type: EventType.Typing,
|
||||
content: {
|
||||
user_ids: ["@someone:else"],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(false);
|
||||
ext.onResponse({
|
||||
rooms: {
|
||||
"!something:else": {
|
||||
type: EventType.Typing,
|
||||
content: {
|
||||
user_ids: [selfUserId],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(room.getMember(selfUserId)?.typing).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ExtensionReceipts", () => {
|
||||
let ext: Extension;
|
||||
|
||||
const generateReceiptResponse = (
|
||||
userId: string, roomId: string, eventId: string, recType: string, ts: number,
|
||||
) => {
|
||||
return {
|
||||
rooms: {
|
||||
[roomId]: {
|
||||
type: EventType.Receipt,
|
||||
content: {
|
||||
[eventId]: {
|
||||
[recType]: {
|
||||
[userId]: {
|
||||
ts: ts,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
ext = findExtension("receipts");
|
||||
});
|
||||
|
||||
it("gets enabled on the initial request only", () => {
|
||||
expect(ext.onRequest(true)).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(ext.onRequest(false)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("processes receipts", async () => {
|
||||
const roomId = "!room:id";
|
||||
const alice = "@alice:alice";
|
||||
const lastEvent = mkOwnEvent(EventType.RoomMessage, { body: "hello" });
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
name: "Room with receipts",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
{
|
||||
type: EventType.RoomMember,
|
||||
state_key: alice,
|
||||
content: { membership: "join" },
|
||||
sender: alice,
|
||||
origin_server_ts: Date.now(),
|
||||
event_id: "$alice",
|
||||
},
|
||||
lastEvent,
|
||||
],
|
||||
initial: true,
|
||||
});
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
expect(room.getReadReceiptForUserId(alice, true)).toBeNull();
|
||||
ext.onResponse(
|
||||
generateReceiptResponse(alice, roomId, lastEvent.event_id, "m.read", 1234567),
|
||||
);
|
||||
const receipt = room.getReadReceiptForUserId(alice);
|
||||
expect(receipt).toBeDefined();
|
||||
expect(receipt?.eventId).toEqual(lastEvent.event_id);
|
||||
expect(receipt?.data.ts).toEqual(1234567);
|
||||
expect(receipt?.data.thread_id).toBeFalsy();
|
||||
});
|
||||
|
||||
it("gracefully handles missing rooms when receiving receipts", async () => {
|
||||
const roomId = "!room:id";
|
||||
const alice = "@alice:alice";
|
||||
const eventId = "$something";
|
||||
ext.onResponse(
|
||||
generateReceiptResponse(alice, roomId, eventId, "m.read", 1234567),
|
||||
);
|
||||
// we expect it not to crash
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1112,6 +1112,156 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("custom room subscriptions", () => {
|
||||
beforeAll(setupClient);
|
||||
afterAll(teardownClient);
|
||||
|
||||
const roomA = "!a";
|
||||
const roomB = "!b";
|
||||
const roomC = "!c";
|
||||
const roomD = "!d";
|
||||
|
||||
const defaultSub = {
|
||||
timeline_limit: 1,
|
||||
required_state: [["m.room.create", ""]],
|
||||
};
|
||||
|
||||
const customSubName1 = "sub1";
|
||||
const customSub1 = {
|
||||
timeline_limit: 2,
|
||||
required_state: [["*", "*"]],
|
||||
};
|
||||
|
||||
const customSubName2 = "sub2";
|
||||
const customSub2 = {
|
||||
timeline_limit: 3,
|
||||
required_state: [["*", "*"]],
|
||||
};
|
||||
|
||||
it("should be possible to use custom subscriptions on startup", async () => {
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
|
||||
// the intention is for clients to set this up at startup
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub1);
|
||||
slidingSync.addCustomSubscription(customSubName2, customSub2);
|
||||
// then call these depending on the kind of room / context
|
||||
slidingSync.useCustomSubscription(roomA, customSubName1);
|
||||
slidingSync.useCustomSubscription(roomB, customSubName1);
|
||||
slidingSync.useCustomSubscription(roomC, customSubName2);
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA, roomB, roomC, roomD]));
|
||||
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomA]).toEqual(customSub1);
|
||||
expect(body.room_subscriptions[roomB]).toEqual(customSub1);
|
||||
expect(body.room_subscriptions[roomC]).toEqual(customSub2);
|
||||
expect(body.room_subscriptions[roomD]).toEqual(defaultSub);
|
||||
}).respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend!.flushAllExpected();
|
||||
slidingSync.stop();
|
||||
});
|
||||
|
||||
it("should be possible to use custom subscriptions mid-connection", async () => {
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
|
||||
// the intention is for clients to set this up at startup
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub1);
|
||||
slidingSync.addCustomSubscription(customSubName2, customSub2);
|
||||
// initially no subs
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
}).respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// now the user clicks on a room which uses the default sub
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomA]).toEqual(defaultSub);
|
||||
}).respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// now the user clicks on a room which uses a custom sub
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomB]).toEqual(customSub1);
|
||||
expect(body.unsubscribe_rooms).toEqual([roomA]);
|
||||
}).respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.useCustomSubscription(roomB, customSubName1);
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomB]));
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// now the user uses a different sub for the same room: we don't unsub but just resend
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomB]).toEqual(customSub2);
|
||||
expect(body.unsubscribe_rooms).toBeFalsy();
|
||||
}).respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.useCustomSubscription(roomB, customSubName2);
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomB]));
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
slidingSync.stop();
|
||||
});
|
||||
|
||||
it("uses the default subscription for unknown subscription names", async () => {
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub1);
|
||||
slidingSync.useCustomSubscription(roomA, "unknown name");
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
|
||||
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomA]).toEqual(defaultSub);
|
||||
}).respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend!.flushAllExpected();
|
||||
slidingSync.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe("extensions", () => {
|
||||
beforeAll(setupClient);
|
||||
afterAll(teardownClient);
|
||||
|
||||
@@ -17,3 +17,9 @@ limitations under the License.
|
||||
import DOMException from "domexception";
|
||||
|
||||
global.DOMException = DOMException;
|
||||
|
||||
jest.mock("../src/http-api/utils", () => ({
|
||||
...jest.requireActual("../src/http-api/utils"),
|
||||
// We mock timeoutSignal otherwise it causes tests to leave timers running
|
||||
timeoutSignal: () => new AbortController().signal,
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
class JestSlowTestReporter {
|
||||
constructor(globalConfig, options) {
|
||||
this._globalConfig = globalConfig;
|
||||
this._options = options;
|
||||
this._slowTests = [];
|
||||
this._slowTestSuites = [];
|
||||
}
|
||||
|
||||
onRunComplete() {
|
||||
const displayResult = (result, isTestSuite) => {
|
||||
if (!isTestSuite) console.log();
|
||||
|
||||
result.sort((a, b) => b.duration - a.duration);
|
||||
const rootPathRegex = new RegExp(`^${process.cwd()}`);
|
||||
const slowestTests = result.slice(0, this._options.numTests || 10);
|
||||
const slowTestTime = this._slowTestTime(slowestTests);
|
||||
const allTestTime = this._allTestTime(result);
|
||||
const percentTime = (slowTestTime / allTestTime) * 100;
|
||||
|
||||
if (isTestSuite) {
|
||||
console.log(
|
||||
`Top ${slowestTests.length} slowest test suites (${slowTestTime / 1000} seconds,` +
|
||||
` ${percentTime.toFixed(1)}% of total time):`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`Top ${slowestTests.length} slowest tests (${slowTestTime / 1000} seconds,` +
|
||||
` ${percentTime.toFixed(1)}% of total time):`,
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < slowestTests.length; i++) {
|
||||
const duration = slowestTests[i].duration;
|
||||
const filePath = slowestTests[i].filePath.replace(rootPathRegex, '.');
|
||||
|
||||
if (isTestSuite) {
|
||||
console.log(` ${duration / 1000} seconds ${filePath}`);
|
||||
} else {
|
||||
const fullName = slowestTests[i].fullName;
|
||||
console.log(` ${fullName}`);
|
||||
console.log(` ${duration / 1000} seconds ${filePath}`);
|
||||
}
|
||||
}
|
||||
console.log();
|
||||
};
|
||||
|
||||
displayResult(this._slowTests);
|
||||
displayResult(this._slowTestSuites, true);
|
||||
}
|
||||
|
||||
onTestResult(test, testResult) {
|
||||
this._slowTestSuites.push({
|
||||
duration: testResult.perfStats.runtime,
|
||||
filePath: testResult.testFilePath,
|
||||
});
|
||||
for (let i = 0; i < testResult.testResults.length; i++) {
|
||||
this._slowTests.push({
|
||||
duration: testResult.testResults[i].duration,
|
||||
fullName: testResult.testResults[i].fullName,
|
||||
filePath: testResult.testFilePath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_slowTestTime(slowestTests) {
|
||||
let slowTestTime = 0;
|
||||
for (let i = 0; i < slowestTests.length; i++) {
|
||||
slowTestTime += slowestTests[i].duration;
|
||||
}
|
||||
return slowTestTime;
|
||||
}
|
||||
|
||||
_allTestTime(result) {
|
||||
let allTestTime = 0;
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
allTestTime += result[i].duration;
|
||||
}
|
||||
return allTestTime;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = JestSlowTestReporter;
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// Jest now uses @sinonjs/fake-timers which exposes tickAsync() and a number of
|
||||
// other async methods which break the event loop, letting scheduled promise
|
||||
// callbacks run. Unfortunately, Jest doesn't expose these, so we have to do
|
||||
// it manually (this is what sinon does under the hood). We do both in a loop
|
||||
// until the thing we expect happens: hopefully this is the least flakey way
|
||||
// and avoids assuming anything about the app's behaviour.
|
||||
const realSetTimeout = setTimeout;
|
||||
export function flushPromises() {
|
||||
return new Promise(r => {
|
||||
realSetTimeout(r, 1);
|
||||
});
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import EventEmitter from "events";
|
||||
import '../olm-loader';
|
||||
|
||||
import { logger } from '../../src/logger';
|
||||
import { IContent, IEvent, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { IContent, IEvent, IEventRelation, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { ClientEvent, EventType, IPusher, MatrixClient, MsgType } from "../../src";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { eventMapperFor } from "../../src/event-mapper";
|
||||
@@ -78,6 +78,7 @@ interface IEventOpts {
|
||||
user?: string;
|
||||
unsigned?: IUnsigned;
|
||||
redacts?: string;
|
||||
ts?: number;
|
||||
}
|
||||
|
||||
let testEventIndex = 1; // counter for events, easier for comparison of randomly generated events
|
||||
@@ -109,6 +110,7 @@ export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixC
|
||||
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
|
||||
txn_id: "~" + Math.random(),
|
||||
redacts: opts.redacts,
|
||||
origin_server_ts: opts.ts ?? 0,
|
||||
};
|
||||
if (opts.skey !== undefined) {
|
||||
event.state_key = opts.skey;
|
||||
@@ -237,11 +239,13 @@ export function mkMembershipCustom<T>(
|
||||
});
|
||||
}
|
||||
|
||||
interface IMessageOpts {
|
||||
export interface IMessageOpts {
|
||||
room?: string;
|
||||
user: string;
|
||||
msg?: string;
|
||||
event?: boolean;
|
||||
relatesTo?: IEventRelation;
|
||||
ts?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -269,6 +273,10 @@ export function mkMessage(
|
||||
},
|
||||
};
|
||||
|
||||
if (opts.relatesTo) {
|
||||
eventOpts.content["m.relates_to"] = opts.relatesTo;
|
||||
}
|
||||
|
||||
if (!eventOpts.content.body) {
|
||||
eventOpts.content.body = "Random->" + Math.random();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
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 { RelationType } from "../../src/@types/event";
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { Thread } from "../../src/models/thread";
|
||||
import { mkMessage } from "./test-utils";
|
||||
|
||||
export const makeThreadEvent = ({ rootEventId, replyToEventId, ...props }: any & {
|
||||
rootEventId: string; replyToEventId: string; event?: boolean;
|
||||
}): MatrixEvent => mkMessage({
|
||||
...props,
|
||||
relatesTo: {
|
||||
event_id: rootEventId,
|
||||
rel_type: "m.thread",
|
||||
['m.in_reply_to']: {
|
||||
event_id: replyToEventId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
type MakeThreadEventsProps = {
|
||||
roomId: Room["roomId"];
|
||||
// root message user id
|
||||
authorId: string;
|
||||
// user ids of thread replies
|
||||
// cycled through until thread length is fulfilled
|
||||
participantUserIds: string[];
|
||||
// number of messages in the thread, root message included
|
||||
// optional, default 2
|
||||
length?: number;
|
||||
ts?: number;
|
||||
// provide to set current_user_participated accurately
|
||||
currentUserId?: string;
|
||||
};
|
||||
|
||||
export const makeThreadEvents = ({
|
||||
roomId, authorId, participantUserIds, length = 2, ts = 1, currentUserId,
|
||||
}: MakeThreadEventsProps): { rootEvent: MatrixEvent, events: MatrixEvent[] } => {
|
||||
const rootEvent = mkMessage({
|
||||
user: authorId,
|
||||
room: roomId,
|
||||
msg: 'root event message ' + Math.random(),
|
||||
ts,
|
||||
event: true,
|
||||
});
|
||||
|
||||
const rootEventId = rootEvent.getId();
|
||||
const events = [rootEvent];
|
||||
|
||||
for (let i = 1; i < length; i++) {
|
||||
const prevEvent = events[i - 1];
|
||||
const replyToEventId = prevEvent.getId();
|
||||
const user = participantUserIds[i % participantUserIds.length];
|
||||
events.push(makeThreadEvent({
|
||||
user,
|
||||
room: roomId,
|
||||
event: true,
|
||||
msg: `reply ${i} by ${user}`,
|
||||
rootEventId,
|
||||
replyToEventId,
|
||||
// replies are 1ms after each other
|
||||
ts: ts + i,
|
||||
}));
|
||||
}
|
||||
|
||||
rootEvent.setUnsigned({
|
||||
"m.relations": {
|
||||
[RelationType.Thread]: {
|
||||
latest_event: events[events.length - 1],
|
||||
count: length,
|
||||
current_user_participated: [...participantUserIds, authorId].includes(currentUserId ?? ""),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return { rootEvent, events };
|
||||
};
|
||||
|
||||
type MakeThreadProps = {
|
||||
room: Room;
|
||||
client: MatrixClient;
|
||||
authorId: string;
|
||||
participantUserIds: string[];
|
||||
length?: number;
|
||||
ts?: number;
|
||||
};
|
||||
|
||||
export const mkThread = ({
|
||||
room,
|
||||
client,
|
||||
authorId,
|
||||
participantUserIds,
|
||||
length = 2,
|
||||
ts = 1,
|
||||
}: MakeThreadProps): { thread: Thread, rootEvent: MatrixEvent, events: MatrixEvent[] } => {
|
||||
const { rootEvent, events } = makeThreadEvents({
|
||||
roomId: room.roomId,
|
||||
authorId,
|
||||
participantUserIds,
|
||||
length,
|
||||
ts,
|
||||
currentUserId: client.getUserId() ?? "",
|
||||
});
|
||||
expect(rootEvent).toBeTruthy();
|
||||
|
||||
for (const evt of events) {
|
||||
room?.reEmitter.reEmit(evt, [
|
||||
MatrixEventEvent.BeforeRedaction,
|
||||
]);
|
||||
}
|
||||
|
||||
const thread = room.createThread(rootEvent.getId() ?? "", rootEvent, events, true);
|
||||
// So that we do not have to mock the thread loading
|
||||
thread.initialEventsFetched = true;
|
||||
thread.addEvents(events, true);
|
||||
|
||||
return { thread, rootEvent, events };
|
||||
};
|
||||
+390
-29
@@ -14,6 +14,32 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ClientEvent,
|
||||
ClientEventHandlerMap,
|
||||
EventType,
|
||||
GroupCall,
|
||||
GroupCallIntent,
|
||||
GroupCallType,
|
||||
IContent,
|
||||
ISendEventResponse,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
RoomState,
|
||||
RoomStateEvent,
|
||||
RoomStateEventHandlerMap,
|
||||
} from "../../src";
|
||||
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { CallEvent, CallEventHandlerMap, MatrixCall } from "../../src/webrtc/call";
|
||||
import { CallEventHandlerEvent, CallEventHandlerEventHandlerMap } from "../../src/webrtc/callEventHandler";
|
||||
import { CallFeed } from "../../src/webrtc/callFeed";
|
||||
import { GroupCallEventHandlerMap } from "../../src/webrtc/groupCall";
|
||||
import { GroupCallEventHandlerEvent } from "../../src/webrtc/groupCallEventHandler";
|
||||
import { IScreensharingOpts, MediaHandler } from "../../src/webrtc/mediaHandler";
|
||||
|
||||
export const DUMMY_SDP = (
|
||||
"v=0\r\n" +
|
||||
"o=- 5022425983810148698 2 IN IP4 127.0.0.1\r\n" +
|
||||
@@ -54,8 +80,50 @@ export const DUMMY_SDP = (
|
||||
"a=ssrc:3619738545 cname:2RWtmqhXLdoF4sOi\r\n"
|
||||
);
|
||||
|
||||
export const USERMEDIA_STREAM_ID = "mock_stream_from_media_handler";
|
||||
export const SCREENSHARE_STREAM_ID = "mock_screen_stream_from_media_handler";
|
||||
|
||||
class MockMediaStreamAudioSourceNode {
|
||||
public connect() {}
|
||||
}
|
||||
|
||||
class MockAnalyser {
|
||||
public getFloatFrequencyData() { return 0.0; }
|
||||
}
|
||||
|
||||
export class MockAudioContext {
|
||||
constructor() {}
|
||||
public createAnalyser() { return new MockAnalyser(); }
|
||||
public createMediaStreamSource() { return new MockMediaStreamAudioSourceNode(); }
|
||||
public close() {}
|
||||
}
|
||||
|
||||
export class MockRTCPeerConnection {
|
||||
localDescription: RTCSessionDescription;
|
||||
private static instances: MockRTCPeerConnection[] = [];
|
||||
|
||||
private negotiationNeededListener?: () => void;
|
||||
public iceCandidateListener?: (e: RTCPeerConnectionIceEvent) => void;
|
||||
public onTrackListener?: (e: RTCTrackEvent) => void;
|
||||
public needsNegotiation = false;
|
||||
public readyToNegotiate: Promise<void>;
|
||||
private onReadyToNegotiate?: () => void;
|
||||
public localDescription: RTCSessionDescription;
|
||||
public signalingState: RTCSignalingState = "stable";
|
||||
public transceivers: MockRTCRtpTransceiver[] = [];
|
||||
|
||||
public static triggerAllNegotiations(): void {
|
||||
for (const inst of this.instances) {
|
||||
inst.doNegotiation();
|
||||
}
|
||||
}
|
||||
|
||||
public static hasAnyPendingNegotiations(): boolean {
|
||||
return this.instances.some(i => i.needsNegotiation);
|
||||
}
|
||||
|
||||
public static resetInstances() {
|
||||
this.instances = [];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.localDescription = {
|
||||
@@ -63,34 +131,133 @@ export class MockRTCPeerConnection {
|
||||
type: 'offer',
|
||||
toJSON: function() { },
|
||||
};
|
||||
|
||||
this.readyToNegotiate = new Promise<void>(resolve => {
|
||||
this.onReadyToNegotiate = resolve;
|
||||
});
|
||||
|
||||
MockRTCPeerConnection.instances.push(this);
|
||||
}
|
||||
|
||||
addEventListener() { }
|
||||
createDataChannel(label: string, opts: RTCDataChannelInit) { return { label, ...opts }; }
|
||||
createOffer() {
|
||||
return Promise.resolve({});
|
||||
public addEventListener(type: string, listener: () => void) {
|
||||
if (type === 'negotiationneeded') {
|
||||
this.negotiationNeededListener = listener;
|
||||
} else if (type == 'icecandidate') {
|
||||
this.iceCandidateListener = listener;
|
||||
} else if (type == 'track') {
|
||||
this.onTrackListener = listener;
|
||||
}
|
||||
}
|
||||
setRemoteDescription() {
|
||||
public createDataChannel(label: string, opts: RTCDataChannelInit) { return { label, ...opts }; }
|
||||
public createOffer() {
|
||||
return Promise.resolve({
|
||||
type: 'offer',
|
||||
sdp: DUMMY_SDP,
|
||||
});
|
||||
}
|
||||
public createAnswer() {
|
||||
return Promise.resolve({
|
||||
type: 'answer',
|
||||
sdp: DUMMY_SDP,
|
||||
});
|
||||
}
|
||||
public setRemoteDescription() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
setLocalDescription() {
|
||||
public setLocalDescription() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
close() { }
|
||||
getStats() { return []; }
|
||||
addTrack(track: MockMediaStreamTrack) { return new MockRTCRtpSender(track); }
|
||||
public close() { }
|
||||
public getStats() { return []; }
|
||||
public addTransceiver(track: MockMediaStreamTrack): MockRTCRtpTransceiver {
|
||||
this.needsNegotiation = true;
|
||||
if (this.onReadyToNegotiate) this.onReadyToNegotiate();
|
||||
|
||||
const newSender = new MockRTCRtpSender(track);
|
||||
const newReceiver = new MockRTCRtpReceiver(track);
|
||||
|
||||
const newTransceiver = new MockRTCRtpTransceiver(this);
|
||||
newTransceiver.sender = newSender as unknown as RTCRtpSender;
|
||||
newTransceiver.receiver = newReceiver as unknown as RTCRtpReceiver;
|
||||
|
||||
this.transceivers.push(newTransceiver);
|
||||
|
||||
return newTransceiver;
|
||||
}
|
||||
public addTrack(track: MockMediaStreamTrack): MockRTCRtpSender {
|
||||
return this.addTransceiver(track).sender as unknown as MockRTCRtpSender;
|
||||
}
|
||||
|
||||
public removeTrack() {
|
||||
this.needsNegotiation = true;
|
||||
if (this.onReadyToNegotiate) this.onReadyToNegotiate();
|
||||
}
|
||||
|
||||
public getTransceivers(): MockRTCRtpTransceiver[] { return this.transceivers; }
|
||||
public getSenders(): MockRTCRtpSender[] {
|
||||
return this.transceivers.map(t => t.sender as unknown as MockRTCRtpSender);
|
||||
}
|
||||
|
||||
public doNegotiation() {
|
||||
if (this.needsNegotiation && this.negotiationNeededListener) {
|
||||
this.needsNegotiation = false;
|
||||
this.negotiationNeededListener();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MockRTCRtpSender {
|
||||
constructor(public track: MockMediaStreamTrack) { }
|
||||
|
||||
replaceTrack(track: MockMediaStreamTrack) { this.track = track; }
|
||||
public replaceTrack(track: MockMediaStreamTrack) { this.track = track; }
|
||||
}
|
||||
|
||||
export class MockRTCRtpReceiver {
|
||||
constructor(public track: MockMediaStreamTrack) { }
|
||||
}
|
||||
|
||||
export class MockRTCRtpTransceiver {
|
||||
constructor(private peerConn: MockRTCPeerConnection) {}
|
||||
|
||||
public sender?: RTCRtpSender;
|
||||
public receiver?: RTCRtpReceiver;
|
||||
|
||||
public set direction(_: string) {
|
||||
this.peerConn.needsNegotiation = true;
|
||||
}
|
||||
|
||||
public setCodecPreferences = jest.fn<void, RTCRtpCodecCapability[]>();
|
||||
}
|
||||
|
||||
export class MockMediaStreamTrack {
|
||||
constructor(public readonly id: string, public readonly kind: "audio" | "video", public enabled = true) { }
|
||||
|
||||
stop() { }
|
||||
public stop = jest.fn<void, []>();
|
||||
|
||||
public listeners: [string, (...args: any[]) => any][] = [];
|
||||
public isStopped = false;
|
||||
public settings?: MediaTrackSettings;
|
||||
|
||||
public getSettings(): MediaTrackSettings { return this.settings!; }
|
||||
|
||||
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
|
||||
// implementation
|
||||
public dispatchEvent(eventType: string) {
|
||||
this.listeners.forEach(([t, c]) => {
|
||||
if (t !== eventType) return;
|
||||
c();
|
||||
});
|
||||
}
|
||||
public addEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.push([eventType, callback]);
|
||||
}
|
||||
public removeEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.filter(([t, c]) => {
|
||||
return t !== eventType || c !== callback;
|
||||
});
|
||||
}
|
||||
|
||||
public typed(): MediaStreamTrack { return this as unknown as MediaStreamTrack; }
|
||||
}
|
||||
|
||||
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
|
||||
@@ -101,46 +268,240 @@ export class MockMediaStream {
|
||||
private tracks: MockMediaStreamTrack[] = [],
|
||||
) {}
|
||||
|
||||
listeners: [string, (...args: any[]) => any][] = [];
|
||||
public listeners: [string, (...args: any[]) => any][] = [];
|
||||
public isStopped = false;
|
||||
|
||||
dispatchEvent(eventType: string) {
|
||||
public dispatchEvent(eventType: string) {
|
||||
this.listeners.forEach(([t, c]) => {
|
||||
if (t !== eventType) return;
|
||||
c();
|
||||
});
|
||||
}
|
||||
getTracks() { return this.tracks; }
|
||||
getAudioTracks() { return this.tracks.filter((track) => track.kind === "audio"); }
|
||||
getVideoTracks() { return this.tracks.filter((track) => track.kind === "video"); }
|
||||
addEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
public getTracks() { return this.tracks; }
|
||||
public getAudioTracks() { return this.tracks.filter((track) => track.kind === "audio"); }
|
||||
public getVideoTracks() { return this.tracks.filter((track) => track.kind === "video"); }
|
||||
public addEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.push([eventType, callback]);
|
||||
}
|
||||
removeEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
public removeEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.filter(([t, c]) => {
|
||||
return t !== eventType || c !== callback;
|
||||
});
|
||||
}
|
||||
addTrack(track: MockMediaStreamTrack) {
|
||||
public addTrack(track: MockMediaStreamTrack) {
|
||||
this.tracks.push(track);
|
||||
this.dispatchEvent("addtrack");
|
||||
}
|
||||
removeTrack(track: MockMediaStreamTrack) { this.tracks.splice(this.tracks.indexOf(track), 1); }
|
||||
public removeTrack(track: MockMediaStreamTrack) { this.tracks.splice(this.tracks.indexOf(track), 1); }
|
||||
|
||||
public clone(): MediaStream {
|
||||
return new MockMediaStream(this.id + ".clone", this.tracks).typed();
|
||||
}
|
||||
|
||||
public isCloneOf(stream: MediaStream) {
|
||||
return this.id === stream.id + ".clone";
|
||||
}
|
||||
|
||||
// syntactic sugar for typing
|
||||
public typed(): MediaStream {
|
||||
return this as unknown as MediaStream;
|
||||
}
|
||||
}
|
||||
|
||||
export class MockMediaDeviceInfo {
|
||||
constructor(
|
||||
public kind: "audio" | "video",
|
||||
public kind: "audioinput" | "videoinput" | "audiooutput",
|
||||
) { }
|
||||
|
||||
public typed(): MediaDeviceInfo { return this as unknown as MediaDeviceInfo; }
|
||||
}
|
||||
|
||||
export class MockMediaHandler {
|
||||
getUserMediaStream(audio: boolean, video: boolean) {
|
||||
const tracks: MockMediaStreamTrack[] = [];
|
||||
if (audio) tracks.push(new MockMediaStreamTrack("audio_track", "audio"));
|
||||
if (video) tracks.push(new MockMediaStreamTrack("video_track", "video"));
|
||||
public userMediaStreams: MockMediaStream[] = [];
|
||||
public screensharingStreams: MockMediaStream[] = [];
|
||||
|
||||
return new MockMediaStream("mock_stream_from_media_handler", tracks);
|
||||
public getUserMediaStream(audio: boolean, video: boolean) {
|
||||
const tracks: MockMediaStreamTrack[] = [];
|
||||
if (audio) tracks.push(new MockMediaStreamTrack("usermedia_audio_track", "audio"));
|
||||
if (video) tracks.push(new MockMediaStreamTrack("usermedia_video_track", "video"));
|
||||
|
||||
const stream = new MockMediaStream(USERMEDIA_STREAM_ID, tracks);
|
||||
this.userMediaStreams.push(stream);
|
||||
return stream;
|
||||
}
|
||||
stopUserMediaStream() { }
|
||||
hasAudioDevice() { return true; }
|
||||
public stopUserMediaStream(stream: MockMediaStream) {
|
||||
stream.isStopped = true;
|
||||
}
|
||||
public getScreensharingStream = jest.fn((opts?: IScreensharingOpts) => {
|
||||
const tracks = [new MockMediaStreamTrack("screenshare_video_track", "video")];
|
||||
if (opts?.audio) tracks.push(new MockMediaStreamTrack("screenshare_audio_track", "audio"));
|
||||
|
||||
const stream = new MockMediaStream(SCREENSHARE_STREAM_ID, tracks);
|
||||
this.screensharingStreams.push(stream);
|
||||
return stream;
|
||||
});
|
||||
public stopScreensharingStream(stream: MockMediaStream) {
|
||||
stream.isStopped = true;
|
||||
}
|
||||
public hasAudioDevice() { return true; }
|
||||
public hasVideoDevice() { return true; }
|
||||
public stopAllStreams() {}
|
||||
|
||||
public typed(): MediaHandler { return this as unknown as MediaHandler; }
|
||||
}
|
||||
|
||||
export class MockMediaDevices {
|
||||
public enumerateDevices = jest.fn<Promise<MediaDeviceInfo[]>, []>().mockResolvedValue([
|
||||
new MockMediaDeviceInfo("audioinput").typed(),
|
||||
new MockMediaDeviceInfo("videoinput").typed(),
|
||||
]);
|
||||
|
||||
public getUserMedia = jest.fn<Promise<MediaStream>, [MediaStreamConstraints]>().mockReturnValue(
|
||||
Promise.resolve(new MockMediaStream("local_stream").typed()),
|
||||
);
|
||||
|
||||
public getDisplayMedia = jest.fn<Promise<MediaStream>, [MediaStreamConstraints]>().mockReturnValue(
|
||||
Promise.resolve(new MockMediaStream("local_display_stream").typed()),
|
||||
);
|
||||
|
||||
public typed(): MediaDevices { return this as unknown as MediaDevices; }
|
||||
}
|
||||
|
||||
type EmittedEvents = CallEventHandlerEvent | CallEvent | ClientEvent | RoomStateEvent | GroupCallEventHandlerEvent;
|
||||
type EmittedEventMap = CallEventHandlerEventHandlerMap &
|
||||
CallEventHandlerMap &
|
||||
ClientEventHandlerMap &
|
||||
RoomStateEventHandlerMap &
|
||||
GroupCallEventHandlerMap;
|
||||
|
||||
export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, EmittedEventMap> {
|
||||
public mediaHandler = new MockMediaHandler();
|
||||
|
||||
constructor(public userId: string, public deviceId: string, public sessionId: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
public groupCallEventHandler = {
|
||||
groupCalls: new Map<string, GroupCall>(),
|
||||
};
|
||||
|
||||
public callEventHandler = {
|
||||
calls: new Map<string, MatrixCall>(),
|
||||
};
|
||||
|
||||
public sendStateEvent = jest.fn<Promise<ISendEventResponse>, [
|
||||
roomId: string, eventType: EventType, content: any, statekey: string,
|
||||
]>();
|
||||
public sendToDevice = jest.fn<Promise<{}>, [
|
||||
eventType: string,
|
||||
contentMap: { [userId: string]: { [deviceId: string]: Record<string, any> } },
|
||||
txnId?: string,
|
||||
]>();
|
||||
|
||||
public getMediaHandler(): MediaHandler { return this.mediaHandler.typed(); }
|
||||
|
||||
public getUserId(): string { return this.userId; }
|
||||
|
||||
public getDeviceId(): string { return this.deviceId; }
|
||||
public getSessionId(): string { return this.sessionId; }
|
||||
|
||||
public getTurnServers = () => [];
|
||||
public isFallbackICEServerAllowed = () => false;
|
||||
public reEmitter = new ReEmitter(new TypedEventEmitter());
|
||||
public getUseE2eForGroupCall = () => false;
|
||||
public checkTurnServers = () => null;
|
||||
|
||||
public getSyncState = jest.fn<SyncState | null, []>().mockReturnValue(SyncState.Syncing);
|
||||
|
||||
public getRooms = jest.fn<Room[], []>().mockReturnValue([]);
|
||||
public getRoom = jest.fn();
|
||||
|
||||
public supportsExperimentalThreads(): boolean { return true; }
|
||||
public async decryptEventIfNeeded(): Promise<void> {}
|
||||
|
||||
public typed(): MatrixClient { return this as unknown as MatrixClient; }
|
||||
|
||||
public emitRoomState(event: MatrixEvent, state: RoomState): void {
|
||||
this.emit(
|
||||
RoomStateEvent.Events,
|
||||
event,
|
||||
state,
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class MockCallFeed {
|
||||
constructor(
|
||||
public userId: string,
|
||||
public deviceId: string | undefined,
|
||||
public stream: MockMediaStream,
|
||||
) {}
|
||||
|
||||
public measureVolumeActivity(val: boolean) {}
|
||||
public dispose() {}
|
||||
|
||||
public typed(): CallFeed {
|
||||
return this as unknown as CallFeed;
|
||||
}
|
||||
}
|
||||
|
||||
export function installWebRTCMocks() {
|
||||
global.navigator = {
|
||||
mediaDevices: new MockMediaDevices().typed(),
|
||||
} as unknown as Navigator;
|
||||
|
||||
global.window = {
|
||||
// @ts-ignore Mock
|
||||
RTCPeerConnection: MockRTCPeerConnection,
|
||||
// @ts-ignore Mock
|
||||
RTCSessionDescription: {},
|
||||
// @ts-ignore Mock
|
||||
RTCIceCandidate: {},
|
||||
getUserMedia: () => new MockMediaStream("local_stream"),
|
||||
};
|
||||
// @ts-ignore Mock
|
||||
global.document = {};
|
||||
|
||||
// @ts-ignore Mock
|
||||
global.AudioContext = MockAudioContext;
|
||||
|
||||
// @ts-ignore Mock
|
||||
global.RTCRtpReceiver = {
|
||||
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
|
||||
codecs: [],
|
||||
headerExtensions: [],
|
||||
}),
|
||||
};
|
||||
|
||||
// @ts-ignore Mock
|
||||
global.RTCRtpSender = {
|
||||
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
|
||||
codecs: [],
|
||||
headerExtensions: [],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function makeMockGroupCallStateEvent(roomId: string, groupCallId: string, content: IContent = {
|
||||
"m.type": GroupCallType.Video,
|
||||
"m.intent": GroupCallIntent.Prompt,
|
||||
}): MatrixEvent {
|
||||
return {
|
||||
getType: jest.fn().mockReturnValue(EventType.GroupCallPrefix),
|
||||
getRoomId: jest.fn().mockReturnValue(roomId),
|
||||
getTs: jest.fn().mockReturnValue(0),
|
||||
getContent: jest.fn().mockReturnValue(content),
|
||||
getStateKey: jest.fn().mockReturnValue(groupCallId),
|
||||
} as unknown as MatrixEvent;
|
||||
}
|
||||
|
||||
export function makeMockGroupCallMemberStateEvent(roomId: string, groupCallId: string): MatrixEvent {
|
||||
return {
|
||||
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
||||
getRoomId: jest.fn().mockReturnValue(roomId),
|
||||
getTs: jest.fn().mockReturnValue(0),
|
||||
getContent: jest.fn().mockReturnValue({}),
|
||||
getStateKey: jest.fn().mockReturnValue(groupCallId),
|
||||
} as unknown as MatrixEvent;
|
||||
}
|
||||
|
||||
@@ -1122,4 +1122,22 @@ describe("Crypto", function() {
|
||||
expect(free).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("start", () => {
|
||||
let client: TestClient;
|
||||
|
||||
beforeEach(async () => {
|
||||
client = new TestClient("@alice:example.org", "aliceweb");
|
||||
await client.client.initCrypto();
|
||||
});
|
||||
|
||||
afterEach(async function() {
|
||||
await client!.stop();
|
||||
});
|
||||
|
||||
// start() is a no-op nowadays, so there's not much to test here.
|
||||
it("should complete successfully", async () => {
|
||||
await client!.client.crypto!.start();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
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 '../../olm-loader';
|
||||
import { TestClient } from '../../TestClient';
|
||||
import { logger } from '../../../src/logger';
|
||||
import { DEHYDRATION_ALGORITHM } from '../../../src/crypto/dehydration';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
describe("Dehydration", () => {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running dehydration unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
return global.Olm.init();
|
||||
});
|
||||
|
||||
it("should rehydrate a dehydrated device", async () => {
|
||||
const key = new Uint8Array([1, 2, 3]);
|
||||
const alice = new TestClient(
|
||||
"@alice:example.com", "Osborne2", undefined, undefined,
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getDehydrationKey: async t => key,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const dehydratedDevice = new Olm.Account();
|
||||
dehydratedDevice.create();
|
||||
|
||||
alice.httpBackend.when("GET", "/dehydrated_device").respond(200, {
|
||||
device_id: "ABCDEFG",
|
||||
device_data: {
|
||||
algorithm: DEHYDRATION_ALGORITHM,
|
||||
account: dehydratedDevice.pickle(new Uint8Array(key)),
|
||||
},
|
||||
});
|
||||
alice.httpBackend.when("POST", "/dehydrated_device/claim").respond(200, {
|
||||
success: true,
|
||||
});
|
||||
|
||||
expect((await Promise.all([
|
||||
alice.client.rehydrateDevice(),
|
||||
alice.httpBackend.flushAllExpected(),
|
||||
]))[0])
|
||||
.toEqual("ABCDEFG");
|
||||
|
||||
expect(alice.client.getDeviceId()).toEqual("ABCDEFG");
|
||||
});
|
||||
|
||||
it("should dehydrate a device", async () => {
|
||||
const key = new Uint8Array([1, 2, 3]);
|
||||
const alice = new TestClient(
|
||||
"@alice:example.com", "Osborne2", undefined, undefined,
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getDehydrationKey: async t => key,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await alice.client.initCrypto();
|
||||
|
||||
alice.httpBackend.when("GET", "/room_keys/version").respond(404, {
|
||||
errcode: "M_NOT_FOUND",
|
||||
});
|
||||
|
||||
let pickledAccount = "";
|
||||
|
||||
alice.httpBackend.when("PUT", "/dehydrated_device")
|
||||
.check((req) => {
|
||||
expect(req.data.device_data).toMatchObject({
|
||||
algorithm: DEHYDRATION_ALGORITHM,
|
||||
account: expect.any(String),
|
||||
});
|
||||
pickledAccount = req.data.device_data.account;
|
||||
})
|
||||
.respond(200, {
|
||||
device_id: "ABCDEFG",
|
||||
});
|
||||
alice.httpBackend.when("POST", "/keys/upload/ABCDEFG")
|
||||
.check((req) => {
|
||||
expect(req.data).toMatchObject({
|
||||
"device_keys": expect.objectContaining({
|
||||
algorithms: expect.any(Array),
|
||||
device_id: "ABCDEFG",
|
||||
user_id: "@alice:example.com",
|
||||
keys: expect.objectContaining({
|
||||
"ed25519:ABCDEFG": expect.any(String),
|
||||
"curve25519:ABCDEFG": expect.any(String),
|
||||
}),
|
||||
signatures: expect.objectContaining({
|
||||
"@alice:example.com": expect.objectContaining({
|
||||
"ed25519:ABCDEFG": expect.any(String),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
"one_time_keys": expect.any(Object),
|
||||
"org.matrix.msc2732.fallback_keys": expect.any(Object),
|
||||
});
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
try {
|
||||
const deviceId =
|
||||
(await Promise.all([
|
||||
alice.client.createDehydratedDevice(new Uint8Array(key), {}),
|
||||
alice.httpBackend.flushAllExpected(),
|
||||
]))[0];
|
||||
|
||||
expect(deviceId).toEqual("ABCDEFG");
|
||||
expect(deviceId).not.toEqual("");
|
||||
|
||||
// try to rehydrate the dehydrated device
|
||||
const rehydrated = new Olm.Account();
|
||||
try {
|
||||
rehydrated.unpickle(new Uint8Array(key), pickledAccount);
|
||||
} finally {
|
||||
rehydrated.free();
|
||||
}
|
||||
} finally {
|
||||
alice.client?.crypto?.dehydrationManager?.stop();
|
||||
alice.client?.crypto?.deviceList.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -18,7 +18,7 @@ import "../../../olm-loader";
|
||||
import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { SAS } from "../../../../src/crypto/verification/SAS";
|
||||
import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
|
||||
import { makeTestClients } from './util';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -31,14 +31,9 @@ describe("verification request integration tests with crypto layer", function()
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
setupWebcrypto();
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
teardownWebcrypto();
|
||||
});
|
||||
|
||||
it("should request and accept a verification", async function() {
|
||||
const [[alice, bob], clearTestClientTimeouts] = await makeTestClients(
|
||||
[
|
||||
|
||||
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import "../../../olm-loader";
|
||||
import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
|
||||
import { makeTestClients } from './util';
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { ISasEvent, SAS, SasEvent } from "../../../../src/crypto/verification/SAS";
|
||||
import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
|
||||
@@ -41,14 +41,9 @@ describe("SAS verification", function() {
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
setupWebcrypto();
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
teardownWebcrypto();
|
||||
});
|
||||
|
||||
it("should error on an unexpected event", async function() {
|
||||
//channel, baseApis, userId, deviceId, startEvent, request
|
||||
const request = {
|
||||
|
||||
@@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning';
|
||||
import '../../../olm-loader';
|
||||
import { MatrixClient, MatrixEvent } from '../../../../src/matrix';
|
||||
import { encodeBase64 } from "../../../../src/crypto/olmlib";
|
||||
import { setupWebcrypto, teardownWebcrypto } from './util';
|
||||
import { VerificationBase } from '../../../../src/crypto/verification/Base';
|
||||
import { MatrixClient, MatrixEvent } from '../../../../src';
|
||||
import "../../../../src/crypto"; // import this to cycle-break
|
||||
import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning';
|
||||
import { VerificationRequest } from '../../../../src/crypto/verification/request/VerificationRequest';
|
||||
import { IVerificationChannel } from '../../../../src/crypto/verification/request/Channel';
|
||||
import { VerificationBase } from '../../../../src/crypto/verification/Base';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -35,14 +36,9 @@ const testKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
|
||||
|
||||
describe("self-verifications", () => {
|
||||
beforeAll(function() {
|
||||
setupWebcrypto();
|
||||
return global.Olm.init();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
teardownWebcrypto();
|
||||
});
|
||||
|
||||
it("triggers a request for key sharing upon completion", async () => {
|
||||
const userId = "@test:localhost";
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import nodeCrypto from "crypto";
|
||||
|
||||
import { TestClient } from '../../../TestClient';
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { IRoomTimelineData } from "../../../../src/models/event-timeline-set";
|
||||
@@ -118,16 +116,3 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[]
|
||||
|
||||
return [clients, destroy];
|
||||
}
|
||||
|
||||
export function setupWebcrypto() {
|
||||
global.crypto = {
|
||||
getRandomValues: (buf) => {
|
||||
return nodeCrypto.randomFillSync(buf as any);
|
||||
},
|
||||
} as unknown as Crypto;
|
||||
}
|
||||
|
||||
export function teardownWebcrypto() {
|
||||
// @ts-ignore undefined != Crypto
|
||||
global.crypto = undefined;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import { ToDeviceChannel } from
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { MatrixClient } from "../../../../src/client";
|
||||
import { setupWebcrypto, teardownWebcrypto } from "./util";
|
||||
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
|
||||
import { VerificationBase } from "../../../../src/crypto/verification/Base";
|
||||
|
||||
@@ -147,14 +146,6 @@ async function distributeEvent(
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("verification request unit tests", function() {
|
||||
beforeAll(function() {
|
||||
setupWebcrypto();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
teardownWebcrypto();
|
||||
});
|
||||
|
||||
it("transition from UNSENT to DONE through happy path", async function() {
|
||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// We have to use EventEmitter here to mock part of the matrix-widget-api
|
||||
// project, which doesn't know about our TypeEventEmitter implementation at all
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
import { MockedObject } from "jest-mock";
|
||||
import {
|
||||
WidgetApi,
|
||||
WidgetApiToWidgetAction,
|
||||
MatrixCapabilities,
|
||||
ITurnServer,
|
||||
IRoomEvent,
|
||||
} from "matrix-widget-api";
|
||||
|
||||
import { createRoomWidgetClient, MsgType } from "../../src/matrix";
|
||||
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { ICapabilities } from "../../src/embedded";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { ToDeviceBatch } from "../../src/models/ToDeviceMessage";
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
|
||||
class MockWidgetApi extends EventEmitter {
|
||||
public start = jest.fn();
|
||||
public requestCapability = jest.fn();
|
||||
public requestCapabilities = jest.fn();
|
||||
public requestCapabilityForRoomTimeline = jest.fn();
|
||||
public requestCapabilityToSendEvent = jest.fn();
|
||||
public requestCapabilityToReceiveEvent = jest.fn();
|
||||
public requestCapabilityToSendMessage = jest.fn();
|
||||
public requestCapabilityToReceiveMessage = jest.fn();
|
||||
public requestCapabilityToSendState = jest.fn();
|
||||
public requestCapabilityToReceiveState = jest.fn();
|
||||
public requestCapabilityToSendToDevice = jest.fn();
|
||||
public requestCapabilityToReceiveToDevice = jest.fn();
|
||||
public sendRoomEvent = jest.fn(() => ({ event_id: `$${Math.random()}` }));
|
||||
public sendStateEvent = jest.fn();
|
||||
public sendToDevice = jest.fn();
|
||||
public readStateEvents = jest.fn(() => []);
|
||||
public getTurnServers = jest.fn(() => []);
|
||||
|
||||
public transport = { reply: jest.fn() };
|
||||
}
|
||||
|
||||
describe("RoomWidgetClient", () => {
|
||||
let widgetApi: MockedObject<WidgetApi>;
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
widgetApi = new MockWidgetApi() as unknown as MockedObject<WidgetApi>;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
const makeClient = async (capabilities: ICapabilities): Promise<void> => {
|
||||
const baseUrl = "https://example.org";
|
||||
client = createRoomWidgetClient(widgetApi, capabilities, "!1:example.org", { baseUrl });
|
||||
expect(widgetApi.start).toHaveBeenCalled(); // needs to have been called early in order to not miss messages
|
||||
widgetApi.emit("ready");
|
||||
await client.startClient();
|
||||
};
|
||||
|
||||
describe("events", () => {
|
||||
it("sends", async () => {
|
||||
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToSendEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");
|
||||
await client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 });
|
||||
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
|
||||
"org.matrix.rageshake_request", { request_id: 123 }, "!1:example.org",
|
||||
);
|
||||
});
|
||||
|
||||
it("receives", async () => {
|
||||
const event = new MatrixEvent({
|
||||
type: "org.matrix.rageshake_request",
|
||||
event_id: "$pduhfiidph",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
content: { request_id: 123 },
|
||||
}).getEffectiveEvent();
|
||||
|
||||
await makeClient({ receiveEvent: ["org.matrix.rageshake_request"] });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToReceiveEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");
|
||||
|
||||
const emittedEvent = new Promise<MatrixEvent>(resolve => client.once(ClientEvent.Event, resolve));
|
||||
const emittedSync = new Promise<SyncState>(resolve => client.once(ClientEvent.Sync, resolve));
|
||||
widgetApi.emit(
|
||||
`action:${WidgetApiToWidgetAction.SendEvent}`,
|
||||
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
|
||||
);
|
||||
|
||||
// The client should've emitted about the received event
|
||||
expect((await emittedEvent).getEffectiveEvent()).toEqual(event);
|
||||
expect(await emittedSync).toEqual(SyncState.Syncing);
|
||||
// It should've also inserted the event into the room object
|
||||
const room = client.getRoom("!1:example.org");
|
||||
expect(room).not.toBeNull();
|
||||
expect(room!.getLiveTimeline().getEvents().map(e => e.getEffectiveEvent())).toEqual([event]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("messages", () => {
|
||||
it("requests permissions for specific message types", async () => {
|
||||
await makeClient({ sendMessage: [MsgType.Text], receiveMessage: [MsgType.Text] });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToSendMessage).toHaveBeenCalledWith(MsgType.Text);
|
||||
expect(widgetApi.requestCapabilityToReceiveMessage).toHaveBeenCalledWith(MsgType.Text);
|
||||
});
|
||||
|
||||
it("requests permissions for all message types", async () => {
|
||||
await makeClient({ sendMessage: true, receiveMessage: true });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToSendMessage).toHaveBeenCalledWith();
|
||||
expect(widgetApi.requestCapabilityToReceiveMessage).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
// No point in testing sending and receiving since it's done exactly the
|
||||
// same way as non-message events
|
||||
});
|
||||
|
||||
describe("state events", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: "org.example.foo",
|
||||
event_id: "$sfkjfsksdkfsd",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
state_key: "bar",
|
||||
content: { hello: "world" },
|
||||
}).getEffectiveEvent();
|
||||
|
||||
it("sends", async () => {
|
||||
await makeClient({ sendState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToSendState).toHaveBeenCalledWith("org.example.foo", "bar");
|
||||
await client.sendStateEvent("!1:example.org", "org.example.foo", { hello: "world" }, "bar");
|
||||
expect(widgetApi.sendStateEvent).toHaveBeenCalledWith(
|
||||
"org.example.foo", "bar", { hello: "world" }, "!1:example.org",
|
||||
);
|
||||
});
|
||||
|
||||
it("receives", async () => {
|
||||
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
|
||||
|
||||
const emittedEvent = new Promise<MatrixEvent>(resolve => client.once(ClientEvent.Event, resolve));
|
||||
const emittedSync = new Promise<SyncState>(resolve => client.once(ClientEvent.Sync, resolve));
|
||||
widgetApi.emit(
|
||||
`action:${WidgetApiToWidgetAction.SendEvent}`,
|
||||
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
|
||||
);
|
||||
|
||||
// The client should've emitted about the received event
|
||||
expect((await emittedEvent).getEffectiveEvent()).toEqual(event);
|
||||
expect(await emittedSync).toEqual(SyncState.Syncing);
|
||||
// It should've also inserted the event into the room object
|
||||
const room = client.getRoom("!1:example.org");
|
||||
expect(room).not.toBeNull();
|
||||
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
|
||||
});
|
||||
|
||||
it("backfills", async () => {
|
||||
widgetApi.readStateEvents.mockImplementation(async (eventType, limit, stateKey) =>
|
||||
eventType === "org.example.foo" && (limit ?? Infinity) > 0 && stateKey === "bar"
|
||||
? [event as IRoomEvent]
|
||||
: [],
|
||||
);
|
||||
|
||||
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
|
||||
|
||||
const room = client.getRoom("!1:example.org");
|
||||
expect(room).not.toBeNull();
|
||||
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
|
||||
});
|
||||
});
|
||||
|
||||
describe("to-device messages", () => {
|
||||
const unencryptedContentMap = {
|
||||
"@alice:example.org": { "*": { hello: "alice!" } },
|
||||
"@bob:example.org": { bobDesktop: { hello: "bob!" } },
|
||||
};
|
||||
|
||||
it("sends unencrypted (sendToDevice)", async () => {
|
||||
await makeClient({ sendToDevice: ["org.example.foo"] });
|
||||
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
|
||||
|
||||
await client.sendToDevice("org.example.foo", unencryptedContentMap);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, unencryptedContentMap);
|
||||
});
|
||||
|
||||
it("sends unencrypted (queueToDevice)", async () => {
|
||||
await makeClient({ sendToDevice: ["org.example.foo"] });
|
||||
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
|
||||
|
||||
const batch: ToDeviceBatch = {
|
||||
eventType: "org.example.foo",
|
||||
batch: [
|
||||
{ userId: "@alice:example.org", deviceId: "*", payload: { hello: "alice!" } },
|
||||
{ userId: "@bob:example.org", deviceId: "bobDesktop", payload: { hello: "bob!" } },
|
||||
],
|
||||
};
|
||||
await client.queueToDevice(batch);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, unencryptedContentMap);
|
||||
});
|
||||
|
||||
it("sends encrypted (encryptAndSendToDevices)", async () => {
|
||||
await makeClient({ sendToDevice: ["org.example.foo"] });
|
||||
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
|
||||
|
||||
const payload = { type: "org.example.foo", hello: "world" };
|
||||
await client.encryptAndSendToDevices(
|
||||
[
|
||||
{ userId: "@alice:example.org", deviceInfo: new DeviceInfo("aliceWeb") },
|
||||
{ userId: "@bob:example.org", deviceInfo: new DeviceInfo("bobDesktop") },
|
||||
],
|
||||
payload,
|
||||
);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", true, {
|
||||
"@alice:example.org": { aliceWeb: payload },
|
||||
"@bob:example.org": { bobDesktop: payload },
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ encrypted: false, title: "unencrypted" },
|
||||
{ encrypted: true, title: "encrypted" },
|
||||
])("receives $title", async ({ encrypted }) => {
|
||||
await makeClient({ receiveToDevice: ["org.example.foo"] });
|
||||
expect(widgetApi.requestCapabilityToReceiveToDevice).toHaveBeenCalledWith("org.example.foo");
|
||||
|
||||
const event = {
|
||||
type: "org.example.foo",
|
||||
sender: "@alice:example.org",
|
||||
encrypted,
|
||||
content: { hello: "world" },
|
||||
};
|
||||
|
||||
const emittedEvent = new Promise<MatrixEvent>(resolve => client.once(ClientEvent.ToDeviceEvent, resolve));
|
||||
const emittedSync = new Promise<SyncState>(resolve => client.once(ClientEvent.Sync, resolve));
|
||||
widgetApi.emit(
|
||||
`action:${WidgetApiToWidgetAction.SendToDevice}`,
|
||||
new CustomEvent(`action:${WidgetApiToWidgetAction.SendToDevice}`, { detail: { data: event } }),
|
||||
);
|
||||
|
||||
expect((await emittedEvent).getEffectiveEvent()).toEqual({
|
||||
type: event.type,
|
||||
sender: event.sender,
|
||||
content: event.content,
|
||||
});
|
||||
expect((await emittedEvent).isEncrypted()).toEqual(encrypted);
|
||||
expect(await emittedSync).toEqual(SyncState.Syncing);
|
||||
});
|
||||
});
|
||||
|
||||
it("gets TURN servers", async () => {
|
||||
const server1: ITurnServer = {
|
||||
uris: [
|
||||
"turn:turn.example.com:3478?transport=udp",
|
||||
"turn:10.20.30.40:3478?transport=tcp",
|
||||
"turns:10.20.30.40:443?transport=tcp",
|
||||
],
|
||||
username: "1443779631:@user:example.com",
|
||||
password: "JlKfBy1QwLrO20385QyAtEyIv0=",
|
||||
};
|
||||
const server2: ITurnServer = {
|
||||
uris: [
|
||||
"turn:turn.example.com:3478?transport=udp",
|
||||
"turn:10.20.30.40:3478?transport=tcp",
|
||||
"turns:10.20.30.40:443?transport=tcp",
|
||||
],
|
||||
username: "1448999322:@user:example.com",
|
||||
password: "hunter2",
|
||||
};
|
||||
const clientServer1: IClientTurnServer = {
|
||||
urls: server1.uris,
|
||||
username: server1.username,
|
||||
credential: server1.password,
|
||||
};
|
||||
const clientServer2: IClientTurnServer = {
|
||||
urls: server2.uris,
|
||||
username: server2.username,
|
||||
credential: server2.password,
|
||||
};
|
||||
|
||||
let emitServer2: () => void;
|
||||
const getServer2 = new Promise<ITurnServer>(resolve => emitServer2 = () => resolve(server2));
|
||||
widgetApi.getTurnServers.mockImplementation(async function* () {
|
||||
yield server1;
|
||||
yield await getServer2;
|
||||
});
|
||||
|
||||
await makeClient({ turnServers: true });
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC3846TurnServers);
|
||||
|
||||
// The first server should've arrived immediately
|
||||
expect(client.getTurnServers()).toEqual([clientServer1]);
|
||||
|
||||
// Subsequent servers arrive asynchronously and should emit an event
|
||||
const emittedServer = new Promise<IClientTurnServer[]>(resolve =>
|
||||
client.once(ClientEvent.TurnServers, resolve),
|
||||
);
|
||||
emitServer2!();
|
||||
expect(await emittedServer).toEqual([clientServer2]);
|
||||
expect(client.getTurnServers()).toEqual([clientServer2]);
|
||||
});
|
||||
});
|
||||
@@ -55,6 +55,23 @@ describe('EventTimelineSet', () => {
|
||||
});
|
||||
};
|
||||
|
||||
const mkThreadResponse = (root: MatrixEvent) => utils.mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
user: userA,
|
||||
room: roomId,
|
||||
content: {
|
||||
"body": "Thread response :: " + Math.random(),
|
||||
"m.relates_to": {
|
||||
"event_id": root.getId(),
|
||||
"m.in_reply_to": {
|
||||
"event_id": root.getId(),
|
||||
},
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
}, room.client);
|
||||
|
||||
beforeEach(() => {
|
||||
client = utils.mock(MatrixClient, 'MatrixClient');
|
||||
client.reEmitter = utils.mock(ReEmitter, 'ReEmitter');
|
||||
@@ -117,6 +134,13 @@ describe('EventTimelineSet', () => {
|
||||
});
|
||||
|
||||
describe('addEventToTimeline', () => {
|
||||
let thread: Thread;
|
||||
|
||||
beforeEach(() => {
|
||||
(client.supportsExperimentalThreads as jest.Mock).mockReturnValue(true);
|
||||
thread = new Thread("!thread_id:server", messageEvent, { room, client });
|
||||
});
|
||||
|
||||
it("Adds event to timeline", () => {
|
||||
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
@@ -144,6 +168,58 @@ describe('EventTimelineSet', () => {
|
||||
);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should not add an event to a timeline that does not belong to the timelineSet", () => {
|
||||
const eventTimelineSet2 = new EventTimelineSet(room);
|
||||
const liveTimeline2 = eventTimelineSet2.getLiveTimeline();
|
||||
expect(liveTimeline2.getEvents().length).toStrictEqual(0);
|
||||
|
||||
expect(() => {
|
||||
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline2, {
|
||||
toStartOfTimeline: true,
|
||||
});
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
it("should not add a threaded reply to the main room timeline", () => {
|
||||
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
|
||||
const threadedReplyEvent = mkThreadResponse(messageEvent);
|
||||
|
||||
eventTimelineSet.addEventToTimeline(threadedReplyEvent, liveTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
});
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
});
|
||||
|
||||
it("should not add a normal message to the timelineSet representing a thread", () => {
|
||||
const eventTimelineSetForThread = new EventTimelineSet(room, {}, client, thread);
|
||||
const liveTimeline = eventTimelineSetForThread.getLiveTimeline();
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
|
||||
eventTimelineSetForThread.addEventToTimeline(messageEvent, liveTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
});
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
});
|
||||
|
||||
describe('non-room timeline', () => {
|
||||
it('Adds event to timeline', () => {
|
||||
const nonRoomEventTimelineSet = new EventTimelineSet(
|
||||
// This is what we're specifically testing against, a timeline
|
||||
// without a `room` defined
|
||||
undefined,
|
||||
);
|
||||
const nonRoomEventTimeline = new EventTimeline(nonRoomEventTimelineSet);
|
||||
|
||||
expect(nonRoomEventTimeline.getEvents().length).toStrictEqual(0);
|
||||
nonRoomEventTimelineSet.addEventToTimeline(messageEvent, nonRoomEventTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
});
|
||||
expect(nonRoomEventTimeline.getEvents().length).toStrictEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggregateRelations', () => {
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { mocked } from 'jest-mock';
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { EventTimeline } from "../../src/models/event-timeline";
|
||||
import { Direction, EventTimeline } from "../../src/models/event-timeline";
|
||||
import { RoomState } from "../../src/models/room-state";
|
||||
import { MatrixClient } from "../../src/matrix";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { RoomMember } from "../../src/models/room-member";
|
||||
import { EventTimelineSet } from "../../src/models/event-timeline-set";
|
||||
|
||||
jest.mock("../../src/models/room-state");
|
||||
|
||||
describe("EventTimeline", function() {
|
||||
const roomId = "!foo:bar";
|
||||
const userA = "@alice:bar";
|
||||
@@ -23,7 +21,14 @@ describe("EventTimeline", function() {
|
||||
const timelineSet = new EventTimelineSet(room);
|
||||
jest.spyOn(room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet);
|
||||
|
||||
return new EventTimeline(timelineSet);
|
||||
const timeline = new EventTimeline(timelineSet);
|
||||
// We manually stub the methods we'll be mocking out later instead of mocking the whole module
|
||||
// otherwise the default member property values (e.g. paginationToken) will be incorrect
|
||||
timeline.getState(Direction.Backward)!.setStateEvents = jest.fn();
|
||||
timeline.getState(Direction.Backward)!.getSentinelMember = jest.fn();
|
||||
timeline.getState(Direction.Forward)!.setStateEvents = jest.fn();
|
||||
timeline.getState(Direction.Forward)!.getSentinelMember = jest.fn();
|
||||
return timeline;
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
@@ -55,13 +60,13 @@ describe("EventTimeline", function() {
|
||||
];
|
||||
timeline.initialiseState(events);
|
||||
// @ts-ignore private prop
|
||||
const timelineStartState = timeline.startState;
|
||||
const timelineStartState = timeline.startState!;
|
||||
expect(mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(
|
||||
events,
|
||||
{ timelineWasEmpty: undefined },
|
||||
);
|
||||
// @ts-ignore private prop
|
||||
const timelineEndState = timeline.endState;
|
||||
const timelineEndState = timeline.endState!;
|
||||
expect(mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(
|
||||
events,
|
||||
{ timelineWasEmpty: undefined },
|
||||
@@ -98,7 +103,17 @@ describe("EventTimeline", function() {
|
||||
expect(timeline.getPaginationToken(EventTimeline.FORWARDS)).toBe(null);
|
||||
});
|
||||
|
||||
it("setPaginationToken should set token", function() {
|
||||
it("setPaginationToken should set token", function() {
|
||||
timeline.setPaginationToken("back", EventTimeline.BACKWARDS);
|
||||
timeline.setPaginationToken("fwd", EventTimeline.FORWARDS);
|
||||
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("back");
|
||||
expect(timeline.getPaginationToken(EventTimeline.FORWARDS)).toEqual("fwd");
|
||||
});
|
||||
|
||||
it("should be able to store pagination tokens for mixed room timelines", () => {
|
||||
const timelineSet = new EventTimelineSet(undefined);
|
||||
const timeline = new EventTimeline(timelineSet);
|
||||
|
||||
timeline.setPaginationToken("back", EventTimeline.BACKWARDS);
|
||||
timeline.setPaginationToken("fwd", EventTimeline.FORWARDS);
|
||||
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("back");
|
||||
@@ -185,14 +200,14 @@ describe("EventTimeline", function() {
|
||||
sentinel.name = "Old Alice";
|
||||
sentinel.membership = "join";
|
||||
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)).getSentinelMember
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)).getSentinelMember
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
@@ -225,14 +240,14 @@ describe("EventTimeline", function() {
|
||||
sentinel.name = "Old Alice";
|
||||
sentinel.membership = "join";
|
||||
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)).getSentinelMember
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)).getSentinelMember
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
@@ -269,15 +284,15 @@ describe("EventTimeline", function() {
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: false });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
||||
|
||||
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
|
||||
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).
|
||||
toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined });
|
||||
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
|
||||
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).
|
||||
toHaveBeenCalledWith([events[1]], { timelineWasEmpty: undefined });
|
||||
|
||||
expect(events[0].forwardLooking).toBe(true);
|
||||
expect(events[1].forwardLooking).toBe(true);
|
||||
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).
|
||||
not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -298,15 +313,15 @@ describe("EventTimeline", function() {
|
||||
timeline.addEvent(events[0], { toStartOfTimeline: true });
|
||||
timeline.addEvent(events[1], { toStartOfTimeline: true });
|
||||
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).
|
||||
toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined });
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).
|
||||
toHaveBeenCalledWith([events[1]], { timelineWasEmpty: undefined });
|
||||
|
||||
expect(events[0].forwardLooking).toBe(false);
|
||||
expect(events[1].forwardLooking).toBe(false);
|
||||
|
||||
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
|
||||
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).
|
||||
not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
import { sleep } from "../../../src/utils";
|
||||
|
||||
jest.mock("../../../src/utils");
|
||||
// setupTests mocks `timeoutSignal` due to hanging timers
|
||||
jest.unmock("../../../src/http-api/utils");
|
||||
|
||||
describe("timeoutSignal", () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -1703,4 +1703,39 @@ describe("MatrixClient", function() {
|
||||
expect(newSourceRoom._state.get(PolicyScope.User)?.[eventId]).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("using E2EE in group calls", () => {
|
||||
const opts = {
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: identityServerUrl,
|
||||
accessToken: "my.access.token",
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: userId,
|
||||
};
|
||||
|
||||
it("enables E2EE by default", () => {
|
||||
const client = new MatrixClient(opts);
|
||||
|
||||
expect(client.getUseE2eForGroupCall()).toBe(true);
|
||||
});
|
||||
|
||||
it("enables E2EE when enabled explicitly", () => {
|
||||
const client = new MatrixClient({
|
||||
useE2eForGroupCall: true,
|
||||
...opts,
|
||||
});
|
||||
|
||||
expect(client.getUseE2eForGroupCall()).toBe(true);
|
||||
});
|
||||
|
||||
it("disables E2EE if disabled explicitly", () => {
|
||||
const client = new MatrixClient({
|
||||
useE2eForGroupCall: false,
|
||||
...opts,
|
||||
});
|
||||
|
||||
expect(client.getUseE2eForGroupCall()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,11 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "../../../src/client";
|
||||
import { Room } from "../../../src/models/room";
|
||||
import { Thread } from "../../../src/models/thread";
|
||||
import { mkThread } from "../../test-utils/thread";
|
||||
import { TestClient } from "../../TestClient";
|
||||
|
||||
describe('Thread', () => {
|
||||
describe("constructor", () => {
|
||||
@@ -25,4 +29,52 @@ describe('Thread', () => {
|
||||
}).toThrow("element-web#22141: A thread requires a room in order to function");
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasUserReadEvent", () => {
|
||||
const myUserId = "@bob:example.org";
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
const testClient = new TestClient(
|
||||
myUserId,
|
||||
"DEVICE",
|
||||
"ACCESS_TOKEN",
|
||||
undefined,
|
||||
{ timelineSupport: false },
|
||||
);
|
||||
client = testClient.client;
|
||||
room = new Room("123", client, myUserId);
|
||||
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("considers own events with no RR as read", () => {
|
||||
const { thread, events } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: myUserId,
|
||||
participantUserIds: [myUserId],
|
||||
length: 2,
|
||||
});
|
||||
|
||||
expect(thread.hasUserReadEvent(myUserId, events.at(-1)!.getId() ?? "")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("considers other events with no RR as unread", () => {
|
||||
const { thread, events } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: myUserId,
|
||||
participantUserIds: ["@alice:example.org"],
|
||||
length: 2,
|
||||
});
|
||||
|
||||
expect(thread.hasUserReadEvent("@alice:example.org", events.at(-1)!.getId() ?? "")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ let event: MatrixEvent;
|
||||
let threadEvent: MatrixEvent;
|
||||
|
||||
const ROOM_ID = "!roomId:example.org";
|
||||
let THREAD_ID;
|
||||
let THREAD_ID: string;
|
||||
|
||||
function mkPushAction(notify, highlight): IActionsObject {
|
||||
return {
|
||||
@@ -76,7 +76,7 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
event: true,
|
||||
}, mockClient);
|
||||
|
||||
THREAD_ID = event.getId();
|
||||
THREAD_ID = event.getId()!;
|
||||
threadEvent = mkEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
@@ -108,6 +108,16 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
|
||||
});
|
||||
|
||||
it("does not change the room count when there's no unread count", () => {
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, 0);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
|
||||
|
||||
fixNotificationCountOnDecryption(mockClient, event);
|
||||
|
||||
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Total)).toBe(1);
|
||||
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
|
||||
});
|
||||
|
||||
it("changes the thread count to highlight on decryption", () => {
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1);
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
|
||||
@@ -118,6 +128,16 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(1);
|
||||
});
|
||||
|
||||
it("does not change the room count when there's no unread count", () => {
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
|
||||
|
||||
fixNotificationCountOnDecryption(mockClient, event);
|
||||
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0);
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
|
||||
});
|
||||
|
||||
it("emits events", () => {
|
||||
const cb = jest.fn();
|
||||
room.on(RoomEvent.UnreadNotifications, cb);
|
||||
|
||||
@@ -22,6 +22,8 @@ import { MatrixClient } from "../../src/client";
|
||||
import { ToDeviceBatch } from '../../src/models/ToDeviceMessage';
|
||||
import { logger } from '../../src/logger';
|
||||
import { IStore } from '../../src/store';
|
||||
import { flushPromises } from '../test-utils/flushPromises';
|
||||
import { removeElement } from "../../src/utils";
|
||||
|
||||
const FAKE_USER = "@alice:example.org";
|
||||
const FAKE_DEVICE_ID = "AAAAAAAA";
|
||||
@@ -47,19 +49,6 @@ enum StoreType {
|
||||
IndexedDB = 'IndexedDB',
|
||||
}
|
||||
|
||||
// Jest now uses @sinonjs/fake-timers which exposes tickAsync() and a number of
|
||||
// other async methods which break the event loop, letting scheduled promise
|
||||
// callbacks run. Unfortunately, Jest doesn't expose these, so we have to do
|
||||
// it manually (this is what sinon does under the hood). We do both in a loop
|
||||
// until the thing we expect happens: hopefully this is the least flakey way
|
||||
// and avoids assuming anything about the app's behaviour.
|
||||
const realSetTimeout = setTimeout;
|
||||
function flushPromises() {
|
||||
return new Promise(r => {
|
||||
realSetTimeout(r, 1);
|
||||
});
|
||||
}
|
||||
|
||||
async function flushAndRunTimersUntil(cond: () => boolean) {
|
||||
while (!cond()) {
|
||||
await flushPromises();
|
||||
@@ -75,6 +64,8 @@ describe.each([
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(async function() {
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.useRealTimers();
|
||||
httpBackend = new MockHttpBackend();
|
||||
|
||||
let store: IStore;
|
||||
@@ -300,7 +291,7 @@ describe.each([
|
||||
],
|
||||
});
|
||||
|
||||
expect(await httpBackend.flush(undefined, 1, 1)).toEqual(1);
|
||||
expect(await httpBackend.flush(undefined, 1, 20)).toEqual(1);
|
||||
await flushPromises();
|
||||
|
||||
const dummyEvent = new MatrixEvent({
|
||||
@@ -328,12 +319,12 @@ describe.each([
|
||||
});
|
||||
}
|
||||
|
||||
const expectedCounts = [20, 1];
|
||||
httpBackend.when(
|
||||
"PUT", "/sendToDevice/org.example.foo/",
|
||||
).check((request) => {
|
||||
expect(Object.keys(request.data.messages).length).toEqual(20);
|
||||
expect(removeElement(expectedCounts, c => c === Object.keys(request.data.messages).length)).toBeTruthy();
|
||||
}).respond(200, {});
|
||||
|
||||
httpBackend.when(
|
||||
"PUT", "/sendToDevice/org.example.foo/",
|
||||
).check((request) => {
|
||||
|
||||
@@ -16,10 +16,11 @@ limitations under the License.
|
||||
|
||||
import MockHttpBackend from 'matrix-mock-request';
|
||||
|
||||
import { ReceiptType } from '../../src/@types/read_receipts';
|
||||
import { MAIN_ROOM_TIMELINE, ReceiptType } from '../../src/@types/read_receipts';
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { Feature, ServerSupport } from '../../src/feature';
|
||||
import { EventType } from '../../src/matrix';
|
||||
import { MAIN_ROOM_TIMELINE } from '../../src/models/read-receipt';
|
||||
import { synthesizeReceipt } from '../../src/models/read-receipt';
|
||||
import { encodeUri } from '../../src/utils';
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
|
||||
@@ -69,15 +70,8 @@ const roomEvent = utils.mkEvent({
|
||||
},
|
||||
});
|
||||
|
||||
function mockServerSideSupport(client, hasServerSideSupport) {
|
||||
const doesServerSupportUnstableFeature = client.doesServerSupportUnstableFeature;
|
||||
client.doesServerSupportUnstableFeature = (unstableFeature) => {
|
||||
if (unstableFeature === "org.matrix.msc3771") {
|
||||
return Promise.resolve(hasServerSideSupport);
|
||||
} else {
|
||||
return doesServerSupportUnstableFeature(unstableFeature);
|
||||
}
|
||||
};
|
||||
function mockServerSideSupport(client, serverSideSupport: ServerSupport) {
|
||||
client.canSupport.set(Feature.ThreadUnreadNotifications, serverSideSupport);
|
||||
}
|
||||
|
||||
describe("Read receipt", () => {
|
||||
@@ -103,13 +97,31 @@ describe("Read receipt", () => {
|
||||
expect(request.data.thread_id).toEqual(THREAD_ID);
|
||||
}).respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, true);
|
||||
mockServerSideSupport(client, ServerSupport.Stable);
|
||||
client.sendReceipt(threadEvent, ReceiptType.Read, {});
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("sends an unthreaded receipt", async () => {
|
||||
httpBackend.when(
|
||||
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
|
||||
$roomId: ROOM_ID,
|
||||
$receiptType: ReceiptType.Read,
|
||||
$eventId: threadEvent.getId()!,
|
||||
}),
|
||||
).check((request) => {
|
||||
expect(request.data.thread_id).toBeUndefined();
|
||||
}).respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, ServerSupport.Stable);
|
||||
client.sendReadReceipt(threadEvent, ReceiptType.Read, true);
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("sends a room read receipt", async () => {
|
||||
httpBackend.when(
|
||||
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
|
||||
@@ -121,7 +133,7 @@ describe("Read receipt", () => {
|
||||
expect(request.data.thread_id).toEqual(MAIN_ROOM_TIMELINE);
|
||||
}).respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, true);
|
||||
mockServerSideSupport(client, ServerSupport.Stable);
|
||||
client.sendReceipt(roomEvent, ReceiptType.Read, {});
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
@@ -139,7 +151,7 @@ describe("Read receipt", () => {
|
||||
expect(request.data.thread_id).toBeUndefined();
|
||||
}).respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, false);
|
||||
mockServerSideSupport(client, ServerSupport.Unsupported);
|
||||
client.sendReceipt(threadEvent, ReceiptType.Read, {});
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
@@ -157,11 +169,27 @@ describe("Read receipt", () => {
|
||||
expect(request.data).toEqual({});
|
||||
}).respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, false);
|
||||
mockServerSideSupport(client, ServerSupport.Unsupported);
|
||||
client.sendReceipt(threadEvent, ReceiptType.Read, undefined);
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await flushPromises();
|
||||
});
|
||||
});
|
||||
|
||||
describe("synthesizeReceipt", () => {
|
||||
it.each([
|
||||
{ event: roomEvent, destinationId: MAIN_ROOM_TIMELINE },
|
||||
{ event: threadEvent, destinationId: threadEvent.threadRootId! },
|
||||
])("adds the receipt to $destinationId", ({ event, destinationId }) => {
|
||||
const userId = "@bob:example.org";
|
||||
const receiptType = ReceiptType.Read;
|
||||
|
||||
const fakeReadReceipt = synthesizeReceipt(userId, event, receiptType);
|
||||
|
||||
const content = fakeReadReceipt.getContent()[event.getId()!][receiptType][userId];
|
||||
|
||||
expect(content.thread_id).toEqual(destinationId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -152,7 +152,7 @@ describe("RoomState", function() {
|
||||
it("should return a single MatrixEvent if a state_key was specified",
|
||||
function() {
|
||||
const event = state.getStateEvents("m.room.member", userA);
|
||||
expect(event.getContent()).toMatchObject({
|
||||
expect(event?.getContent()).toMatchObject({
|
||||
membership: "join",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,9 +38,8 @@ import { RoomState } from "../../src/models/room-state";
|
||||
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
import { ReceiptType } from "../../src/@types/read_receipts";
|
||||
import { ReceiptType, WrappedReceipt } from "../../src/@types/read_receipts";
|
||||
import { FeatureSupport, Thread, ThreadEvent, THREAD_RELATION_TYPE } from "../../src/models/thread";
|
||||
import { WrappedReceipt } from "../../src/models/read-receipt";
|
||||
import { Crypto } from "../../src/crypto";
|
||||
|
||||
describe("Room", function() {
|
||||
|
||||
@@ -364,6 +364,63 @@ describe("SyncAccumulator", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("should accumulate threaded read receipts", () => {
|
||||
const receipt1 = {
|
||||
type: "m.receipt",
|
||||
room_id: "!foo:bar",
|
||||
content: {
|
||||
"$event1:localhost": {
|
||||
[ReceiptType.Read]: {
|
||||
"@alice:localhost": { ts: 1, thread_id: "main" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const receipt2 = {
|
||||
type: "m.receipt",
|
||||
room_id: "!foo:bar",
|
||||
content: {
|
||||
"$event2:localhost": {
|
||||
[ReceiptType.Read]: {
|
||||
"@alice:localhost": { ts: 2, thread_id: "$123" }, // does not clobbers event1 receipt
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
sa.accumulate(syncSkeleton({
|
||||
ephemeral: {
|
||||
events: [receipt1],
|
||||
},
|
||||
}));
|
||||
sa.accumulate(syncSkeleton({
|
||||
ephemeral: {
|
||||
events: [receipt2],
|
||||
},
|
||||
}));
|
||||
|
||||
expect(
|
||||
sa.getJSON().roomsData.join["!foo:bar"].ephemeral.events.length,
|
||||
).toEqual(1);
|
||||
expect(
|
||||
sa.getJSON().roomsData.join["!foo:bar"].ephemeral.events[0],
|
||||
).toEqual({
|
||||
type: "m.receipt",
|
||||
room_id: "!foo:bar",
|
||||
content: {
|
||||
"$event1:localhost": {
|
||||
[ReceiptType.Read]: {
|
||||
"@alice:localhost": { ts: 1, thread_id: "main" },
|
||||
},
|
||||
},
|
||||
"$event2:localhost": {
|
||||
[ReceiptType.Read]: {
|
||||
"@alice:localhost": { ts: 2, thread_id: "$123" },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe("summary field", function() {
|
||||
function createSyncResponseWithSummary(summary) {
|
||||
return {
|
||||
|
||||
+1034
-343
File diff suppressed because it is too large
Load Diff
@@ -20,22 +20,117 @@ import {
|
||||
EventTimeline,
|
||||
EventTimelineSet,
|
||||
EventType,
|
||||
GroupCallIntent,
|
||||
GroupCallType,
|
||||
IRoomTimelineData,
|
||||
MatrixCall,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
RoomEvent,
|
||||
RoomMember,
|
||||
} from "../../../src";
|
||||
import { MatrixClient } from "../../../src/client";
|
||||
import { CallEventHandler, CallEventHandlerEvent } from "../../../src/webrtc/callEventHandler";
|
||||
import { GroupCallEventHandler } from "../../../src/webrtc/groupCallEventHandler";
|
||||
import { SyncState } from "../../../src/sync";
|
||||
import { installWebRTCMocks, MockRTCPeerConnection } from "../../test-utils/webrtc";
|
||||
import { sleep } from "../../../src/utils";
|
||||
|
||||
describe("callEventHandler", () => {
|
||||
it("should ignore a call if invite & hangup come within a single sync", () => {
|
||||
const testClient = new TestClient();
|
||||
const client = testClient.client;
|
||||
const room = new Room("!room:id", client, "@user:id");
|
||||
const timelineData: IRoomTimelineData = { timeline: new EventTimeline(new EventTimelineSet(room, {})) };
|
||||
describe("CallEventHandler", () => {
|
||||
let client: MatrixClient;
|
||||
beforeEach(() => {
|
||||
installWebRTCMocks();
|
||||
|
||||
client = new TestClient("@alice:foo", "somedevice", "token", undefined, {}).client;
|
||||
client.callEventHandler = new CallEventHandler(client);
|
||||
client.callEventHandler.start();
|
||||
client.groupCallEventHandler = new GroupCallEventHandler(client);
|
||||
client.groupCallEventHandler.start();
|
||||
client.sendStateEvent = jest.fn().mockResolvedValue({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
client.callEventHandler!.stop();
|
||||
client.groupCallEventHandler!.stop();
|
||||
});
|
||||
|
||||
const sync = async () => {
|
||||
client.getSyncState = jest.fn().mockReturnValue(SyncState.Syncing);
|
||||
client.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Prepared);
|
||||
|
||||
// We can't await the event processing
|
||||
await sleep(10);
|
||||
};
|
||||
|
||||
it("should enforce inbound toDevice message ordering", async () => {
|
||||
const callEventHandler = client.callEventHandler!;
|
||||
const event1 = new MatrixEvent({
|
||||
type: EventType.CallInvite,
|
||||
content: {
|
||||
call_id: "123",
|
||||
seq: 0,
|
||||
},
|
||||
});
|
||||
callEventHandler["onToDeviceEvent"](event1);
|
||||
|
||||
expect(callEventHandler.callEventBuffer.length).toBe(1);
|
||||
expect(callEventHandler.callEventBuffer[0]).toBe(event1);
|
||||
|
||||
const event2 = new MatrixEvent({
|
||||
type: EventType.CallCandidates,
|
||||
content: {
|
||||
call_id: "123",
|
||||
seq: 1,
|
||||
},
|
||||
});
|
||||
callEventHandler["onToDeviceEvent"](event2);
|
||||
|
||||
expect(callEventHandler.callEventBuffer.length).toBe(2);
|
||||
expect(callEventHandler.callEventBuffer[1]).toBe(event2);
|
||||
|
||||
const event3 = new MatrixEvent({
|
||||
type: EventType.CallCandidates,
|
||||
content: {
|
||||
call_id: "123",
|
||||
seq: 3,
|
||||
},
|
||||
});
|
||||
callEventHandler["onToDeviceEvent"](event3);
|
||||
|
||||
expect(callEventHandler.callEventBuffer.length).toBe(2);
|
||||
expect(callEventHandler.nextSeqByCall.get("123")).toBe(2);
|
||||
expect(callEventHandler.toDeviceEventBuffers.get("123")?.length).toBe(1);
|
||||
|
||||
const event4 = new MatrixEvent({
|
||||
type: EventType.CallCandidates,
|
||||
content: {
|
||||
call_id: "123",
|
||||
seq: 4,
|
||||
},
|
||||
});
|
||||
callEventHandler["onToDeviceEvent"](event4);
|
||||
|
||||
expect(callEventHandler.callEventBuffer.length).toBe(2);
|
||||
expect(callEventHandler.nextSeqByCall.get("123")).toBe(2);
|
||||
expect(callEventHandler.toDeviceEventBuffers.get("123")?.length).toBe(2);
|
||||
|
||||
const event5 = new MatrixEvent({
|
||||
type: EventType.CallCandidates,
|
||||
content: {
|
||||
call_id: "123",
|
||||
seq: 2,
|
||||
},
|
||||
});
|
||||
callEventHandler["onToDeviceEvent"](event5);
|
||||
|
||||
expect(callEventHandler.callEventBuffer.length).toBe(5);
|
||||
expect(callEventHandler.nextSeqByCall.get("123")).toBe(5);
|
||||
expect(callEventHandler.toDeviceEventBuffers.get("123")?.length).toBe(0);
|
||||
});
|
||||
|
||||
it("should ignore a call if invite & hangup come within a single sync", () => {
|
||||
const room = new Room("!room:id", client, "@user:id");
|
||||
const timelineData: IRoomTimelineData = { timeline: new EventTimeline(new EventTimelineSet(room, {})) };
|
||||
|
||||
// Fire off call invite then hangup within a single sync
|
||||
const callInvite = new MatrixEvent({
|
||||
@@ -62,4 +157,117 @@ describe("callEventHandler", () => {
|
||||
|
||||
expect(incomingCallEmitted).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should ignore non-call events", async () => {
|
||||
// @ts-ignore Mock handleCallEvent is private
|
||||
jest.spyOn(client.callEventHandler, "handleCallEvent");
|
||||
jest.spyOn(client, "checkTurnServers").mockReturnValue(Promise.resolve(true));
|
||||
|
||||
const room = new Room("!room:id", client, "@user:id");
|
||||
const timelineData: IRoomTimelineData = { timeline: new EventTimeline(new EventTimelineSet(room, {})) };
|
||||
|
||||
client.emit(RoomEvent.Timeline, new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
room_id: "!room:id",
|
||||
content: {
|
||||
text: "hello",
|
||||
|
||||
},
|
||||
}), room, false, false, timelineData);
|
||||
await sync();
|
||||
|
||||
// @ts-ignore Mock handleCallEvent is private
|
||||
expect(client.callEventHandler.handleCallEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("handleCallEvent()", () => {
|
||||
const incomingCallListener = jest.fn();
|
||||
let timelineData: IRoomTimelineData;
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
room = new Room("!room:id", client, client.getUserId()!);
|
||||
timelineData = { timeline: new EventTimeline(new EventTimelineSet(room, {})) };
|
||||
|
||||
jest.spyOn(client, "checkTurnServers").mockReturnValue(Promise.resolve(true));
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
jest.spyOn(room, "getMember").mockReturnValue({ user_id: client.getUserId() } as unknown as RoomMember);
|
||||
|
||||
client.on(CallEventHandlerEvent.Incoming, incomingCallListener);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
MockRTCPeerConnection.resetInstances();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should create a call when receiving an invite", async () => {
|
||||
client.emit(RoomEvent.Timeline, new MatrixEvent({
|
||||
type: EventType.CallInvite,
|
||||
room_id: "!room:id",
|
||||
content: {
|
||||
call_id: "123",
|
||||
},
|
||||
}), room, false, false, timelineData);
|
||||
await sync();
|
||||
|
||||
expect(incomingCallListener).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle group call event", async () => {
|
||||
let call: MatrixCall;
|
||||
const groupCall = await client.createGroupCall(
|
||||
room.roomId,
|
||||
GroupCallType.Voice,
|
||||
false,
|
||||
GroupCallIntent.Ring,
|
||||
);
|
||||
const SESSION_ID = "sender_session_id";
|
||||
const GROUP_CALL_ID = "group_call_id";
|
||||
const DEVICE_ID = "device_id";
|
||||
|
||||
incomingCallListener.mockImplementation((c) => call = c);
|
||||
jest.spyOn(client.groupCallEventHandler!, "getGroupCallById").mockReturnValue(groupCall);
|
||||
// @ts-ignore Mock onIncomingCall is private
|
||||
jest.spyOn(groupCall, "onIncomingCall");
|
||||
|
||||
await groupCall.enter();
|
||||
client.emit(RoomEvent.Timeline, new MatrixEvent({
|
||||
type: EventType.CallInvite,
|
||||
room_id: "!room:id",
|
||||
content: {
|
||||
call_id: "123",
|
||||
conf_id: GROUP_CALL_ID,
|
||||
device_id: DEVICE_ID,
|
||||
sender_session_id: SESSION_ID,
|
||||
dest_session_id: client.getSessionId(),
|
||||
},
|
||||
}), room, false, false, timelineData);
|
||||
await sync();
|
||||
|
||||
expect(incomingCallListener).toHaveBeenCalled();
|
||||
expect(call!.groupCallId).toBe(GROUP_CALL_ID);
|
||||
// @ts-ignore Mock opponentDeviceId is private
|
||||
expect(call.opponentDeviceId).toBe(DEVICE_ID);
|
||||
expect(call!.getOpponentSessionId()).toBe(SESSION_ID);
|
||||
// @ts-ignore Mock onIncomingCall is private
|
||||
expect(groupCall.onIncomingCall).toHaveBeenCalledWith(call);
|
||||
|
||||
groupCall.terminate(false);
|
||||
});
|
||||
|
||||
it("ignores a call with a different invitee than us", async () => {
|
||||
client.emit(RoomEvent.Timeline, new MatrixEvent({
|
||||
type: EventType.CallInvite,
|
||||
room_id: "!room:id",
|
||||
content: {
|
||||
call_id: "123",
|
||||
invitee: "@bob:bar",
|
||||
},
|
||||
}), room, false, false, timelineData);
|
||||
await sync();
|
||||
|
||||
expect(incomingCallListener).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
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 { mocked } from "jest-mock";
|
||||
|
||||
import { ClientEvent } from "../../../src/client";
|
||||
import { RoomMember } from "../../../src/models/room-member";
|
||||
import { SyncState } from "../../../src/sync";
|
||||
import {
|
||||
GroupCallIntent,
|
||||
GroupCallState,
|
||||
GroupCallType,
|
||||
GroupCallTerminationReason,
|
||||
} from "../../../src/webrtc/groupCall";
|
||||
import { IContent, MatrixEvent } from "../../../src/models/event";
|
||||
import { Room } from "../../../src/models/room";
|
||||
import { RoomState } from "../../../src/models/room-state";
|
||||
import { GroupCallEventHandler, GroupCallEventHandlerEvent } from "../../../src/webrtc/groupCallEventHandler";
|
||||
import { flushPromises } from "../../test-utils/flushPromises";
|
||||
import { makeMockGroupCallStateEvent, MockCallMatrixClient } from "../../test-utils/webrtc";
|
||||
|
||||
const FAKE_USER_ID = "@alice:test.dummy";
|
||||
const FAKE_DEVICE_ID = "AAAAAAA";
|
||||
const FAKE_SESSION_ID = "session1";
|
||||
const FAKE_ROOM_ID = "!roomid:test.dummy";
|
||||
const FAKE_GROUP_CALL_ID = "fakegroupcallid";
|
||||
|
||||
describe('Group Call Event Handler', function() {
|
||||
let groupCallEventHandler: GroupCallEventHandler;
|
||||
let mockClient: MockCallMatrixClient;
|
||||
let mockRoom: Room;
|
||||
let mockMember: RoomMember;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = new MockCallMatrixClient(
|
||||
FAKE_USER_ID, FAKE_DEVICE_ID, FAKE_SESSION_ID,
|
||||
);
|
||||
groupCallEventHandler = new GroupCallEventHandler(mockClient.typed());
|
||||
|
||||
mockMember = {
|
||||
userId: FAKE_USER_ID,
|
||||
membership: "join",
|
||||
} as unknown as RoomMember;
|
||||
|
||||
const mockEvent = makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID);
|
||||
|
||||
mockRoom = {
|
||||
on: () => {},
|
||||
off: () => {},
|
||||
roomId: FAKE_ROOM_ID,
|
||||
currentState: {
|
||||
getStateEvents: jest.fn((type, key) => {
|
||||
if (type === mockEvent.getType()) {
|
||||
return key === undefined ? [mockEvent] : mockEvent;
|
||||
} else {
|
||||
return key === undefined ? [] : null;
|
||||
}
|
||||
}),
|
||||
},
|
||||
getMember: (userId: string) => userId === FAKE_USER_ID ? mockMember : null,
|
||||
} as unknown as Room;
|
||||
|
||||
(mockClient as any).getRoom = jest.fn().mockReturnValue(mockRoom);
|
||||
});
|
||||
|
||||
describe("reacts to state changes", () => {
|
||||
it("terminates call", async () => {
|
||||
await groupCallEventHandler.start();
|
||||
mockClient.emitRoomState(
|
||||
makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID),
|
||||
{ roomId: FAKE_ROOM_ID } as unknown as RoomState,
|
||||
);
|
||||
|
||||
const groupCall = groupCallEventHandler.groupCalls.get(FAKE_ROOM_ID)!;
|
||||
|
||||
expect(groupCall.state).toBe(GroupCallState.LocalCallFeedUninitialized);
|
||||
|
||||
mockClient.emitRoomState(
|
||||
makeMockGroupCallStateEvent(
|
||||
FAKE_ROOM_ID, FAKE_GROUP_CALL_ID, {
|
||||
"m.type": GroupCallType.Video,
|
||||
"m.intent": GroupCallIntent.Prompt,
|
||||
"m.terminated": GroupCallTerminationReason.CallEnded,
|
||||
},
|
||||
),
|
||||
{
|
||||
roomId: FAKE_ROOM_ID,
|
||||
} as unknown as RoomState,
|
||||
);
|
||||
|
||||
expect(groupCall.state).toBe(GroupCallState.Ended);
|
||||
});
|
||||
});
|
||||
|
||||
it("waits until client starts syncing", async () => {
|
||||
mockClient.getSyncState.mockReturnValue(null);
|
||||
let isStarted = false;
|
||||
(async () => {
|
||||
await groupCallEventHandler.start();
|
||||
isStarted = true;
|
||||
})();
|
||||
|
||||
const setSyncState = async (newState: SyncState) => {
|
||||
const oldState = mockClient.getSyncState();
|
||||
mockClient.getSyncState.mockReturnValue(newState);
|
||||
mockClient.emit(ClientEvent.Sync, newState, oldState, undefined);
|
||||
await flushPromises();
|
||||
};
|
||||
|
||||
await flushPromises();
|
||||
expect(isStarted).toEqual(false);
|
||||
|
||||
await setSyncState(SyncState.Prepared);
|
||||
expect(isStarted).toEqual(false);
|
||||
|
||||
await setSyncState(SyncState.Syncing);
|
||||
expect(isStarted).toEqual(true);
|
||||
});
|
||||
|
||||
it("finds existing group calls when started", async () => {
|
||||
const mockClientEmit = mockClient.emit = jest.fn();
|
||||
|
||||
mockClient.getRooms.mockReturnValue([mockRoom]);
|
||||
await groupCallEventHandler.start();
|
||||
|
||||
expect(mockClientEmit).toHaveBeenCalledWith(
|
||||
GroupCallEventHandlerEvent.Incoming,
|
||||
expect.objectContaining({
|
||||
groupCallId: FAKE_GROUP_CALL_ID,
|
||||
}),
|
||||
);
|
||||
|
||||
groupCallEventHandler.stop();
|
||||
});
|
||||
|
||||
it("can wait until a room is ready for group calls", async () => {
|
||||
await groupCallEventHandler.start();
|
||||
|
||||
const prom = groupCallEventHandler.waitUntilRoomReadyForGroupCalls(FAKE_ROOM_ID);
|
||||
let resolved = false;
|
||||
|
||||
(async () => {
|
||||
await prom;
|
||||
resolved = true;
|
||||
})();
|
||||
|
||||
expect(resolved).toEqual(false);
|
||||
mockClient.emit(ClientEvent.Room, mockRoom);
|
||||
|
||||
await prom;
|
||||
expect(resolved).toEqual(true);
|
||||
|
||||
groupCallEventHandler.stop();
|
||||
});
|
||||
|
||||
it("fires events for incoming calls", async () => {
|
||||
const onIncomingGroupCall = jest.fn();
|
||||
mockClient.on(GroupCallEventHandlerEvent.Incoming, onIncomingGroupCall);
|
||||
await groupCallEventHandler.start();
|
||||
|
||||
mockClient.emitRoomState(
|
||||
makeMockGroupCallStateEvent(
|
||||
FAKE_ROOM_ID, FAKE_GROUP_CALL_ID,
|
||||
),
|
||||
{
|
||||
roomId: FAKE_ROOM_ID,
|
||||
} as unknown as RoomState,
|
||||
);
|
||||
|
||||
expect(onIncomingGroupCall).toHaveBeenCalledWith(expect.objectContaining({
|
||||
groupCallId: FAKE_GROUP_CALL_ID,
|
||||
}));
|
||||
|
||||
mockClient.off(GroupCallEventHandlerEvent.Incoming, onIncomingGroupCall);
|
||||
});
|
||||
|
||||
it("handles data channel", async () => {
|
||||
await groupCallEventHandler.start();
|
||||
|
||||
const dataChannelOptions = {
|
||||
"maxPacketLifeTime": "life_time",
|
||||
"maxRetransmits": "retransmits",
|
||||
"ordered": "ordered",
|
||||
"protocol": "protocol",
|
||||
};
|
||||
|
||||
mockClient.emitRoomState(
|
||||
makeMockGroupCallStateEvent(
|
||||
FAKE_ROOM_ID,
|
||||
FAKE_GROUP_CALL_ID,
|
||||
{
|
||||
"m.type": GroupCallType.Video,
|
||||
"m.intent": GroupCallIntent.Prompt,
|
||||
"dataChannelsEnabled": true,
|
||||
dataChannelOptions,
|
||||
},
|
||||
),
|
||||
{
|
||||
roomId: FAKE_ROOM_ID,
|
||||
} as unknown as RoomState,
|
||||
);
|
||||
|
||||
// @ts-ignore Mock dataChannelsEnabled is private
|
||||
expect(groupCallEventHandler.groupCalls.get(FAKE_ROOM_ID)?.dataChannelsEnabled).toBe(true);
|
||||
// @ts-ignore Mock dataChannelOptions is private
|
||||
expect(groupCallEventHandler.groupCalls.get(FAKE_ROOM_ID)?.dataChannelOptions).toStrictEqual(
|
||||
dataChannelOptions,
|
||||
);
|
||||
});
|
||||
|
||||
describe("ignoring invalid group call state events", () => {
|
||||
let mockClientEmit: jest.Func;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClientEmit = mockClient.emit = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
groupCallEventHandler.stop();
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const setupCallAndStart = async (content?: IContent) => {
|
||||
mocked(mockRoom.currentState.getStateEvents).mockReturnValue([
|
||||
makeMockGroupCallStateEvent(
|
||||
FAKE_ROOM_ID,
|
||||
FAKE_GROUP_CALL_ID,
|
||||
content,
|
||||
),
|
||||
] as unknown as MatrixEvent);
|
||||
mockClient.getRooms.mockReturnValue([mockRoom]);
|
||||
await groupCallEventHandler.start();
|
||||
};
|
||||
|
||||
it("ignores terminated calls", async () => {
|
||||
await setupCallAndStart({
|
||||
"m.type": GroupCallType.Video,
|
||||
"m.intent": GroupCallIntent.Prompt,
|
||||
"m.terminated": GroupCallTerminationReason.CallEnded,
|
||||
});
|
||||
|
||||
expect(mockClientEmit).not.toHaveBeenCalledWith(
|
||||
GroupCallEventHandlerEvent.Incoming,
|
||||
expect.objectContaining({
|
||||
groupCallId: FAKE_GROUP_CALL_ID,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores calls with invalid type", async () => {
|
||||
await setupCallAndStart({
|
||||
"m.type": "fake_type",
|
||||
"m.intent": GroupCallIntent.Prompt,
|
||||
});
|
||||
|
||||
expect(mockClientEmit).not.toHaveBeenCalledWith(
|
||||
GroupCallEventHandlerEvent.Incoming,
|
||||
expect.objectContaining({
|
||||
groupCallId: FAKE_GROUP_CALL_ID,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores calls with invalid intent", async () => {
|
||||
await setupCallAndStart({
|
||||
"m.type": GroupCallType.Video,
|
||||
"m.intent": "fake_intent",
|
||||
});
|
||||
|
||||
expect(mockClientEmit).not.toHaveBeenCalledWith(
|
||||
GroupCallEventHandlerEvent.Incoming,
|
||||
expect.objectContaining({
|
||||
groupCallId: FAKE_GROUP_CALL_ID,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores calls without a room", async () => {
|
||||
mockClient.getRoom.mockReturnValue(undefined);
|
||||
|
||||
await setupCallAndStart();
|
||||
|
||||
expect(mockClientEmit).not.toHaveBeenCalledWith(
|
||||
GroupCallEventHandlerEvent.Incoming,
|
||||
expect.objectContaining({
|
||||
groupCallId: FAKE_GROUP_CALL_ID,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,462 @@
|
||||
/*
|
||||
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 { GroupCall, MatrixCall, MatrixClient } from "../../../src";
|
||||
import { MediaHandler, MediaHandlerEvent } from "../../../src/webrtc/mediaHandler";
|
||||
import { MockMediaDeviceInfo, MockMediaDevices, MockMediaStream, MockMediaStreamTrack } from "../../test-utils/webrtc";
|
||||
|
||||
const FAKE_AUDIO_INPUT_ID = "aaaaaaaa";
|
||||
const FAKE_VIDEO_INPUT_ID = "vvvvvvvv";
|
||||
const FAKE_DESKTOP_SOURCE_ID = "ddddddd";
|
||||
|
||||
describe('Media Handler', function() {
|
||||
let mockMediaDevices: MockMediaDevices;
|
||||
let mediaHandler: MediaHandler;
|
||||
let calls: Map<string, MatrixCall>;
|
||||
let groupCalls: Map<string, GroupCall>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockMediaDevices = new MockMediaDevices();
|
||||
|
||||
global.navigator = {
|
||||
mediaDevices: mockMediaDevices.typed(),
|
||||
} as unknown as Navigator;
|
||||
|
||||
calls = new Map();
|
||||
groupCalls = new Map();
|
||||
|
||||
mediaHandler = new MediaHandler({
|
||||
callEventHandler: {
|
||||
calls,
|
||||
},
|
||||
groupCallEventHandler: {
|
||||
groupCalls,
|
||||
},
|
||||
} as unknown as MatrixClient);
|
||||
});
|
||||
|
||||
it("does not trigger update after restore media settings ", () => {
|
||||
mediaHandler.restoreMediaSettings(FAKE_AUDIO_INPUT_ID, FAKE_VIDEO_INPUT_ID);
|
||||
|
||||
expect(mockMediaDevices.getUserMedia).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sets device IDs on restore media settings", async () => {
|
||||
mediaHandler.restoreMediaSettings(FAKE_AUDIO_INPUT_ID, FAKE_VIDEO_INPUT_ID);
|
||||
|
||||
await mediaHandler.getUserMediaStream(true, true);
|
||||
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledWith(expect.objectContaining({
|
||||
audio: expect.objectContaining({
|
||||
deviceId: { ideal: FAKE_AUDIO_INPUT_ID },
|
||||
}),
|
||||
video: expect.objectContaining({
|
||||
deviceId: { ideal: FAKE_VIDEO_INPUT_ID },
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
it("sets audio device ID", async () => {
|
||||
await mediaHandler.setAudioInput(FAKE_AUDIO_INPUT_ID);
|
||||
|
||||
await mediaHandler.getUserMediaStream(true, false);
|
||||
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledWith(expect.objectContaining({
|
||||
audio: expect.objectContaining({
|
||||
deviceId: { ideal: FAKE_AUDIO_INPUT_ID },
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
it("sets audio settings", async () => {
|
||||
await mediaHandler.setAudioSettings({
|
||||
autoGainControl: false,
|
||||
echoCancellation: true,
|
||||
noiseSuppression: false,
|
||||
});
|
||||
|
||||
await mediaHandler.getUserMediaStream(true, false);
|
||||
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledWith(expect.objectContaining({
|
||||
audio: expect.objectContaining({
|
||||
autoGainControl: { ideal: false },
|
||||
echoCancellation: { ideal: true },
|
||||
noiseSuppression: { ideal: false },
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
it("sets video device ID", async () => {
|
||||
await mediaHandler.setVideoInput(FAKE_VIDEO_INPUT_ID);
|
||||
|
||||
await mediaHandler.getUserMediaStream(false, true);
|
||||
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledWith(expect.objectContaining({
|
||||
video: expect.objectContaining({
|
||||
deviceId: { ideal: FAKE_VIDEO_INPUT_ID },
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
it("sets media inputs", async () => {
|
||||
await mediaHandler.setMediaInputs(FAKE_AUDIO_INPUT_ID, FAKE_VIDEO_INPUT_ID);
|
||||
|
||||
await mediaHandler.getUserMediaStream(true, true);
|
||||
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledWith(expect.objectContaining({
|
||||
audio: expect.objectContaining({
|
||||
deviceId: { ideal: FAKE_AUDIO_INPUT_ID },
|
||||
}),
|
||||
video: expect.objectContaining({
|
||||
deviceId: { ideal: FAKE_VIDEO_INPUT_ID },
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
describe("updateLocalUsermediaStreams", () => {
|
||||
let localStreamsChangedHandler: jest.Mock<void, []>;
|
||||
|
||||
beforeEach(() => {
|
||||
localStreamsChangedHandler = jest.fn();
|
||||
mediaHandler.on(MediaHandlerEvent.LocalStreamsChanged, localStreamsChangedHandler);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mediaHandler.off(MediaHandlerEvent.LocalStreamsChanged, localStreamsChangedHandler);
|
||||
});
|
||||
|
||||
it("does nothing if it has no streams", async () => {
|
||||
mediaHandler.updateLocalUsermediaStreams();
|
||||
expect(mockMediaDevices.getUserMedia).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not emit LocalStreamsChanged if it had no streams", async () => {
|
||||
await mediaHandler.updateLocalUsermediaStreams();
|
||||
|
||||
expect(localStreamsChangedHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("with existing streams", () => {
|
||||
let stopTrack: jest.Mock<void, []>;
|
||||
let updateLocalUsermediaStream: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
stopTrack = jest.fn();
|
||||
|
||||
mediaHandler.userMediaStreams = [
|
||||
{
|
||||
getTracks: () => [{
|
||||
stop: stopTrack,
|
||||
} as unknown as MediaStreamTrack],
|
||||
} as unknown as MediaStream,
|
||||
];
|
||||
|
||||
updateLocalUsermediaStream = jest.fn();
|
||||
});
|
||||
|
||||
it("stops existing streams", async () => {
|
||||
mediaHandler.updateLocalUsermediaStreams();
|
||||
expect(stopTrack).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("replaces streams on calls", async () => {
|
||||
calls.set("some_call", {
|
||||
hasLocalUserMediaAudioTrack: true,
|
||||
hasLocalUserMediaVideoTrack: true,
|
||||
callHasEnded: jest.fn().mockReturnValue(false),
|
||||
updateLocalUsermediaStream,
|
||||
} as unknown as MatrixCall);
|
||||
|
||||
await mediaHandler.updateLocalUsermediaStreams();
|
||||
expect(updateLocalUsermediaStream).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("doesn't replace streams on ended calls", async () => {
|
||||
calls.set("some_call", {
|
||||
hasLocalUserMediaAudioTrack: true,
|
||||
hasLocalUserMediaVideoTrack: true,
|
||||
callHasEnded: jest.fn().mockReturnValue(true),
|
||||
updateLocalUsermediaStream,
|
||||
} as unknown as MatrixCall);
|
||||
|
||||
await mediaHandler.updateLocalUsermediaStreams();
|
||||
expect(updateLocalUsermediaStream).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("replaces streams on group calls", async () => {
|
||||
groupCalls.set("some_group_call", {
|
||||
localCallFeed: {},
|
||||
updateLocalUsermediaStream,
|
||||
} as unknown as GroupCall);
|
||||
|
||||
await mediaHandler.updateLocalUsermediaStreams();
|
||||
expect(updateLocalUsermediaStream).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("doesn't replace streams on group calls with no localCallFeed", async () => {
|
||||
groupCalls.set("some_group_call", {
|
||||
localCallFeed: null,
|
||||
updateLocalUsermediaStream,
|
||||
} as unknown as GroupCall);
|
||||
|
||||
await mediaHandler.updateLocalUsermediaStreams();
|
||||
expect(updateLocalUsermediaStream).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("emits LocalStreamsChanged", async () => {
|
||||
await mediaHandler.updateLocalUsermediaStreams();
|
||||
|
||||
expect(localStreamsChangedHandler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasAudioDevice", () => {
|
||||
it("returns true if the system has audio inputs", async () => {
|
||||
expect(await mediaHandler.hasAudioDevice()).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns false if the system has no audio inputs", async () => {
|
||||
mockMediaDevices.enumerateDevices.mockReturnValue(Promise.resolve([
|
||||
new MockMediaDeviceInfo("videoinput").typed(),
|
||||
]));
|
||||
expect(await mediaHandler.hasAudioDevice()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasVideoDevice", () => {
|
||||
it("returns true if the system has video inputs", async () => {
|
||||
expect(await mediaHandler.hasVideoDevice()).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns false if the system has no video inputs", async () => {
|
||||
mockMediaDevices.enumerateDevices.mockReturnValue(Promise.resolve([
|
||||
new MockMediaDeviceInfo("audioinput").typed(),
|
||||
]));
|
||||
expect(await mediaHandler.hasVideoDevice()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUserMediaStream", () => {
|
||||
beforeEach(() => {
|
||||
// replace this with one that returns a new object each time so we can
|
||||
// tell whether we've ended up with the same stream
|
||||
mockMediaDevices.getUserMedia.mockImplementation((constraints: MediaStreamConstraints) => {
|
||||
const stream = new MockMediaStream("local_stream");
|
||||
if (constraints.audio) {
|
||||
const track = new MockMediaStreamTrack("audio_track", "audio");
|
||||
track.settings = { deviceId: FAKE_AUDIO_INPUT_ID };
|
||||
stream.addTrack(track);
|
||||
}
|
||||
if (constraints.video) {
|
||||
const track = new MockMediaStreamTrack("video_track", "video");
|
||||
track.settings = { deviceId: FAKE_VIDEO_INPUT_ID };
|
||||
stream.addTrack(track);
|
||||
}
|
||||
|
||||
return Promise.resolve(stream.typed());
|
||||
});
|
||||
|
||||
mediaHandler.restoreMediaSettings(FAKE_AUDIO_INPUT_ID, FAKE_VIDEO_INPUT_ID);
|
||||
});
|
||||
|
||||
it("returns the same stream for reusable streams", async () => {
|
||||
const stream1 = await mediaHandler.getUserMediaStream(true, false);
|
||||
const stream2 = await mediaHandler.getUserMediaStream(true, false) as unknown as MockMediaStream;
|
||||
|
||||
expect(stream2.isCloneOf(stream1)).toEqual(true);
|
||||
});
|
||||
|
||||
it("doesn't re-use stream if reusable is false", async () => {
|
||||
const stream1 = await mediaHandler.getUserMediaStream(true, false, false);
|
||||
const stream2 = await mediaHandler.getUserMediaStream(true, false) as unknown as MockMediaStream;
|
||||
|
||||
expect(stream2.isCloneOf(stream1)).toEqual(false);
|
||||
});
|
||||
|
||||
it("doesn't re-use stream if existing stream lacks audio", async () => {
|
||||
const stream1 = await mediaHandler.getUserMediaStream(false, true);
|
||||
const stream2 = await mediaHandler.getUserMediaStream(true, false) as unknown as MockMediaStream;
|
||||
|
||||
expect(stream2.isCloneOf(stream1)).toEqual(false);
|
||||
});
|
||||
|
||||
it("doesn't re-use stream if existing stream lacks video", async () => {
|
||||
const stream1 = await mediaHandler.getUserMediaStream(true, false);
|
||||
const stream2 = await mediaHandler.getUserMediaStream(false, true) as unknown as MockMediaStream;
|
||||
|
||||
expect(stream2.isCloneOf(stream1)).toEqual(false);
|
||||
});
|
||||
|
||||
it("strips unwanted audio tracks from re-used stream", async () => {
|
||||
const stream1 = await mediaHandler.getUserMediaStream(true, true);
|
||||
const stream2 = await mediaHandler.getUserMediaStream(false, true) as unknown as MockMediaStream;
|
||||
|
||||
expect(stream2.isCloneOf(stream1)).toEqual(true);
|
||||
expect(stream2.getAudioTracks().length).toEqual(0);
|
||||
});
|
||||
|
||||
it("strips unwanted video tracks from re-used stream", async () => {
|
||||
const stream1 = await mediaHandler.getUserMediaStream(true, true);
|
||||
const stream2 = await mediaHandler.getUserMediaStream(true, false) as unknown as MockMediaStream;
|
||||
|
||||
expect(stream2.isCloneOf(stream1)).toEqual(true);
|
||||
expect(stream2.getVideoTracks().length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getScreensharingStream", () => {
|
||||
it("gets any screen sharing stream when called with no args", async () => {
|
||||
const stream = await mediaHandler.getScreensharingStream();
|
||||
expect(stream).toBeTruthy();
|
||||
expect(stream.getTracks()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("re-uses streams", async () => {
|
||||
const stream = await mediaHandler.getScreensharingStream(undefined, true);
|
||||
|
||||
expect(mockMediaDevices.getDisplayMedia).toHaveBeenCalled();
|
||||
mockMediaDevices.getDisplayMedia.mockClear();
|
||||
|
||||
const stream2 = await mediaHandler.getScreensharingStream() as unknown as MockMediaStream;
|
||||
|
||||
expect(mockMediaDevices.getDisplayMedia).not.toHaveBeenCalled();
|
||||
|
||||
expect(stream2.isCloneOf(stream)).toEqual(true);
|
||||
});
|
||||
|
||||
it("passes through desktopCapturerSourceId for Electron", async () => {
|
||||
await mediaHandler.getScreensharingStream({
|
||||
desktopCapturerSourceId: FAKE_DESKTOP_SOURCE_ID,
|
||||
});
|
||||
|
||||
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledWith(expect.objectContaining({
|
||||
video: {
|
||||
mandatory: expect.objectContaining({
|
||||
chromeMediaSource: "desktop",
|
||||
chromeMediaSourceId: FAKE_DESKTOP_SOURCE_ID,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
it("emits LocalStreamsChanged", async () => {
|
||||
const onLocalStreamChanged = jest.fn();
|
||||
mediaHandler.on(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
|
||||
await mediaHandler.getScreensharingStream();
|
||||
expect(onLocalStreamChanged).toHaveBeenCalled();
|
||||
|
||||
mediaHandler.off(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stopUserMediaStream", () => {
|
||||
let stream: MediaStream;
|
||||
|
||||
beforeEach(async () => {
|
||||
stream = await mediaHandler.getUserMediaStream(true, false);
|
||||
});
|
||||
|
||||
it("stops tracks on streams", async () => {
|
||||
const mockTrack = new MockMediaStreamTrack("audio_track", "audio");
|
||||
stream.addTrack(mockTrack.typed());
|
||||
|
||||
mediaHandler.stopUserMediaStream(stream);
|
||||
expect(mockTrack.stop).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("removes stopped streams", async () => {
|
||||
expect(mediaHandler.userMediaStreams).toContain(stream);
|
||||
mediaHandler.stopUserMediaStream(stream);
|
||||
expect(mediaHandler.userMediaStreams).not.toContain(stream);
|
||||
});
|
||||
|
||||
it("emits LocalStreamsChanged", async () => {
|
||||
const onLocalStreamChanged = jest.fn();
|
||||
mediaHandler.on(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
|
||||
mediaHandler.stopUserMediaStream(stream);
|
||||
expect(onLocalStreamChanged).toHaveBeenCalled();
|
||||
|
||||
mediaHandler.off(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stopUserMediaStream", () => {
|
||||
let stream: MediaStream;
|
||||
|
||||
beforeEach(async () => {
|
||||
stream = await mediaHandler.getScreensharingStream();
|
||||
});
|
||||
|
||||
it("stops tracks on streams", async () => {
|
||||
const mockTrack = new MockMediaStreamTrack("audio_track", "audio");
|
||||
stream.addTrack(mockTrack.typed());
|
||||
|
||||
mediaHandler.stopScreensharingStream(stream);
|
||||
expect(mockTrack.stop).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("removes stopped streams", async () => {
|
||||
expect(mediaHandler.screensharingStreams).toContain(stream);
|
||||
mediaHandler.stopScreensharingStream(stream);
|
||||
expect(mediaHandler.screensharingStreams).not.toContain(stream);
|
||||
});
|
||||
|
||||
it("emits LocalStreamsChanged", async () => {
|
||||
const onLocalStreamChanged = jest.fn();
|
||||
mediaHandler.on(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
|
||||
mediaHandler.stopScreensharingStream(stream);
|
||||
expect(onLocalStreamChanged).toHaveBeenCalled();
|
||||
|
||||
mediaHandler.off(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stopAllStreams", () => {
|
||||
let userMediaStream: MediaStream;
|
||||
let screenSharingStream: MediaStream;
|
||||
|
||||
beforeEach(async () => {
|
||||
userMediaStream = await mediaHandler.getUserMediaStream(true, false);
|
||||
screenSharingStream = await mediaHandler.getScreensharingStream();
|
||||
});
|
||||
|
||||
it("stops tracks on streams", async () => {
|
||||
const mockUserMediaTrack = new MockMediaStreamTrack("audio_track", "audio");
|
||||
userMediaStream.addTrack(mockUserMediaTrack.typed());
|
||||
|
||||
const mockScreenshareTrack = new MockMediaStreamTrack("audio_track", "audio");
|
||||
screenSharingStream.addTrack(mockScreenshareTrack.typed());
|
||||
|
||||
mediaHandler.stopAllStreams();
|
||||
|
||||
expect(mockUserMediaTrack.stop).toHaveBeenCalled();
|
||||
expect(mockScreenshareTrack.stop).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("removes stopped streams", async () => {
|
||||
expect(mediaHandler.userMediaStreams).toContain(userMediaStream);
|
||||
expect(mediaHandler.screensharingStreams).toContain(screenSharingStream);
|
||||
mediaHandler.stopAllStreams();
|
||||
expect(mediaHandler.userMediaStreams).not.toContain(userMediaStream);
|
||||
expect(mediaHandler.screensharingStreams).not.toContain(screenSharingStream);
|
||||
});
|
||||
|
||||
it("emits LocalStreamsChanged", async () => {
|
||||
const onLocalStreamChanged = jest.fn();
|
||||
mediaHandler.on(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
|
||||
mediaHandler.stopAllStreams();
|
||||
expect(onLocalStreamChanged).toHaveBeenCalled();
|
||||
|
||||
mediaHandler.off(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -30,7 +30,7 @@ export enum TweakName {
|
||||
|
||||
export type Tweak<N extends TweakName, V> = {
|
||||
set_tweak: N;
|
||||
value: V;
|
||||
value?: V;
|
||||
};
|
||||
|
||||
export type TweakHighlight = Tweak<TweakName.Highlight, boolean>;
|
||||
@@ -76,7 +76,8 @@ export interface IPushRuleCondition<N extends ConditionKind | string> {
|
||||
|
||||
export interface IEventMatchCondition extends IPushRuleCondition<ConditionKind.EventMatch> {
|
||||
key: string;
|
||||
pattern: string;
|
||||
pattern?: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export interface IContainsDisplayNameCondition extends IPushRuleCondition<ConditionKind.ContainsDisplayName> {
|
||||
@@ -168,8 +169,8 @@ export interface IPusher {
|
||||
lang: string;
|
||||
profile_tag?: string;
|
||||
pushkey: string;
|
||||
enabled?: boolean | null | undefined;
|
||||
"org.matrix.msc3881.enabled"?: boolean | null | undefined;
|
||||
enabled?: boolean | null;
|
||||
"org.matrix.msc3881.enabled"?: boolean | null;
|
||||
device_id?: string | null;
|
||||
"org.matrix.msc3881.device_id"?: string | null;
|
||||
}
|
||||
|
||||
@@ -87,6 +87,10 @@ export enum EventType {
|
||||
RoomKeyRequest = "m.room_key_request",
|
||||
ForwardedRoomKey = "m.forwarded_room_key",
|
||||
Dummy = "m.dummy",
|
||||
|
||||
// Group call events
|
||||
GroupCallPrefix = "org.matrix.msc3401.call",
|
||||
GroupCallMemberPrefix = "org.matrix.msc3401.call.member",
|
||||
}
|
||||
|
||||
export enum RelationType {
|
||||
|
||||
@@ -19,3 +19,38 @@ export enum ReceiptType {
|
||||
FullyRead = "m.fully_read",
|
||||
ReadPrivate = "m.read.private",
|
||||
}
|
||||
|
||||
export const MAIN_ROOM_TIMELINE = "main";
|
||||
|
||||
export interface Receipt {
|
||||
ts: number;
|
||||
thread_id?: string;
|
||||
}
|
||||
|
||||
export interface WrappedReceipt {
|
||||
eventId: string;
|
||||
data: Receipt;
|
||||
}
|
||||
|
||||
export interface CachedReceipt {
|
||||
type: ReceiptType;
|
||||
userId: string;
|
||||
data: Receipt;
|
||||
}
|
||||
|
||||
export type ReceiptCache = {[eventId: string]: CachedReceipt[]};
|
||||
|
||||
export interface ReceiptContent {
|
||||
[eventId: string]: {
|
||||
[key in ReceiptType]: {
|
||||
[userId: string]: Receipt;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// We will only hold a synthetic receipt if we do not have a real receipt or the synthetic is newer.
|
||||
export type Receipts = {
|
||||
[receiptType: string]: {
|
||||
[userId: string]: [WrappedReceipt | null, WrappedReceipt | null]; // Pair<real receipt, synthetic receipt> (both nullable)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -114,5 +114,6 @@ export interface ISearchResults {
|
||||
count?: number;
|
||||
next_batch?: string;
|
||||
pendingRequest?: Promise<ISearchResults>;
|
||||
abortSignal?: AbortSignal;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
+3
-3
@@ -22,7 +22,7 @@ import { EventEmitter } from "events";
|
||||
import { ListenerMap, TypedEventEmitter } from "./models/typed-event-emitter";
|
||||
|
||||
export class ReEmitter {
|
||||
constructor(private readonly target: EventEmitter) {}
|
||||
public constructor(private readonly target: EventEmitter) {}
|
||||
|
||||
// Map from emitter to event name to re-emitter
|
||||
private reEmitters = new Map<EventEmitter, Map<string, (...args: any[]) => void>>();
|
||||
@@ -38,7 +38,7 @@ export class ReEmitter {
|
||||
// We include the source as the last argument for event handlers which may need it,
|
||||
// such as read receipt listeners on the client class which won't have the context
|
||||
// of the room.
|
||||
const forSource = (...args: any[]) => {
|
||||
const forSource = (...args: any[]): void => {
|
||||
// EventEmitter special cases 'error' to make the emit function throw if no
|
||||
// handler is attached, which sort of makes sense for making sure that something
|
||||
// handles an error, but for re-emitting, there could be a listener on the original
|
||||
@@ -74,7 +74,7 @@ export class TypedReEmitter<
|
||||
Events extends string,
|
||||
Arguments extends ListenerMap<Events>,
|
||||
> extends ReEmitter {
|
||||
constructor(target: TypedEventEmitter<Events, Arguments>) {
|
||||
public constructor(target: TypedEventEmitter<Events, Arguments>) {
|
||||
super(target);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export class ToDeviceMessageQueue {
|
||||
private retryTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
private retryAttempts = 0;
|
||||
|
||||
constructor(private client: MatrixClient) {
|
||||
public constructor(private client: MatrixClient) {
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
|
||||
+48
-32
@@ -32,6 +32,28 @@ export enum AutoDiscoveryAction {
|
||||
FAIL_ERROR = "FAIL_ERROR",
|
||||
}
|
||||
|
||||
enum AutoDiscoveryError {
|
||||
Invalid = "Invalid homeserver discovery response",
|
||||
GenericFailure = "Failed to get autodiscovery configuration from server",
|
||||
InvalidHsBaseUrl = "Invalid base_url for m.homeserver",
|
||||
InvalidHomeserver = "Homeserver URL does not appear to be a valid Matrix homeserver",
|
||||
InvalidIsBaseUrl = "Invalid base_url for m.identity_server",
|
||||
InvalidIdentityServer = "Identity server URL does not appear to be a valid identity server",
|
||||
InvalidIs = "Invalid identity server discovery response",
|
||||
MissingWellknown = "No .well-known JSON file found",
|
||||
InvalidJson = "Invalid JSON",
|
||||
}
|
||||
|
||||
interface WellKnownConfig extends Omit<IWellKnownConfig, "error"> {
|
||||
state: AutoDiscoveryAction;
|
||||
error?: IWellKnownConfig["error"] | null;
|
||||
}
|
||||
|
||||
interface ClientConfig {
|
||||
"m.homeserver": WellKnownConfig;
|
||||
"m.identity_server": WellKnownConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilities for automatically discovery resources, such as homeservers
|
||||
* for users to log in to.
|
||||
@@ -42,36 +64,25 @@ export class AutoDiscovery {
|
||||
// translate the meaning of the states in the spec, but also
|
||||
// support our own if needed.
|
||||
|
||||
public static readonly ERROR_INVALID = "Invalid homeserver discovery response";
|
||||
public static readonly ERROR_INVALID = AutoDiscoveryError.Invalid;
|
||||
|
||||
public static readonly ERROR_GENERIC_FAILURE = "Failed to get autodiscovery configuration from server";
|
||||
public static readonly ERROR_GENERIC_FAILURE = AutoDiscoveryError.GenericFailure;
|
||||
|
||||
public static readonly ERROR_INVALID_HS_BASE_URL = "Invalid base_url for m.homeserver";
|
||||
public static readonly ERROR_INVALID_HS_BASE_URL = AutoDiscoveryError.InvalidHsBaseUrl;
|
||||
|
||||
public static readonly ERROR_INVALID_HOMESERVER = "Homeserver URL does not appear to be a valid Matrix homeserver";
|
||||
public static readonly ERROR_INVALID_HOMESERVER = AutoDiscoveryError.InvalidHomeserver;
|
||||
|
||||
public static readonly ERROR_INVALID_IS_BASE_URL = "Invalid base_url for m.identity_server";
|
||||
public static readonly ERROR_INVALID_IS_BASE_URL = AutoDiscoveryError.InvalidIsBaseUrl;
|
||||
|
||||
// eslint-disable-next-line
|
||||
public static readonly ERROR_INVALID_IDENTITY_SERVER = "Identity server URL does not appear to be a valid identity server";
|
||||
public static readonly ERROR_INVALID_IDENTITY_SERVER = AutoDiscoveryError.InvalidIdentityServer;
|
||||
|
||||
public static readonly ERROR_INVALID_IS = "Invalid identity server discovery response";
|
||||
public static readonly ERROR_INVALID_IS = AutoDiscoveryError.InvalidIs;
|
||||
|
||||
public static readonly ERROR_MISSING_WELLKNOWN = "No .well-known JSON file found";
|
||||
public static readonly ERROR_MISSING_WELLKNOWN = AutoDiscoveryError.MissingWellknown;
|
||||
|
||||
public static readonly ERROR_INVALID_JSON = "Invalid JSON";
|
||||
public static readonly ERROR_INVALID_JSON = AutoDiscoveryError.InvalidJson;
|
||||
|
||||
public static readonly ALL_ERRORS = [
|
||||
AutoDiscovery.ERROR_INVALID,
|
||||
AutoDiscovery.ERROR_GENERIC_FAILURE,
|
||||
AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
|
||||
AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
|
||||
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
|
||||
AutoDiscovery.ERROR_INVALID_IS,
|
||||
AutoDiscovery.ERROR_MISSING_WELLKNOWN,
|
||||
AutoDiscovery.ERROR_INVALID_JSON,
|
||||
];
|
||||
public static readonly ALL_ERRORS = Object.keys(AutoDiscoveryError);
|
||||
|
||||
/**
|
||||
* The auto discovery failed. The client is expected to communicate
|
||||
@@ -120,13 +131,13 @@ export class AutoDiscovery {
|
||||
* configuration, which may include error states. Rejects on unexpected
|
||||
* failure, not when verification fails.
|
||||
*/
|
||||
public static async fromDiscoveryConfig(wellknown: any): Promise<IClientWellKnown> {
|
||||
public static async fromDiscoveryConfig(wellknown: any): Promise<ClientConfig> {
|
||||
// Step 1 is to get the config, which is provided to us here.
|
||||
|
||||
// We default to an error state to make the first few checks easier to
|
||||
// write. We'll update the properties of this object over the duration
|
||||
// of this function.
|
||||
const clientConfig = {
|
||||
const clientConfig: ClientConfig = {
|
||||
"m.homeserver": {
|
||||
state: AutoDiscovery.FAIL_ERROR,
|
||||
error: AutoDiscovery.ERROR_INVALID,
|
||||
@@ -197,7 +208,7 @@ export class AutoDiscovery {
|
||||
if (wellknown["m.identity_server"]) {
|
||||
// We prepare a failing identity server response to save lines later
|
||||
// in this branch.
|
||||
const failingClientConfig = {
|
||||
const failingClientConfig: ClientConfig = {
|
||||
"m.homeserver": clientConfig["m.homeserver"],
|
||||
"m.identity_server": {
|
||||
state: AutoDiscovery.FAIL_PROMPT,
|
||||
@@ -279,7 +290,7 @@ export class AutoDiscovery {
|
||||
* configuration, which may include error states. Rejects on unexpected
|
||||
* failure, not when discovery fails.
|
||||
*/
|
||||
public static async findClientConfig(domain: string): Promise<IClientWellKnown> {
|
||||
public static async findClientConfig(domain: string): Promise<ClientConfig> {
|
||||
if (!domain || typeof(domain) !== "string" || domain.length === 0) {
|
||||
throw new Error("'domain' must be a string of non-zero length");
|
||||
}
|
||||
@@ -298,7 +309,7 @@ export class AutoDiscovery {
|
||||
// We default to an error state to make the first few checks easier to
|
||||
// write. We'll update the properties of this object over the duration
|
||||
// of this function.
|
||||
const clientConfig = {
|
||||
const clientConfig: ClientConfig = {
|
||||
"m.homeserver": {
|
||||
state: AutoDiscovery.FAIL_ERROR,
|
||||
error: AutoDiscovery.ERROR_INVALID,
|
||||
@@ -367,18 +378,18 @@ export class AutoDiscovery {
|
||||
* @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid.
|
||||
* @private
|
||||
*/
|
||||
private static sanitizeWellKnownUrl(url: string): string | boolean {
|
||||
private static sanitizeWellKnownUrl(url: string): string | false {
|
||||
if (!url) return false;
|
||||
|
||||
try {
|
||||
let parsed = null;
|
||||
let parsed: URL | undefined;
|
||||
try {
|
||||
parsed = new URL(url);
|
||||
} catch (e) {
|
||||
logger.error("Could not parse url", e);
|
||||
}
|
||||
|
||||
if (!parsed || !parsed.hostname) return false;
|
||||
if (!parsed?.hostname) return false;
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
||||
|
||||
const port = parsed.port ? `:${parsed.port}` : "";
|
||||
@@ -448,12 +459,17 @@ export class AutoDiscovery {
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err as Error | string | undefined;
|
||||
const error = err as AutoDiscoveryError | string | undefined;
|
||||
let reason = "";
|
||||
if (typeof error === "object") {
|
||||
reason = (<Error>error)?.message;
|
||||
}
|
||||
|
||||
return {
|
||||
error,
|
||||
raw: {},
|
||||
action: AutoDiscoveryAction.FAIL_PROMPT,
|
||||
reason: (<Error>error)?.message || "General failure",
|
||||
reason: reason || "General failure",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -463,7 +479,7 @@ export class AutoDiscovery {
|
||||
action: AutoDiscoveryAction.SUCCESS,
|
||||
};
|
||||
} catch (err) {
|
||||
const error = err as Error | string | undefined;
|
||||
const error = err as Error;
|
||||
return {
|
||||
error,
|
||||
raw: {},
|
||||
|
||||
+237
-132
@@ -35,6 +35,7 @@ import { StubStore } from "./store/stub";
|
||||
import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall, supportsMatrixCall } from "./webrtc/call";
|
||||
import { Filter, IFilterDefinition, IRoomEventFilter } from "./filter";
|
||||
import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from './webrtc/callEventHandler';
|
||||
import { GroupCallEventHandlerEvent, GroupCallEventHandlerEventHandlerMap } from './webrtc/groupCallEventHandler';
|
||||
import * as utils from './utils';
|
||||
import { replaceParam, QueryDict, sleep } from './utils';
|
||||
import { Direction, EventTimeline } from "./models/event-timeline";
|
||||
@@ -64,12 +65,14 @@ import {
|
||||
FileType,
|
||||
UploadResponse,
|
||||
HTTPError,
|
||||
IRequestOpts,
|
||||
} from "./http-api";
|
||||
import {
|
||||
Crypto,
|
||||
CryptoEvent,
|
||||
CryptoEventHandlerMap,
|
||||
fixBackupKey,
|
||||
ICryptoCallbacks,
|
||||
IBootstrapCrossSigningOpts,
|
||||
ICheckOwnCrossSigningTrustOpts,
|
||||
IMegolmSessionData,
|
||||
@@ -99,29 +102,9 @@ import {
|
||||
} from "./crypto/keybackup";
|
||||
import { IIdentityServerProvider } from "./@types/IIdentityServerProvider";
|
||||
import { MatrixScheduler } from "./scheduler";
|
||||
import {
|
||||
IAuthData,
|
||||
ICryptoCallbacks,
|
||||
IMinimalEvent,
|
||||
IRoomEvent,
|
||||
IStateEvent,
|
||||
NotificationCountType,
|
||||
BeaconEvent,
|
||||
BeaconEventHandlerMap,
|
||||
RoomEvent,
|
||||
RoomEventHandlerMap,
|
||||
RoomMemberEvent,
|
||||
RoomMemberEventHandlerMap,
|
||||
RoomStateEvent,
|
||||
RoomStateEventHandlerMap,
|
||||
INotificationsResponse,
|
||||
IFilterResponse,
|
||||
ITagsResponse,
|
||||
IStatusResponse,
|
||||
IPushRule,
|
||||
PushRuleActionName,
|
||||
IAuthDict,
|
||||
} from "./matrix";
|
||||
import { BeaconEvent, BeaconEventHandlerMap } from "./models/beacon";
|
||||
import { IAuthData, IAuthDict } from "./interactive-auth";
|
||||
import { IMinimalEvent, IRoomEvent, IStateEvent } from "./sync-accumulator";
|
||||
import {
|
||||
CrossSigningKey,
|
||||
IAddSecretStorageKeyOpts,
|
||||
@@ -136,7 +119,9 @@ import { VerificationRequest } from "./crypto/verification/request/VerificationR
|
||||
import { VerificationBase as Verification } from "./crypto/verification/Base";
|
||||
import * as ContentHelpers from "./content-helpers";
|
||||
import { CrossSigningInfo, DeviceTrustLevel, ICacheCallbacks, UserTrustLevel } from "./crypto/CrossSigning";
|
||||
import { Room, RoomNameState } from "./models/room";
|
||||
import { Room, NotificationCountType, RoomEvent, RoomEventHandlerMap, RoomNameState } from "./models/room";
|
||||
import { RoomMemberEvent, RoomMemberEventHandlerMap } from "./models/room-member";
|
||||
import { RoomStateEvent, RoomStateEventHandlerMap } from "./models/room-state";
|
||||
import {
|
||||
IAddThreePidOnlyBody,
|
||||
IBindThreePidBody,
|
||||
@@ -153,6 +138,10 @@ import {
|
||||
IRoomDirectoryOptions,
|
||||
ISearchOpts,
|
||||
ISendEventResponse,
|
||||
INotificationsResponse,
|
||||
IFilterResponse,
|
||||
ITagsResponse,
|
||||
IStatusResponse,
|
||||
} from "./@types/requests";
|
||||
import {
|
||||
EventType,
|
||||
@@ -184,13 +173,29 @@ import {
|
||||
} from "./@types/search";
|
||||
import { ISynapseAdminDeactivateResponse, ISynapseAdminWhoisResponse } from "./@types/synapse";
|
||||
import { IHierarchyRoom } from "./@types/spaces";
|
||||
import { IPusher, IPusherRequest, IPushRules, PushRuleAction, PushRuleKind, RuleId } from "./@types/PushRules";
|
||||
import {
|
||||
IPusher,
|
||||
IPusherRequest,
|
||||
IPushRule,
|
||||
IPushRules,
|
||||
PushRuleAction,
|
||||
PushRuleActionName,
|
||||
PushRuleKind,
|
||||
RuleId,
|
||||
} from "./@types/PushRules";
|
||||
import { IThreepid } from "./@types/threepids";
|
||||
import { CryptoStore } from "./crypto/store/base";
|
||||
import {
|
||||
GroupCall,
|
||||
IGroupCallDataChannelOptions,
|
||||
GroupCallIntent,
|
||||
GroupCallType,
|
||||
} from "./webrtc/groupCall";
|
||||
import { MediaHandler } from "./webrtc/mediaHandler";
|
||||
import { GroupCallEventHandler } from "./webrtc/groupCallEventHandler";
|
||||
import { LoginTokenPostResponse, ILoginFlowsResponse, IRefreshTokenResponse, SSOAction } from "./@types/auth";
|
||||
import { TypedEventEmitter } from "./models/typed-event-emitter";
|
||||
import { ReceiptType } from "./@types/read_receipts";
|
||||
import { MAIN_ROOM_TIMELINE, ReceiptType } from "./@types/read_receipts";
|
||||
import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync";
|
||||
import { SlidingSyncSdk } from "./sliding-sync-sdk";
|
||||
import {
|
||||
@@ -205,7 +210,6 @@ import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
|
||||
import { UnstableValue } from "./NamespacedValue";
|
||||
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue";
|
||||
import { ToDeviceBatch } from "./models/ToDeviceMessage";
|
||||
import { MAIN_ROOM_TIMELINE } from "./models/read-receipt";
|
||||
import { IgnoredInvites } from "./models/invites-ignorer";
|
||||
import { UIARequest, UIAResponse } from "./@types/uia";
|
||||
import { LocalNotificationSettings } from "./@types/local_notifications";
|
||||
@@ -359,6 +363,12 @@ export interface ICreateClientOpts {
|
||||
*/
|
||||
fallbackICEServerAllowed?: boolean;
|
||||
|
||||
/**
|
||||
* If true, to-device signalling for group calls will be encrypted
|
||||
* with Olm. Default: true.
|
||||
*/
|
||||
useE2eForGroupCall?: boolean;
|
||||
|
||||
cryptoCallbacks?: ICryptoCallbacks;
|
||||
|
||||
/**
|
||||
@@ -507,7 +517,7 @@ export interface IUploadKeySignaturesResponse {
|
||||
}
|
||||
|
||||
export interface IPreviewUrlResponse {
|
||||
[key: string]: string | number;
|
||||
[key: string]: undefined | string | number;
|
||||
"og:title": string;
|
||||
"og:type": string;
|
||||
"og:url": string;
|
||||
@@ -705,8 +715,9 @@ export interface IMyDevice {
|
||||
display_name?: string;
|
||||
last_seen_ip?: string;
|
||||
last_seen_ts?: number;
|
||||
[UNSTABLE_MSC3852_LAST_SEEN_UA.stable]?: string;
|
||||
[UNSTABLE_MSC3852_LAST_SEEN_UA.unstable]?: string;
|
||||
// UNSTABLE_MSC3852_LAST_SEEN_UA
|
||||
last_seen_user_agent?: string;
|
||||
"org.matrix.msc3852.last_seen_user_agent"?: string;
|
||||
}
|
||||
|
||||
export interface Keys {
|
||||
@@ -824,6 +835,7 @@ export enum ClientEvent {
|
||||
DeleteRoom = "deleteRoom",
|
||||
SyncUnexpectedError = "sync.unexpectedError",
|
||||
ClientWellKnown = "WellKnown.client",
|
||||
ReceivedVoipEvent = "received_voip_event",
|
||||
TurnServers = "turnServers",
|
||||
TurnServersError = "turnServers.error",
|
||||
}
|
||||
@@ -883,6 +895,10 @@ export type EmittedEvents = ClientEvent
|
||||
| UserEvents
|
||||
| CallEvent // re-emitted by call.ts using Object.values
|
||||
| CallEventHandlerEvent.Incoming
|
||||
| GroupCallEventHandlerEvent.Incoming
|
||||
| GroupCallEventHandlerEvent.Outgoing
|
||||
| GroupCallEventHandlerEvent.Ended
|
||||
| GroupCallEventHandlerEvent.Participants
|
||||
| HttpApiEvent.SessionLoggedOut
|
||||
| HttpApiEvent.NoConsent
|
||||
| BeaconEvent;
|
||||
@@ -896,6 +912,7 @@ export type ClientEventHandlerMap = {
|
||||
[ClientEvent.DeleteRoom]: (roomId: string) => void;
|
||||
[ClientEvent.SyncUnexpectedError]: (error: Error) => void;
|
||||
[ClientEvent.ClientWellKnown]: (data: IClientWellKnown) => void;
|
||||
[ClientEvent.ReceivedVoipEvent]: (event: MatrixEvent) => void;
|
||||
[ClientEvent.TurnServers]: (servers: ITurnServer[]) => void;
|
||||
[ClientEvent.TurnServersError]: (error: Error, fatal: boolean) => void;
|
||||
} & RoomEventHandlerMap
|
||||
@@ -905,6 +922,7 @@ export type ClientEventHandlerMap = {
|
||||
& RoomMemberEventHandlerMap
|
||||
& UserEventHandlerMap
|
||||
& CallEventHandlerEventHandlerMap
|
||||
& GroupCallEventHandlerEventHandlerMap
|
||||
& CallEventHandlerMap
|
||||
& HttpApiEventHandlerMap
|
||||
& BeaconEventHandlerMap;
|
||||
@@ -935,6 +953,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
public crypto?: Crypto; // XXX: Intended private, used in code.
|
||||
public cryptoCallbacks: ICryptoCallbacks; // XXX: Intended private, used in code.
|
||||
public callEventHandler?: CallEventHandler; // XXX: Intended private, used in code.
|
||||
public groupCallEventHandler?: GroupCallEventHandler;
|
||||
public supportsCallTransfer = false; // XXX: Intended private, used in code.
|
||||
public forceTURN = false; // XXX: Intended private, used in code.
|
||||
public iceCandidatePoolSize = 0; // XXX: Intended private, used in code.
|
||||
@@ -983,14 +1002,16 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
protected exportedOlmDeviceToImport?: IExportedOlmDevice;
|
||||
protected txnCtr = 0;
|
||||
protected mediaHandler = new MediaHandler(this);
|
||||
protected sessionId: string;
|
||||
protected pendingEventEncryption = new Map<string, Promise<void>>();
|
||||
|
||||
private useE2eForGroupCall = true;
|
||||
private toDeviceMessageQueue: ToDeviceMessageQueue;
|
||||
|
||||
// A manager for determining which invites should be ignored.
|
||||
public readonly ignoredInvites: IgnoredInvites;
|
||||
|
||||
constructor(opts: IMatrixClientCreateOpts) {
|
||||
public constructor(opts: IMatrixClientCreateOpts) {
|
||||
super();
|
||||
|
||||
opts.baseUrl = utils.ensureNoTrailingSlash(opts.baseUrl);
|
||||
@@ -1003,6 +1024,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
this.usingExternalCrypto = opts.usingExternalCrypto ?? false;
|
||||
this.store = opts.store || new StubStore();
|
||||
this.deviceId = opts.deviceId || null;
|
||||
this.sessionId = randomString(10);
|
||||
|
||||
const userId = opts.userId || null;
|
||||
this.credentials = { userId };
|
||||
@@ -1061,6 +1083,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
if (supportsMatrixCall()) {
|
||||
this.callEventHandler = new CallEventHandler(this);
|
||||
this.groupCallEventHandler = new GroupCallEventHandler(this);
|
||||
this.canSupportVoip = true;
|
||||
// Start listening for calls after the initial sync is done
|
||||
// We do not need to backfill the call event buffer
|
||||
@@ -1079,6 +1102,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
this.supportsCallTransfer = opts.supportsCallTransfer || false;
|
||||
this.fallbackICEServerAllowed = opts.fallbackICEServerAllowed || false;
|
||||
|
||||
if (opts.useE2eForGroupCall !== undefined) this.useE2eForGroupCall = opts.useE2eForGroupCall;
|
||||
|
||||
// List of which rooms have encryption enabled: separate from crypto because
|
||||
// we still want to know which rooms are encrypted even if crypto is disabled:
|
||||
// we don't want to start sending unencrypted events to them.
|
||||
@@ -1175,7 +1200,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
if (this.crypto) {
|
||||
this.crypto.uploadDeviceKeys();
|
||||
this.crypto.start();
|
||||
}
|
||||
|
||||
// periodically poll for turn servers if we support voip
|
||||
@@ -1209,7 +1233,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
// shallow-copy the opts dict before modifying and storing it
|
||||
this.clientOpts = Object.assign({}, opts) as IStoredClientOpts;
|
||||
this.clientOpts.crypto = this.crypto;
|
||||
this.clientOpts.canResetEntireTimeline = (roomId) => {
|
||||
this.clientOpts.canResetEntireTimeline = (roomId): boolean => {
|
||||
if (!this.canResetTimelineCallback) {
|
||||
return false;
|
||||
}
|
||||
@@ -1236,7 +1260,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* High level helper method to stop the client from polling and allow a
|
||||
* clean shutdown.
|
||||
*/
|
||||
public stopClient() {
|
||||
public stopClient(): void {
|
||||
this.crypto?.stop(); // crypto might have been initialised even if the client wasn't fully started
|
||||
|
||||
if (!this.clientRunning) return; // already stopped
|
||||
@@ -1251,7 +1275,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
this.peekSync?.stopPeeking();
|
||||
|
||||
this.callEventHandler?.stop();
|
||||
this.groupCallEventHandler?.stop();
|
||||
this.callEventHandler = undefined;
|
||||
this.groupCallEventHandler = undefined;
|
||||
|
||||
global.clearInterval(this.checkTurnServersIntervalID);
|
||||
this.checkTurnServersIntervalID = undefined;
|
||||
@@ -1482,6 +1508,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return this.deviceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the session ID of this client
|
||||
* @return {string} session ID
|
||||
*/
|
||||
public getSessionId(): string {
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the runtime environment supports VoIP calling.
|
||||
* @return {boolean} True if VoIP is supported.
|
||||
@@ -1503,7 +1537,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* when creating the client.
|
||||
* @param {boolean} force True to force use of TURN servers
|
||||
*/
|
||||
public setForceTURN(force: boolean) {
|
||||
public setForceTURN(force: boolean): void {
|
||||
this.forceTURN = force;
|
||||
}
|
||||
|
||||
@@ -1511,10 +1545,19 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* Set whether to advertise transfer support to other parties on Matrix calls.
|
||||
* @param {boolean} support True to advertise the 'm.call.transferee' capability
|
||||
*/
|
||||
public setSupportsCallTransfer(support: boolean) {
|
||||
public setSupportsCallTransfer(support: boolean): void {
|
||||
this.supportsCallTransfer = support;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if to-device signalling for group calls will be encrypted with Olm.
|
||||
* If false, it will be sent unencrypted.
|
||||
* @returns boolean Whether group call signalling will be encrypted
|
||||
*/
|
||||
public getUseE2eForGroupCall(): boolean {
|
||||
return this.useE2eForGroupCall;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new call.
|
||||
* The place*Call methods on the returned call can be used to actually place a call
|
||||
@@ -1526,6 +1569,67 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return createNewMatrixCall(this, roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new group call and sends the associated state event
|
||||
* to alert other members that the room now has a group call.
|
||||
*
|
||||
* @param {string} roomId The room the call is to be placed in.
|
||||
* @return {GroupCall}
|
||||
*/
|
||||
public async createGroupCall(
|
||||
roomId: string,
|
||||
type: GroupCallType,
|
||||
isPtt: boolean,
|
||||
intent: GroupCallIntent,
|
||||
dataChannelsEnabled?: boolean,
|
||||
dataChannelOptions?: IGroupCallDataChannelOptions,
|
||||
): Promise<GroupCall> {
|
||||
if (this.getGroupCallForRoom(roomId)) {
|
||||
throw new Error(`${roomId} already has an existing group call`);
|
||||
}
|
||||
|
||||
const room = this.getRoom(roomId);
|
||||
|
||||
if (!room) {
|
||||
throw new Error(`Cannot find room ${roomId}`);
|
||||
}
|
||||
|
||||
return new GroupCall(
|
||||
this,
|
||||
room,
|
||||
type,
|
||||
isPtt,
|
||||
intent,
|
||||
undefined,
|
||||
dataChannelsEnabled,
|
||||
dataChannelOptions,
|
||||
).create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until an initial state for the given room has been processed by the
|
||||
* client and the client is aware of any ongoing group calls. Awaiting on
|
||||
* the promise returned by this method before calling getGroupCallForRoom()
|
||||
* avoids races where getGroupCallForRoom is called before the state for that
|
||||
* room has been processed. It does not, however, fix other races, eg. two
|
||||
* clients both creating a group call at the same time.
|
||||
* @param roomId The room ID to wait for
|
||||
* @returns A promise that resolves once existing group calls in the room
|
||||
* have been processed.
|
||||
*/
|
||||
public waitUntilRoomReadyForGroupCalls(roomId: string): Promise<void> {
|
||||
return this.groupCallEventHandler!.waitUntilRoomReadyForGroupCalls(roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an existing group call for the provided room.
|
||||
* @param roomId
|
||||
* @returns {GroupCall} The group call or null if it doesn't already exist.
|
||||
*/
|
||||
public getGroupCallForRoom(roomId: string): GroupCall | null {
|
||||
return this.groupCallEventHandler!.groupCalls.get(roomId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current sync state.
|
||||
* @return {?SyncState} the sync state, which may be null.
|
||||
@@ -1575,7 +1679,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* and may change without warning.</b>
|
||||
* @param {boolean} guest True if this is a guest account.
|
||||
*/
|
||||
public setGuest(guest: boolean) {
|
||||
public setGuest(guest: boolean): void {
|
||||
// EXPERIMENTAL:
|
||||
// If the token is a macaroon, it should be encoded in it that it is a 'guest'
|
||||
// access token, which means that the SDK can determine this entirely without
|
||||
@@ -1618,7 +1722,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* @param {EventTimelineSet} set
|
||||
*/
|
||||
public setNotifTimelineSet(set: EventTimelineSet) {
|
||||
public setNotifTimelineSet(set: EventTimelineSet): void {
|
||||
this.notifTimelineSet = set;
|
||||
}
|
||||
|
||||
@@ -2003,11 +2107,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* @param {boolean} value whether to blacklist all unverified devices by default
|
||||
*/
|
||||
public setGlobalBlacklistUnverifiedDevices(value: boolean) {
|
||||
public setGlobalBlacklistUnverifiedDevices(value: boolean): boolean {
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this.crypto.setGlobalBlacklistUnverifiedDevices(value);
|
||||
this.crypto.globalBlacklistUnverifiedDevices = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2017,7 +2122,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this.crypto.getGlobalBlacklistUnverifiedDevices();
|
||||
return this.crypto.globalBlacklistUnverifiedDevices;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2030,11 +2135,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* @param {boolean} value whether error on unknown devices
|
||||
*/
|
||||
public setGlobalErrorOnUnknownDevices(value: boolean) {
|
||||
public setGlobalErrorOnUnknownDevices(value: boolean): void {
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this.crypto.setGlobalErrorOnUnknownDevices(value);
|
||||
this.crypto.globalErrorOnUnknownDevices = value;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2046,7 +2151,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this.crypto.getGlobalErrorOnUnknownDevices();
|
||||
return this.crypto.globalErrorOnUnknownDevices;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2175,11 +2280,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* send, in order to speed up sending of the message.
|
||||
* @param {module:models/room} room the room the event is in
|
||||
*/
|
||||
public prepareToEncrypt(room: Room) {
|
||||
public prepareToEncrypt(room: Room): void {
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this.crypto.prepareToEncrypt(room);
|
||||
this.crypto.prepareToEncrypt(room);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2220,7 +2325,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* auth data as an object. Can be called multiple times, first with an empty
|
||||
* authDict, to obtain the flows.
|
||||
*/
|
||||
public bootstrapCrossSigning(opts: IBootstrapCrossSigningOpts) {
|
||||
public bootstrapCrossSigning(opts: IBootstrapCrossSigningOpts): Promise<void> {
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
@@ -2248,11 +2353,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* @param {boolean} val True to trust cross-signed devices
|
||||
*/
|
||||
public setCryptoTrustCrossSignedDevices(val: boolean) {
|
||||
public setCryptoTrustCrossSignedDevices(val: boolean): void {
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this.crypto.setCryptoTrustCrossSignedDevices(val);
|
||||
this.crypto.setCryptoTrustCrossSignedDevices(val);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2393,7 +2498,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param {Array} keys The IDs of the keys to use to encrypt the secret or null/undefined
|
||||
* to use the default (will throw if no default key is set).
|
||||
*/
|
||||
public storeSecret(name: string, secret: string, keys?: string[]) {
|
||||
public storeSecret(name: string, secret: string, keys?: string[]): Promise<void> {
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
@@ -2471,7 +2576,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* @param {string} keyId The new default key ID
|
||||
*/
|
||||
public setDefaultSecretStorageKeyId(keyId: string) {
|
||||
public setDefaultSecretStorageKeyId(keyId: string): Promise<void> {
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
@@ -2612,7 +2717,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* This should not normally be necessary.
|
||||
*/
|
||||
public forceDiscardSession(roomId: string) {
|
||||
public forceDiscardSession(roomId: string): void {
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-End encryption disabled");
|
||||
}
|
||||
@@ -2736,7 +2841,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
/**
|
||||
* Disable backing up of keys.
|
||||
*/
|
||||
public disableKeyBackup() {
|
||||
public disableKeyBackup(): void {
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
@@ -2929,7 +3034,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
|
||||
const path = this.makeKeyBackupPath(roomId!, sessionId!, version!);
|
||||
const path = this.makeKeyBackupPath(roomId!, sessionId!, version);
|
||||
await this.http.authedRequest(
|
||||
Method.Put, path.path, path.queryData, data,
|
||||
{ prefix: ClientPrefix.V3 },
|
||||
@@ -2940,7 +3045,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* Marks all group sessions as needing to be backed up and schedules them to
|
||||
* upload in the background as soon as possible.
|
||||
*/
|
||||
public async scheduleAllGroupSessionsForBackup() {
|
||||
public async scheduleAllGroupSessionsForBackup(): Promise<void> {
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
@@ -3121,7 +3226,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
opts: IKeyBackupRestoreOpts,
|
||||
): Promise<IKeyBackupRestoreResult> {
|
||||
const privKey = decodeRecoveryKey(recoveryKey);
|
||||
return this.restoreKeyBackup(privKey, targetRoomId, targetSessionId, backupInfo, opts);
|
||||
return this.restoreKeyBackup(privKey, targetRoomId!, targetSessionId!, backupInfo, opts);
|
||||
}
|
||||
|
||||
public async restoreKeyBackupWithCache(
|
||||
@@ -3283,7 +3388,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
|
||||
const path = this.makeKeyBackupPath(roomId, sessionId, version);
|
||||
const path = this.makeKeyBackupPath(roomId!, sessionId!, version);
|
||||
await this.http.authedRequest(
|
||||
Method.Delete, path.path, path.queryData, undefined,
|
||||
{ prefix: ClientPrefix.V3 },
|
||||
@@ -3297,7 +3402,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param {array} userIds a list of users to share with. The keys will be sent to
|
||||
* all of the user's current devices.
|
||||
*/
|
||||
public async sendSharedHistoryKeys(roomId: string, userIds: string[]) {
|
||||
public async sendSharedHistoryKeys(roomId: string, userIds: string[]): Promise<void> {
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
@@ -3525,10 +3630,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
let signPromise: Promise<IThirdPartySigned | void> = Promise.resolve();
|
||||
|
||||
if (opts.inviteSignUrl) {
|
||||
signPromise = this.http.requestOtherUrl<IThirdPartySigned>(
|
||||
Method.Post,
|
||||
new URL(opts.inviteSignUrl), { mxid: this.credentials.userId },
|
||||
);
|
||||
const url = new URL(opts.inviteSignUrl);
|
||||
url.searchParams.set("mxid", this.credentials.userId!);
|
||||
signPromise = this.http.requestOtherUrl<IThirdPartySigned>(Method.Post, url);
|
||||
}
|
||||
|
||||
const queryString: Record<string, string | string[]> = {};
|
||||
@@ -3581,7 +3685,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param {MatrixEvent} event Event to cancel
|
||||
* @throws Error if the event is not in QUEUED, NOT_SENT or ENCRYPTING state
|
||||
*/
|
||||
public cancelPendingEvent(event: MatrixEvent) {
|
||||
public cancelPendingEvent(event: MatrixEvent): void {
|
||||
if (![EventStatus.QUEUED, EventStatus.NOT_SENT, EventStatus.ENCRYPTING].includes(event.status!)) {
|
||||
throw new Error("cannot cancel an event with status " + event.status);
|
||||
}
|
||||
@@ -3730,7 +3834,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
public async unstable_createLiveBeacon(
|
||||
roomId: Room["roomId"],
|
||||
beaconInfoContent: MBeaconInfoEventContent,
|
||||
) {
|
||||
): Promise<ISendEventResponse> {
|
||||
return this.unstable_setLiveBeacon(roomId, beaconInfoContent);
|
||||
}
|
||||
|
||||
@@ -3745,7 +3849,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
public async unstable_setLiveBeacon(
|
||||
roomId: string,
|
||||
beaconInfoContent: MBeaconInfoEventContent,
|
||||
) {
|
||||
): Promise<ISendEventResponse> {
|
||||
return this.sendStateEvent(roomId, M_BEACON_INFO.name, beaconInfoContent, this.getUserId()!);
|
||||
}
|
||||
|
||||
@@ -3775,7 +3879,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
roomId: string,
|
||||
threadId: string | null,
|
||||
eventType: string | IContent,
|
||||
content: IContent | string,
|
||||
content?: IContent | string,
|
||||
txnId?: string,
|
||||
): Promise<ISendEventResponse> {
|
||||
if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) {
|
||||
@@ -3787,10 +3891,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
// If we expect that an event is part of a thread but is missing the relation
|
||||
// we need to add it manually, as well as the reply fallback
|
||||
if (threadId && !content["m.relates_to"]?.rel_type) {
|
||||
const isReply = !!content["m.relates_to"]?.["m.in_reply_to"];
|
||||
content["m.relates_to"] = {
|
||||
...content["m.relates_to"],
|
||||
if (threadId && !content!["m.relates_to"]?.rel_type) {
|
||||
const isReply = !!content!["m.relates_to"]?.["m.in_reply_to"];
|
||||
content!["m.relates_to"] = {
|
||||
...content!["m.relates_to"],
|
||||
"rel_type": THREAD_RELATION_TYPE.name,
|
||||
"event_id": threadId,
|
||||
// Set is_falling_back to true unless this is actually intended to be a reply
|
||||
@@ -3798,7 +3902,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
};
|
||||
const thread = this.getRoom(roomId)?.getThread(threadId);
|
||||
if (thread && !isReply) {
|
||||
content["m.relates_to"]["m.in_reply_to"] = {
|
||||
content!["m.relates_to"]["m.in_reply_to"] = {
|
||||
"event_id": thread.lastReply((ev: MatrixEvent) => {
|
||||
return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status;
|
||||
})?.getId() ?? threadId,
|
||||
@@ -3888,9 +3992,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param room
|
||||
* @param event
|
||||
* @returns {Promise} returns a promise which resolves with the result of the send request
|
||||
* @private
|
||||
*/
|
||||
private encryptAndSendEvent(room: Room | null, event: MatrixEvent): Promise<ISendEventResponse> {
|
||||
protected encryptAndSendEvent(room: Room | null, event: MatrixEvent): Promise<ISendEventResponse> {
|
||||
let cancelled = false;
|
||||
// Add an extra Promise.resolve() to turn synchronous exceptions into promise rejections,
|
||||
// so that we can handle synchronous and asynchronous exceptions with the
|
||||
@@ -4018,7 +4121,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return this.isRoomEncrypted(roomId) ? EventType.RoomMessageEncrypted : eventType;
|
||||
}
|
||||
|
||||
private updatePendingEventStatus(room: Room | null, event: MatrixEvent, newStatus: EventStatus) {
|
||||
protected updatePendingEventStatus(room: Room | null, event: MatrixEvent, newStatus: EventStatus): void {
|
||||
if (room) {
|
||||
room.updatePendingEvent(event, newStatus);
|
||||
} else {
|
||||
@@ -4097,7 +4200,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
if (!eventId?.startsWith(EVENT_ID_PREFIX)) {
|
||||
opts = txnId as IRedactOpts;
|
||||
txnId = eventId;
|
||||
eventId = threadId;
|
||||
eventId = threadId!;
|
||||
threadId = null;
|
||||
}
|
||||
const reason = opts?.reason;
|
||||
@@ -4130,7 +4233,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
public sendMessage(
|
||||
roomId: string,
|
||||
threadId: string | null | IContent,
|
||||
content: IContent | string,
|
||||
content?: IContent | string,
|
||||
txnId?: string,
|
||||
): Promise<ISendEventResponse> {
|
||||
if (typeof threadId !== "string" && threadId !== null) {
|
||||
@@ -4180,7 +4283,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
return this.sendEvent(
|
||||
roomId,
|
||||
threadId as (string | null),
|
||||
threadId as string | null,
|
||||
eventType,
|
||||
sendContent,
|
||||
txnId,
|
||||
@@ -4314,7 +4417,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
public sendImageMessage(
|
||||
roomId: string,
|
||||
threadId: string | null,
|
||||
url: string | IImageInfo,
|
||||
url?: string | IImageInfo,
|
||||
info?: IImageInfo | string,
|
||||
text = "Image",
|
||||
): Promise<ISendEventResponse> {
|
||||
@@ -4358,7 +4461,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
public sendStickerMessage(
|
||||
roomId: string,
|
||||
threadId: string | null,
|
||||
url: string | IImageInfo,
|
||||
url?: string | IImageInfo,
|
||||
info?: IImageInfo | string,
|
||||
text = "Sticker",
|
||||
): Promise<ISendEventResponse> {
|
||||
@@ -4484,6 +4587,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param {ReceiptType} receiptType The kind of receipt e.g. "m.read". Other than
|
||||
* ReceiptType.Read are experimental!
|
||||
* @param {object} body Additional content to send alongside the receipt.
|
||||
* @param {boolean} unthreaded An unthreaded receipt will clear room+thread notifications
|
||||
* @return {Promise} Resolves: to an empty object {}
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
@@ -4491,6 +4595,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
event: MatrixEvent,
|
||||
receiptType: ReceiptType,
|
||||
body: any,
|
||||
unthreaded = false,
|
||||
): Promise<{}> {
|
||||
if (this.isGuest()) {
|
||||
return Promise.resolve({}); // guests cannot send receipts so don't bother.
|
||||
@@ -4502,12 +4607,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
$eventId: event.getId()!,
|
||||
});
|
||||
|
||||
// TODO: Add a check for which spec version this will be released in
|
||||
if (await this.doesServerSupportUnstableFeature("org.matrix.msc3771")) {
|
||||
const supportsThreadRR = this.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported;
|
||||
if (supportsThreadRR && !unthreaded) {
|
||||
const isThread = !!event.threadRootId;
|
||||
body.thread_id = isThread
|
||||
? event.threadRootId
|
||||
: MAIN_ROOM_TIMELINE;
|
||||
body = {
|
||||
...body,
|
||||
thread_id: isThread
|
||||
? event.threadRootId
|
||||
: MAIN_ROOM_TIMELINE,
|
||||
};
|
||||
}
|
||||
|
||||
const promise = this.http.authedRequest<{}>(Method.Post, path, undefined, body || {});
|
||||
@@ -4529,6 +4637,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
public async sendReadReceipt(
|
||||
event: MatrixEvent | null,
|
||||
receiptType = ReceiptType.Read,
|
||||
unthreaded = false,
|
||||
): Promise<{} | undefined> {
|
||||
if (!event) return;
|
||||
const eventId = event.getId()!;
|
||||
@@ -4537,7 +4646,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
throw new Error(`Cannot set read receipt to a pending event (${eventId})`);
|
||||
}
|
||||
|
||||
return this.sendReceipt(event, receiptType, {});
|
||||
return this.sendReceipt(event, receiptType, {}, unthreaded);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4835,12 +4944,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const populationResults: { [roomId: string]: Error } = {};
|
||||
const promises: Promise<any>[] = [];
|
||||
|
||||
const doLeave = (roomId: string) => {
|
||||
const doLeave = (roomId: string): Promise<void> => {
|
||||
return this.leave(roomId).then(() => {
|
||||
delete populationResults[roomId];
|
||||
}).catch((err) => {
|
||||
// suppress error
|
||||
populationResults[roomId] = err;
|
||||
return null; // suppress error
|
||||
});
|
||||
};
|
||||
|
||||
@@ -5224,13 +5333,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
];
|
||||
|
||||
// Here we handle non-thread timelines only, but still process any thread events to populate thread summaries.
|
||||
let timeline = timelineSet.getTimelineForEvent(events[0].getId()!);
|
||||
let timeline = timelineSet.getTimelineForEvent(events[0].getId());
|
||||
if (timeline) {
|
||||
timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(res.state.map(mapper));
|
||||
timeline.getState(EventTimeline.BACKWARDS)!.setUnknownStateEvents(res.state.map(mapper));
|
||||
} else {
|
||||
timeline = timelineSet.addTimeline();
|
||||
timeline.initialiseState(res.state.map(mapper));
|
||||
timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end;
|
||||
timeline.getState(EventTimeline.FORWARDS)!.paginationToken = res.end;
|
||||
}
|
||||
|
||||
const [timelineEvents, threadedEvents] = timelineSet.room.partitionThreadedEvents(events);
|
||||
@@ -5319,7 +5428,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
// Here we handle non-thread timelines only, but still process any thread events to populate thread summaries.
|
||||
let timeline = timelineSet.getTimelineForEvent(event.getId());
|
||||
if (timeline) {
|
||||
timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(res.state.map(mapper));
|
||||
timeline.getState(EventTimeline.BACKWARDS)!.setUnknownStateEvents(res.state.map(mapper));
|
||||
} else {
|
||||
timeline = timelineSet.addTimeline();
|
||||
timeline.initialiseState(res.state.map(mapper));
|
||||
@@ -5380,7 +5489,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
// Here we handle non-thread timelines only, but still process any thread events to populate thread
|
||||
// summaries.
|
||||
const timeline = timelineSet.getLiveTimeline();
|
||||
timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(res.state.map(mapper));
|
||||
timeline.getState(EventTimeline.BACKWARDS)!.setUnknownStateEvents(res.state.map(mapper));
|
||||
|
||||
timelineSet.addEventsToTimeline(events, true, timeline, null);
|
||||
if (!resOlder.next_batch) {
|
||||
@@ -5683,7 +5792,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
eventTimeline.getFilter(),
|
||||
).then((res) => {
|
||||
if (res.state) {
|
||||
const roomState = eventTimeline.getState(dir);
|
||||
const roomState = eventTimeline.getState(dir)!;
|
||||
const stateEvents = res.state.map(this.getEventMapper());
|
||||
roomState.setUnknownStateEvents(stateEvents);
|
||||
}
|
||||
@@ -5721,8 +5830,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
).then(async (res) => {
|
||||
const mapper = this.getEventMapper();
|
||||
const matrixEvents = res.chunk.map(mapper);
|
||||
for (const event of matrixEvents) {
|
||||
await eventTimeline.getTimelineSet()?.thread?.processEvent(event);
|
||||
|
||||
// Process latest events first
|
||||
for (const event of matrixEvents.slice().reverse()) {
|
||||
await thread?.processEvent(event);
|
||||
const sender = event.getSender()!;
|
||||
if (!backwards || thread?.getEventReadUpTo(sender) === null) {
|
||||
room.addLocalEchoReceipt(sender, event, ReceiptType.Read);
|
||||
}
|
||||
}
|
||||
|
||||
const newToken = res.next_batch;
|
||||
@@ -5758,7 +5873,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
eventTimeline.getFilter(),
|
||||
).then((res) => {
|
||||
if (res.state) {
|
||||
const roomState = eventTimeline.getState(dir);
|
||||
const roomState = eventTimeline.getState(dir)!;
|
||||
const stateEvents = res.state.map(this.getEventMapper());
|
||||
roomState.setUnknownStateEvents(stateEvents);
|
||||
}
|
||||
@@ -5795,7 +5910,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* Reset the notifTimelineSet entirely, paginating in some historical notifs as
|
||||
* a starting point for subsequent pagination.
|
||||
*/
|
||||
public resetNotifTimelineSet() {
|
||||
public resetNotifTimelineSet(): void {
|
||||
if (!this.notifTimelineSet) {
|
||||
return;
|
||||
}
|
||||
@@ -5840,7 +5955,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
/**
|
||||
* Stop any ongoing room peeking.
|
||||
*/
|
||||
public stopPeeking() {
|
||||
public stopPeeking(): void {
|
||||
if (this.peekSync) {
|
||||
this.peekSync.stopPeeking();
|
||||
this.peekSync = null;
|
||||
@@ -6114,15 +6229,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
// There can be only room-kind push rule per room
|
||||
// and its id is the room id.
|
||||
if (this.pushRules) {
|
||||
if (!this.pushRules[scope] || !this.pushRules[scope].room) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < this.pushRules[scope].room.length; i++) {
|
||||
const rule = this.pushRules[scope].room[i];
|
||||
if (rule.rule_id === roomId) {
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
return this.pushRules[scope]?.room?.find(rule => rule.rule_id === roomId);
|
||||
} else {
|
||||
throw new Error(
|
||||
"SyncApi.sync() must be done before accessing to push rules.",
|
||||
@@ -6293,7 +6400,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
next_batch: searchResults.next_batch,
|
||||
};
|
||||
|
||||
const promise = this.search(searchOpts)
|
||||
const promise = this.search(searchOpts, searchResults.abortSignal)
|
||||
.then(res => this.processRoomEventsSearch(searchResults, res))
|
||||
.finally(() => {
|
||||
searchResults.pendingRequest = undefined;
|
||||
@@ -6472,8 +6579,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
// create a new filter
|
||||
const createdFilter = await this.createFilter(filter.getDefinition());
|
||||
|
||||
// debuglog("Created new filter ID %s: %s", createdFilter.filterId,
|
||||
// JSON.stringify(createdFilter.getDefinition()));
|
||||
this.store.setFilterIdByName(filterName, createdFilter.filterId);
|
||||
return createdFilter.filterId!;
|
||||
}
|
||||
@@ -6495,7 +6600,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
private startCallEventHandler = (): void => {
|
||||
if (this.isInitialSyncComplete()) {
|
||||
this.callEventHandler?.start();
|
||||
this.callEventHandler!.start();
|
||||
this.groupCallEventHandler!.start();
|
||||
this.off(ClientEvent.Sync, this.startCallEventHandler);
|
||||
}
|
||||
};
|
||||
@@ -6583,7 +6689,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* @param {boolean} allow
|
||||
*/
|
||||
public setFallbackICEServerAllowed(allow: boolean) {
|
||||
public setFallbackICEServerAllowed(allow: boolean): void {
|
||||
this.fallbackICEServerAllowed = allow;
|
||||
}
|
||||
|
||||
@@ -6924,7 +7030,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* Default: returns false.
|
||||
* @param {Function} cb The callback which will be invoked.
|
||||
*/
|
||||
public setCanResetTimelineCallback(cb: ResetTimelineCallback) {
|
||||
public setCanResetTimelineCallback(cb: ResetTimelineCallback): void {
|
||||
this.canResetTimelineCallback = cb;
|
||||
}
|
||||
|
||||
@@ -7064,7 +7170,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* Set the identity server URL of this client
|
||||
* @param {string} url New identity server URL
|
||||
*/
|
||||
public setIdentityServerUrl(url: string) {
|
||||
public setIdentityServerUrl(url: string): void {
|
||||
this.idBaseUrl = utils.ensureNoTrailingSlash(url);
|
||||
this.http.setIdBaseUrl(this.idBaseUrl);
|
||||
}
|
||||
@@ -7081,7 +7187,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* Set the access token associated with this account.
|
||||
* @param {string} token The new access token.
|
||||
*/
|
||||
public setAccessToken(token: string) {
|
||||
public setAccessToken(token: string): void {
|
||||
this.http.opts.accessToken = token;
|
||||
}
|
||||
|
||||
@@ -7640,6 +7746,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param {string} eventType
|
||||
* @param {Object} content
|
||||
* @param {string} stateKey
|
||||
* @param {IRequestOpts} opts Options for the request function.
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
@@ -7648,6 +7755,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
eventType: string,
|
||||
content: any,
|
||||
stateKey = "",
|
||||
opts: IRequestOpts = {},
|
||||
): Promise<ISendEventResponse> {
|
||||
const pathParams = {
|
||||
$roomId: roomId,
|
||||
@@ -7658,7 +7766,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
if (stateKey !== undefined) {
|
||||
path = utils.encodeUri(path + "/$stateKey", pathParams);
|
||||
}
|
||||
return this.http.authedRequest(Method.Put, path, undefined, content);
|
||||
return this.http.authedRequest(Method.Put, path, undefined, content, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -8342,17 +8450,19 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.next_batch the batch token to pass in the query string
|
||||
* @param {Object} opts.body the JSON object to pass to the request body.
|
||||
* @param {AbortSignal=} abortSignal optional signal used to cancel the http request.
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
public search(
|
||||
opts: { body: ISearchRequestBody, next_batch?: string }, // eslint-disable-line camelcase
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<ISearchResponse> {
|
||||
const queryParams: any = {};
|
||||
if (opts.next_batch) {
|
||||
queryParams.next_batch = opts.next_batch;
|
||||
}
|
||||
return this.http.authedRequest(Method.Post, "/search", queryParams, opts.body);
|
||||
return this.http.authedRequest(Method.Post, "/search", queryParams, opts.body, { abortSignal });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -8433,9 +8543,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
keyAlgorithm = "signed_curve25519";
|
||||
}
|
||||
|
||||
for (let i = 0; i < devices.length; ++i) {
|
||||
const userId = devices[i][0];
|
||||
const deviceId = devices[i][1];
|
||||
for (const [userId, deviceId] of devices) {
|
||||
const query = queries[userId] || {};
|
||||
queries[userId] = query;
|
||||
query[deviceId] = keyAlgorithm;
|
||||
@@ -8654,11 +8762,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
clientSecret: string,
|
||||
msisdnToken: string,
|
||||
): Promise<any> { // TODO: Types
|
||||
const u = new URL(url);
|
||||
u.searchParams.set("sid", sid);
|
||||
u.searchParams.set("client_secret", clientSecret);
|
||||
u.searchParams.set("token", msisdnToken);
|
||||
return this.http.requestOtherUrl(Method.Post, u);
|
||||
const params = {
|
||||
sid: sid,
|
||||
client_secret: clientSecret,
|
||||
token: msisdnToken,
|
||||
};
|
||||
return this.http.requestOtherUrl(Method.Post, url, params);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9235,12 +9344,8 @@ export function fixNotificationCountOnDecryption(cli: MatrixClient, event: Matri
|
||||
if (!room || !cli.getUserId()) return;
|
||||
|
||||
const isThreadEvent = !!event.threadRootId && !event.isThreadRoot;
|
||||
const currentCount = (isThreadEvent
|
||||
? room.getThreadUnreadNotificationCount(
|
||||
event.threadRootId,
|
||||
NotificationCountType.Highlight,
|
||||
)
|
||||
: room.getUnreadNotificationCount(NotificationCountType.Highlight)) ?? 0;
|
||||
|
||||
const currentCount = room.getUnreadCountForEventContext(NotificationCountType.Highlight, event);
|
||||
|
||||
// Ensure the unread counts are kept up to date if the event is encrypted
|
||||
// We also want to make sure that the notification count goes up if we already
|
||||
@@ -9272,7 +9377,7 @@ export function fixNotificationCountOnDecryption(cli: MatrixClient, event: Matri
|
||||
// Fix 'Mentions Only' rooms from not having the right badge count
|
||||
const totalCount = (isThreadEvent
|
||||
? room.getThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Total)
|
||||
: room.getUnreadNotificationCount(NotificationCountType.Total)) ?? 0;
|
||||
: room.getRoomUnreadNotificationCount(NotificationCountType.Total)) ?? 0;
|
||||
|
||||
if (totalCount < newCount) {
|
||||
if (isThreadEvent) {
|
||||
|
||||
+21
-20
@@ -33,6 +33,7 @@ import {
|
||||
LegacyLocationEventContent,
|
||||
} from "./@types/location";
|
||||
import { MRoomTopicEventContent, MTopicContent, M_TOPIC } from "./@types/topic";
|
||||
import { IContent } from "./models/event";
|
||||
|
||||
/**
|
||||
* Generates the content for a HTML Message event
|
||||
@@ -40,7 +41,7 @@ import { MRoomTopicEventContent, MTopicContent, M_TOPIC } from "./@types/topic";
|
||||
* @param {string} htmlBody the HTML representation of the message
|
||||
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
|
||||
*/
|
||||
export function makeHtmlMessage(body: string, htmlBody: string) {
|
||||
export function makeHtmlMessage(body: string, htmlBody: string): IContent {
|
||||
return {
|
||||
msgtype: MsgType.Text,
|
||||
format: "org.matrix.custom.html",
|
||||
@@ -55,7 +56,7 @@ export function makeHtmlMessage(body: string, htmlBody: string) {
|
||||
* @param {string} htmlBody the HTML representation of the notice
|
||||
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
|
||||
*/
|
||||
export function makeHtmlNotice(body: string, htmlBody: string) {
|
||||
export function makeHtmlNotice(body: string, htmlBody: string): IContent {
|
||||
return {
|
||||
msgtype: MsgType.Notice,
|
||||
format: "org.matrix.custom.html",
|
||||
@@ -70,7 +71,7 @@ export function makeHtmlNotice(body: string, htmlBody: string) {
|
||||
* @param {string} htmlBody the HTML representation of the emote
|
||||
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
|
||||
*/
|
||||
export function makeHtmlEmote(body: string, htmlBody: string) {
|
||||
export function makeHtmlEmote(body: string, htmlBody: string): IContent {
|
||||
return {
|
||||
msgtype: MsgType.Emote,
|
||||
format: "org.matrix.custom.html",
|
||||
@@ -84,7 +85,7 @@ export function makeHtmlEmote(body: string, htmlBody: string) {
|
||||
* @param {string} body the plaintext body of the emote
|
||||
* @returns {{msgtype: string, body: string}}
|
||||
*/
|
||||
export function makeTextMessage(body: string) {
|
||||
export function makeTextMessage(body: string): IContent {
|
||||
return {
|
||||
msgtype: MsgType.Text,
|
||||
body: body,
|
||||
@@ -96,7 +97,7 @@ export function makeTextMessage(body: string) {
|
||||
* @param {string} body the plaintext body of the notice
|
||||
* @returns {{msgtype: string, body: string}}
|
||||
*/
|
||||
export function makeNotice(body: string) {
|
||||
export function makeNotice(body: string): IContent {
|
||||
return {
|
||||
msgtype: MsgType.Notice,
|
||||
body: body,
|
||||
@@ -108,7 +109,7 @@ export function makeNotice(body: string) {
|
||||
* @param {string} body the plaintext body of the emote
|
||||
* @returns {{msgtype: string, body: string}}
|
||||
*/
|
||||
export function makeEmoteMessage(body: string) {
|
||||
export function makeEmoteMessage(body: string): IContent {
|
||||
return {
|
||||
msgtype: MsgType.Emote,
|
||||
body: body,
|
||||
@@ -118,12 +119,12 @@ export function makeEmoteMessage(body: string) {
|
||||
/** Location content helpers */
|
||||
|
||||
export const getTextForLocationEvent = (
|
||||
uri: string,
|
||||
uri: string | undefined,
|
||||
assetType: LocationAssetType,
|
||||
timestamp: number,
|
||||
description?: string,
|
||||
timestamp?: number,
|
||||
description?: string | null,
|
||||
): string => {
|
||||
const date = `at ${new Date(timestamp).toISOString()}`;
|
||||
const date = `at ${new Date(timestamp!).toISOString()}`;
|
||||
const assetName = assetType === LocationAssetType.Self ? 'User' : undefined;
|
||||
const quotedDescription = description ? `"${description}"` : undefined;
|
||||
|
||||
@@ -147,10 +148,10 @@ export const getTextForLocationEvent = (
|
||||
export const makeLocationContent = (
|
||||
// this is first but optional
|
||||
// to avoid a breaking change
|
||||
text: string | undefined,
|
||||
uri: string,
|
||||
timestamp: number,
|
||||
description?: string,
|
||||
text?: string,
|
||||
uri?: string,
|
||||
timestamp?: number,
|
||||
description?: string | null,
|
||||
assetType?: LocationAssetType,
|
||||
): LegacyLocationEventContent & MLocationEventContent => {
|
||||
const defaultedText = text ??
|
||||
@@ -187,7 +188,7 @@ export const parseLocationEvent = (wireEventContent: LocationEventWireContent):
|
||||
const assetType = asset?.type ?? LocationAssetType.Self;
|
||||
const fallbackText = text ?? wireEventContent.body;
|
||||
|
||||
return makeLocationContent(fallbackText, geoUri, timestamp, description, assetType);
|
||||
return makeLocationContent(fallbackText, geoUri, timestamp ?? undefined, description, assetType);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -201,7 +202,7 @@ export type MakeTopicContent = (
|
||||
export const makeTopicContent: MakeTopicContent = (topic, htmlTopic) => {
|
||||
const renderings = [{ body: topic, mimetype: "text/plain" }];
|
||||
if (isProvided(htmlTopic)) {
|
||||
renderings.push({ body: htmlTopic, mimetype: "text/html" });
|
||||
renderings.push({ body: htmlTopic!, mimetype: "text/html" });
|
||||
}
|
||||
return { topic, [M_TOPIC.name]: renderings };
|
||||
};
|
||||
@@ -247,14 +248,14 @@ export const makeBeaconInfoContent: MakeBeaconInfoContent = (
|
||||
|
||||
export type BeaconInfoState = MBeaconInfoContent & {
|
||||
assetType?: LocationAssetType;
|
||||
timestamp: number;
|
||||
timestamp?: number;
|
||||
};
|
||||
/**
|
||||
* Flatten beacon info event content
|
||||
*/
|
||||
export const parseBeaconInfoContent = (content: MBeaconInfoEventContent): BeaconInfoState => {
|
||||
const { description, timeout, live } = content;
|
||||
const timestamp = M_TIMESTAMP.findIn<number>(content);
|
||||
const timestamp = M_TIMESTAMP.findIn<number>(content) ?? undefined;
|
||||
const asset = M_ASSET.findIn<MAssetContent>(content);
|
||||
|
||||
return {
|
||||
@@ -290,14 +291,14 @@ export const makeBeaconContent: MakeBeaconContent = (
|
||||
},
|
||||
});
|
||||
|
||||
export type BeaconLocationState = MLocationContent & {
|
||||
export type BeaconLocationState = Omit<MLocationContent, "uri"> & {
|
||||
uri?: string; // override from MLocationContent to allow optionals
|
||||
timestamp?: number;
|
||||
};
|
||||
|
||||
export const parseBeaconContent = (content: MBeaconEventContent): BeaconLocationState => {
|
||||
const location = M_LOCATION.findIn<MLocationContent>(content);
|
||||
const timestamp = M_TIMESTAMP.findIn<number>(content);
|
||||
const timestamp = M_TIMESTAMP.findIn<number>(content) ?? undefined;
|
||||
|
||||
return {
|
||||
description: location?.description,
|
||||
|
||||
@@ -21,7 +21,7 @@ limitations under the License.
|
||||
|
||||
import { PkSigning } from "@matrix-org/olm";
|
||||
|
||||
import { decodeBase64, encodeBase64, pkSign, pkVerify } from './olmlib';
|
||||
import { decodeBase64, encodeBase64, IObject, pkSign, pkVerify } from './olmlib';
|
||||
import { logger } from '../logger';
|
||||
import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store';
|
||||
import { decryptAES, encryptAES } from './aes';
|
||||
@@ -29,7 +29,7 @@ import { DeviceInfo } from "./deviceinfo";
|
||||
import { SecretStorage } from "./SecretStorage";
|
||||
import { ICrossSigningKey, ISignedKey, MatrixClient } from "../client";
|
||||
import { OlmDevice } from "./OlmDevice";
|
||||
import { ICryptoCallbacks } from "../matrix";
|
||||
import { ICryptoCallbacks } from ".";
|
||||
import { ISignatures } from "../@types/signed";
|
||||
import { CryptoStore, SecretStorePrivateKeys } from "./store/base";
|
||||
import { ISecretStorageKeyInfo } from "./api";
|
||||
@@ -74,7 +74,7 @@ export class CrossSigningInfo {
|
||||
* Requires getCrossSigningKey and saveCrossSigningKeys
|
||||
* @param {object} cacheCallbacks Callbacks used to interact with the cache
|
||||
*/
|
||||
constructor(
|
||||
public constructor(
|
||||
public readonly userId: string,
|
||||
private callbacks: ICryptoCallbacks = {},
|
||||
private cacheCallbacks: ICacheCallbacks = {},
|
||||
@@ -175,7 +175,7 @@ export class CrossSigningInfo {
|
||||
// check what SSSS keys have encrypted the master key (if any)
|
||||
const stored = await secretStorage.isStored("m.cross_signing.master") || {};
|
||||
// then check which of those SSSS keys have also encrypted the SSK and USK
|
||||
function intersect(s: Record<string, ISecretStorageKeyInfo>) {
|
||||
function intersect(s: Record<string, ISecretStorageKeyInfo>): void {
|
||||
for (const k of Object.keys(stored)) {
|
||||
if (!s[k]) {
|
||||
delete stored[k];
|
||||
@@ -586,7 +586,14 @@ export class CrossSigningInfo {
|
||||
}
|
||||
}
|
||||
|
||||
function deviceToObject(device: DeviceInfo, userId: string) {
|
||||
interface DeviceObject extends IObject {
|
||||
algorithms: string[];
|
||||
keys: Record<string, string>;
|
||||
device_id: string;
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
function deviceToObject(device: DeviceInfo, userId: string): DeviceObject {
|
||||
return {
|
||||
algorithms: device.algorithms,
|
||||
keys: device.keys,
|
||||
@@ -606,7 +613,7 @@ export enum CrossSigningLevel {
|
||||
* Represents the ways in which we trust a user
|
||||
*/
|
||||
export class UserTrustLevel {
|
||||
constructor(
|
||||
public constructor(
|
||||
private readonly crossSigningVerified: boolean,
|
||||
private readonly crossSigningVerifiedBefore: boolean,
|
||||
private readonly tofu: boolean,
|
||||
@@ -646,7 +653,7 @@ export class UserTrustLevel {
|
||||
* Represents the ways in which we trust a device
|
||||
*/
|
||||
export class DeviceTrustLevel {
|
||||
constructor(
|
||||
public constructor(
|
||||
public readonly crossSigningVerified: boolean,
|
||||
public readonly tofu: boolean,
|
||||
private readonly localVerified: boolean,
|
||||
@@ -775,7 +782,7 @@ export async function requestKeysDuringVerification(
|
||||
// CrossSigningInfo.getCrossSigningKey() to validate/cache
|
||||
const crossSigning = new CrossSigningInfo(
|
||||
original.userId,
|
||||
{ getCrossSigningKey: async (type) => {
|
||||
{ getCrossSigningKey: async (type): Promise<Uint8Array> => {
|
||||
logger.debug("Cross-signing: requesting secret", type, deviceId);
|
||||
const { promise } = client.requestSecret(
|
||||
`m.cross_signing.${type}`, [deviceId],
|
||||
@@ -801,7 +808,7 @@ export async function requestKeysDuringVerification(
|
||||
});
|
||||
|
||||
// also request and cache the key backup key
|
||||
const backupKeyPromise = (async () => {
|
||||
const backupKeyPromise = (async (): Promise<void> => {
|
||||
const cachedKey = await client.crypto!.getSessionBackupPrivateKey();
|
||||
if (!cachedKey) {
|
||||
logger.info("No cached backup key found. Requesting...");
|
||||
|
||||
@@ -102,7 +102,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
||||
|
||||
private readonly serialiser: DeviceListUpdateSerialiser;
|
||||
|
||||
constructor(
|
||||
public constructor(
|
||||
baseApis: MatrixClient,
|
||||
private readonly cryptoStore: CryptoStore,
|
||||
olmDevice: OlmDevice,
|
||||
@@ -117,7 +117,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
||||
/**
|
||||
* Load the device tracking state from storage
|
||||
*/
|
||||
public async load() {
|
||||
public async load(): Promise<void> {
|
||||
await this.cryptoStore.doTxn(
|
||||
'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
|
||||
this.cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
|
||||
@@ -150,7 +150,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
||||
}
|
||||
}
|
||||
|
||||
public stop() {
|
||||
public stop(): void {
|
||||
if (this.saveTimer !== null) {
|
||||
clearTimeout(this.saveTimer);
|
||||
}
|
||||
@@ -230,7 +230,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
||||
}, delay);
|
||||
}
|
||||
|
||||
return savePromise!;
|
||||
return savePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -693,7 +693,7 @@ class DeviceListUpdateSerialiser {
|
||||
* @param {object} olmDevice The Olm Device
|
||||
* @param {object} deviceList The device list object, the device list to be updated
|
||||
*/
|
||||
constructor(
|
||||
public constructor(
|
||||
private readonly baseApis: MatrixClient,
|
||||
private readonly olmDevice: OlmDevice,
|
||||
private readonly deviceList: DeviceList,
|
||||
|
||||
@@ -19,16 +19,15 @@ import { IContent, MatrixEvent } from "../models/event";
|
||||
import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning";
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { Method, ClientPrefix } from "../http-api";
|
||||
import { Crypto, IBootstrapCrossSigningOpts } from "./index";
|
||||
import { Crypto, ICryptoCallbacks, IBootstrapCrossSigningOpts } from "./index";
|
||||
import {
|
||||
ClientEvent,
|
||||
CrossSigningKeys,
|
||||
ClientEventHandlerMap,
|
||||
CrossSigningKeys,
|
||||
ICrossSigningKey,
|
||||
ICryptoCallbacks,
|
||||
ISignedKey,
|
||||
KeySignatures,
|
||||
} from "../matrix";
|
||||
} from "../client";
|
||||
import { ISecretStorageKeyInfo } from "./api";
|
||||
import { IKeyBackupInfo } from "./keybackup";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
@@ -62,7 +61,7 @@ export class EncryptionSetupBuilder {
|
||||
* @param {Object.<String, MatrixEvent>} accountData pre-existing account data, will only be read, not written.
|
||||
* @param {CryptoCallbacks} delegateCryptoCallbacks crypto callbacks to delegate to if the key isn't in cache yet
|
||||
*/
|
||||
constructor(accountData: Record<string, MatrixEvent>, delegateCryptoCallbacks?: ICryptoCallbacks) {
|
||||
public constructor(accountData: Record<string, MatrixEvent>, delegateCryptoCallbacks?: ICryptoCallbacks) {
|
||||
this.accountDataClientAdapter = new AccountDataClientAdapter(accountData);
|
||||
this.crossSigningCallbacks = new CrossSigningCallbacks();
|
||||
this.ssssCryptoCallbacks = new SSSSCryptoCallbacks(delegateCryptoCallbacks);
|
||||
@@ -193,7 +192,7 @@ export class EncryptionSetupOperation {
|
||||
* @param {Object} keyBackupInfo
|
||||
* @param {Object} keySignatures
|
||||
*/
|
||||
constructor(
|
||||
public constructor(
|
||||
private readonly accountData: Map<string, object>,
|
||||
private readonly crossSigningKeys?: ICrossSigningKeys,
|
||||
private readonly keyBackupInfo?: IKeyBackupInfo,
|
||||
@@ -273,7 +272,7 @@ class AccountDataClientAdapter
|
||||
/**
|
||||
* @param {Object.<String, MatrixEvent>} existingValues existing account data
|
||||
*/
|
||||
constructor(private readonly existingValues: Record<string, MatrixEvent>) {
|
||||
public constructor(private readonly existingValues: Record<string, MatrixEvent>) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -343,7 +342,7 @@ class CrossSigningCallbacks implements ICryptoCallbacks, ICacheCallbacks {
|
||||
return Promise.resolve(this.privateKeys.get(type) ?? null);
|
||||
}
|
||||
|
||||
public saveCrossSigningKeys(privateKeys: Record<string, Uint8Array>) {
|
||||
public saveCrossSigningKeys(privateKeys: Record<string, Uint8Array>): void {
|
||||
for (const [type, privateKey] of Object.entries(privateKeys)) {
|
||||
this.privateKeys.set(type, privateKey);
|
||||
}
|
||||
@@ -357,7 +356,7 @@ class CrossSigningCallbacks implements ICryptoCallbacks, ICacheCallbacks {
|
||||
class SSSSCryptoCallbacks {
|
||||
private readonly privateKeys = new Map<string, Uint8Array>();
|
||||
|
||||
constructor(private readonly delegateCryptoCallbacks?: ICryptoCallbacks) {}
|
||||
public constructor(private readonly delegateCryptoCallbacks?: ICryptoCallbacks) {}
|
||||
|
||||
public async getSecretStorageKey(
|
||||
{ keys }: { keys: Record<string, ISecretStorageKeyInfo> },
|
||||
|
||||
+13
-13
@@ -177,13 +177,13 @@ export class OlmDevice {
|
||||
// Used by olm to serialise prekey message decryptions
|
||||
public olmPrekeyPromise: Promise<any> = Promise.resolve(); // set by consumers
|
||||
|
||||
constructor(private readonly cryptoStore: CryptoStore) {
|
||||
public constructor(private readonly cryptoStore: CryptoStore) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {array} The version of Olm.
|
||||
*/
|
||||
static getOlmVersion(): [number, number, number] {
|
||||
public static getOlmVersion(): [number, number, number] {
|
||||
return global.Olm.get_library_version();
|
||||
}
|
||||
|
||||
@@ -804,7 +804,7 @@ export class OlmDevice {
|
||||
log,
|
||||
);
|
||||
|
||||
return info!;
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -916,6 +916,7 @@ export class OlmDevice {
|
||||
}
|
||||
|
||||
public async recordSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
|
||||
logger.info(`Recording problem on olm session with ${deviceKey} of type ${type}. Recreating: ${fixed}`);
|
||||
await this.cryptoStore.storeEndToEndSessionProblem(deviceKey, type, fixed);
|
||||
}
|
||||
|
||||
@@ -1139,17 +1140,14 @@ export class OlmDevice {
|
||||
}
|
||||
|
||||
if (existingSession) {
|
||||
logger.log(
|
||||
"Update for megolm session "
|
||||
+ senderKey + "/" + sessionId,
|
||||
);
|
||||
logger.log(`Update for megolm session ${senderKey}|${sessionId}`);
|
||||
if (existingSession.first_known_index() <= session.first_known_index()) {
|
||||
if (!existingSessionData!.untrusted || extraSessionData.untrusted) {
|
||||
// existing session has less-than-or-equal index
|
||||
// (i.e. can decrypt at least as much), and the
|
||||
// new session's trust does not win over the old
|
||||
// session's trust, so keep it
|
||||
logger.log(`Keeping existing megolm session ${sessionId}`);
|
||||
logger.log(`Keeping existing megolm session ${senderKey}|${sessionId}`);
|
||||
return;
|
||||
}
|
||||
if (existingSession.first_known_index() < session.first_known_index()) {
|
||||
@@ -1164,7 +1162,7 @@ export class OlmDevice {
|
||||
) {
|
||||
logger.info(
|
||||
"Upgrading trust of existing megolm session " +
|
||||
sessionId + " based on newly-received trusted session",
|
||||
`${senderKey}|${sessionId} based on newly-received trusted session`,
|
||||
);
|
||||
existingSessionData!.untrusted = false;
|
||||
this.cryptoStore.storeEndToEndInboundGroupSession(
|
||||
@@ -1172,7 +1170,7 @@ export class OlmDevice {
|
||||
);
|
||||
} else {
|
||||
logger.warn(
|
||||
"Newly-received megolm session " + sessionId +
|
||||
`Newly-received megolm session ${senderKey}|$sessionId}` +
|
||||
" does not match existing session! Keeping existing session",
|
||||
);
|
||||
}
|
||||
@@ -1183,8 +1181,8 @@ export class OlmDevice {
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"Storing megolm session " + senderKey + "/" + sessionId +
|
||||
" with first index " + session.first_known_index(),
|
||||
`Storing megolm session ${senderKey}|${sessionId} with first index `+
|
||||
session.first_known_index(),
|
||||
);
|
||||
|
||||
const sessionData = Object.assign({}, extraSessionData, {
|
||||
@@ -1517,7 +1515,9 @@ export class OlmDevice {
|
||||
});
|
||||
}
|
||||
|
||||
async getSharedHistoryInboundGroupSessions(roomId: string): Promise<[senderKey: string, sessionId: string][]> {
|
||||
public async getSharedHistoryInboundGroupSessions(
|
||||
roomId: string,
|
||||
): Promise<[senderKey: string, sessionId: string][]> {
|
||||
let result: Promise<[senderKey: string, sessionId: string][]>;
|
||||
await this.cryptoStore.doTxn(
|
||||
'readonly', [
|
||||
|
||||
@@ -100,21 +100,14 @@ export class OutgoingRoomKeyRequestManager {
|
||||
// of sendOutgoingRoomKeyRequests
|
||||
private sendOutgoingRoomKeyRequestsRunning = false;
|
||||
|
||||
private clientRunning = false;
|
||||
private clientRunning = true;
|
||||
|
||||
constructor(
|
||||
public constructor(
|
||||
private readonly baseApis: MatrixClient,
|
||||
private readonly deviceId: string,
|
||||
private readonly cryptoStore: CryptoStore,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Called when the client is started. Sets background processes running.
|
||||
*/
|
||||
public start(): void {
|
||||
this.clientRunning = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the client is stopped. Stops any running background processes.
|
||||
*/
|
||||
@@ -359,7 +352,7 @@ export class OutgoingRoomKeyRequestManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const startSendingOutgoingRoomKeyRequests = () => {
|
||||
const startSendingOutgoingRoomKeyRequests = (): void => {
|
||||
if (this.sendOutgoingRoomKeyRequestsRunning) {
|
||||
throw new Error("RoomKeyRequestSend already in progress!");
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export class RoomList {
|
||||
// Object of roomId -> room e2e info object (body of the m.room.encryption event)
|
||||
private roomEncryption: Record<string, IRoomEncryption> = {};
|
||||
|
||||
constructor(private readonly cryptoStore?: CryptoStore) {}
|
||||
public constructor(private readonly cryptoStore?: CryptoStore) {}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
await this.cryptoStore!.doTxn(
|
||||
|
||||
@@ -19,8 +19,9 @@ import * as olmlib from './olmlib';
|
||||
import { encodeBase64 } from './olmlib';
|
||||
import { randomString } from '../randomstring';
|
||||
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from './aes';
|
||||
import { ClientEvent, IContent, ICryptoCallbacks, MatrixEvent } from '../matrix';
|
||||
import { ClientEventHandlerMap, MatrixClient } from "../client";
|
||||
import { ICryptoCallbacks } from ".";
|
||||
import { IContent, MatrixEvent } from "../models/event";
|
||||
import { ClientEvent, ClientEventHandlerMap, MatrixClient } from "../client";
|
||||
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from './api';
|
||||
import { TypedEventEmitter } from '../models/typed-event-emitter';
|
||||
import { defer, IDeferred } from "../utils";
|
||||
@@ -77,7 +78,7 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
|
||||
// as you don't request any secrets.
|
||||
// A better solution would probably be to split this class up into secret storage and
|
||||
// secret sharing which are really two separate things, even though they share an MSC.
|
||||
constructor(
|
||||
public constructor(
|
||||
private readonly accountDataAdapter: IAccountDataClient,
|
||||
private readonly cryptoCallbacks: ICryptoCallbacks,
|
||||
private readonly baseApis: B,
|
||||
@@ -380,7 +381,7 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
|
||||
const deferred = defer<string>();
|
||||
this.requests.set(requestId, { name, devices, deferred });
|
||||
|
||||
const cancel = (reason: string) => {
|
||||
const cancel = (reason: string): void => {
|
||||
// send cancellation event
|
||||
const cancelData = {
|
||||
action: "request_cancellation",
|
||||
|
||||
@@ -78,7 +78,7 @@ export abstract class EncryptionAlgorithm {
|
||||
protected readonly baseApis: MatrixClient;
|
||||
protected readonly roomId?: string;
|
||||
|
||||
constructor(params: IParams) {
|
||||
public constructor(params: IParams) {
|
||||
this.userId = params.userId;
|
||||
this.deviceId = params.deviceId;
|
||||
this.crypto = params.crypto;
|
||||
@@ -150,7 +150,7 @@ export abstract class DecryptionAlgorithm {
|
||||
protected readonly baseApis: MatrixClient;
|
||||
protected readonly roomId?: string;
|
||||
|
||||
constructor(params: DecryptionClassParams) {
|
||||
public constructor(params: DecryptionClassParams) {
|
||||
this.userId = params.userId;
|
||||
this.crypto = params.crypto;
|
||||
this.olmDevice = params.olmDevice;
|
||||
@@ -242,7 +242,7 @@ export abstract class DecryptionAlgorithm {
|
||||
export class DecryptionError extends Error {
|
||||
public readonly detailedString: string;
|
||||
|
||||
constructor(public readonly code: string, msg: string, details?: Record<string, string | Error>) {
|
||||
public constructor(public readonly code: string, msg: string, details?: Record<string, string | Error>) {
|
||||
super(msg);
|
||||
this.code = code;
|
||||
this.name = 'DecryptionError';
|
||||
@@ -272,7 +272,7 @@ function detailedStringForDecryptionError(err: DecryptionError, details?: Record
|
||||
* @extends Error
|
||||
*/
|
||||
export class UnknownDeviceError extends Error {
|
||||
constructor(
|
||||
public constructor(
|
||||
msg: string,
|
||||
public readonly devices: Record<string, Record<string, object>>,
|
||||
public event?: MatrixEvent,
|
||||
|
||||
@@ -38,7 +38,7 @@ import { IOlmSessionResult } from "../olmlib";
|
||||
import { DeviceInfoMap } from "../DeviceList";
|
||||
import { MatrixEvent } from "../../models/event";
|
||||
import { EventType, MsgType } from '../../@types/event';
|
||||
import { IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index";
|
||||
import { IEncryptedContent, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index";
|
||||
import { RoomKeyRequestState } from '../OutgoingRoomKeyRequestManager';
|
||||
import { OlmGroupSessionExtraData } from "../../@types/crypto";
|
||||
import { MatrixError } from "../../http-api";
|
||||
@@ -105,12 +105,6 @@ interface IPayload extends Partial<IMessage> {
|
||||
algorithm?: string;
|
||||
sender_key?: string;
|
||||
}
|
||||
|
||||
interface IEncryptedContent {
|
||||
algorithm: string;
|
||||
sender_key: string;
|
||||
ciphertext: Record<string, string>;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
interface SharedWithData {
|
||||
@@ -142,7 +136,7 @@ class OutboundSessionInfo {
|
||||
public sharedWithDevices: Record<string, Record<string, SharedWithData>> = {};
|
||||
public blockedDevicesNotified: Record<string, Record<string, boolean>> = {};
|
||||
|
||||
constructor(public readonly sessionId: string, public readonly sharedHistory = false) {
|
||||
public constructor(public readonly sessionId: string, public readonly sharedHistory = false) {
|
||||
this.creationTime = new Date().getTime();
|
||||
}
|
||||
|
||||
@@ -254,7 +248,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
|
||||
protected readonly roomId: string;
|
||||
|
||||
constructor(params: IParams & Required<Pick<IParams, "roomId">>) {
|
||||
public constructor(params: IParams & Required<Pick<IParams, "roomId">>) {
|
||||
super(params);
|
||||
this.roomId = params.roomId;
|
||||
|
||||
@@ -353,7 +347,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
singleOlmCreationPhase: boolean,
|
||||
blocked: IBlockedMap,
|
||||
session: OutboundSessionInfo,
|
||||
) {
|
||||
): Promise<void> {
|
||||
// now check if we need to share with any devices
|
||||
const shareMap: Record<string, DeviceInfo[]> = {};
|
||||
|
||||
@@ -392,13 +386,13 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
(async (): Promise<void> => {
|
||||
// share keys with devices that we already have a session for
|
||||
logger.debug(`Sharing keys with existing Olm sessions in ${this.roomId}`, olmSessions);
|
||||
await this.shareKeyWithOlmSessions(session, key, payload, olmSessions);
|
||||
logger.debug(`Shared keys with existing Olm sessions in ${this.roomId}`);
|
||||
})(),
|
||||
(async () => {
|
||||
(async (): Promise<void> => {
|
||||
logger.debug(
|
||||
`Sharing keys (start phase 1) with new Olm sessions in ${this.roomId}`,
|
||||
devicesWithoutSession,
|
||||
@@ -421,7 +415,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
if (!singleOlmCreationPhase && (Date.now() - start < 10000)) {
|
||||
// perform the second phase of olm session creation if requested,
|
||||
// and if the first phase didn't take too long
|
||||
(async () => {
|
||||
(async (): Promise<void> => {
|
||||
// Retry sending keys to devices that we were unable to establish
|
||||
// an olm session for. This time, we use a longer timeout, but we
|
||||
// do this in the background and don't block anything else while we
|
||||
@@ -458,7 +452,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
}
|
||||
logger.debug(`Shared keys (all phases done) with new Olm sessions in ${this.roomId}`);
|
||||
})(),
|
||||
(async () => {
|
||||
(async (): Promise<void> => {
|
||||
logger.debug(`There are ${Object.entries(blocked).length} blocked devices in ${this.roomId}`,
|
||||
Object.entries(blocked));
|
||||
|
||||
@@ -702,28 +696,27 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
): Promise<void> {
|
||||
const obSessionInfo = this.outboundSessions[sessionId];
|
||||
if (!obSessionInfo) {
|
||||
logger.debug(`megolm session ${sessionId} not found: not re-sharing keys`);
|
||||
logger.debug(`megolm session ${senderKey}|${sessionId} not found: not re-sharing keys`);
|
||||
return;
|
||||
}
|
||||
|
||||
// The chain index of the key we previously sent this device
|
||||
if (obSessionInfo.sharedWithDevices[userId] === undefined) {
|
||||
logger.debug(`megolm session ${sessionId} never shared with user ${userId}`);
|
||||
logger.debug(`megolm session ${senderKey}|${sessionId} never shared with user ${userId}`);
|
||||
return;
|
||||
}
|
||||
const sessionSharedData = obSessionInfo.sharedWithDevices[userId][device.deviceId];
|
||||
if (sessionSharedData === undefined) {
|
||||
logger.debug(
|
||||
"megolm session ID " + sessionId + " never shared with device " +
|
||||
userId + ":" + device.deviceId,
|
||||
`megolm session ${senderKey}|${sessionId} never shared with device ${userId}:${device.deviceId}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionSharedData.deviceKey !== device.getIdentityKey()) {
|
||||
logger.warn(
|
||||
`Session has been shared with device ${device.deviceId} but with identity ` +
|
||||
`key ${sessionSharedData.deviceKey}. Key is now ${device.getIdentityKey()}!`,
|
||||
`Megolm session ${senderKey}|${sessionId} has been shared with device ${device.deviceId} but ` +
|
||||
`with identity key ${sessionSharedData.deviceKey}. Key is now ${device.getIdentityKey()}!`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -736,7 +729,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
|
||||
if (!key) {
|
||||
logger.warn(
|
||||
`No inbound session key found for megolm ${sessionId}: not re-sharing keys`,
|
||||
`No inbound session key found for megolm session ${senderKey}|${sessionId}: not re-sharing keys`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -782,7 +775,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
[device.deviceId]: encryptedContent,
|
||||
},
|
||||
});
|
||||
logger.debug(`Re-shared key for megolm session ${sessionId} with ${userId}:${device.deviceId}`);
|
||||
logger.debug(`Re-shared key for megolm session ${senderKey}|${sessionId} with ${userId}:${device.deviceId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -816,7 +809,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
errorDevices: IOlmDevice[],
|
||||
otkTimeout: number,
|
||||
failedServers?: string[],
|
||||
) {
|
||||
): Promise<void> {
|
||||
logger.debug(`Ensuring Olm sessions for devices in ${this.roomId}`);
|
||||
const devicemap = await olmlib.ensureOlmSessionsForDevices(
|
||||
this.olmDevice, this.baseApis, devicesByUser, false, otkTimeout, failedServers,
|
||||
@@ -976,12 +969,12 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
|
||||
this.encryptionPreparation = {
|
||||
startTime: Date.now(),
|
||||
promise: (async () => {
|
||||
promise: (async (): Promise<void> => {
|
||||
try {
|
||||
logger.debug(`Getting devices in ${this.roomId}`);
|
||||
const [devicesInRoom, blocked] = await this.getDevicesInRoom(room);
|
||||
|
||||
if (this.crypto.getGlobalErrorOnUnknownDevices()) {
|
||||
if (this.crypto.globalErrorOnUnknownDevices) {
|
||||
// Drop unknown devices for now. When the message gets sent, we'll
|
||||
// throw an error, but we'll still be prepared to send to the known
|
||||
// devices.
|
||||
@@ -1034,7 +1027,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
|
||||
// check if any of these devices are not yet known to the user.
|
||||
// if so, warn the user so they can verify or ignore.
|
||||
if (this.crypto.getGlobalErrorOnUnknownDevices()) {
|
||||
if (this.crypto.globalErrorOnUnknownDevices) {
|
||||
this.checkForUnknownDevices(devicesInRoom);
|
||||
}
|
||||
|
||||
@@ -1169,7 +1162,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
});
|
||||
|
||||
// The global value is treated as a default for when rooms don't specify a value.
|
||||
let isBlacklisting = this.crypto.getGlobalBlacklistUnverifiedDevices();
|
||||
let isBlacklisting = this.crypto.globalBlacklistUnverifiedDevices;
|
||||
const isRoomBlacklisting = room.getBlacklistUnverifiedDevices();
|
||||
if (typeof isRoomBlacklisting === 'boolean') {
|
||||
isBlacklisting = isRoomBlacklisting;
|
||||
@@ -1238,7 +1231,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
|
||||
protected readonly roomId: string;
|
||||
|
||||
constructor(params: DecryptionClassParams<IParams & Required<Pick<IParams, "roomId">>>) {
|
||||
public constructor(params: DecryptionClassParams<IParams & Required<Pick<IParams, "roomId">>>) {
|
||||
super(params);
|
||||
this.roomId = params.roomId;
|
||||
}
|
||||
@@ -1318,6 +1311,10 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
content.sender_key, event.getTs() - 120000,
|
||||
);
|
||||
if (problem) {
|
||||
logger.info(
|
||||
`When handling UISI from ${event.getSender()} (sender key ${content.sender_key}): ` +
|
||||
`recent session problem with that sender: ${problem}`,
|
||||
);
|
||||
let problemDescription = PROBLEM_DESCRIPTIONS[problem.type as "no_olm"] || PROBLEM_DESCRIPTIONS.unknown;
|
||||
if (problem.fixed) {
|
||||
problemDescription +=
|
||||
@@ -1660,6 +1657,9 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: switch this to use encryptAndSendToDevices() rather than duplicating it?
|
||||
|
||||
await olmlib.ensureOlmSessionsForDevices(
|
||||
this.olmDevice, this.baseApis, { [sender]: [device] }, false,
|
||||
);
|
||||
@@ -1717,6 +1717,8 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
const deviceInfo = this.crypto.getStoredDevice(userId, deviceId)!;
|
||||
const body = keyRequest.requestBody;
|
||||
|
||||
// XXX: switch this to use encryptAndSendToDevices()?
|
||||
|
||||
this.olmlib.ensureOlmSessionsForDevices(
|
||||
this.olmDevice, this.baseApis, {
|
||||
[userId]: [deviceInfo],
|
||||
@@ -1903,13 +1905,15 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
public async sendSharedHistoryInboundSessions(devicesByUser: Record<string, DeviceInfo[]>): Promise<void> {
|
||||
await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser);
|
||||
|
||||
logger.log("sendSharedHistoryInboundSessions to users", Object.keys(devicesByUser));
|
||||
|
||||
const sharedHistorySessions = await this.olmDevice.getSharedHistoryInboundGroupSessions(this.roomId);
|
||||
logger.log("shared-history sessions", sharedHistorySessions);
|
||||
logger.log(
|
||||
`Sharing history in ${this.roomId} with users ${Object.keys(devicesByUser)}`,
|
||||
sharedHistorySessions.map(([senderKey, sessionId]) => `${senderKey}|${sessionId}`),
|
||||
);
|
||||
for (const [senderKey, sessionId] of sharedHistorySessions) {
|
||||
const payload = await this.buildKeyForwardingMessage(this.roomId, senderKey, sessionId);
|
||||
|
||||
// FIXME: use encryptAndSendToDevices() rather than duplicating it here.
|
||||
const promises: Promise<unknown>[] = [];
|
||||
const contentMap: Record<string, Record<string, IEncryptedContent>> = {};
|
||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||
|
||||
@@ -119,12 +119,10 @@ class OlmEncryption extends EncryptionAlgorithm {
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
for (let i = 0; i < users.length; ++i) {
|
||||
const userId = users[i];
|
||||
for (const userId of users) {
|
||||
const devices = this.crypto.getStoredDevicesForUser(userId) || [];
|
||||
|
||||
for (let j = 0; j < devices.length; ++j) {
|
||||
const deviceInfo = devices[j];
|
||||
for (const deviceInfo of devices) {
|
||||
const key = deviceInfo.getIdentityKey();
|
||||
if (key == this.olmDevice.deviceCurve25519Key) {
|
||||
// don't bother sending to ourself
|
||||
@@ -304,8 +302,7 @@ class OlmDecryption extends DecryptionAlgorithm {
|
||||
|
||||
// try each session in turn.
|
||||
const decryptionErrors: Record<string, string> = {};
|
||||
for (let i = 0; i < sessionIds.length; i++) {
|
||||
const sessionId = sessionIds[i];
|
||||
for (const sessionId of sessionIds) {
|
||||
try {
|
||||
const payload = await this.olmDevice.decryptMessage(
|
||||
theirDeviceIdentityKey, sessionId, message.type, message.body,
|
||||
|
||||
@@ -120,7 +120,7 @@ export class BackupManager {
|
||||
private sendingBackups: boolean; // Are we currently sending backups?
|
||||
private sessionLastCheckAttemptedTime: Record<string, number> = {}; // When did we last try to check the server for a given session id?
|
||||
|
||||
constructor(private readonly baseApis: MatrixClient, public readonly getKey: GetKey) {
|
||||
public constructor(private readonly baseApis: MatrixClient, public readonly getKey: GetKey) {
|
||||
this.checkedForBackup = false;
|
||||
this.sendingBackups = false;
|
||||
}
|
||||
@@ -302,7 +302,7 @@ export class BackupManager {
|
||||
|| now - this.sessionLastCheckAttemptedTime[targetSessionId!] > KEY_BACKUP_CHECK_RATE_LIMIT
|
||||
) {
|
||||
this.sessionLastCheckAttemptedTime[targetSessionId!] = now;
|
||||
await this.baseApis.restoreKeyBackupWithCache(targetRoomId, targetSessionId, this.backupInfo, {});
|
||||
await this.baseApis.restoreKeyBackupWithCache(targetRoomId!, targetSessionId!, this.backupInfo, {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -609,7 +609,7 @@ export class BackupManager {
|
||||
export class Curve25519 implements BackupAlgorithm {
|
||||
public static algorithmName = "m.megolm_backup.v1.curve25519-aes-sha2";
|
||||
|
||||
constructor(
|
||||
public constructor(
|
||||
public authData: ICurve25519AuthData,
|
||||
private publicKey: any, // FIXME: PkEncryption
|
||||
private getKey: () => Promise<Uint8Array>,
|
||||
@@ -661,7 +661,7 @@ export class Curve25519 implements BackupAlgorithm {
|
||||
}
|
||||
}
|
||||
|
||||
public get untrusted() { return true; }
|
||||
public get untrusted(): boolean { return true; }
|
||||
|
||||
public async encryptSession(data: Record<string, any>): Promise<any> {
|
||||
const plainText: Record<string, any> = Object.assign({}, data);
|
||||
@@ -680,8 +680,7 @@ export class Curve25519 implements BackupAlgorithm {
|
||||
const backupPubKey = decryption.init_with_private_key(privKey);
|
||||
|
||||
if (backupPubKey !== this.authData.public_key) {
|
||||
// eslint-disable-next-line no-throw-literal
|
||||
throw { errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY };
|
||||
throw new MatrixError({ errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY });
|
||||
}
|
||||
|
||||
const keys: IMegolmSessionData[] = [];
|
||||
@@ -736,7 +735,7 @@ const UNSTABLE_MSC3270_NAME = new UnstableValue(
|
||||
export class Aes256 implements BackupAlgorithm {
|
||||
public static algorithmName = UNSTABLE_MSC3270_NAME.name;
|
||||
|
||||
constructor(
|
||||
public constructor(
|
||||
public readonly authData: IAes256AuthData,
|
||||
private readonly key: Uint8Array,
|
||||
) {}
|
||||
@@ -787,7 +786,7 @@ export class Aes256 implements BackupAlgorithm {
|
||||
}
|
||||
}
|
||||
|
||||
public get untrusted() { return false; }
|
||||
public get untrusted(): boolean { return false; }
|
||||
|
||||
public encryptSession(data: Record<string, any>): Promise<any> {
|
||||
const plainText: Record<string, any> = Object.assign({}, data);
|
||||
|
||||
@@ -62,7 +62,7 @@ export class DehydrationManager {
|
||||
private keyInfo?: {[props: string]: any};
|
||||
private deviceDisplayName?: string;
|
||||
|
||||
constructor(private readonly crypto: Crypto) {
|
||||
public constructor(private readonly crypto: Crypto) {
|
||||
this.getDehydrationKeyFromCache();
|
||||
}
|
||||
|
||||
@@ -294,7 +294,7 @@ export class DehydrationManager {
|
||||
}
|
||||
}
|
||||
|
||||
public stop() {
|
||||
public stop(): void {
|
||||
if (this.timeoutId) {
|
||||
global.clearTimeout(this.timeoutId);
|
||||
this.timeoutId = undefined;
|
||||
|
||||
@@ -94,7 +94,7 @@ export class DeviceInfo {
|
||||
public unsigned: Record<string, any> = {};
|
||||
public signatures: ISignatures = {};
|
||||
|
||||
constructor(public readonly deviceId: string) {}
|
||||
public constructor(public readonly deviceId: string) {}
|
||||
|
||||
/**
|
||||
* Prepare a DeviceInfo for JSON serialisation in the session store
|
||||
|
||||
+69
-65
@@ -127,6 +127,29 @@ export interface IBootstrapCrossSigningOpts {
|
||||
authUploadDeviceSigningKeys?(makeRequest: (authData: any) => Promise<{}>): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ICryptoCallbacks {
|
||||
getCrossSigningKey?: (keyType: string, pubKey: string) => Promise<Uint8Array | null>;
|
||||
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
|
||||
shouldUpgradeDeviceVerifications?: (
|
||||
users: Record<string, any>
|
||||
) => Promise<string[]>;
|
||||
getSecretStorageKey?: (
|
||||
keys: {keys: Record<string, ISecretStorageKeyInfo>}, name: string
|
||||
) => Promise<[string, Uint8Array] | null>;
|
||||
cacheSecretStorageKey?: (
|
||||
keyId: string, keyInfo: ISecretStorageKeyInfo, key: Uint8Array
|
||||
) => void;
|
||||
onSecretRequested?: (
|
||||
userId: string, deviceId: string,
|
||||
requestId: string, secretName: string, deviceTrust: DeviceTrustLevel
|
||||
) => Promise<string>;
|
||||
getDehydrationKey?: (
|
||||
keyInfo: ISecretStorageKeyInfo,
|
||||
checkFunc: (key: Uint8Array) => void,
|
||||
) => Promise<Uint8Array>;
|
||||
getBackupKey?: () => Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
interface IRoomKey {
|
||||
room_id: string;
|
||||
@@ -255,7 +278,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
/**
|
||||
* @return {string} The version of Olm.
|
||||
*/
|
||||
static getOlmVersion(): [number, number, number] {
|
||||
public static getOlmVersion(): [number, number, number] {
|
||||
return OlmDevice.getOlmVersion();
|
||||
}
|
||||
|
||||
@@ -285,8 +308,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
|
||||
private deviceKeys: Record<string, string> = {}; // type: key
|
||||
|
||||
private globalBlacklistUnverifiedDevices = false;
|
||||
private globalErrorOnUnknownDevices = true;
|
||||
public globalBlacklistUnverifiedDevices = false;
|
||||
public globalErrorOnUnknownDevices = true;
|
||||
|
||||
// list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
|
||||
// we received in the current sync.
|
||||
@@ -349,7 +372,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
* Each element can either be a string from MatrixClient.verificationMethods
|
||||
* or a class that implements a verification method.
|
||||
*/
|
||||
constructor(
|
||||
public constructor(
|
||||
public readonly baseApis: MatrixClient,
|
||||
public readonly userId: string,
|
||||
private readonly deviceId: string,
|
||||
@@ -442,7 +465,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
|
||||
// Assuming no app-supplied callback, default to getting from SSSS.
|
||||
if (!cryptoCallbacks.getCrossSigningKey && cryptoCallbacks.getSecretStorageKey) {
|
||||
cryptoCallbacks.getCrossSigningKey = async (type) => {
|
||||
cryptoCallbacks.getCrossSigningKey = async (type): Promise<Uint8Array | null> => {
|
||||
return CrossSigningInfo.getFromSecretStorage(type, this.secretStorage);
|
||||
};
|
||||
}
|
||||
@@ -686,7 +709,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
);
|
||||
|
||||
// Reset the cross-signing keys
|
||||
const resetCrossSigning = async () => {
|
||||
const resetCrossSigning = async (): Promise<void> => {
|
||||
crossSigningInfo.resetKeys();
|
||||
// Sign master key with device key
|
||||
await this.signObject(crossSigningInfo.keys.master);
|
||||
@@ -823,12 +846,12 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
*/
|
||||
// TODO this does not resolve with what it says it does
|
||||
public async bootstrapSecretStorage({
|
||||
createSecretStorageKey = async () => ({} as IRecoveryKey),
|
||||
createSecretStorageKey = async (): Promise<IRecoveryKey> => ({} as IRecoveryKey),
|
||||
keyBackupInfo,
|
||||
setupNewKeyBackup,
|
||||
setupNewSecretStorage,
|
||||
getKeyBackupPassphrase,
|
||||
}: ICreateSecretStorageOpts = {}) {
|
||||
}: ICreateSecretStorageOpts = {}): Promise<void> {
|
||||
logger.log("Bootstrapping Secure Secret Storage");
|
||||
const delegateCryptoCallbacks = this.baseApis.cryptoCallbacks;
|
||||
const builder = new EncryptionSetupBuilder(
|
||||
@@ -845,7 +868,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
let newKeyId: string | null = null;
|
||||
|
||||
// create a new SSSS key and set it as default
|
||||
const createSSSS = async (opts: IAddSecretStorageKeyOpts, privateKey?: Uint8Array) => {
|
||||
const createSSSS = async (opts: IAddSecretStorageKeyOpts, privateKey?: Uint8Array): Promise<string> => {
|
||||
if (privateKey) {
|
||||
opts.key = privateKey;
|
||||
}
|
||||
@@ -861,7 +884,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
return keyId;
|
||||
};
|
||||
|
||||
const ensureCanCheckPassphrase = async (keyId: string, keyInfo: ISecretStorageKeyInfo) => {
|
||||
const ensureCanCheckPassphrase = async (keyId: string, keyInfo: ISecretStorageKeyInfo): Promise<void> => {
|
||||
if (!keyInfo.mac) {
|
||||
const key = await this.baseApis.cryptoCallbacks.getSecretStorageKey?.(
|
||||
{ keys: { [keyId]: keyInfo } }, "",
|
||||
@@ -880,7 +903,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
}
|
||||
};
|
||||
|
||||
const signKeyBackupWithCrossSigning = async (keyBackupAuthData: IKeyBackupInfo["auth_data"]) => {
|
||||
const signKeyBackupWithCrossSigning = async (keyBackupAuthData: IKeyBackupInfo["auth_data"]): Promise<void> => {
|
||||
if (
|
||||
this.crossSigningInfo.getId() &&
|
||||
await this.crossSigningInfo.isStoredInKeyCache("master")
|
||||
@@ -1218,7 +1241,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
const signedDevice = await this.crossSigningInfo.signDevice(this.userId, device);
|
||||
logger.info(`Starting background key sig upload for ${this.deviceId}`);
|
||||
|
||||
const upload = ({ shouldEmit = false }) => {
|
||||
const upload = ({ shouldEmit = false }): Promise<void> => {
|
||||
return this.baseApis.uploadKeySignatures({
|
||||
[this.userId]: {
|
||||
[this.deviceId]: signedDevice!,
|
||||
@@ -1452,7 +1475,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
/*
|
||||
* Event handler for DeviceList's userNewDevices event
|
||||
*/
|
||||
private onDeviceListUserCrossSigningUpdated = async (userId: string) => {
|
||||
private onDeviceListUserCrossSigningUpdated = async (userId: string): Promise<void> => {
|
||||
if (userId === this.userId) {
|
||||
// An update to our own cross-signing key.
|
||||
// Get the new key first:
|
||||
@@ -1634,7 +1657,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
|
||||
const keysToUpload = Object.keys(keySignatures);
|
||||
if (keysToUpload.length) {
|
||||
const upload = ({ shouldEmit = false }) => {
|
||||
const upload = ({ shouldEmit = false }): Promise<void> => {
|
||||
logger.info(`Starting background key sig upload for ${keysToUpload}`);
|
||||
return this.baseApis.uploadKeySignatures({ [this.userId]: keySignatures })
|
||||
.then((response) => {
|
||||
@@ -1752,9 +1775,11 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
eventEmitter.on(MatrixEventEvent.Decrypted, this.onTimelineEvent);
|
||||
}
|
||||
|
||||
/** Start background processes related to crypto */
|
||||
/**
|
||||
* @deprecated this does nothing and will be removed in a future version
|
||||
*/
|
||||
public start(): void {
|
||||
this.outgoingRoomKeyRequestManager.start();
|
||||
logger.warn("MatrixClient.crypto.start() is deprecated");
|
||||
}
|
||||
|
||||
/** Stop background processes related to crypto */
|
||||
@@ -1788,6 +1813,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
* do not specify a value.
|
||||
*
|
||||
* @param {boolean} value whether to blacklist all unverified devices by default
|
||||
*
|
||||
* @deprecated For external code, use {@link MatrixClient#setGlobalBlacklistUnverifiedDevices}. For
|
||||
* internal code, set {@link #globalBlacklistUnverifiedDevices} directly.
|
||||
*/
|
||||
public setGlobalBlacklistUnverifiedDevices(value: boolean): void {
|
||||
this.globalBlacklistUnverifiedDevices = value;
|
||||
@@ -1795,34 +1823,14 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
|
||||
/**
|
||||
* @return {boolean} whether to blacklist all unverified devices by default
|
||||
*
|
||||
* @deprecated For external code, use {@link MatrixClient#getGlobalBlacklistUnverifiedDevices}. For
|
||||
* internal code, reference {@link #globalBlacklistUnverifiedDevices} directly.
|
||||
*/
|
||||
public getGlobalBlacklistUnverifiedDevices(): boolean {
|
||||
return this.globalBlacklistUnverifiedDevices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether sendMessage in a room with unknown and unverified devices
|
||||
* should throw an error and not send them message. This has 'Global' for
|
||||
* symmetry with setGlobalBlacklistUnverifiedDevices but there is currently
|
||||
* no room-level equivalent for this setting.
|
||||
*
|
||||
* This API is currently UNSTABLE and may change or be removed without notice.
|
||||
*
|
||||
* @param {boolean} value whether error on unknown devices
|
||||
*/
|
||||
public setGlobalErrorOnUnknownDevices(value: boolean): void {
|
||||
this.globalErrorOnUnknownDevices = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean} whether to error on unknown devices
|
||||
*
|
||||
* This API is currently UNSTABLE and may change or be removed without notice.
|
||||
*/
|
||||
public getGlobalErrorOnUnknownDevices(): boolean {
|
||||
return this.globalErrorOnUnknownDevices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload the device keys to the homeserver.
|
||||
* @return {object} A promise that will resolve when the keys are uploaded.
|
||||
@@ -1856,7 +1864,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
}
|
||||
}
|
||||
|
||||
public setNeedsNewFallback(needsNewFallback: boolean) {
|
||||
public setNeedsNewFallback(needsNewFallback: boolean): void {
|
||||
this.needsNewFallback = needsNewFallback;
|
||||
}
|
||||
|
||||
@@ -1865,7 +1873,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
}
|
||||
|
||||
// check if it's time to upload one-time keys, and do so if so.
|
||||
private maybeUploadOneTimeKeys() {
|
||||
private maybeUploadOneTimeKeys(): void {
|
||||
// frequency with which to check & upload one-time keys
|
||||
const uploadPeriod = 1000 * 60; // one minute
|
||||
|
||||
@@ -1911,7 +1919,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
// out stale private keys that won't receive a message.
|
||||
const keyLimit = Math.floor(maxOneTimeKeys / 2);
|
||||
|
||||
const uploadLoop = async (keyCount: number) => {
|
||||
const uploadLoop = async (keyCount: number): Promise<void> => {
|
||||
while (keyLimit > keyCount || this.getNeedsNewFallback()) {
|
||||
// Ask olm to generate new one time keys, then upload them to synapse.
|
||||
if (keyLimit > keyCount) {
|
||||
@@ -2147,7 +2155,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
);
|
||||
const device = await this.crossSigningInfo.signUser(xsk);
|
||||
if (device) {
|
||||
const upload = async ({ shouldEmit = false }) => {
|
||||
const upload = async ({ shouldEmit = false }): Promise<void> => {
|
||||
logger.info("Uploading signature for " + userId + "...");
|
||||
const response = await this.baseApis.uploadKeySignatures({
|
||||
[userId]: {
|
||||
@@ -2238,7 +2246,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
}
|
||||
|
||||
if (device) {
|
||||
const upload = async ({ shouldEmit = false }) => {
|
||||
const upload = async ({ shouldEmit = false }): Promise<void> => {
|
||||
logger.info("Uploading signature for " + deviceId);
|
||||
const response = await this.baseApis.uploadKeySignatures({
|
||||
[userId]: {
|
||||
@@ -2379,9 +2387,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
*/
|
||||
public async getOlmSessionsForUser(userId: string): Promise<Record<string, IUserOlmSession>> {
|
||||
const devices = this.getStoredDevicesForUser(userId) || [];
|
||||
const result = {};
|
||||
for (let j = 0; j < devices.length; ++j) {
|
||||
const device = devices[j];
|
||||
const result: { [deviceId: string]: IUserOlmSession } = {};
|
||||
for (const device of devices) {
|
||||
const deviceKey = device.getIdentityKey();
|
||||
const sessions = await this.olmDevice.getSessionInfoForDevice(deviceKey);
|
||||
|
||||
@@ -2638,7 +2645,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
* @returns {Promise} when all devices for the room have been fetched and marked to track
|
||||
*/
|
||||
public trackRoomDevices(roomId: string): Promise<void> {
|
||||
const trackMembers = async () => {
|
||||
const trackMembers = async (): Promise<void> => {
|
||||
// not an encrypted room
|
||||
if (!this.roomEncryptors.has(roomId)) {
|
||||
return;
|
||||
@@ -2682,14 +2689,11 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
): Promise<Record<string, Record<string, olmlib.IOlmSessionResult>>> {
|
||||
const devicesByUser: Record<string, DeviceInfo[]> = {};
|
||||
|
||||
for (let i = 0; i < users.length; ++i) {
|
||||
const userId = users[i];
|
||||
for (const userId of users) {
|
||||
devicesByUser[userId] = [];
|
||||
|
||||
const devices = this.getStoredDevicesForUser(userId) || [];
|
||||
for (let j = 0; j < devices.length; ++j) {
|
||||
const deviceInfo = devices[j];
|
||||
|
||||
for (const deviceInfo of devices) {
|
||||
const key = deviceInfo.getIdentityKey();
|
||||
if (key == this.olmDevice.deviceCurve25519Key) {
|
||||
// don't bother setting up session to ourself
|
||||
@@ -2743,7 +2747,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
let failures = 0;
|
||||
const total = keys.length;
|
||||
|
||||
function updateProgress() {
|
||||
function updateProgress(): void {
|
||||
opts.progressCallback?.({
|
||||
stage: "load_keys",
|
||||
successes,
|
||||
@@ -3183,7 +3187,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
}
|
||||
}
|
||||
|
||||
private onMembership = (event: MatrixEvent, member: RoomMember, oldMembership?: string) => {
|
||||
private onMembership = (event: MatrixEvent, member: RoomMember, oldMembership?: string): void => {
|
||||
try {
|
||||
this.onRoomMembership(event, member, oldMembership);
|
||||
} catch (e) {
|
||||
@@ -3265,9 +3269,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Got room key withheld event from ${event.getSender()} (${content.sender_key}) `
|
||||
+ `for ${content.algorithm}/${content.room_id}/${content.session_id} `
|
||||
+ `with reason ${content.code} (${content.reason})`,
|
||||
`Got room key withheld event from ${event.getSender()} `
|
||||
+ `for ${content.algorithm} session ${content.sender_key}|${content.session_id} `
|
||||
+ `in room ${content.room_id} with code ${content.code} (${content.reason})`,
|
||||
);
|
||||
|
||||
const alg = this.getRoomDecryptor(content.room_id, content.algorithm);
|
||||
@@ -3336,7 +3340,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
if (!InRoomChannel.validateEvent(event, this.baseApis)) {
|
||||
return;
|
||||
}
|
||||
const createRequest = (event: MatrixEvent) => {
|
||||
const createRequest = (event: MatrixEvent): VerificationRequest => {
|
||||
const channel = new InRoomChannel(this.baseApis, event.getRoomId()!);
|
||||
return new VerificationRequest(
|
||||
channel, this.verificationMethods, this.baseApis);
|
||||
@@ -3357,7 +3361,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
eventIdListener = resolve;
|
||||
statusListener = () => {
|
||||
statusListener = (): void => {
|
||||
if (event.status == EventStatus.CANCELLED) {
|
||||
reject(new Error("Event status set to CANCELLED."));
|
||||
}
|
||||
@@ -3416,7 +3420,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
// retry decryption for all events sent by the sender_key. This will
|
||||
// update the events to show a message indicating that the olm session was
|
||||
// wedged.
|
||||
const retryDecryption = () => {
|
||||
const retryDecryption = (): void => {
|
||||
const roomDecryptors = this.getRoomDecryptors(olmlib.MEGOLM_ALGORITHM);
|
||||
for (const decryptor of roomDecryptors) {
|
||||
decryptor.retryDecryptionFromSender(deviceKey);
|
||||
@@ -3688,7 +3692,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
return;
|
||||
}
|
||||
|
||||
req.share = () => {
|
||||
req.share = (): void => {
|
||||
decryptor.shareKeysWithDevice(req);
|
||||
};
|
||||
|
||||
@@ -3859,14 +3863,14 @@ export class IncomingRoomKeyRequest {
|
||||
public readonly requestBody: IRoomKeyRequestBody;
|
||||
public share: () => void;
|
||||
|
||||
constructor(event: MatrixEvent) {
|
||||
public constructor(event: MatrixEvent) {
|
||||
const content = event.getContent();
|
||||
|
||||
this.userId = event.getSender()!;
|
||||
this.deviceId = content.requesting_device_id;
|
||||
this.requestId = content.request_id;
|
||||
this.requestBody = content.body || {};
|
||||
this.share = () => {
|
||||
this.share = (): void => {
|
||||
throw new Error("don't know how to share keys for this request yet");
|
||||
};
|
||||
}
|
||||
@@ -3884,7 +3888,7 @@ class IncomingRoomKeyRequestCancellation {
|
||||
public readonly deviceId: string;
|
||||
public readonly requestId: string;
|
||||
|
||||
constructor(event: MatrixEvent) {
|
||||
public constructor(event: MatrixEvent) {
|
||||
const content = event.getContent();
|
||||
|
||||
this.userId = event.getSender()!;
|
||||
|
||||
+12
-9
@@ -82,18 +82,22 @@ export async function encryptMessageForDevice(
|
||||
recipientUserId: string,
|
||||
recipientDevice: DeviceInfo,
|
||||
payloadFields: Record<string, any>,
|
||||
) {
|
||||
): Promise<void> {
|
||||
const deviceKey = recipientDevice.getIdentityKey();
|
||||
const sessionId = await olmDevice.getSessionIdForDevice(deviceKey);
|
||||
if (sessionId === null) {
|
||||
// If we don't have a session for a device then
|
||||
// we can't encrypt a message for it.
|
||||
logger.log(
|
||||
`[olmlib.encryptMessageForDevice] Unable to find Olm session for device ` +
|
||||
`${recipientUserId}:${recipientDevice.deviceId}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log(
|
||||
"Using sessionid " + sessionId + " for device " +
|
||||
recipientUserId + ":" + recipientDevice.deviceId,
|
||||
`[olmlib.encryptMessageForDevice] Using Olm session ${sessionId} for device ` +
|
||||
`${recipientUserId}:${recipientDevice.deviceId}`,
|
||||
);
|
||||
|
||||
const payload = {
|
||||
@@ -169,7 +173,7 @@ export async function getExistingOlmSessions(
|
||||
for (const deviceInfo of devices) {
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
const key = deviceInfo.getIdentityKey();
|
||||
promises.push((async () => {
|
||||
promises.push((async (): Promise<void> => {
|
||||
const sessionId = await olmDevice.getSessionIdForDevice(
|
||||
key, true,
|
||||
);
|
||||
@@ -252,7 +256,7 @@ export async function ensureOlmSessionsForDevices(
|
||||
// conditions. If we find that we already have a session, then
|
||||
// we'll resolve
|
||||
olmDevice.sessionsInProgress[key] = new Promise(resolve => {
|
||||
resolveSession[key] = (v: any) => {
|
||||
resolveSession[key] = (v: any): void => {
|
||||
delete olmDevice.sessionsInProgress[key];
|
||||
resolve(v);
|
||||
};
|
||||
@@ -335,8 +339,7 @@ export async function ensureOlmSessionsForDevices(
|
||||
const promises: Promise<void>[] = [];
|
||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||
const userRes = otkResult[userId] || {};
|
||||
for (let j = 0; j < devices.length; j++) {
|
||||
const deviceInfo = devices[j];
|
||||
for (const deviceInfo of devices) {
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
const key = deviceInfo.getIdentityKey();
|
||||
|
||||
@@ -460,7 +463,7 @@ export async function verifySignature(
|
||||
signingUserId: string,
|
||||
signingDeviceId: string,
|
||||
signingKey: string,
|
||||
) {
|
||||
): Promise<void> {
|
||||
const signKeyId = "ed25519:" + signingDeviceId;
|
||||
const signatures = obj.signatures || {};
|
||||
const userSigs = signatures[signingUserId] || {};
|
||||
@@ -524,7 +527,7 @@ export function pkSign(obj: IObject, key: PkSigning, userId: string, pubKey: str
|
||||
* @param {string} pubKey The public key to use to verify
|
||||
* @param {string} userId The user ID who signed the object
|
||||
*/
|
||||
export function pkVerify(obj: IObject, pubKey: string, userId: string) {
|
||||
export function pkVerify(obj: IObject, pubKey: string, userId: string): void {
|
||||
const keyId = "ed25519:" + pubKey;
|
||||
if (!(obj.signatures && obj.signatures[userId] && obj.signatures[userId][keyId])) {
|
||||
throw new Error("No signature");
|
||||
|
||||
@@ -48,11 +48,11 @@ export class Backend implements CryptoStore {
|
||||
/**
|
||||
* @param {IDBDatabase} db
|
||||
*/
|
||||
constructor(private db: IDBDatabase) {
|
||||
public constructor(private db: IDBDatabase) {
|
||||
// make sure we close the db on `onversionchange` - otherwise
|
||||
// attempts to delete the database will block (and subsequent
|
||||
// attempts to re-create it will also block).
|
||||
db.onversionchange = () => {
|
||||
db.onversionchange = (): void => {
|
||||
logger.log(`versionchange for indexeddb ${this.db.name}: closing`);
|
||||
db.close();
|
||||
};
|
||||
@@ -103,7 +103,7 @@ export class Backend implements CryptoStore {
|
||||
`enqueueing key request for ${requestBody.room_id} / ` +
|
||||
requestBody.session_id,
|
||||
);
|
||||
txn.oncomplete = () => {resolve(request);};
|
||||
txn.oncomplete = (): void => {resolve(request);};
|
||||
const store = txn.objectStore("outgoingRoomKeyRequests");
|
||||
store.add(request);
|
||||
});
|
||||
@@ -157,7 +157,7 @@ export class Backend implements CryptoStore {
|
||||
requestBody.session_id,
|
||||
]);
|
||||
|
||||
cursorReq.onsuccess = () => {
|
||||
cursorReq.onsuccess = (): void => {
|
||||
const cursor = cursorReq.result;
|
||||
if (!cursor) {
|
||||
// no match found
|
||||
@@ -201,7 +201,7 @@ export class Backend implements CryptoStore {
|
||||
let stateIndex = 0;
|
||||
let result: OutgoingRoomKeyRequest;
|
||||
|
||||
function onsuccess(this: IDBRequest<IDBCursorWithValue>) {
|
||||
function onsuccess(this: IDBRequest<IDBCursorWithValue | null>): void {
|
||||
const cursor = this.result;
|
||||
if (cursor) {
|
||||
// got a match
|
||||
@@ -243,8 +243,8 @@ export class Backend implements CryptoStore {
|
||||
const index = store.index("state");
|
||||
const request = index.getAll(wantedState);
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = (): void => resolve(request.result);
|
||||
request.onerror = (): void => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -256,7 +256,7 @@ export class Backend implements CryptoStore {
|
||||
let stateIndex = 0;
|
||||
const results: OutgoingRoomKeyRequest[] = [];
|
||||
|
||||
function onsuccess(this: IDBRequest<IDBCursorWithValue>) {
|
||||
function onsuccess(this: IDBRequest<IDBCursorWithValue | null>): void {
|
||||
const cursor = this.result;
|
||||
if (cursor) {
|
||||
const keyReq = cursor.value;
|
||||
@@ -309,7 +309,7 @@ export class Backend implements CryptoStore {
|
||||
): Promise<OutgoingRoomKeyRequest | null> {
|
||||
let result: OutgoingRoomKeyRequest | null = null;
|
||||
|
||||
function onsuccess(this: IDBRequest<IDBCursorWithValue>) {
|
||||
function onsuccess(this: IDBRequest<IDBCursorWithValue | null>): void {
|
||||
const cursor = this.result;
|
||||
if (!cursor) {
|
||||
return;
|
||||
@@ -348,7 +348,7 @@ export class Backend implements CryptoStore {
|
||||
): Promise<OutgoingRoomKeyRequest | null> {
|
||||
const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
|
||||
const cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId);
|
||||
cursorReq.onsuccess = () => {
|
||||
cursorReq.onsuccess = (): void => {
|
||||
const cursor = cursorReq.result;
|
||||
if (!cursor) {
|
||||
return;
|
||||
@@ -371,7 +371,7 @@ export class Backend implements CryptoStore {
|
||||
public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void): void {
|
||||
const objectStore = txn.objectStore("account");
|
||||
const getReq = objectStore.get("-");
|
||||
getReq.onsuccess = function() {
|
||||
getReq.onsuccess = function(): void {
|
||||
try {
|
||||
func(getReq.result || null);
|
||||
} catch (e) {
|
||||
@@ -391,7 +391,7 @@ export class Backend implements CryptoStore {
|
||||
): void {
|
||||
const objectStore = txn.objectStore("account");
|
||||
const getReq = objectStore.get("crossSigningKeys");
|
||||
getReq.onsuccess = function() {
|
||||
getReq.onsuccess = function(): void {
|
||||
try {
|
||||
func(getReq.result || null);
|
||||
} catch (e) {
|
||||
@@ -407,7 +407,7 @@ export class Backend implements CryptoStore {
|
||||
): void {
|
||||
const objectStore = txn.objectStore("account");
|
||||
const getReq = objectStore.get(`ssss_cache:${type}`);
|
||||
getReq.onsuccess = function() {
|
||||
getReq.onsuccess = function(): void {
|
||||
try {
|
||||
func(getReq.result || null);
|
||||
} catch (e) {
|
||||
@@ -435,7 +435,7 @@ export class Backend implements CryptoStore {
|
||||
public countEndToEndSessions(txn: IDBTransaction, func: (count: number) => void): void {
|
||||
const objectStore = txn.objectStore("sessions");
|
||||
const countReq = objectStore.count();
|
||||
countReq.onsuccess = function() {
|
||||
countReq.onsuccess = function(): void {
|
||||
try {
|
||||
func(countReq.result);
|
||||
} catch (e) {
|
||||
@@ -453,7 +453,7 @@ export class Backend implements CryptoStore {
|
||||
const idx = objectStore.index("deviceKey");
|
||||
const getReq = idx.openCursor(deviceKey);
|
||||
const results: Parameters<Parameters<Backend["getEndToEndSessions"]>[2]>[0] = {};
|
||||
getReq.onsuccess = function() {
|
||||
getReq.onsuccess = function(): void {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
results[cursor.value.sessionId] = {
|
||||
@@ -479,7 +479,7 @@ export class Backend implements CryptoStore {
|
||||
): void {
|
||||
const objectStore = txn.objectStore("sessions");
|
||||
const getReq = objectStore.get([deviceKey, sessionId]);
|
||||
getReq.onsuccess = function() {
|
||||
getReq.onsuccess = function(): void {
|
||||
try {
|
||||
if (getReq.result) {
|
||||
func({
|
||||
@@ -498,7 +498,7 @@ export class Backend implements CryptoStore {
|
||||
public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo | null) => void): void {
|
||||
const objectStore = txn.objectStore("sessions");
|
||||
const getReq = objectStore.openCursor();
|
||||
getReq.onsuccess = function() {
|
||||
getReq.onsuccess = function(): void {
|
||||
try {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
@@ -546,7 +546,7 @@ export class Backend implements CryptoStore {
|
||||
const objectStore = txn.objectStore("session_problems");
|
||||
const index = objectStore.index("deviceKey");
|
||||
const req = index.getAll(deviceKey);
|
||||
req.onsuccess = () => {
|
||||
req.onsuccess = (): void => {
|
||||
const problems = req.result;
|
||||
if (!problems.length) {
|
||||
result = null;
|
||||
@@ -583,7 +583,7 @@ export class Backend implements CryptoStore {
|
||||
return new Promise<void>((resolve) => {
|
||||
const { userId, deviceInfo } = device;
|
||||
const getReq = objectStore.get([userId, deviceInfo.deviceId]);
|
||||
getReq.onsuccess = function() {
|
||||
getReq.onsuccess = function(): void {
|
||||
if (!getReq.result) {
|
||||
objectStore.put({ userId, deviceId: deviceInfo.deviceId });
|
||||
ret.push(device);
|
||||
@@ -608,7 +608,7 @@ export class Backend implements CryptoStore {
|
||||
let withheld: IWithheld | null | boolean = false;
|
||||
const objectStore = txn.objectStore("inbound_group_sessions");
|
||||
const getReq = objectStore.get([senderCurve25519Key, sessionId]);
|
||||
getReq.onsuccess = function() {
|
||||
getReq.onsuccess = function(): void {
|
||||
try {
|
||||
if (getReq.result) {
|
||||
session = getReq.result.session;
|
||||
@@ -625,7 +625,7 @@ export class Backend implements CryptoStore {
|
||||
|
||||
const withheldObjectStore = txn.objectStore("inbound_group_sessions_withheld");
|
||||
const withheldGetReq = withheldObjectStore.get([senderCurve25519Key, sessionId]);
|
||||
withheldGetReq.onsuccess = function() {
|
||||
withheldGetReq.onsuccess = function(): void {
|
||||
try {
|
||||
if (withheldGetReq.result) {
|
||||
withheld = withheldGetReq.result.session;
|
||||
@@ -644,7 +644,7 @@ export class Backend implements CryptoStore {
|
||||
public getAllEndToEndInboundGroupSessions(txn: IDBTransaction, func: (session: ISession | null) => void): void {
|
||||
const objectStore = txn.objectStore("inbound_group_sessions");
|
||||
const getReq = objectStore.openCursor();
|
||||
getReq.onsuccess = function() {
|
||||
getReq.onsuccess = function(): void {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
try {
|
||||
@@ -677,7 +677,7 @@ export class Backend implements CryptoStore {
|
||||
const addReq = objectStore.add({
|
||||
senderCurve25519Key, sessionId, session: sessionData,
|
||||
});
|
||||
addReq.onerror = (ev) => {
|
||||
addReq.onerror = (ev): void => {
|
||||
if (addReq.error?.name === 'ConstraintError') {
|
||||
// This stops the error from triggering the txn's onerror
|
||||
ev.stopPropagation();
|
||||
@@ -722,7 +722,7 @@ export class Backend implements CryptoStore {
|
||||
public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void {
|
||||
const objectStore = txn.objectStore("device_data");
|
||||
const getReq = objectStore.get("-");
|
||||
getReq.onsuccess = function() {
|
||||
getReq.onsuccess = function(): void {
|
||||
try {
|
||||
func(getReq.result || null);
|
||||
} catch (e) {
|
||||
@@ -745,7 +745,7 @@ export class Backend implements CryptoStore {
|
||||
const rooms: Parameters<Parameters<Backend["getEndToEndRooms"]>[1]>[0] = {};
|
||||
const objectStore = txn.objectStore("rooms");
|
||||
const getReq = objectStore.openCursor();
|
||||
getReq.onsuccess = function() {
|
||||
getReq.onsuccess = function(): void {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
rooms[cursor.key as string] = cursor.value;
|
||||
@@ -771,17 +771,17 @@ export class Backend implements CryptoStore {
|
||||
"readonly",
|
||||
);
|
||||
txn.onerror = reject;
|
||||
txn.oncomplete = function() {
|
||||
txn.oncomplete = function(): void {
|
||||
resolve(sessions);
|
||||
};
|
||||
const objectStore = txn.objectStore("sessions_needing_backup");
|
||||
const sessionStore = txn.objectStore("inbound_group_sessions");
|
||||
const getReq = objectStore.openCursor();
|
||||
getReq.onsuccess = function() {
|
||||
getReq.onsuccess = function(): void {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
const sessionGetReq = sessionStore.get(cursor.key);
|
||||
sessionGetReq.onsuccess = function() {
|
||||
sessionGetReq.onsuccess = function(): void {
|
||||
sessions.push({
|
||||
senderKey: sessionGetReq.result.senderCurve25519Key,
|
||||
sessionId: sessionGetReq.result.sessionId,
|
||||
@@ -804,7 +804,7 @@ export class Backend implements CryptoStore {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = objectStore.count();
|
||||
req.onerror = reject;
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
req.onsuccess = (): void => resolve(req.result);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -852,7 +852,7 @@ export class Backend implements CryptoStore {
|
||||
}
|
||||
const objectStore = txn.objectStore("shared_history_inbound_group_sessions");
|
||||
const req = objectStore.get([roomId]);
|
||||
req.onsuccess = () => {
|
||||
req.onsuccess = (): void => {
|
||||
const { sessions } = req.result || { sessions: [] };
|
||||
sessions.push([senderKey, sessionId]);
|
||||
objectStore.put({ roomId, sessions });
|
||||
@@ -871,7 +871,7 @@ export class Backend implements CryptoStore {
|
||||
const objectStore = txn.objectStore("shared_history_inbound_group_sessions");
|
||||
const req = objectStore.get([roomId]);
|
||||
return new Promise((resolve, reject) => {
|
||||
req.onsuccess = () => {
|
||||
req.onsuccess = (): void => {
|
||||
const { sessions } = req.result || { sessions: [] };
|
||||
resolve(sessions);
|
||||
};
|
||||
@@ -891,7 +891,7 @@ export class Backend implements CryptoStore {
|
||||
}
|
||||
const objectStore = txn.objectStore("parked_shared_history");
|
||||
const req = objectStore.get([roomId]);
|
||||
req.onsuccess = () => {
|
||||
req.onsuccess = (): void => {
|
||||
const { parked } = req.result || { parked: [] };
|
||||
parked.push(parkedData);
|
||||
objectStore.put({ roomId, parked });
|
||||
@@ -909,7 +909,7 @@ export class Backend implements CryptoStore {
|
||||
}
|
||||
const cursorReq = txn.objectStore("parked_shared_history").openCursor(roomId);
|
||||
return new Promise((resolve, reject) => {
|
||||
cursorReq.onsuccess = () => {
|
||||
cursorReq.onsuccess = (): void => {
|
||||
const cursor = cursorReq.result;
|
||||
if (!cursor) {
|
||||
resolve([]);
|
||||
@@ -957,32 +957,32 @@ export class Backend implements CryptoStore {
|
||||
|
||||
type DbMigration = (db: IDBDatabase) => void;
|
||||
const DB_MIGRATIONS: DbMigration[] = [
|
||||
(db) => { createDatabase(db); },
|
||||
(db) => { db.createObjectStore("account"); },
|
||||
(db) => {
|
||||
(db): void => { createDatabase(db); },
|
||||
(db): void => { db.createObjectStore("account"); },
|
||||
(db): void => {
|
||||
const sessionsStore = db.createObjectStore("sessions", {
|
||||
keyPath: ["deviceKey", "sessionId"],
|
||||
});
|
||||
sessionsStore.createIndex("deviceKey", "deviceKey");
|
||||
},
|
||||
(db) => {
|
||||
(db): void => {
|
||||
db.createObjectStore("inbound_group_sessions", {
|
||||
keyPath: ["senderCurve25519Key", "sessionId"],
|
||||
});
|
||||
},
|
||||
(db) => { db.createObjectStore("device_data"); },
|
||||
(db) => { db.createObjectStore("rooms"); },
|
||||
(db) => {
|
||||
(db): void => { db.createObjectStore("device_data"); },
|
||||
(db): void => { db.createObjectStore("rooms"); },
|
||||
(db): void => {
|
||||
db.createObjectStore("sessions_needing_backup", {
|
||||
keyPath: ["senderCurve25519Key", "sessionId"],
|
||||
});
|
||||
},
|
||||
(db) => {
|
||||
(db): void => {
|
||||
db.createObjectStore("inbound_group_sessions_withheld", {
|
||||
keyPath: ["senderCurve25519Key", "sessionId"],
|
||||
});
|
||||
},
|
||||
(db) => {
|
||||
(db): void => {
|
||||
const problemsStore = db.createObjectStore("session_problems", {
|
||||
keyPath: ["deviceKey", "time"],
|
||||
});
|
||||
@@ -992,12 +992,12 @@ const DB_MIGRATIONS: DbMigration[] = [
|
||||
keyPath: ["userId", "deviceId"],
|
||||
});
|
||||
},
|
||||
(db) => {
|
||||
(db): void => {
|
||||
db.createObjectStore("shared_history_inbound_group_sessions", {
|
||||
keyPath: ["roomId"],
|
||||
});
|
||||
},
|
||||
(db) => {
|
||||
(db): void => {
|
||||
db.createObjectStore("parked_shared_history", {
|
||||
keyPath: ["roomId"],
|
||||
});
|
||||
@@ -1037,7 +1037,7 @@ interface IWrappedIDBTransaction extends IDBTransaction {
|
||||
* Aborts a transaction with a given exception
|
||||
* The transaction promise will be rejected with this exception.
|
||||
*/
|
||||
function abortWithException(txn: IDBTransaction, e: Error) {
|
||||
function abortWithException(txn: IDBTransaction, e: Error): void {
|
||||
// We cheekily stick our exception onto the transaction object here
|
||||
// We could alternatively make the thing we pass back to the app
|
||||
// an object containing the transaction and exception.
|
||||
@@ -1052,13 +1052,13 @@ function abortWithException(txn: IDBTransaction, e: Error) {
|
||||
|
||||
function promiseifyTxn<T>(txn: IDBTransaction): Promise<T | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
txn.oncomplete = () => {
|
||||
txn.oncomplete = (): void => {
|
||||
if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
|
||||
reject((txn as IWrappedIDBTransaction)._mx_abortexception);
|
||||
}
|
||||
resolve(null);
|
||||
};
|
||||
txn.onerror = (event) => {
|
||||
txn.onerror = (event): void => {
|
||||
if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
|
||||
reject((txn as IWrappedIDBTransaction)._mx_abortexception);
|
||||
} else {
|
||||
@@ -1066,7 +1066,7 @@ function promiseifyTxn<T>(txn: IDBTransaction): Promise<T | null> {
|
||||
reject(txn.error);
|
||||
}
|
||||
};
|
||||
txn.onabort = (event) => {
|
||||
txn.onabort = (event): void => {
|
||||
if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
|
||||
reject((txn as IWrappedIDBTransaction)._mx_abortexception);
|
||||
} else {
|
||||
|
||||
@@ -73,7 +73,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
* @param {IDBFactory} indexedDB global indexedDB instance
|
||||
* @param {string} dbName name of db to connect to
|
||||
*/
|
||||
constructor(private readonly indexedDB: IDBFactory, private readonly dbName: string) {}
|
||||
public constructor(private readonly indexedDB: IDBFactory, private readonly dbName: string) {}
|
||||
|
||||
/**
|
||||
* Ensure the database exists and is up-to-date, or fall back to
|
||||
@@ -99,24 +99,24 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
|
||||
const req = this.indexedDB.open(this.dbName, IndexedDBCryptoStoreBackend.VERSION);
|
||||
|
||||
req.onupgradeneeded = (ev) => {
|
||||
req.onupgradeneeded = (ev): void => {
|
||||
const db = req.result;
|
||||
const oldVersion = ev.oldVersion;
|
||||
IndexedDBCryptoStoreBackend.upgradeDatabase(db, oldVersion);
|
||||
};
|
||||
|
||||
req.onblocked = () => {
|
||||
req.onblocked = (): void => {
|
||||
logger.log(
|
||||
`can't yet open IndexedDBCryptoStore because it is open elsewhere`,
|
||||
);
|
||||
};
|
||||
|
||||
req.onerror = (ev) => {
|
||||
req.onerror = (ev): void => {
|
||||
logger.log("Error connecting to indexeddb", ev);
|
||||
reject(req.error);
|
||||
};
|
||||
|
||||
req.onsuccess = () => {
|
||||
req.onsuccess = (): void => {
|
||||
const db = req.result;
|
||||
|
||||
logger.log(`connected to indexeddb ${this.dbName}`);
|
||||
@@ -179,18 +179,18 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
logger.log(`Removing indexeddb instance: ${this.dbName}`);
|
||||
const req = this.indexedDB.deleteDatabase(this.dbName);
|
||||
|
||||
req.onblocked = () => {
|
||||
req.onblocked = (): void => {
|
||||
logger.log(
|
||||
`can't yet delete IndexedDBCryptoStore because it is open elsewhere`,
|
||||
);
|
||||
};
|
||||
|
||||
req.onerror = (ev) => {
|
||||
req.onerror = (ev): void => {
|
||||
logger.log("Error deleting data from indexeddb", ev);
|
||||
reject(req.error);
|
||||
};
|
||||
|
||||
req.onsuccess = () => {
|
||||
req.onsuccess = (): void => {
|
||||
logger.log(`Removed indexeddb instance: ${this.dbName}`);
|
||||
resolve();
|
||||
};
|
||||
@@ -322,7 +322,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
* @param {function(string)} func Called with the account pickle
|
||||
*/
|
||||
public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void) {
|
||||
public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void): void {
|
||||
this.backend!.getAccount(txn, func);
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
return false;
|
||||
}
|
||||
|
||||
constructor(private readonly store: Storage) {
|
||||
public constructor(private readonly store: Storage) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
setJsonItem(this.store, key, problems);
|
||||
}
|
||||
|
||||
async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
|
||||
public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
|
||||
const key = keyEndToEndSessionProblems(deviceKey);
|
||||
const problems = getJsonItem<IProblem[]>(this.store, key) || [];
|
||||
if (!problems.length) {
|
||||
@@ -408,7 +408,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
setJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`, key);
|
||||
}
|
||||
|
||||
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T): Promise<T> {
|
||||
public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T): Promise<T> {
|
||||
return Promise.resolve(func(null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +279,7 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
|
||||
// Olm Account
|
||||
|
||||
public getAccount(txn: unknown, func: (accountPickle: string | null) => void) {
|
||||
public getAccount(txn: unknown, func: (accountPickle: string | null) => void): void {
|
||||
func(this.account);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ import { ListenerMap, TypedEventEmitter } from "../../models/typed-event-emitter
|
||||
const timeoutException = new Error("Verification timed out");
|
||||
|
||||
export class SwitchStartEventError extends Error {
|
||||
constructor(public readonly startEvent: MatrixEvent | null) {
|
||||
public constructor(public readonly startEvent: MatrixEvent | null) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -91,7 +91,7 @@ export class VerificationBase<
|
||||
* @param {object} [request] the key verification request object related to
|
||||
* this verification, if any
|
||||
*/
|
||||
constructor(
|
||||
public constructor(
|
||||
public readonly channel: IVerificationChannel,
|
||||
public readonly baseApis: MatrixClient,
|
||||
public readonly userId: string,
|
||||
@@ -286,12 +286,12 @@ export class VerificationBase<
|
||||
if (this.promise) return this.promise;
|
||||
|
||||
this.promise = new Promise((resolve, reject) => {
|
||||
this.resolve = (...args) => {
|
||||
this.resolve = (...args): void => {
|
||||
this._done = true;
|
||||
this.endTimer();
|
||||
resolve(...args);
|
||||
};
|
||||
this.reject = (e: Error | MatrixEvent) => {
|
||||
this.reject = (e: Error | MatrixEvent): void => {
|
||||
this._done = true;
|
||||
this.endTimer();
|
||||
reject(e);
|
||||
|
||||
@@ -154,7 +154,7 @@ interface IQrData {
|
||||
}
|
||||
|
||||
export class QRCodeData {
|
||||
constructor(
|
||||
public constructor(
|
||||
public readonly mode: Mode,
|
||||
private readonly sharedSecret: string,
|
||||
// only set when mode is MODE_VERIFY_OTHER_USER, master key of other party at time of generating QR code
|
||||
@@ -283,21 +283,21 @@ export class QRCodeData {
|
||||
private static generateBuffer(qrData: IQrData): Buffer {
|
||||
let buf = Buffer.alloc(0); // we'll concat our way through life
|
||||
|
||||
const appendByte = (b) => {
|
||||
const appendByte = (b): void => {
|
||||
const tmpBuf = Buffer.from([b]);
|
||||
buf = Buffer.concat([buf, tmpBuf]);
|
||||
};
|
||||
const appendInt = (i) => {
|
||||
const appendInt = (i): void => {
|
||||
const tmpBuf = Buffer.alloc(2);
|
||||
tmpBuf.writeInt16BE(i, 0);
|
||||
buf = Buffer.concat([buf, tmpBuf]);
|
||||
};
|
||||
const appendStr = (s, enc, withLengthPrefix = true) => {
|
||||
const appendStr = (s, enc, withLengthPrefix = true): void => {
|
||||
const tmpBuf = Buffer.from(s, enc);
|
||||
if (withLengthPrefix) appendInt(tmpBuf.byteLength);
|
||||
buf = Buffer.concat([buf, tmpBuf]);
|
||||
};
|
||||
const appendEncBase64 = (b64) => {
|
||||
const appendEncBase64 = (b64): void => {
|
||||
const b = decodeBase64(b64);
|
||||
const tmpBuf = Buffer.from(b);
|
||||
buf = Buffer.concat([buf, tmpBuf]);
|
||||
|
||||
@@ -170,10 +170,12 @@ const macMethods = {
|
||||
"hmac-sha256": "calculate_mac_long_kdf",
|
||||
};
|
||||
|
||||
function calculateMAC(olmSAS: OlmSAS, method: string) {
|
||||
return function(...args) {
|
||||
type Method = keyof typeof macMethods;
|
||||
|
||||
function calculateMAC(olmSAS: OlmSAS, method: Method) {
|
||||
return function(...args): string {
|
||||
const macFunction = olmSAS[macMethods[method]];
|
||||
const mac = macFunction.apply(olmSAS, args);
|
||||
const mac: string = macFunction.apply(olmSAS, args);
|
||||
logger.log("SAS calculateMAC:", method, args, mac);
|
||||
return mac;
|
||||
};
|
||||
@@ -208,7 +210,7 @@ const calculateKeyAgreement = {
|
||||
*/
|
||||
const KEY_AGREEMENT_LIST = ["curve25519-hkdf-sha256", "curve25519"];
|
||||
const HASHES_LIST = ["sha256"];
|
||||
const MAC_LIST = ["org.matrix.msc3783.hkdf-hmac-sha256", "hkdf-hmac-sha256", "hmac-sha256"];
|
||||
const MAC_LIST: Method[] = ["org.matrix.msc3783.hkdf-hmac-sha256", "hkdf-hmac-sha256", "hmac-sha256"];
|
||||
const SAS_LIST = Object.keys(sasGenerators);
|
||||
|
||||
const KEY_AGREEMENT_SET = new Set(KEY_AGREEMENT_LIST);
|
||||
@@ -300,13 +302,13 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
|
||||
keyAgreement: string,
|
||||
sasMethods: string[],
|
||||
olmSAS: OlmSAS,
|
||||
macMethod: string,
|
||||
macMethod: Method,
|
||||
): Promise<void> {
|
||||
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
|
||||
const verifySAS = new Promise<void>((resolve, reject) => {
|
||||
this.sasEvent = {
|
||||
sas: generateSas(sasBytes, sasMethods),
|
||||
confirm: async () => {
|
||||
confirm: async (): Promise<void> => {
|
||||
try {
|
||||
await this.sendMAC(olmSAS, macMethod);
|
||||
resolve();
|
||||
@@ -443,7 +445,7 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
|
||||
}
|
||||
}
|
||||
|
||||
private sendMAC(olmSAS: OlmSAS, method: string): Promise<void> {
|
||||
private sendMAC(olmSAS: OlmSAS, method: Method): Promise<void> {
|
||||
const mac = {};
|
||||
const keyList: string[] = [];
|
||||
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
|
||||
@@ -475,7 +477,7 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
|
||||
return this.send(EventType.KeyVerificationMac, { mac, keys });
|
||||
}
|
||||
|
||||
private async checkMAC(olmSAS: OlmSAS, content: IContent, method: string): Promise<void> {
|
||||
private async checkMAC(olmSAS: OlmSAS, content: IContent, method: Method): Promise<void> {
|
||||
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
|
||||
+ this.userId + this.deviceId
|
||||
+ this.baseApis.getUserId() + this.baseApis.deviceId
|
||||
|
||||
@@ -44,7 +44,7 @@ export class InRoomChannel implements IVerificationChannel {
|
||||
* @param {string} roomId id of the room where verification events should be posted in, should be a DM with the given user.
|
||||
* @param {string} userId id of user that the verification request is directed at, should be present in the room.
|
||||
*/
|
||||
constructor(
|
||||
public constructor(
|
||||
private readonly client: MatrixClient,
|
||||
public readonly roomId: string,
|
||||
public userId?: string,
|
||||
|
||||
@@ -42,7 +42,7 @@ export class ToDeviceChannel implements IVerificationChannel {
|
||||
public request?: VerificationRequest;
|
||||
|
||||
// userId and devices of user we're about to verify
|
||||
constructor(
|
||||
public constructor(
|
||||
private readonly client: MatrixClient,
|
||||
public readonly userId: string,
|
||||
private readonly devices: string[],
|
||||
|
||||
@@ -116,7 +116,7 @@ export class VerificationRequest<
|
||||
public _cancellingUserId?: string; // Used in tests only
|
||||
private _verifier?: VerificationBase<any, any>;
|
||||
|
||||
constructor(
|
||||
public constructor(
|
||||
public readonly channel: C,
|
||||
private readonly verificationMethods: Map<VerificationMethod, typeof VerificationBase>,
|
||||
private readonly client: MatrixClient,
|
||||
@@ -498,7 +498,7 @@ export class VerificationRequest<
|
||||
*/
|
||||
public waitFor(fn: (request: VerificationRequest) => boolean): Promise<VerificationRequest> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const check = () => {
|
||||
const check = (): boolean => {
|
||||
let handled = false;
|
||||
if (fn(this)) {
|
||||
resolve(this);
|
||||
@@ -539,7 +539,7 @@ export class VerificationRequest<
|
||||
|
||||
private calculatePhaseTransitions(): ITransition[] {
|
||||
const transitions: ITransition[] = [{ phase: PHASE_UNSENT }];
|
||||
const phase = () => transitions[transitions.length - 1].phase;
|
||||
const phase = (): Phase => transitions[transitions.length - 1].phase;
|
||||
|
||||
// always pass by .request first to be sure channel.userId has been set
|
||||
const hasRequestByThem = this.eventsByThem.has(REQUEST_TYPE);
|
||||
@@ -816,7 +816,7 @@ export class VerificationRequest<
|
||||
}
|
||||
}
|
||||
|
||||
private cancelOnTimeout = async () => {
|
||||
private cancelOnTimeout = async (): Promise<void> => {
|
||||
try {
|
||||
if (this.initiatedByMe) {
|
||||
await this.cancel({
|
||||
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
/*
|
||||
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 {
|
||||
WidgetApi,
|
||||
WidgetApiToWidgetAction,
|
||||
MatrixCapabilities,
|
||||
IWidgetApiRequest,
|
||||
IWidgetApiAcknowledgeResponseData,
|
||||
ISendEventToWidgetActionRequest,
|
||||
ISendToDeviceToWidgetActionRequest,
|
||||
ISendEventFromWidgetResponseData,
|
||||
} from "matrix-widget-api";
|
||||
|
||||
import { IEvent, IContent, EventStatus } from "./models/event";
|
||||
import { ISendEventResponse } from "./@types/requests";
|
||||
import { EventType } from "./@types/event";
|
||||
import { logger } from "./logger";
|
||||
import { MatrixClient, ClientEvent, IMatrixClientCreateOpts, IStartClientOpts } from "./client";
|
||||
import { SyncApi, SyncState } from "./sync";
|
||||
import { SlidingSyncSdk } from "./sliding-sync-sdk";
|
||||
import { MatrixEvent } from "./models/event";
|
||||
import { User } from "./models/user";
|
||||
import { Room } from "./models/room";
|
||||
import { ToDeviceBatch } from "./models/ToDeviceMessage";
|
||||
import { DeviceInfo } from "./crypto/deviceinfo";
|
||||
import { IOlmDevice } from "./crypto/algorithms/megolm";
|
||||
|
||||
interface IStateEventRequest {
|
||||
eventType: string;
|
||||
stateKey?: string;
|
||||
}
|
||||
|
||||
export interface ICapabilities {
|
||||
/**
|
||||
* Event types that this client expects to send.
|
||||
*/
|
||||
sendEvent?: string[];
|
||||
/**
|
||||
* Event types that this client expects to receive.
|
||||
*/
|
||||
receiveEvent?: string[];
|
||||
|
||||
/**
|
||||
* Message types that this client expects to send, or true for all message
|
||||
* types.
|
||||
*/
|
||||
sendMessage?: string[] | true;
|
||||
/**
|
||||
* Message types that this client expects to receive, or true for all
|
||||
* message types.
|
||||
*/
|
||||
receiveMessage?: string[] | true;
|
||||
|
||||
/**
|
||||
* Types of state events that this client expects to send.
|
||||
*/
|
||||
sendState?: IStateEventRequest[];
|
||||
/**
|
||||
* Types of state events that this client expects to receive.
|
||||
*/
|
||||
receiveState?: IStateEventRequest[];
|
||||
|
||||
/**
|
||||
* To-device event types that this client expects to send.
|
||||
*/
|
||||
sendToDevice?: string[];
|
||||
/**
|
||||
* To-device event types that this client expects to receive.
|
||||
*/
|
||||
receiveToDevice?: string[];
|
||||
|
||||
/**
|
||||
* Whether this client needs access to TURN servers.
|
||||
* @default false
|
||||
*/
|
||||
turnServers?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A MatrixClient that routes its requests through the widget API instead of the
|
||||
* real CS API.
|
||||
* @experimental This class is considered unstable!
|
||||
*/
|
||||
export class RoomWidgetClient extends MatrixClient {
|
||||
private room?: Room;
|
||||
private widgetApiReady = new Promise<void>(resolve => this.widgetApi.once("ready", resolve));
|
||||
private lifecycle?: AbortController;
|
||||
private syncState: SyncState | null = null;
|
||||
|
||||
public constructor(
|
||||
private readonly widgetApi: WidgetApi,
|
||||
private readonly capabilities: ICapabilities,
|
||||
private readonly roomId: string,
|
||||
opts: IMatrixClientCreateOpts,
|
||||
) {
|
||||
super(opts);
|
||||
|
||||
// Request capabilities for the functionality this client needs to support
|
||||
if (
|
||||
capabilities.sendEvent?.length
|
||||
|| capabilities.receiveEvent?.length
|
||||
|| capabilities.sendMessage === true
|
||||
|| (Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length)
|
||||
|| capabilities.receiveMessage === true
|
||||
|| (Array.isArray(capabilities.receiveMessage) && capabilities.receiveMessage.length)
|
||||
|| capabilities.sendState?.length
|
||||
|| capabilities.receiveState?.length
|
||||
) {
|
||||
widgetApi.requestCapabilityForRoomTimeline(roomId);
|
||||
}
|
||||
capabilities.sendEvent?.forEach(eventType =>
|
||||
widgetApi.requestCapabilityToSendEvent(eventType),
|
||||
);
|
||||
capabilities.receiveEvent?.forEach(eventType =>
|
||||
widgetApi.requestCapabilityToReceiveEvent(eventType),
|
||||
);
|
||||
if (capabilities.sendMessage === true) {
|
||||
widgetApi.requestCapabilityToSendMessage();
|
||||
} else if (Array.isArray(capabilities.sendMessage)) {
|
||||
capabilities.sendMessage.forEach(msgType =>
|
||||
widgetApi.requestCapabilityToSendMessage(msgType),
|
||||
);
|
||||
}
|
||||
if (capabilities.receiveMessage === true) {
|
||||
widgetApi.requestCapabilityToReceiveMessage();
|
||||
} else if (Array.isArray(capabilities.receiveMessage)) {
|
||||
capabilities.receiveMessage.forEach(msgType =>
|
||||
widgetApi.requestCapabilityToReceiveMessage(msgType),
|
||||
);
|
||||
}
|
||||
capabilities.sendState?.forEach(({ eventType, stateKey }) =>
|
||||
widgetApi.requestCapabilityToSendState(eventType, stateKey),
|
||||
);
|
||||
capabilities.receiveState?.forEach(({ eventType, stateKey }) =>
|
||||
widgetApi.requestCapabilityToReceiveState(eventType, stateKey),
|
||||
);
|
||||
capabilities.sendToDevice?.forEach(eventType =>
|
||||
widgetApi.requestCapabilityToSendToDevice(eventType),
|
||||
);
|
||||
capabilities.receiveToDevice?.forEach(eventType =>
|
||||
widgetApi.requestCapabilityToReceiveToDevice(eventType),
|
||||
);
|
||||
if (capabilities.turnServers) {
|
||||
widgetApi.requestCapability(MatrixCapabilities.MSC3846TurnServers);
|
||||
}
|
||||
|
||||
widgetApi.on(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
|
||||
widgetApi.on(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
|
||||
|
||||
// Open communication with the host
|
||||
widgetApi.start();
|
||||
}
|
||||
|
||||
public async startClient(opts: IStartClientOpts = {}): Promise<void> {
|
||||
this.lifecycle = new AbortController();
|
||||
|
||||
// Create our own user object artificially (instead of waiting for sync)
|
||||
// so it's always available, even if the user is not in any rooms etc.
|
||||
const userId = this.getUserId();
|
||||
if (userId) {
|
||||
this.store.storeUser(new User(userId));
|
||||
}
|
||||
|
||||
// Even though we have no access token and cannot sync, the sync class
|
||||
// still has some valuable helper methods that we make use of, so we
|
||||
// instantiate it anyways
|
||||
if (opts.slidingSync) {
|
||||
this.syncApi = new SlidingSyncSdk(opts.slidingSync, this, opts);
|
||||
} else {
|
||||
this.syncApi = new SyncApi(this, opts);
|
||||
}
|
||||
|
||||
this.room = this.syncApi.createRoom(this.roomId);
|
||||
this.store.storeRoom(this.room);
|
||||
|
||||
await this.widgetApiReady;
|
||||
|
||||
// Backfill the requested events
|
||||
// We only get the most recent event for every type + state key combo,
|
||||
// so it doesn't really matter what order we inject them in
|
||||
await Promise.all(
|
||||
this.capabilities.receiveState?.map(async ({ eventType, stateKey }) => {
|
||||
const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey, [this.roomId]);
|
||||
const events = rawEvents.map(rawEvent => new MatrixEvent(rawEvent as Partial<IEvent>));
|
||||
|
||||
await this.syncApi!.injectRoomEvents(this.room!, [], events);
|
||||
events.forEach(event => {
|
||||
this.emit(ClientEvent.Event, event);
|
||||
logger.info(`Backfilled event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
|
||||
});
|
||||
}) ?? [],
|
||||
);
|
||||
this.setSyncState(SyncState.Syncing);
|
||||
logger.info("Finished backfilling events");
|
||||
|
||||
// Watch for TURN servers, if requested
|
||||
if (this.capabilities.turnServers) this.watchTurnServers();
|
||||
}
|
||||
|
||||
public stopClient(): void {
|
||||
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
|
||||
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
|
||||
|
||||
super.stopClient();
|
||||
this.lifecycle!.abort(); // Signal to other async tasks that the client has stopped
|
||||
}
|
||||
|
||||
public async joinRoom(roomIdOrAlias: string): Promise<Room> {
|
||||
if (roomIdOrAlias === this.roomId) return this.room!;
|
||||
throw new Error(`Unknown room: ${roomIdOrAlias}`);
|
||||
}
|
||||
|
||||
protected async encryptAndSendEvent(room: Room, event: MatrixEvent): Promise<ISendEventResponse> {
|
||||
let response: ISendEventFromWidgetResponseData;
|
||||
try {
|
||||
response = await this.widgetApi.sendRoomEvent(event.getType(), event.getContent(), room.roomId);
|
||||
} catch (e) {
|
||||
this.updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
|
||||
throw e;
|
||||
}
|
||||
|
||||
room.updatePendingEvent(event, EventStatus.SENT, response.event_id);
|
||||
return { event_id: response.event_id };
|
||||
}
|
||||
|
||||
public async sendStateEvent(
|
||||
roomId: string,
|
||||
eventType: string,
|
||||
content: any,
|
||||
stateKey = "",
|
||||
): Promise<ISendEventResponse> {
|
||||
return await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId);
|
||||
}
|
||||
|
||||
public async sendToDevice(
|
||||
eventType: string,
|
||||
contentMap: { [userId: string]: { [deviceId: string]: Record<string, any> } },
|
||||
): Promise<{}> {
|
||||
await this.widgetApi.sendToDevice(eventType, false, contentMap);
|
||||
return {};
|
||||
}
|
||||
|
||||
public async queueToDevice({ eventType, batch }: ToDeviceBatch): Promise<void> {
|
||||
const contentMap: { [userId: string]: { [deviceId: string]: object } } = {};
|
||||
for (const { userId, deviceId, payload } of batch) {
|
||||
if (!contentMap[userId]) contentMap[userId] = {};
|
||||
contentMap[userId][deviceId] = payload;
|
||||
}
|
||||
|
||||
await this.widgetApi.sendToDevice(eventType, false, contentMap);
|
||||
}
|
||||
|
||||
public async encryptAndSendToDevices(
|
||||
userDeviceInfoArr: IOlmDevice<DeviceInfo>[],
|
||||
payload: object,
|
||||
): Promise<void> {
|
||||
const contentMap: { [userId: string]: { [deviceId: string]: object } } = {};
|
||||
for (const { userId, deviceInfo: { deviceId } } of userDeviceInfoArr) {
|
||||
if (!contentMap[userId]) contentMap[userId] = {};
|
||||
contentMap[userId][deviceId] = payload;
|
||||
}
|
||||
|
||||
await this.widgetApi.sendToDevice((payload as { type: string }).type, true, contentMap);
|
||||
}
|
||||
|
||||
// Overridden since we get TURN servers automatically over the widget API,
|
||||
// and this method would otherwise complain about missing an access token
|
||||
public async checkTurnServers(): Promise<boolean> {
|
||||
return this.turnServers.length > 0;
|
||||
}
|
||||
|
||||
// Overridden since we 'sync' manually without the sync API
|
||||
public getSyncState(): SyncState | null {
|
||||
return this.syncState;
|
||||
}
|
||||
|
||||
private setSyncState(state: SyncState): void {
|
||||
const oldState = this.syncState;
|
||||
this.syncState = state;
|
||||
this.emit(ClientEvent.Sync, state, oldState);
|
||||
}
|
||||
|
||||
private async ack(ev: CustomEvent<IWidgetApiRequest>): Promise<void> {
|
||||
await this.widgetApi.transport.reply<IWidgetApiAcknowledgeResponseData>(ev.detail, {});
|
||||
}
|
||||
|
||||
private onEvent = async (ev: CustomEvent<ISendEventToWidgetActionRequest>): Promise<void> => {
|
||||
ev.preventDefault();
|
||||
|
||||
// Verify the room ID matches, since it's possible for the client to
|
||||
// send us events from other rooms if this widget is always on screen
|
||||
if (ev.detail.data.room_id === this.roomId) {
|
||||
const event = new MatrixEvent(ev.detail.data as Partial<IEvent>);
|
||||
await this.syncApi!.injectRoomEvents(this.room!, [], [event]);
|
||||
this.emit(ClientEvent.Event, event);
|
||||
this.setSyncState(SyncState.Syncing);
|
||||
logger.info(`Received event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
|
||||
} else {
|
||||
const { event_id: eventId, room_id: roomId } = ev.detail.data;
|
||||
logger.info(`Received event ${eventId} for a different room ${roomId}; discarding`);
|
||||
}
|
||||
|
||||
await this.ack(ev);
|
||||
};
|
||||
|
||||
private onToDevice = async (ev: CustomEvent<ISendToDeviceToWidgetActionRequest>): Promise<void> => {
|
||||
ev.preventDefault();
|
||||
|
||||
const event = new MatrixEvent({
|
||||
type: ev.detail.data.type,
|
||||
sender: ev.detail.data.sender,
|
||||
content: ev.detail.data.content as IContent,
|
||||
});
|
||||
// Mark the event as encrypted if it was, using fake contents and keys since those are unknown to us
|
||||
if (ev.detail.data.encrypted) event.makeEncrypted(EventType.RoomMessageEncrypted, {}, "", "");
|
||||
|
||||
this.emit(ClientEvent.ToDeviceEvent, event);
|
||||
this.setSyncState(SyncState.Syncing);
|
||||
await this.ack(ev);
|
||||
};
|
||||
|
||||
private async watchTurnServers(): Promise<void> {
|
||||
const servers = this.widgetApi.getTurnServers();
|
||||
const onClientStopped = (): void => {
|
||||
servers.return(undefined);
|
||||
};
|
||||
this.lifecycle!.signal.addEventListener("abort", onClientStopped);
|
||||
|
||||
try {
|
||||
for await (const server of servers) {
|
||||
this.turnServers = [{
|
||||
urls: server.uris,
|
||||
username: server.username,
|
||||
credential: server.password,
|
||||
}];
|
||||
this.emit(ClientEvent.TurnServers, this.turnServers);
|
||||
logger.log(`Received TURN server: ${server.uris}`);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn("Error watching TURN servers", e);
|
||||
} finally {
|
||||
this.lifecycle!.signal.removeEventListener("abort", onClientStopped);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -29,7 +29,7 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
||||
let preventReEmit = Boolean(options.preventReEmit);
|
||||
const decrypt = options.decrypt !== false;
|
||||
|
||||
function mapper(plainOldJsObject: Partial<IEvent>) {
|
||||
function mapper(plainOldJsObject: Partial<IEvent>): MatrixEvent {
|
||||
if (options.toDevice) {
|
||||
delete plainOldJsObject.room_id;
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export interface IFilterComponent {
|
||||
* @param {Object} filterJson the definition of this filter JSON, e.g. { 'contains_url': true }
|
||||
*/
|
||||
export class FilterComponent {
|
||||
constructor(private filterJson: IFilterComponent, public readonly userId?: string | undefined | null) {}
|
||||
public constructor(private filterJson: IFilterComponent, public readonly userId?: string | undefined | null) {}
|
||||
|
||||
/**
|
||||
* Checks with the filter component matches the given event
|
||||
@@ -150,8 +150,7 @@ export class FilterComponent {
|
||||
},
|
||||
};
|
||||
|
||||
for (let n = 0; n < Object.keys(literalKeys).length; n++) {
|
||||
const name = Object.keys(literalKeys)[n];
|
||||
for (const name in literalKeys) {
|
||||
const matchFunc = literalKeys[name];
|
||||
const notName = "not_" + name;
|
||||
const disallowedValues: string[] = this.filterJson[notName];
|
||||
|
||||
+6
-6
@@ -31,7 +31,7 @@ import { MatrixEvent } from "./models/event";
|
||||
* @param {string} keyNesting
|
||||
* @param {*} val
|
||||
*/
|
||||
function setProp(obj: object, keyNesting: string, val: any) {
|
||||
function setProp(obj: object, keyNesting: string, val: any): void {
|
||||
const nestedKeys = keyNesting.split(".");
|
||||
let currentObj = obj;
|
||||
for (let i = 0; i < (nestedKeys.length - 1); i++) {
|
||||
@@ -88,7 +88,7 @@ interface IRoomFilter {
|
||||
* @prop {?string} filterId The filter ID
|
||||
*/
|
||||
export class Filter {
|
||||
static LAZY_LOADING_MESSAGES_FILTER = {
|
||||
public static LAZY_LOADING_MESSAGES_FILTER = {
|
||||
lazy_load_members: true,
|
||||
};
|
||||
|
||||
@@ -110,7 +110,7 @@ export class Filter {
|
||||
private roomFilter?: FilterComponent;
|
||||
private roomTimelineFilter?: FilterComponent;
|
||||
|
||||
constructor(public readonly userId: string | undefined | null, public filterId?: string) {}
|
||||
public constructor(public readonly userId: string | undefined | null, public filterId?: string) {}
|
||||
|
||||
/**
|
||||
* Get the ID of this filter on your homeserver (if known)
|
||||
@@ -132,7 +132,7 @@ export class Filter {
|
||||
* Set the JSON body of the filter
|
||||
* @param {Object} definition The filter definition
|
||||
*/
|
||||
public setDefinition(definition: IFilterDefinition) {
|
||||
public setDefinition(definition: IFilterDefinition): void {
|
||||
this.definition = definition;
|
||||
|
||||
// This is all ported from synapse's FilterCollection()
|
||||
@@ -225,7 +225,7 @@ export class Filter {
|
||||
* Set the max number of events to return for each room's timeline.
|
||||
* @param {Number} limit The max number of events to return for each room.
|
||||
*/
|
||||
public setTimelineLimit(limit: number) {
|
||||
public setTimelineLimit(limit: number): void {
|
||||
setProp(this.definition, "room.timeline.limit", limit);
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ export class Filter {
|
||||
* @param {boolean} includeLeave True to make rooms the user has left appear
|
||||
* in responses.
|
||||
*/
|
||||
public setIncludeLeaveRooms(includeLeave: boolean) {
|
||||
public setIncludeLeaveRooms(includeLeave: boolean): void {
|
||||
setProp(this.definition, "room.include_leave", includeLeave);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ interface IErrorJson extends Partial<IUsageLimit> {
|
||||
* @param {number} httpStatus The HTTP response status code.
|
||||
*/
|
||||
export class HTTPError extends Error {
|
||||
constructor(msg: string, public readonly httpStatus?: number) {
|
||||
public constructor(msg: string, public readonly httpStatus?: number) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
@@ -49,9 +49,9 @@ export class HTTPError extends Error {
|
||||
*/
|
||||
export class MatrixError extends HTTPError {
|
||||
public readonly errcode?: string;
|
||||
public readonly data: IErrorJson;
|
||||
public data: IErrorJson;
|
||||
|
||||
constructor(
|
||||
public constructor(
|
||||
errorJson: IErrorJson = {},
|
||||
public readonly httpStatus?: number,
|
||||
public url?: string,
|
||||
@@ -79,11 +79,11 @@ export class MatrixError extends HTTPError {
|
||||
* @constructor
|
||||
*/
|
||||
export class ConnectionError extends Error {
|
||||
constructor(message: string, cause?: Error) {
|
||||
public constructor(message: string, cause?: Error) {
|
||||
super(message + (cause ? `: ${cause.message}` : ""));
|
||||
}
|
||||
|
||||
get name() {
|
||||
public get name(): string {
|
||||
return "ConnectionError";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export type ResponseType<T, O extends IHttpOpts> =
|
||||
export class FetchHttpApi<O extends IHttpOpts> {
|
||||
private abortController = new AbortController();
|
||||
|
||||
constructor(
|
||||
public constructor(
|
||||
private eventEmitter: TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>,
|
||||
public readonly opts: O,
|
||||
) {
|
||||
@@ -236,7 +236,7 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
method: Method,
|
||||
url: URL | string,
|
||||
body?: Body,
|
||||
opts: Pick<IRequestOpts, "headers" | "json" | "localTimeoutMs" | "abortSignal"> = {},
|
||||
opts: Pick<IRequestOpts, "headers" | "json" | "localTimeoutMs" | "keepAlive" | "abortSignal"> = {},
|
||||
): Promise<ResponseType<T, O>> {
|
||||
const headers = Object.assign({}, opts.headers || {});
|
||||
const json = opts.json ?? true;
|
||||
@@ -254,6 +254,7 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
}
|
||||
|
||||
const timeout = opts.localTimeoutMs ?? this.opts.localTimeoutMs;
|
||||
const keepAlive = opts.keepAlive ?? false;
|
||||
const signals = [
|
||||
this.abortController.signal,
|
||||
];
|
||||
@@ -286,6 +287,7 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
referrerPolicy: "no-referrer",
|
||||
cache: "no-cache",
|
||||
credentials: "omit", // we send credentials via headers
|
||||
keepalive: keepAlive,
|
||||
});
|
||||
} catch (e) {
|
||||
if ((<Error>e).name === "AbortError") {
|
||||
|
||||
@@ -77,7 +77,7 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
|
||||
if (global.XMLHttpRequest) {
|
||||
const xhr = new global.XMLHttpRequest();
|
||||
|
||||
const timeoutFn = function() {
|
||||
const timeoutFn = function(): void {
|
||||
xhr.abort();
|
||||
defer.reject(new Error("Timeout"));
|
||||
};
|
||||
@@ -85,7 +85,7 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
|
||||
// set an initial timeout of 30s; we'll advance it each time we get a progress notification
|
||||
let timeoutTimer = callbacks.setTimeout(timeoutFn, 30000);
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
xhr.onreadystatechange = function(): void {
|
||||
switch (xhr.readyState) {
|
||||
case global.XMLHttpRequest.DONE:
|
||||
callbacks.clearTimeout(timeoutTimer);
|
||||
@@ -113,7 +113,7 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
|
||||
}
|
||||
};
|
||||
|
||||
xhr.upload.onprogress = (ev: ProgressEvent) => {
|
||||
xhr.upload.onprogress = (ev: ProgressEvent): void => {
|
||||
callbacks.clearTimeout(timeoutTimer);
|
||||
upload.loaded = ev.loaded;
|
||||
upload.total = ev.total;
|
||||
|
||||
@@ -38,6 +38,7 @@ export interface IRequestOpts {
|
||||
headers?: Record<string, string>;
|
||||
abortSignal?: AbortSignal;
|
||||
localTimeoutMs?: number;
|
||||
keepAlive?: boolean; // defaults to false
|
||||
json?: boolean; // defaults to true
|
||||
|
||||
// Set to true to prevent the request function from emitting a Session.logged_out event.
|
||||
|
||||
@@ -36,13 +36,13 @@ export function anySignal(signals: AbortSignal[]): {
|
||||
} {
|
||||
const controller = new AbortController();
|
||||
|
||||
function cleanup() {
|
||||
function cleanup(): void {
|
||||
for (const signal of signals) {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
}
|
||||
}
|
||||
|
||||
function onAbort() {
|
||||
function onAbort(): void {
|
||||
controller.abort();
|
||||
cleanup();
|
||||
}
|
||||
|
||||
@@ -26,13 +26,13 @@ export function exists(indexedDB: IDBFactory, dbName: string): Promise<boolean>
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
let exists = true;
|
||||
const req = indexedDB.open(dbName);
|
||||
req.onupgradeneeded = () => {
|
||||
req.onupgradeneeded = (): void => {
|
||||
// Since we did not provide an explicit version when opening, this event
|
||||
// should only fire if the DB did not exist before at any version.
|
||||
exists = false;
|
||||
};
|
||||
req.onblocked = () => reject(req.error);
|
||||
req.onsuccess = () => {
|
||||
req.onblocked = (): void => reject(req.error);
|
||||
req.onsuccess = (): void => {
|
||||
const db = req.result;
|
||||
db.close();
|
||||
if (!exists) {
|
||||
@@ -45,6 +45,6 @@ export function exists(indexedDB: IDBFactory, dbName: string): Promise<boolean>
|
||||
}
|
||||
resolve(exists);
|
||||
};
|
||||
req.onerror = ev => reject(req.error);
|
||||
req.onerror = (): void => reject(req.error);
|
||||
});
|
||||
}
|
||||
|
||||
+22
-23
@@ -21,6 +21,7 @@ limitations under the License.
|
||||
import { logger } from './logger';
|
||||
import { MatrixClient } from "./client";
|
||||
import { defer, IDeferred } from "./utils";
|
||||
import { MatrixError } from "./http-api";
|
||||
|
||||
const EMAIL_STAGE_TYPE = "m.login.email.identity";
|
||||
const MSISDN_STAGE_TYPE = "m.login.msisdn";
|
||||
@@ -99,7 +100,7 @@ class NoAuthFlowFoundError extends Error {
|
||||
public name = "NoAuthFlowFoundError";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
|
||||
constructor(m: string, public readonly required_stages: string[], public readonly flows: IFlow[]) {
|
||||
public constructor(m: string, public readonly required_stages: string[], public readonly flows: IFlow[]) {
|
||||
super(m);
|
||||
}
|
||||
}
|
||||
@@ -111,7 +112,7 @@ interface IOpts {
|
||||
sessionId?: string;
|
||||
clientSecret?: string;
|
||||
emailSid?: string;
|
||||
doRequest(auth: IAuthData, background: boolean): Promise<IAuthData>;
|
||||
doRequest(auth: IAuthData | null, background: boolean): Promise<IAuthData>;
|
||||
stateUpdated(nextStage: AuthType, status: IStageStatus): void;
|
||||
requestEmailToken(email: string, secret: string, attempt: number, session: string): Promise<{ sid: string }>;
|
||||
busyChanged?(busy: boolean): void;
|
||||
@@ -217,7 +218,7 @@ export class InteractiveAuth {
|
||||
// the promise the will resolve/reject when it completes
|
||||
private submitPromise: Promise<void> | null = null;
|
||||
|
||||
constructor(opts: IOpts) {
|
||||
public constructor(opts: IOpts) {
|
||||
this.matrixClient = opts.matrixClient;
|
||||
this.data = opts.authData || {};
|
||||
this.requestCallback = opts.doRequest;
|
||||
@@ -328,7 +329,7 @@ export class InteractiveAuth {
|
||||
* @param {string} loginType login type for the stage
|
||||
* @return {object?} any parameters from the server for this stage
|
||||
*/
|
||||
public getStageParams(loginType: string): Record<string, any> {
|
||||
public getStageParams(loginType: string): Record<string, any> | undefined {
|
||||
return this.data.params?.[loginType];
|
||||
}
|
||||
|
||||
@@ -418,7 +419,7 @@ export class InteractiveAuth {
|
||||
/**
|
||||
* Requests a new email token and sets the email sid for the validation session
|
||||
*/
|
||||
public requestEmailToken = async () => {
|
||||
public requestEmailToken = async (): Promise<void> => {
|
||||
if (!this.requestingEmailToken) {
|
||||
logger.trace("Requesting email token. Attempt: " + this.emailAttempt);
|
||||
// If we've picked a flow with email auth, we send the email
|
||||
@@ -428,10 +429,10 @@ export class InteractiveAuth {
|
||||
this.requestingEmailToken = true;
|
||||
try {
|
||||
const requestTokenResult = await this.requestEmailTokenCallback(
|
||||
this.inputs.emailAddress,
|
||||
this.inputs.emailAddress!,
|
||||
this.clientSecret,
|
||||
this.emailAttempt++,
|
||||
this.data.session,
|
||||
this.data.session!,
|
||||
);
|
||||
this.emailSid = requestTokenResult.sid;
|
||||
logger.trace("Email token request succeeded");
|
||||
@@ -454,16 +455,16 @@ export class InteractiveAuth {
|
||||
* This can be set to true for requests that just poll to see if auth has
|
||||
* been completed elsewhere.
|
||||
*/
|
||||
private async doRequest(auth: IAuthData, background = false): Promise<void> {
|
||||
private async doRequest(auth: IAuthData | null, background = false): Promise<void> {
|
||||
try {
|
||||
const result = await this.requestCallback(auth, background);
|
||||
this.attemptAuthDeferred!.resolve(result);
|
||||
this.attemptAuthDeferred = null;
|
||||
} catch (error) {
|
||||
// sometimes UI auth errors don't come with flows
|
||||
const errorFlows = error.data?.flows ?? null;
|
||||
const errorFlows = (<MatrixError>error).data?.flows ?? null;
|
||||
const haveFlows = this.data.flows || Boolean(errorFlows);
|
||||
if (error.httpStatus !== 401 || !error.data || !haveFlows) {
|
||||
if ((<MatrixError>error).httpStatus !== 401 || !(<MatrixError>error).data || !haveFlows) {
|
||||
// doesn't look like an interactive-auth failure.
|
||||
if (!background) {
|
||||
this.attemptAuthDeferred?.reject(error);
|
||||
@@ -474,20 +475,23 @@ export class InteractiveAuth {
|
||||
logger.log("Background poll request failed doing UI auth: ignoring", error);
|
||||
}
|
||||
}
|
||||
if (!error.data) {
|
||||
error.data = {};
|
||||
if (!(<MatrixError>error).data) {
|
||||
(<MatrixError>error).data = {};
|
||||
}
|
||||
// if the error didn't come with flows, completed flows or session ID,
|
||||
// copy over the ones we have. Synapse sometimes sends responses without
|
||||
// any UI auth data (eg. when polling for email validation, if the email
|
||||
// has not yet been validated). This appears to be a Synapse bug, which
|
||||
// we workaround here.
|
||||
if (!error.data.flows && !error.data.completed && !error.data.session) {
|
||||
error.data.flows = this.data.flows;
|
||||
error.data.completed = this.data.completed;
|
||||
error.data.session = this.data.session;
|
||||
if (!(<MatrixError>error).data.flows &&
|
||||
!(<MatrixError>error).data.completed &&
|
||||
!(<MatrixError>error).data.session
|
||||
) {
|
||||
(<MatrixError>error).data.flows = this.data.flows;
|
||||
(<MatrixError>error).data.completed = this.data.completed;
|
||||
(<MatrixError>error).data.session = this.data.session;
|
||||
}
|
||||
this.data = error.data;
|
||||
this.data = (<MatrixError>error).data;
|
||||
try {
|
||||
this.startNextAuthStage();
|
||||
} catch (e) {
|
||||
@@ -627,11 +631,6 @@ export class InteractiveAuth {
|
||||
*/
|
||||
private firstUncompletedStage(flow: IFlow): AuthType | undefined {
|
||||
const completed = this.data.completed || [];
|
||||
for (let i = 0; i < flow.stages.length; ++i) {
|
||||
const stageType = flow.stages[i];
|
||||
if (completed.indexOf(stageType) === -1) {
|
||||
return stageType;
|
||||
}
|
||||
}
|
||||
return flow.stages.find(stageType => !completed.includes(stageType));
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -35,7 +35,7 @@ const DEFAULT_NAMESPACE = "matrix";
|
||||
// console methods at initialization time by a factory that looks up the console methods
|
||||
// when logging so we always get the current value of console methods.
|
||||
log.methodFactory = function(methodName, logLevel, loggerName) {
|
||||
return function(this: PrefixedLogger, ...args) {
|
||||
return function(this: PrefixedLogger, ...args): void {
|
||||
/* eslint-disable @typescript-eslint/no-invalid-this */
|
||||
if (this.prefix) {
|
||||
args.unshift(this.prefix);
|
||||
@@ -67,7 +67,7 @@ export interface PrefixedLogger extends Logger {
|
||||
prefix: string;
|
||||
}
|
||||
|
||||
function extendLogger(logger: Logger) {
|
||||
function extendLogger(logger: Logger): void {
|
||||
(<PrefixedLogger>logger).withPrefix = function(prefix: string): PrefixedLogger {
|
||||
const existingPrefix = this.prefix || "";
|
||||
return getPrefixedLogger(existingPrefix + prefix);
|
||||
|
||||
+36
-58
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015-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.
|
||||
@@ -14,14 +14,17 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { WidgetApi } from "matrix-widget-api";
|
||||
|
||||
import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store";
|
||||
import { MemoryStore } from "./store/memory";
|
||||
import { MatrixScheduler } from "./scheduler";
|
||||
import { MatrixClient, ICreateClientOpts } from "./client";
|
||||
import { DeviceTrustLevel } from "./crypto/CrossSigning";
|
||||
import { ISecretStorageKeyInfo } from "./crypto/api";
|
||||
import { RoomWidgetClient, ICapabilities } from "./embedded";
|
||||
import { CryptoStore } from "./crypto/store/base";
|
||||
|
||||
export * from "./client";
|
||||
export * from "./embedded";
|
||||
export * from "./http-api";
|
||||
export * from "./autodiscovery";
|
||||
export * from "./sync-accumulator";
|
||||
@@ -51,11 +54,18 @@ export * from './@types/requests';
|
||||
export * from './@types/search';
|
||||
export * from './models/room-summary';
|
||||
export * as ContentHelpers from "./content-helpers";
|
||||
export type { ICryptoCallbacks } from "./crypto"; // used to be located here
|
||||
export { createNewMatrixCall } from "./webrtc/call";
|
||||
export type { MatrixCall } from "./webrtc/call";
|
||||
export {
|
||||
createNewMatrixCall,
|
||||
} from "./webrtc/call";
|
||||
GroupCallEvent,
|
||||
GroupCallIntent,
|
||||
GroupCallState,
|
||||
GroupCallType,
|
||||
} from "./webrtc/groupCall";
|
||||
export type { GroupCall } from "./webrtc/groupCall";
|
||||
|
||||
let cryptoStoreFactory = () => new MemoryCryptoStore;
|
||||
let cryptoStoreFactory = (): CryptoStore => new MemoryCryptoStore;
|
||||
|
||||
/**
|
||||
* Configure a different factory to be used for creating crypto stores
|
||||
@@ -63,38 +73,24 @@ let cryptoStoreFactory = () => new MemoryCryptoStore;
|
||||
* @param {Function} fac a function which will return a new
|
||||
* {@link module:crypto.store.base~CryptoStore}.
|
||||
*/
|
||||
export function setCryptoStoreFactory(fac) {
|
||||
export function setCryptoStoreFactory(fac: () => CryptoStore): void {
|
||||
cryptoStoreFactory = fac;
|
||||
}
|
||||
|
||||
export interface ICryptoCallbacks {
|
||||
getCrossSigningKey?: (keyType: string, pubKey: string) => Promise<Uint8Array | null>;
|
||||
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
|
||||
shouldUpgradeDeviceVerifications?: (
|
||||
users: Record<string, any>
|
||||
) => Promise<string[]>;
|
||||
getSecretStorageKey?: (
|
||||
keys: {keys: Record<string, ISecretStorageKeyInfo>}, name: string
|
||||
) => Promise<[string, Uint8Array] | null>;
|
||||
cacheSecretStorageKey?: (
|
||||
keyId: string, keyInfo: ISecretStorageKeyInfo, key: Uint8Array
|
||||
) => void;
|
||||
onSecretRequested?: (
|
||||
userId: string, deviceId: string,
|
||||
requestId: string, secretName: string, deviceTrust: DeviceTrustLevel
|
||||
) => Promise<string>;
|
||||
getDehydrationKey?: (
|
||||
keyInfo: ISecretStorageKeyInfo,
|
||||
checkFunc: (key: Uint8Array) => void,
|
||||
) => Promise<Uint8Array>;
|
||||
getBackupKey?: () => Promise<Uint8Array>;
|
||||
function amendClientOpts(opts: ICreateClientOpts): ICreateClientOpts {
|
||||
opts.store = opts.store ?? new MemoryStore({
|
||||
localStorage: global.localStorage,
|
||||
});
|
||||
opts.scheduler = opts.scheduler ?? new MatrixScheduler();
|
||||
opts.cryptoStore = opts.cryptoStore ?? cryptoStoreFactory();
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a Matrix Client. Similar to {@link module:client.MatrixClient}
|
||||
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
|
||||
* @param {(Object)} opts The configuration options for this client. If
|
||||
* this is a string, it is assumed to be the base URL. These configuration
|
||||
* @param {Object} opts The configuration options for this client. These configuration
|
||||
* options will be passed directly to {@link module:client.MatrixClient}.
|
||||
* @param {Object} opts.store If not set, defaults to
|
||||
* {@link module:store/memory.MemoryStore}.
|
||||
@@ -111,33 +107,15 @@ export interface ICryptoCallbacks {
|
||||
* @see {@link module:client.MatrixClient} for the full list of options for
|
||||
* <code>opts</code>.
|
||||
*/
|
||||
export function createClient(opts: ICreateClientOpts) {
|
||||
opts.store = opts.store || new MemoryStore({
|
||||
localStorage: global.localStorage,
|
||||
});
|
||||
opts.scheduler = opts.scheduler || new MatrixScheduler();
|
||||
opts.cryptoStore = opts.cryptoStore || cryptoStoreFactory();
|
||||
return new MatrixClient(opts);
|
||||
export function createClient(opts: ICreateClientOpts): MatrixClient {
|
||||
return new MatrixClient(amendClientOpts(opts));
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper for the request function interface.
|
||||
* @callback requestWrapperFunction
|
||||
* @param {requestFunction} origRequest The underlying request function being
|
||||
* wrapped
|
||||
* @param {Object} opts The options for this HTTP request, given in the same
|
||||
* form as {@link requestFunction}.
|
||||
* @param {requestCallback} callback The request callback.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The request callback interface for performing HTTP requests. This matches the
|
||||
* API for the {@link https://github.com/request/request#requestoptions-callback|
|
||||
* request NPM module}. The SDK will implement a callback which meets this
|
||||
* interface in order to handle the HTTP response.
|
||||
* @callback requestCallback
|
||||
* @param {Error} err The error if one occurred, else falsey.
|
||||
* @param {Object} response The HTTP response which consists of
|
||||
* <code>{statusCode: {Number}, headers: {Object}}</code>
|
||||
* @param {Object} body The parsed HTTP response body.
|
||||
*/
|
||||
export function createRoomWidgetClient(
|
||||
widgetApi: WidgetApi,
|
||||
capabilities: ICapabilities,
|
||||
roomId: string,
|
||||
opts: ICreateClientOpts,
|
||||
): MatrixClient {
|
||||
return new RoomWidgetClient(widgetApi, capabilities, roomId, amendClientOpts(opts));
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ export class MSC3089Branch {
|
||||
let event: MatrixEvent | undefined = room.getUnfilteredTimelineSet().findEventById(this.id);
|
||||
|
||||
// keep scrolling back if needed until we find the event or reach the start of the room:
|
||||
while (!event && room.getLiveTimeline().getState(EventTimeline.BACKWARDS).paginationToken) {
|
||||
while (!event && room.getLiveTimeline().getState(EventTimeline.BACKWARDS)!.paginationToken) {
|
||||
await this.client.scrollback(room, 100);
|
||||
event = room.getUnfilteredTimelineSet().findEventById(this.id);
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ export class MSC3089TreeSpace {
|
||||
const currentPls = this.room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
|
||||
if (Array.isArray(currentPls)) throw new Error("Unexpected return type for power levels");
|
||||
|
||||
const pls = currentPls.getContent() || {};
|
||||
const pls = currentPls?.getContent() || {};
|
||||
const viewLevel = pls['users_default'] || 0;
|
||||
const editLevel = pls['events_default'] || 50;
|
||||
const adminLevel = pls['events']?.[EventType.RoomPowerLevels] || 100;
|
||||
@@ -207,7 +207,7 @@ export class MSC3089TreeSpace {
|
||||
const currentPls = this.room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
|
||||
if (Array.isArray(currentPls)) throw new Error("Unexpected return type for power levels");
|
||||
|
||||
const pls = currentPls.getContent() || {};
|
||||
const pls = currentPls?.getContent() || {};
|
||||
const viewLevel = pls['users_default'] || 0;
|
||||
const editLevel = pls['events_default'] || 50;
|
||||
const adminLevel = pls['events']?.[EventType.RoomPowerLevels] || 100;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user