Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c836cb988e | |||
| 69637f8bac | |||
| 2ce91cf26f | |||
| d895a64f19 | |||
| a83a270069 | |||
| c6e0d845d3 | |||
| 4e4a0f79b9 | |||
| c43f751013 | |||
| b11f7b5122 | |||
| 79a88b5fc9 | |||
| 791c282349 | |||
| f1e6b9717e | |||
| b309a4ecdf | |||
| a986f86c82 | |||
| cbe8a80d10 | |||
| c9ac102668 | |||
| b715799bb5 | |||
| 0a96fa52a2 | |||
| 1485cfd0f2 | |||
| 5d2e606076 | |||
| 51096b62d9 | |||
| 578c5c736e | |||
| 4c67f0391b | |||
| 72e9b74bbf | |||
| 8189942a1f | |||
| 13e3740f70 | |||
| b96ce9229d | |||
| c3f2f0f063 | |||
| a0f0fdf4d4 | |||
| 06ea5f78fc | |||
| d4c652bedc | |||
| 7109274c65 | |||
| a83a337c4d | |||
| 5d3850b038 | |||
| 81b1c56288 | |||
| 7469fa7585 | |||
| 9ee3db1de5 | |||
| 25b3ba5328 | |||
| c7d0d02be7 | |||
| 798a507ee0 | |||
| eabedd9520 | |||
| 0f535f2a01 | |||
| a027e3ecc3 | |||
| 1607ed5b2c | |||
| 35b6365317 | |||
| 14ed84ac33 | |||
| c1fe945dd5 | |||
| 8a50312099 | |||
| 719014d9d5 |
@@ -9,6 +9,5 @@
|
||||
- End with either a period (.) or an exclamation mark (!).
|
||||
- Start with a capital letter.
|
||||
- Feel free to credit yourself, by adding a sentence "Contributed by @github_username." or "Contributed by [Your Name]." to the end of the entry.
|
||||
* [ ] Pull request includes a [sign off](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#sign-off)
|
||||
* [ ] [Code style](https://element-hq.github.io/synapse/latest/code_style.html) is correct
|
||||
(run the [linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters))
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
id-token: write # needed for signing the images with GitHub OIDC Token
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -29,6 +29,9 @@ jobs:
|
||||
- name: Inspect builder
|
||||
run: docker buildx inspect
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3.3.0
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -68,6 +71,7 @@ jobs:
|
||||
type=pep440,pattern={{raw}}
|
||||
|
||||
- name: Build and push all platforms
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
push: true
|
||||
@@ -82,3 +86,14 @@ jobs:
|
||||
# https://github.com/rust-lang/cargo/issues/10583
|
||||
build-args: |
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI=true
|
||||
|
||||
- name: Sign the images with GitHub OIDC Token
|
||||
env:
|
||||
DIGEST: ${{ steps.build-and-push.outputs.digest }}
|
||||
TAGS: ${{ steps.set-tag.outputs.tags }}
|
||||
run: |
|
||||
images=""
|
||||
for tag in ${TAGS}; do
|
||||
images+="${tag}@${DIGEST} "
|
||||
done
|
||||
cosign sign --yes ${images}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
name: Add Version Picker (RUN ONCE)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
add-version-picker:
|
||||
name: Add Version Picker
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.email "action@synapse.bot.com"
|
||||
git config user.name "Action Bot"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup mdbook
|
||||
uses: peaceiris/actions-mdbook@adeb05db28a0c0004681db83893d56c0388ea9ea # v1.2.0
|
||||
with:
|
||||
mdbook-version: '0.4.17'
|
||||
|
||||
- name: Copy files to release branches
|
||||
run: |
|
||||
for version in "v1.98" "v1.97" "v1.96" "v1.95" "v1.94" "v1.93" "v1.92" "v1.91" "v1.90" "v1.89" "v1.88" "v1.87" "v1.86" "v1.85" "v1.84" "v1.83" "v1.82" "v1.81" "v1.80" "v1.79" "v1.78" "v1.77" "v1.76" "v1.75" "v1.74" "v1.73" "v1.72" "v1.71" "v1.70" "v1.69" "v1.68" "v1.67" "v1.66" "v1.65" "v1.64" "v1.63" "v1.62" "v1.61" "v1.60" "v1.59" "v1.58" "v1.57" "v1.56" "v1.55" "v1.54" "v1.53" "v1.52" "v1.51" "v1.50" "v1.49" "v1.48" "v1.47" "v1.46" "v1.45" "v1.44" "v1.43" "v1.42" "v1.41" "v1.40" "v1.39" "v1.38" "v1.37"
|
||||
do
|
||||
git fetch
|
||||
git checkout -b release-$version origin/release-$version
|
||||
|
||||
git checkout develop -- ./book.toml
|
||||
git checkout develop -- ./docs/website_files/version-picker.js
|
||||
git checkout develop -- ./docs/website_files/version-picker.css
|
||||
git checkout develop -- ./docs/website_files/README.md
|
||||
|
||||
echo "window.SYNAPSE_VERSION = '$version';" > ./docs/website_files/version.js
|
||||
|
||||
# Adding version-picker element to index.hbs
|
||||
awk '/<button id="search-toggle" class="icon-button" type="button" title="Search. \(Shortkey: s\)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">/{
|
||||
print; getline; print; getline; print; getline; print;
|
||||
print "\
|
||||
<div class=\"version-picker\">\n\
|
||||
<div class=\"dropdown\">\n\
|
||||
<div class=\"select\">\n\
|
||||
<span></span>\n\
|
||||
<i class=\"fa fa-chevron-down\"></i>\n\
|
||||
</div>\n\
|
||||
<input type=\"hidden\" name=\"version\">\n\
|
||||
<ul class=\"dropdown-menu\">\n\
|
||||
<!-- Versions will be added dynamically in version-picker.js -->\n\
|
||||
</ul>\n\
|
||||
</div>\n\
|
||||
</div>\
|
||||
";
|
||||
next
|
||||
} 1' ./docs/website_files/theme/index.hbs > output.html && mv output.html ./docs/website_files/theme/index.hbs
|
||||
|
||||
git add ./book.toml ./docs/website_files/version-picker.js ./docs/website_files/version-picker.css ./docs/website_files/version.js ./docs/website_files/README.md ./docs/website_files/theme/index.hbs
|
||||
git commit -m "Version picker added for $version docs"
|
||||
git push
|
||||
done
|
||||
|
||||
- name: Build docs for Github Pages
|
||||
run: |
|
||||
git fetch
|
||||
git branch gh-pages origin/gh-pages
|
||||
|
||||
for version in "v1.98" "v1.97" "v1.96" "v1.95" "v1.94" "v1.93" "v1.92" "v1.91" "v1.90" "v1.89" "v1.88" "v1.87" "v1.86" "v1.85" "v1.84" "v1.83" "v1.82" "v1.81" "v1.80" "v1.79" "v1.78" "v1.77" "v1.76" "v1.75" "v1.74" "v1.73" "v1.72" "v1.71" "v1.70" "v1.69" "v1.68" "v1.67" "v1.66" "v1.65" "v1.64" "v1.63" "v1.62" "v1.61" "v1.60" "v1.59" "v1.58" "v1.57" "v1.56" "v1.55" "v1.54" "v1.53" "v1.52" "v1.51" "v1.50" "v1.49" "v1.48" "v1.47" "v1.46" "v1.45" "v1.44" "v1.43" "v1.42" "v1.41" "v1.40" "v1.39" "v1.38" "v1.37"
|
||||
do
|
||||
git checkout release-$version
|
||||
|
||||
mdbook build && cp book/welcome_and_overview.html book/index.html
|
||||
mkdir ver-temp && cp -r book/* ver-temp/
|
||||
rm -r ./book
|
||||
|
||||
git checkout gh-pages
|
||||
rm -r $version
|
||||
mv ver-temp $version
|
||||
|
||||
git add ./$version
|
||||
git commit -m "Version picker deployed for $version docs to Github Pages"
|
||||
done
|
||||
|
||||
- name: Push to gh-pages
|
||||
run: |
|
||||
git checkout gh-pages
|
||||
git status
|
||||
git push
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
# 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@268677152d06ba59fcec7a7f0b5d961b6ccd7e1e # v2.28.0
|
||||
uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0
|
||||
with:
|
||||
workflow: docs-pr.yaml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
cp book/welcome_and_overview.html book/index.html
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: book
|
||||
path: book
|
||||
|
||||
@@ -164,7 +164,7 @@ jobs:
|
||||
if: ${{ always() }}
|
||||
run: /sytest/scripts/tap_to_gha.pl /logs/results.tap
|
||||
- name: Upload SyTest logs
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
name: Sytest Logs - ${{ job.status }} - (${{ join(matrix.*, ', ') }})
|
||||
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
||||
|
||||
- name: Upload debs as artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: debs
|
||||
path: debs/*
|
||||
@@ -156,7 +156,7 @@ jobs:
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI: true
|
||||
CIBW_ENVIRONMENT_PASS_LINUX: CARGO_NET_GIT_FETCH_WITH_CLI
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Wheel
|
||||
path: ./wheelhouse/*.whl
|
||||
@@ -177,7 +177,7 @@ jobs:
|
||||
- name: Build sdist
|
||||
run: python -m build --sdist
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Sdist
|
||||
path: dist/*.tar.gz
|
||||
@@ -194,7 +194,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download all workflow run artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
- name: Build a tarball for the debs
|
||||
run: tar -cvJf debs.tar.xz debs
|
||||
- name: Attach to release
|
||||
|
||||
@@ -12,10 +12,6 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check-signoff:
|
||||
if: "github.event_name == 'pull_request'"
|
||||
uses: "matrix-org/backend-meta/.github/workflows/sign-off.yml@v2"
|
||||
|
||||
# Job to detect what has changed so we don't run e.g. Rust checks on PRs that
|
||||
# don't modify Rust code.
|
||||
changes:
|
||||
@@ -286,10 +282,26 @@ jobs:
|
||||
- check-schema-delta
|
||||
- check-lockfile
|
||||
- lint-clippy
|
||||
- lint-clippy-nightly
|
||||
- lint-rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: "true"
|
||||
- uses: matrix-org/done-action@v2
|
||||
with:
|
||||
needs: ${{ toJSON(needs) }}
|
||||
|
||||
# Various bits are skipped if there was no applicable changes.
|
||||
skippable: |
|
||||
check-sampleconfig
|
||||
check-schema-delta
|
||||
lint
|
||||
lint-mypy
|
||||
lint-newsfile
|
||||
lint-pydantic
|
||||
lint-clippy
|
||||
lint-clippy-nightly
|
||||
lint-rustfmt
|
||||
|
||||
|
||||
calculate-test-jobs:
|
||||
if: ${{ !cancelled() && !failure() }} # Allow previous steps to be skipped, but not fail
|
||||
@@ -496,7 +508,7 @@ jobs:
|
||||
if: ${{ always() }}
|
||||
run: /sytest/scripts/tap_to_gha.pl /logs/results.tap
|
||||
- name: Upload SyTest logs
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
name: Sytest Logs - ${{ job.status }} - (${{ join(matrix.job.*, ', ') }})
|
||||
@@ -594,7 +606,7 @@ jobs:
|
||||
PGPASSWORD: postgres
|
||||
PGDATABASE: postgres
|
||||
- name: "Upload schema differences"
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ failure() && !cancelled() && steps.run_tester_script.outcome == 'failure' }}
|
||||
with:
|
||||
name: Schema dumps
|
||||
@@ -699,6 +711,7 @@ jobs:
|
||||
- complement
|
||||
- cargo-test
|
||||
- cargo-bench
|
||||
- linting-done
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: matrix-org/done-action@v2
|
||||
@@ -706,7 +719,7 @@ jobs:
|
||||
needs: ${{ toJSON(needs) }}
|
||||
|
||||
# Various bits are skipped if there was no applicable changes.
|
||||
# The newsfile and signoff lint may be skipped on non PR builds.
|
||||
# The newsfile lint may be skipped on non PR builds.
|
||||
skippable: |
|
||||
trial
|
||||
trial-olddeps
|
||||
@@ -714,7 +727,6 @@ jobs:
|
||||
portdb
|
||||
export-data
|
||||
complement
|
||||
check-signoff
|
||||
lint-newsfile
|
||||
cargo-test
|
||||
cargo-bench
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
name: Move new issues into the issue triage board
|
||||
|
||||
on:
|
||||
# issues:
|
||||
# types: [ opened ]
|
||||
workflow_dispatch:
|
||||
issues:
|
||||
types: [ opened ]
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
|
||||
@@ -136,7 +136,7 @@ jobs:
|
||||
if: ${{ always() }}
|
||||
run: /sytest/scripts/tap_to_gha.pl /logs/results.tap
|
||||
- name: Upload SyTest logs
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
name: Sytest Logs - ${{ job.status }} - (${{ join(matrix.*, ', ') }})
|
||||
|
||||
+47
@@ -1,3 +1,50 @@
|
||||
# Synapse 1.99.0rc1 (2024-01-09)
|
||||
|
||||
### Features
|
||||
|
||||
- Add [config options](https://element-hq.github.io/synapse/v1.99/usage/configuration/config_documentation.html#server_notices) to set the avatar and the topic of the server notices room, as well as the avatar of the server notices user. ([\#16679](https://github.com/matrix-org/synapse/issues/16679))
|
||||
- Add config option [`email.notif_delay_before_mail`](https://element-hq.github.io/synapse/v1.99/usage/configuration/config_documentation.html#email) to tweak the delay before an email is sent following a notification. ([\#16696](https://github.com/matrix-org/synapse/issues/16696))
|
||||
- Add new configuration option [`sentry.environment`](https://element-hq.github.io/synapse/v1.99/usage/configuration/config_documentation.html#sentry) for improved system monitoring. Contributed by @zeeshanrafiqrana. ([\#16738](https://github.com/matrix-org/synapse/issues/16738))
|
||||
- Filter out rooms from the room directory being served to other homeservers when those rooms block that homeserver by their Access Control Lists. ([\#16759](https://github.com/element-hq/synapse/issues/16759))
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- Fix a long-standing bug where the signing keys generated by Synapse were world-readable. Contributed by Fabian Klemp. ([\#16740](https://github.com/matrix-org/synapse/issues/16740))
|
||||
- Fix email verification redirection. Contributed by Fadhlan Ridhwanallah. ([\#16761](https://github.com/element-hq/synapse/issues/16761))
|
||||
- Fixed a bug that prevented users from being queried by display name if it contains non-ASCII characters. ([\#16767](https://github.com/element-hq/synapse/issues/16767))
|
||||
- Allow reactivate user without password with Admin API in some edge cases. ([\#16770](https://github.com/element-hq/synapse/issues/16770))
|
||||
- Adds the `recursion_depth` parameter to the response of the /relations endpoint if MSC3981 recursion is being performed. ([\#16775](https://github.com/element-hq/synapse/issues/16775))
|
||||
|
||||
### Improved Documentation
|
||||
|
||||
- Added version picker for Synapse documentation. Contributed by @Dmytro27Ind. ([\#16533](https://github.com/matrix-org/synapse/issues/16533))
|
||||
- Clarify that `password_config.enabled: "only_for_reauth"` does not allow new logins to be created using password auth. ([\#16737](https://github.com/matrix-org/synapse/issues/16737))
|
||||
- Remove value from header in configuration documentation for `refresh_token_lifetime`. ([\#16763](https://github.com/element-hq/synapse/issues/16763))
|
||||
- Add another custom statistics collection server to the documentation. Contributed by @loelkes. ([\#16769](https://github.com/element-hq/synapse/issues/16769))
|
||||
|
||||
### Internal Changes
|
||||
|
||||
- Remove run-once workflow after adding the version picker to the documentation. ([\#9453](https://github.com/element-hq/synapse/issues/9453))
|
||||
- Update the implementation of [MSC2965](https://github.com/matrix-org/matrix-spec-proposals/pull/2965) (OIDC Provider discovery). ([\#16726](https://github.com/matrix-org/synapse/issues/16726))
|
||||
- Move the rust stubs inline for better IDE integration. ([\#16757](https://github.com/element-hq/synapse/issues/16757))
|
||||
- Fix sample config doc CI. ([\#16758](https://github.com/element-hq/synapse/issues/16758))
|
||||
- Simplify event internal metadata class. ([\#16762](https://github.com/element-hq/synapse/issues/16762), [\#16780](https://github.com/element-hq/synapse/issues/16780))
|
||||
- Sign the published docker image using [cosign](https://docs.sigstore.dev/). ([\#16774](https://github.com/element-hq/synapse/issues/16774))
|
||||
- Port `EventInternalMetadata` class to Rust. ([\#16782](https://github.com/element-hq/synapse/issues/16782))
|
||||
|
||||
|
||||
|
||||
### Updates to locked dependencies
|
||||
|
||||
* Bump actions/setup-go from 4 to 5. ([\#16749](https://github.com/matrix-org/synapse/issues/16749))
|
||||
* Bump actions/setup-python from 4 to 5. ([\#16748](https://github.com/matrix-org/synapse/issues/16748))
|
||||
* Bump immutabledict from 3.0.0 to 4.0.0. ([\#16743](https://github.com/matrix-org/synapse/issues/16743))
|
||||
* Bump isort from 5.12.0 to 5.13.0. ([\#16745](https://github.com/matrix-org/synapse/issues/16745))
|
||||
* Bump isort from 5.13.0 to 5.13.1. ([\#16752](https://github.com/matrix-org/synapse/issues/16752))
|
||||
* Bump pydantic from 2.5.1 to 2.5.2. ([\#16747](https://github.com/matrix-org/synapse/issues/16747))
|
||||
* Bump ruff from 0.1.6 to 0.1.7. ([\#16746](https://github.com/matrix-org/synapse/issues/16746))
|
||||
* Bump types-setuptools from 68.2.0.2 to 69.0.0.0. ([\#16744](https://github.com/matrix-org/synapse/issues/16744))
|
||||
|
||||
# Synapse 1.98.0 (2023-12-12)
|
||||
|
||||
Synapse 1.98.0 will be the last Synapse release in 2023; the regular release cadence will resume in January 2024.
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
# Welcome to Synapse
|
||||
|
||||
Please see the [contributors' guide](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html) in our rendered documentation.
|
||||
Please see the [contributors' guide](https://element-hq.github.io/synapse/latest/development/contributing_guide.html) in our rendered documentation.
|
||||
|
||||
Generated
+24
-24
@@ -13,9 +13,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.75"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
||||
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
@@ -188,18 +188,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.64"
|
||||
version = "1.0.76"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da"
|
||||
checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3"
|
||||
version = "0.20.0"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04e8453b658fe480c3e70c8ed4e3d3ec33eb74988bd186561b0cc66b85c3bc4b"
|
||||
checksum = "9a89dc7a5850d0e983be1ec2a463a171d20990487c3cfcd68b5363f1ee3d6fe0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cfg-if",
|
||||
@@ -215,9 +215,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-build-config"
|
||||
version = "0.20.0"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a96fe70b176a89cff78f2fa7b3c930081e163d5379b4dcdf993e3ae29ca662e5"
|
||||
checksum = "07426f0d8fe5a601f26293f300afd1a7b1ed5e78b2a705870c5f30893c5163be"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"target-lexicon",
|
||||
@@ -225,9 +225,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-ffi"
|
||||
version = "0.20.0"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "214929900fd25e6604661ed9cf349727c8920d47deff196c4e28165a6ef2a96b"
|
||||
checksum = "dbb7dec17e17766b46bca4f1a4215a85006b4c2ecde122076c562dd058da6cf1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pyo3-build-config",
|
||||
@@ -246,9 +246,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros"
|
||||
version = "0.20.0"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dac53072f717aa1bfa4db832b39de8c875b7c7af4f4a6fe93cdbf9264cf8383b"
|
||||
checksum = "05f738b4e40d50b5711957f142878cfa0f28e054aa0ebdfc3fd137a843f74ed3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"pyo3-macros-backend",
|
||||
@@ -258,9 +258,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros-backend"
|
||||
version = "0.20.0"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7774b5a8282bd4f25f803b1f0d945120be959a36c72e08e7cd031c792fdfd424"
|
||||
checksum = "0fc910d4851847827daf9d6cdd4a823fbdaab5b8818325c5e97a86da79e8881f"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -280,9 +280,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.29"
|
||||
version = "1.0.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105"
|
||||
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -339,18 +339,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.193"
|
||||
version = "1.0.195"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
|
||||
checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.193"
|
||||
version = "1.0.195"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
|
||||
checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -359,9 +359,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.108"
|
||||
version = "1.0.111"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
|
||||
checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@@ -382,9 +382,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.28"
|
||||
version = "2.0.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567"
|
||||
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Added version picker for Synapse documentation. Contributed by @Dmytro27Ind.
|
||||
@@ -1 +0,0 @@
|
||||
Add config options to set the avatar and the topic of the server notices room.
|
||||
@@ -1 +0,0 @@
|
||||
Add a setting to be able to tweak the delay without interaction before an email is sent following a notification.
|
||||
@@ -1 +0,0 @@
|
||||
Update the implementation of [MSC2965](https://github.com/matrix-org/matrix-spec-proposals/pull/2965) (OIDC Provider discovery).
|
||||
@@ -1 +0,0 @@
|
||||
Clarify that `password_config.enabled: "only_for_reauth"` does not allow new logins to be created using password auth.
|
||||
@@ -1 +0,0 @@
|
||||
Add new Sentry configuration option `environment` for improved system monitoring. Contributed by @zeeshanrafiqrana.
|
||||
@@ -1 +0,0 @@
|
||||
Fix a long-standing bug where the signing keys generated by Synapse were world-readable. Contributed by Fabian Klemp.
|
||||
@@ -0,0 +1 @@
|
||||
Improve DB performance of calculating badge counts for push.
|
||||
@@ -0,0 +1 @@
|
||||
Split up deleting devices into batches.
|
||||
@@ -0,0 +1 @@
|
||||
Remove CI check for sign off as we require an CLA signature instead.
|
||||
@@ -0,0 +1 @@
|
||||
Add a link to the "Request log format" explainer on the "Logging sample config" documentation page.
|
||||
@@ -0,0 +1 @@
|
||||
Ensure CI fails when linting fails to make sure auto-merge does the correct thing.
|
||||
@@ -0,0 +1 @@
|
||||
Faster load recents for sync by reducing amount of state pulled out.
|
||||
@@ -0,0 +1 @@
|
||||
Reduce amount of state pulled out when querying federation hierachy.
|
||||
@@ -0,0 +1 @@
|
||||
Pull less state out of the DB when we retry fetching old events during backfill.
|
||||
@@ -0,0 +1 @@
|
||||
Optimize query for fetching to-device messages in `/sync`.
|
||||
@@ -0,0 +1 @@
|
||||
Reject OIDC config when `client_secret` isn't specified, but the auth method requires one.
|
||||
@@ -0,0 +1 @@
|
||||
Faster partial join to room with complex auth graph.
|
||||
Vendored
+12
@@ -1,3 +1,15 @@
|
||||
matrix-synapse-py3 (1.99.0~rc1ubuntu1) UNRELEASED; urgency=medium
|
||||
|
||||
* Fix copyright file with new licensing
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Thu, 11 Jan 2024 13:47:29 +0000
|
||||
|
||||
matrix-synapse-py3 (1.99.0~rc1) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.99.0rc1.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Tue, 09 Jan 2024 13:43:56 +0000
|
||||
|
||||
matrix-synapse-py3 (1.98.0) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.98.0.
|
||||
|
||||
Vendored
+4
@@ -6,6 +6,10 @@ Files: *
|
||||
Copyright: 2014-2017, OpenMarket Ltd, 2017-2018 New Vector Ltd
|
||||
License: Apache-2.0
|
||||
|
||||
Files: *
|
||||
Copyright: 2023 New Vector Ltd
|
||||
License: AGPL-3.0-or-later
|
||||
|
||||
Files: synapse/config/saml2.py
|
||||
Copyright: 2015, Ericsson
|
||||
License: Apache-2.0
|
||||
|
||||
Vendored
+4
-3
@@ -40,9 +40,9 @@ override_dh_shlibdeps:
|
||||
# to be self-contained, but they have interdependencies and
|
||||
# dpkg-shlibdeps doesn't know how to resolve them.
|
||||
#
|
||||
# As of Pillow 7.1.0, these libraries are in
|
||||
# site-packages/Pillow.libs. Previously, they were in
|
||||
# site-packages/PIL/.libs.
|
||||
# As of Pillow 7.1.0, these libraries are in site-packages/Pillow.libs.
|
||||
# Previously, they were in site-packages/PIL/.libs. As of Pillow 10.2.0
|
||||
# the package name is lowercased to site-packages/pillow.libs.
|
||||
#
|
||||
# (we also need to exclude psycopg2, of course, since we've already
|
||||
# dealt with that.)
|
||||
@@ -50,6 +50,7 @@ override_dh_shlibdeps:
|
||||
dh_shlibdeps \
|
||||
-X site-packages/PIL/.libs \
|
||||
-X site-packages/Pillow.libs \
|
||||
-X site-packages/pillow.libs \
|
||||
-X site-packages/psycopg2
|
||||
|
||||
override_dh_virtualenv:
|
||||
|
||||
@@ -9,3 +9,4 @@
|
||||
# https://element-hq.github.io/synapse/latest/setup/installation.html.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
|
||||
@@ -149,10 +149,11 @@ Body parameters:
|
||||
granting them access to the Admin API, among other things.
|
||||
- `deactivated` - **bool**, optional. If unspecified, deactivation state will be left unchanged.
|
||||
|
||||
Note: the `password` field must also be set if both of the following are true:
|
||||
- `deactivated` is set to `false` and the user was previously deactivated (you are reactivating this user)
|
||||
- Users are allowed to set their password on this homeserver (both `password_config.enabled` and
|
||||
`password_config.localdb_enabled` config options are set to `true`).
|
||||
Note:
|
||||
- For the password field there is no strict check of the necessity for its presence.
|
||||
It is possible to have active users without a password, e.g. when authenticating with OIDC is configured.
|
||||
You must check yourself whether a password is required when reactivating a user or not.
|
||||
- It is not possible to set a password if the config option `password_config.localdb_enabled` is set `false`.
|
||||
Users' passwords are wiped upon account deactivation, hence the need to set a new one here.
|
||||
|
||||
Note: a user cannot be erased with this API. For more details on
|
||||
@@ -223,7 +224,7 @@ The following parameters should be set in the URL:
|
||||
**or** displaynames that contain this value.
|
||||
- `guests` - string representing a bool - Is optional and if `false` will **exclude** guest users.
|
||||
Defaults to `true` to include guest users. This parameter is not supported when MSC3861 is enabled. [See #15582](https://github.com/matrix-org/synapse/pull/15582)
|
||||
- `admins` - Optional flag to filter admins. If `true`, only admins are queried. If `false`, admins are excluded from
|
||||
- `admins` - Optional flag to filter admins. If `true`, only admins are queried. If `false`, admins are excluded from
|
||||
the query. When the flag is absent (the default), **both** admins and non-admins are included in the search results.
|
||||
- `deactivated` - string representing a bool - Is optional and if `true` will **include** deactivated users.
|
||||
Defaults to `false` to exclude deactivated users.
|
||||
@@ -272,7 +273,7 @@ The following fields are returned in the JSON response body:
|
||||
- `is_guest` - bool - Status if that user is a guest account.
|
||||
- `admin` - bool - Status if that user is a server administrator.
|
||||
- `user_type` - string - Type of the user. Normal users are type `None`.
|
||||
This allows user type specific behaviour. There are also types `support` and `bot`.
|
||||
This allows user type specific behaviour. There are also types `support` and `bot`.
|
||||
- `deactivated` - bool - Status if that user has been marked as deactivated.
|
||||
- `erased` - bool - Status if that user has been marked as erased.
|
||||
- `shadow_banned` - bool - Status if that user has been marked as shadow banned.
|
||||
@@ -887,7 +888,7 @@ The following fields are returned in the JSON response body:
|
||||
|
||||
### Create a device
|
||||
|
||||
Creates a new device for a specific `user_id` and `device_id`. Does nothing if the `device_id`
|
||||
Creates a new device for a specific `user_id` and `device_id`. Does nothing if the `device_id`
|
||||
exists already.
|
||||
|
||||
The API is:
|
||||
@@ -1254,11 +1255,11 @@ The following parameters should be set in the URL:
|
||||
|
||||
## Check username availability
|
||||
|
||||
Checks to see if a username is available, and valid, for the server. See [the client-server
|
||||
Checks to see if a username is available, and valid, for the server. See [the client-server
|
||||
API](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-register-available)
|
||||
for more information.
|
||||
|
||||
This endpoint will work even if registration is disabled on the server, unlike
|
||||
This endpoint will work even if registration is disabled on the server, unlike
|
||||
`/_matrix/client/r0/register/available`.
|
||||
|
||||
The API is:
|
||||
|
||||
@@ -4,16 +4,16 @@ This document aims to get you started with contributing to Synapse!
|
||||
|
||||
# 1. Who can contribute to Synapse?
|
||||
|
||||
Everyone is welcome to contribute code to [Synapse](https://github.com/element-hq/synapse),
|
||||
provided that they are willing to
|
||||
license their contributions under the same license as the project itself. We
|
||||
follow a simple 'inbound=outbound' model for contributions: the act of
|
||||
submitting an 'inbound' contribution means that the contributor agrees to
|
||||
license the code under the same terms as the project's overall 'outbound'
|
||||
license - in our case, this is almost always Apache Software License v2 (see
|
||||
[LICENSE](https://github.com/element-hq/synapse/blob/develop/LICENSE)).
|
||||
Everyone is welcome to contribute code to
|
||||
[Synapse](https://github.com/element-hq/synapse), provided that they are willing
|
||||
to license their contributions to Element under a [Contributor License
|
||||
Agreement](https://cla-assistant.io/element-hq/synapse) (CLA). This ensures that
|
||||
their contribution will be made available under an OSI-approved open-source
|
||||
license, currently Affero General Public License v3 (AGPLv3).
|
||||
|
||||
TODO THIS NEEDS UPDATING
|
||||
Please see the
|
||||
[Element blog post](https://element.io/blog/synapse-now-lives-at-github-com-element-hq-synapse/)
|
||||
for the full rationale.
|
||||
|
||||
# 2. What do I need?
|
||||
|
||||
@@ -499,81 +499,19 @@ separate pull requests.)
|
||||
|
||||
## Sign off
|
||||
|
||||
In order to have a concrete record that your contribution is intentional
|
||||
and you agree to license it under the same terms as the project's license, we've adopted the
|
||||
same lightweight approach that the Linux Kernel
|
||||
[submitting patches process](
|
||||
https://www.kernel.org/doc/html/latest/process/submitting-patches.html#sign-your-work-the-developer-s-certificate-of-origin>),
|
||||
[Docker](https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
||||
projects use: the DCO ([Developer Certificate of Origin](http://developercertificate.org/)).
|
||||
This is a simple declaration that you wrote
|
||||
the contribution or otherwise have the right to contribute it to Matrix:
|
||||
After you make a PR a comment from @CLAassistant will appear asking you to sign
|
||||
the [CLA](https://cla-assistant.io/element-hq/synapse).
|
||||
This will link a page to allow you to confirm that you have read and agreed to
|
||||
the CLA by signing in with GitHub.
|
||||
|
||||
```
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
```
|
||||
|
||||
If you agree to this for your contribution, then all that's needed is to
|
||||
include the line in your commit or pull request comment:
|
||||
|
||||
```
|
||||
Signed-off-by: Your Name <your@email.example.org>
|
||||
```
|
||||
Alternatively, you can sign off before opening a PR by going to
|
||||
<https://cla-assistant.io/element-hq/synapse>.
|
||||
|
||||
We accept contributions under a legally identifiable name, such as
|
||||
your name on government documentation or common-law names (names
|
||||
claimed by legitimate usage or repute). Unfortunately, we cannot
|
||||
accept anonymous contributions at this time.
|
||||
|
||||
Git allows you to add this signoff automatically when using the `-s`
|
||||
flag to `git commit`, which uses the name and email set in your
|
||||
`user.name` and `user.email` git configs.
|
||||
|
||||
### Private Sign off
|
||||
|
||||
If you would like to provide your legal name privately to the Matrix.org
|
||||
Foundation (instead of in a public commit or comment), you can do so
|
||||
by emailing your legal name and a link to the pull request to
|
||||
[dco@matrix.org](mailto:dco@matrix.org?subject=Private%20sign%20off).
|
||||
It helps to include "sign off" or similar in the subject line. You will then
|
||||
be instructed further.
|
||||
|
||||
Once private sign off is complete, doing so for future contributions will not
|
||||
be required.
|
||||
|
||||
# 10. Turn feedback into better code.
|
||||
|
||||
|
||||
@@ -74,3 +74,4 @@ consider using one of the following known implementations:
|
||||
|
||||
* [Matrix.org's Panopticon](https://github.com/matrix-org/panopticon)
|
||||
* [Famedly's Barad-dûr](https://gitlab.com/famedly/infra/services/barad-dur)
|
||||
* [Synapse Usage Exporter](https://github.com/loelkes/synapse-usage-exporter) for Prometheus
|
||||
|
||||
@@ -2680,7 +2680,7 @@ Example configuration:
|
||||
refreshable_access_token_lifetime: 10m
|
||||
```
|
||||
---
|
||||
### `refresh_token_lifetime: 24h`
|
||||
### `refresh_token_lifetime`
|
||||
|
||||
Time that a refresh token remains valid for (provided that it is not
|
||||
exchanged for another one first).
|
||||
|
||||
@@ -11,6 +11,9 @@ Note that a default logging configuration (shown below) is created automatically
|
||||
the homeserver config when following the [installation instructions](../../setup/installation.md).
|
||||
It should be named `<SERVERNAME>.log.config` by default.
|
||||
|
||||
Hint: If you're looking for a guide on what each of the fields in the "Processed request" log lines mean,
|
||||
see [Request log format](../administration/request_log.md).
|
||||
|
||||
```yaml
|
||||
{{#include ../../sample_log_config.yaml}}
|
||||
```
|
||||
|
||||
Generated
+179
-175
@@ -64,17 +64,17 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte
|
||||
|
||||
[[package]]
|
||||
name = "authlib"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients."
|
||||
optional = true
|
||||
python-versions = "*"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "Authlib-1.2.1-py2.py3-none-any.whl", hash = "sha256:c88984ea00149a90e3537c964327da930779afa4564e354edfd98410bea01911"},
|
||||
{file = "Authlib-1.2.1.tar.gz", hash = "sha256:421f7c6b468d907ca2d9afede256f068f87e34d23dd221c07d13d4c234726afb"},
|
||||
{file = "Authlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:9637e4de1fb498310a56900b3e2043a206b03cb11c05422014b0302cbc814be3"},
|
||||
{file = "Authlib-1.3.0.tar.gz", hash = "sha256:959ea62a5b7b5123c5059758296122b57cd2585ae2ed1c0622c21b371ffdae06"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cryptography = ">=3.2"
|
||||
cryptography = "*"
|
||||
|
||||
[[package]]
|
||||
name = "automat"
|
||||
@@ -832,13 +832,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "immutabledict"
|
||||
version = "4.0.0"
|
||||
version = "4.1.0"
|
||||
description = "Immutable wrapper around dictionaries (a fork of frozendict)"
|
||||
optional = false
|
||||
python-versions = ">=3.8,<4.0"
|
||||
files = [
|
||||
{file = "immutabledict-4.0.0-py3-none-any.whl", hash = "sha256:7b28ffd8a0fbd7c6068ba8ba7a6aa0e50a158e9aae33b22d1dedd03f9aac33b6"},
|
||||
{file = "immutabledict-4.0.0.tar.gz", hash = "sha256:fabf47437531e8bf65a3b5b47d501e65579323b2d1fe58f8ae01491c1fd29bf7"},
|
||||
{file = "immutabledict-4.1.0-py3-none-any.whl", hash = "sha256:c176e99aa90aedb81716ad35218bb2055d049b549626db4523dbe011cf2f32ac"},
|
||||
{file = "immutabledict-4.1.0.tar.gz", hash = "sha256:93d100ccd2cd09a1fd3f136b9328c6e59529ba341de8bb499437f6819159fe8a"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1096,110 +1096,96 @@ pyasn1 = ">=0.4.6"
|
||||
|
||||
[[package]]
|
||||
name = "lxml"
|
||||
version = "4.9.3"
|
||||
version = "5.1.0"
|
||||
description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
|
||||
optional = true
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*"
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "lxml-4.9.3-cp27-cp27m-macosx_11_0_x86_64.whl", hash = "sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c"},
|
||||
{file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d"},
|
||||
{file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef"},
|
||||
{file = "lxml-4.9.3-cp27-cp27m-win32.whl", hash = "sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7"},
|
||||
{file = "lxml-4.9.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1"},
|
||||
{file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb"},
|
||||
{file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e"},
|
||||
{file = "lxml-4.9.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991"},
|
||||
{file = "lxml-4.9.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd"},
|
||||
{file = "lxml-4.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c"},
|
||||
{file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8"},
|
||||
{file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76"},
|
||||
{file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23"},
|
||||
{file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f"},
|
||||
{file = "lxml-4.9.3-cp310-cp310-win32.whl", hash = "sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85"},
|
||||
{file = "lxml-4.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d"},
|
||||
{file = "lxml-4.9.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5"},
|
||||
{file = "lxml-4.9.3-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf"},
|
||||
{file = "lxml-4.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a"},
|
||||
{file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f"},
|
||||
{file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b"},
|
||||
{file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120"},
|
||||
{file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6"},
|
||||
{file = "lxml-4.9.3-cp311-cp311-win32.whl", hash = "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305"},
|
||||
{file = "lxml-4.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc"},
|
||||
{file = "lxml-4.9.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4"},
|
||||
{file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be"},
|
||||
{file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13"},
|
||||
{file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9"},
|
||||
{file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5"},
|
||||
{file = "lxml-4.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8"},
|
||||
{file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7"},
|
||||
{file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2"},
|
||||
{file = "lxml-4.9.3-cp35-cp35m-win32.whl", hash = "sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d"},
|
||||
{file = "lxml-4.9.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833"},
|
||||
{file = "lxml-4.9.3-cp36-cp36m-macosx_11_0_x86_64.whl", hash = "sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12"},
|
||||
{file = "lxml-4.9.3-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5"},
|
||||
{file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98"},
|
||||
{file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190"},
|
||||
{file = "lxml-4.9.3-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2"},
|
||||
{file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c"},
|
||||
{file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584"},
|
||||
{file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287"},
|
||||
{file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458"},
|
||||
{file = "lxml-4.9.3-cp36-cp36m-win32.whl", hash = "sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477"},
|
||||
{file = "lxml-4.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf"},
|
||||
{file = "lxml-4.9.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601"},
|
||||
{file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129"},
|
||||
{file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4"},
|
||||
{file = "lxml-4.9.3-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d"},
|
||||
{file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693"},
|
||||
{file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4"},
|
||||
{file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a"},
|
||||
{file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02"},
|
||||
{file = "lxml-4.9.3-cp37-cp37m-win32.whl", hash = "sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f"},
|
||||
{file = "lxml-4.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52"},
|
||||
{file = "lxml-4.9.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc"},
|
||||
{file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac"},
|
||||
{file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db"},
|
||||
{file = "lxml-4.9.3-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce"},
|
||||
{file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42"},
|
||||
{file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa"},
|
||||
{file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40"},
|
||||
{file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7"},
|
||||
{file = "lxml-4.9.3-cp38-cp38-win32.whl", hash = "sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574"},
|
||||
{file = "lxml-4.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96"},
|
||||
{file = "lxml-4.9.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340"},
|
||||
{file = "lxml-4.9.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7"},
|
||||
{file = "lxml-4.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b"},
|
||||
{file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da"},
|
||||
{file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e"},
|
||||
{file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d"},
|
||||
{file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432"},
|
||||
{file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69"},
|
||||
{file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50"},
|
||||
{file = "lxml-4.9.3-cp39-cp39-win32.whl", hash = "sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2"},
|
||||
{file = "lxml-4.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2"},
|
||||
{file = "lxml-4.9.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35"},
|
||||
{file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0"},
|
||||
{file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3"},
|
||||
{file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b"},
|
||||
{file = "lxml-4.9.3-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b"},
|
||||
{file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7"},
|
||||
{file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d"},
|
||||
{file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b"},
|
||||
{file = "lxml-4.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a"},
|
||||
{file = "lxml-4.9.3-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0"},
|
||||
{file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694"},
|
||||
{file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7"},
|
||||
{file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4"},
|
||||
{file = "lxml-4.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9"},
|
||||
{file = "lxml-4.9.3.tar.gz", hash = "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c"},
|
||||
{file = "lxml-5.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:704f5572ff473a5f897745abebc6df40f22d4133c1e0a1f124e4f2bd3330ff7e"},
|
||||
{file = "lxml-5.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d3c0f8567ffe7502d969c2c1b809892dc793b5d0665f602aad19895f8d508da"},
|
||||
{file = "lxml-5.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5fcfbebdb0c5d8d18b84118842f31965d59ee3e66996ac842e21f957eb76138c"},
|
||||
{file = "lxml-5.1.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f37c6d7106a9d6f0708d4e164b707037b7380fcd0b04c5bd9cae1fb46a856fb"},
|
||||
{file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2befa20a13f1a75c751f47e00929fb3433d67eb9923c2c0b364de449121f447c"},
|
||||
{file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22b7ee4c35f374e2c20337a95502057964d7e35b996b1c667b5c65c567d2252a"},
|
||||
{file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bf8443781533b8d37b295016a4b53c1494fa9a03573c09ca5104550c138d5c05"},
|
||||
{file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82bddf0e72cb2af3cbba7cec1d2fd11fda0de6be8f4492223d4a268713ef2147"},
|
||||
{file = "lxml-5.1.0-cp310-cp310-win32.whl", hash = "sha256:b66aa6357b265670bb574f050ffceefb98549c721cf28351b748be1ef9577d93"},
|
||||
{file = "lxml-5.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:4946e7f59b7b6a9e27bef34422f645e9a368cb2be11bf1ef3cafc39a1f6ba68d"},
|
||||
{file = "lxml-5.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:14deca1460b4b0f6b01f1ddc9557704e8b365f55c63070463f6c18619ebf964f"},
|
||||
{file = "lxml-5.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed8c3d2cd329bf779b7ed38db176738f3f8be637bb395ce9629fc76f78afe3d4"},
|
||||
{file = "lxml-5.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:436a943c2900bb98123b06437cdd30580a61340fbdb7b28aaf345a459c19046a"},
|
||||
{file = "lxml-5.1.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acb6b2f96f60f70e7f34efe0c3ea34ca63f19ca63ce90019c6cbca6b676e81fa"},
|
||||
{file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af8920ce4a55ff41167ddbc20077f5698c2e710ad3353d32a07d3264f3a2021e"},
|
||||
{file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cfced4a069003d8913408e10ca8ed092c49a7f6cefee9bb74b6b3e860683b45"},
|
||||
{file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9e5ac3437746189a9b4121db2a7b86056ac8786b12e88838696899328fc44bb2"},
|
||||
{file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4c9bda132ad108b387c33fabfea47866af87f4ea6ffb79418004f0521e63204"},
|
||||
{file = "lxml-5.1.0-cp311-cp311-win32.whl", hash = "sha256:bc64d1b1dab08f679fb89c368f4c05693f58a9faf744c4d390d7ed1d8223869b"},
|
||||
{file = "lxml-5.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5ab722ae5a873d8dcee1f5f45ddd93c34210aed44ff2dc643b5025981908cda"},
|
||||
{file = "lxml-5.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9aa543980ab1fbf1720969af1d99095a548ea42e00361e727c58a40832439114"},
|
||||
{file = "lxml-5.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6f11b77ec0979f7e4dc5ae081325a2946f1fe424148d3945f943ceaede98adb8"},
|
||||
{file = "lxml-5.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a36c506e5f8aeb40680491d39ed94670487ce6614b9d27cabe45d94cd5d63e1e"},
|
||||
{file = "lxml-5.1.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f643ffd2669ffd4b5a3e9b41c909b72b2a1d5e4915da90a77e119b8d48ce867a"},
|
||||
{file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16dd953fb719f0ffc5bc067428fc9e88f599e15723a85618c45847c96f11f431"},
|
||||
{file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16018f7099245157564d7148165132c70adb272fb5a17c048ba70d9cc542a1a1"},
|
||||
{file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82cd34f1081ae4ea2ede3d52f71b7be313756e99b4b5f829f89b12da552d3aa3"},
|
||||
{file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:19a1bc898ae9f06bccb7c3e1dfd73897ecbbd2c96afe9095a6026016e5ca97b8"},
|
||||
{file = "lxml-5.1.0-cp312-cp312-win32.whl", hash = "sha256:13521a321a25c641b9ea127ef478b580b5ec82aa2e9fc076c86169d161798b01"},
|
||||
{file = "lxml-5.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:1ad17c20e3666c035db502c78b86e58ff6b5991906e55bdbef94977700c72623"},
|
||||
{file = "lxml-5.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:24ef5a4631c0b6cceaf2dbca21687e29725b7c4e171f33a8f8ce23c12558ded1"},
|
||||
{file = "lxml-5.1.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d2900b7f5318bc7ad8631d3d40190b95ef2aa8cc59473b73b294e4a55e9f30f"},
|
||||
{file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:601f4a75797d7a770daed8b42b97cd1bb1ba18bd51a9382077a6a247a12aa38d"},
|
||||
{file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4b68c961b5cc402cbd99cca5eb2547e46ce77260eb705f4d117fd9c3f932b95"},
|
||||
{file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:afd825e30f8d1f521713a5669b63657bcfe5980a916c95855060048b88e1adb7"},
|
||||
{file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:262bc5f512a66b527d026518507e78c2f9c2bd9eb5c8aeeb9f0eb43fcb69dc67"},
|
||||
{file = "lxml-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:e856c1c7255c739434489ec9c8aa9cdf5179785d10ff20add308b5d673bed5cd"},
|
||||
{file = "lxml-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c7257171bb8d4432fe9d6fdde4d55fdbe663a63636a17f7f9aaba9bcb3153ad7"},
|
||||
{file = "lxml-5.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b9e240ae0ba96477682aa87899d94ddec1cc7926f9df29b1dd57b39e797d5ab5"},
|
||||
{file = "lxml-5.1.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a96f02ba1bcd330807fc060ed91d1f7a20853da6dd449e5da4b09bfcc08fdcf5"},
|
||||
{file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3898ae2b58eeafedfe99e542a17859017d72d7f6a63de0f04f99c2cb125936"},
|
||||
{file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61c5a7edbd7c695e54fca029ceb351fc45cd8860119a0f83e48be44e1c464862"},
|
||||
{file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3aeca824b38ca78d9ee2ab82bd9883083d0492d9d17df065ba3b94e88e4d7ee6"},
|
||||
{file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8f52fe6859b9db71ee609b0c0a70fea5f1e71c3462ecf144ca800d3f434f0764"},
|
||||
{file = "lxml-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:d42e3a3fc18acc88b838efded0e6ec3edf3e328a58c68fbd36a7263a874906c8"},
|
||||
{file = "lxml-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:eac68f96539b32fce2c9b47eb7c25bb2582bdaf1bbb360d25f564ee9e04c542b"},
|
||||
{file = "lxml-5.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ae15347a88cf8af0949a9872b57a320d2605ae069bcdf047677318bc0bba45b1"},
|
||||
{file = "lxml-5.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c26aab6ea9c54d3bed716b8851c8bfc40cb249b8e9880e250d1eddde9f709bf5"},
|
||||
{file = "lxml-5.1.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:342e95bddec3a698ac24378d61996b3ee5ba9acfeb253986002ac53c9a5f6f84"},
|
||||
{file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:725e171e0b99a66ec8605ac77fa12239dbe061482ac854d25720e2294652eeaa"},
|
||||
{file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d184e0d5c918cff04cdde9dbdf9600e960161d773666958c9d7b565ccc60c45"},
|
||||
{file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:98f3f020a2b736566c707c8e034945c02aa94e124c24f77ca097c446f81b01f1"},
|
||||
{file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d48fc57e7c1e3df57be5ae8614bab6d4e7b60f65c5457915c26892c41afc59e"},
|
||||
{file = "lxml-5.1.0-cp38-cp38-win32.whl", hash = "sha256:7ec465e6549ed97e9f1e5ed51c657c9ede767bc1c11552f7f4d022c4df4a977a"},
|
||||
{file = "lxml-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:b21b4031b53d25b0858d4e124f2f9131ffc1530431c6d1321805c90da78388d1"},
|
||||
{file = "lxml-5.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:52427a7eadc98f9e62cb1368a5079ae826f94f05755d2d567d93ee1bc3ceb354"},
|
||||
{file = "lxml-5.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a2a2c724d97c1eb8cf966b16ca2915566a4904b9aad2ed9a09c748ffe14f969"},
|
||||
{file = "lxml-5.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:843b9c835580d52828d8f69ea4302537337a21e6b4f1ec711a52241ba4a824f3"},
|
||||
{file = "lxml-5.1.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b99f564659cfa704a2dd82d0684207b1aadf7d02d33e54845f9fc78e06b7581"},
|
||||
{file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8b0c78e7aac24979ef09b7f50da871c2de2def043d468c4b41f512d831e912"},
|
||||
{file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bcf86dfc8ff3e992fed847c077bd875d9e0ba2fa25d859c3a0f0f76f07f0c8d"},
|
||||
{file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:49a9b4af45e8b925e1cd6f3b15bbba2c81e7dba6dce170c677c9cda547411e14"},
|
||||
{file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:280f3edf15c2a967d923bcfb1f8f15337ad36f93525828b40a0f9d6c2ad24890"},
|
||||
{file = "lxml-5.1.0-cp39-cp39-win32.whl", hash = "sha256:ed7326563024b6e91fef6b6c7a1a2ff0a71b97793ac33dbbcf38f6005e51ff6e"},
|
||||
{file = "lxml-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:8d7b4beebb178e9183138f552238f7e6613162a42164233e2bda00cb3afac58f"},
|
||||
{file = "lxml-5.1.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9bd0ae7cc2b85320abd5e0abad5ccee5564ed5f0cc90245d2f9a8ef330a8deae"},
|
||||
{file = "lxml-5.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c1d679df4361408b628f42b26a5d62bd3e9ba7f0c0e7969f925021554755aa"},
|
||||
{file = "lxml-5.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2ad3a8ce9e8a767131061a22cd28fdffa3cd2dc193f399ff7b81777f3520e372"},
|
||||
{file = "lxml-5.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:304128394c9c22b6569eba2a6d98392b56fbdfbad58f83ea702530be80d0f9df"},
|
||||
{file = "lxml-5.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d74fcaf87132ffc0447b3c685a9f862ffb5b43e70ea6beec2fb8057d5d2a1fea"},
|
||||
{file = "lxml-5.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:8cf5877f7ed384dabfdcc37922c3191bf27e55b498fecece9fd5c2c7aaa34c33"},
|
||||
{file = "lxml-5.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:877efb968c3d7eb2dad540b6cabf2f1d3c0fbf4b2d309a3c141f79c7e0061324"},
|
||||
{file = "lxml-5.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f14a4fb1c1c402a22e6a341a24c1341b4a3def81b41cd354386dcb795f83897"},
|
||||
{file = "lxml-5.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:25663d6e99659544ee8fe1b89b1a8c0aaa5e34b103fab124b17fa958c4a324a6"},
|
||||
{file = "lxml-5.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8b9f19df998761babaa7f09e6bc169294eefafd6149aaa272081cbddc7ba4ca3"},
|
||||
{file = "lxml-5.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e53d7e6a98b64fe54775d23a7c669763451340c3d44ad5e3a3b48a1efbdc96f"},
|
||||
{file = "lxml-5.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c3cd1fc1dc7c376c54440aeaaa0dcc803d2126732ff5c6b68ccd619f2e64be4f"},
|
||||
{file = "lxml-5.1.0.tar.gz", hash = "sha256:3eea6ed6e6c918e468e693c41ef07f3c3acc310b70ddd9cc72d9ef84bc9564ca"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
cssselect = ["cssselect (>=0.7)"]
|
||||
html5 = ["html5lib"]
|
||||
htmlsoup = ["BeautifulSoup4"]
|
||||
source = ["Cython (>=0.29.35)"]
|
||||
source = ["Cython (>=3.0.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "lxml-stubs"
|
||||
@@ -1616,70 +1602,88 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "10.1.0"
|
||||
version = "10.2.0"
|
||||
description = "Python Imaging Library (Fork)"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "Pillow-10.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1ab05f3db77e98f93964697c8efc49c7954b08dd61cff526b7f2531a22410106"},
|
||||
{file = "Pillow-10.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6932a7652464746fcb484f7fc3618e6503d2066d853f68a4bd97193a3996e273"},
|
||||
{file = "Pillow-10.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f63b5a68daedc54c7c3464508d8c12075e56dcfbd42f8c1bf40169061ae666"},
|
||||
{file = "Pillow-10.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0949b55eb607898e28eaccb525ab104b2d86542a85c74baf3a6dc24002edec2"},
|
||||
{file = "Pillow-10.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ae88931f93214777c7a3aa0a8f92a683f83ecde27f65a45f95f22d289a69e593"},
|
||||
{file = "Pillow-10.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b0eb01ca85b2361b09480784a7931fc648ed8b7836f01fb9241141b968feb1db"},
|
||||
{file = "Pillow-10.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d27b5997bdd2eb9fb199982bb7eb6164db0426904020dc38c10203187ae2ff2f"},
|
||||
{file = "Pillow-10.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7df5608bc38bd37ef585ae9c38c9cd46d7c81498f086915b0f97255ea60c2818"},
|
||||
{file = "Pillow-10.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:41f67248d92a5e0a2076d3517d8d4b1e41a97e2df10eb8f93106c89107f38b57"},
|
||||
{file = "Pillow-10.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1fb29c07478e6c06a46b867e43b0bcdb241b44cc52be9bc25ce5944eed4648e7"},
|
||||
{file = "Pillow-10.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2cdc65a46e74514ce742c2013cd4a2d12e8553e3a2563c64879f7c7e4d28bce7"},
|
||||
{file = "Pillow-10.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50d08cd0a2ecd2a8657bd3d82c71efd5a58edb04d9308185d66c3a5a5bed9610"},
|
||||
{file = "Pillow-10.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:062a1610e3bc258bff2328ec43f34244fcec972ee0717200cb1425214fe5b839"},
|
||||
{file = "Pillow-10.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:61f1a9d247317fa08a308daaa8ee7b3f760ab1809ca2da14ecc88ae4257d6172"},
|
||||
{file = "Pillow-10.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a646e48de237d860c36e0db37ecaecaa3619e6f3e9d5319e527ccbc8151df061"},
|
||||
{file = "Pillow-10.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:47e5bf85b80abc03be7455c95b6d6e4896a62f6541c1f2ce77a7d2bb832af262"},
|
||||
{file = "Pillow-10.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a92386125e9ee90381c3369f57a2a50fa9e6aa8b1cf1d9c4b200d41a7dd8e992"},
|
||||
{file = "Pillow-10.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:0f7c276c05a9767e877a0b4c5050c8bee6a6d960d7f0c11ebda6b99746068c2a"},
|
||||
{file = "Pillow-10.1.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:a89b8312d51715b510a4fe9fc13686283f376cfd5abca8cd1c65e4c76e21081b"},
|
||||
{file = "Pillow-10.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:00f438bb841382b15d7deb9a05cc946ee0f2c352653c7aa659e75e592f6fa17d"},
|
||||
{file = "Pillow-10.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d929a19f5469b3f4df33a3df2983db070ebb2088a1e145e18facbc28cae5b27"},
|
||||
{file = "Pillow-10.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a92109192b360634a4489c0c756364c0c3a2992906752165ecb50544c251312"},
|
||||
{file = "Pillow-10.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:0248f86b3ea061e67817c47ecbe82c23f9dd5d5226200eb9090b3873d3ca32de"},
|
||||
{file = "Pillow-10.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9882a7451c680c12f232a422730f986a1fcd808da0fd428f08b671237237d651"},
|
||||
{file = "Pillow-10.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1c3ac5423c8c1da5928aa12c6e258921956757d976405e9467c5f39d1d577a4b"},
|
||||
{file = "Pillow-10.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:806abdd8249ba3953c33742506fe414880bad78ac25cc9a9b1c6ae97bedd573f"},
|
||||
{file = "Pillow-10.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:eaed6977fa73408b7b8a24e8b14e59e1668cfc0f4c40193ea7ced8e210adf996"},
|
||||
{file = "Pillow-10.1.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:fe1e26e1ffc38be097f0ba1d0d07fcade2bcfd1d023cda5b29935ae8052bd793"},
|
||||
{file = "Pillow-10.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7a7e3daa202beb61821c06d2517428e8e7c1aab08943e92ec9e5755c2fc9ba5e"},
|
||||
{file = "Pillow-10.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24fadc71218ad2b8ffe437b54876c9382b4a29e030a05a9879f615091f42ffc2"},
|
||||
{file = "Pillow-10.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1d323703cfdac2036af05191b969b910d8f115cf53093125e4058f62012c9a"},
|
||||
{file = "Pillow-10.1.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:912e3812a1dbbc834da2b32299b124b5ddcb664ed354916fd1ed6f193f0e2d01"},
|
||||
{file = "Pillow-10.1.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7dbaa3c7de82ef37e7708521be41db5565004258ca76945ad74a8e998c30af8d"},
|
||||
{file = "Pillow-10.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9d7bc666bd8c5a4225e7ac71f2f9d12466ec555e89092728ea0f5c0c2422ea80"},
|
||||
{file = "Pillow-10.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baada14941c83079bf84c037e2d8b7506ce201e92e3d2fa0d1303507a8538212"},
|
||||
{file = "Pillow-10.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:2ef6721c97894a7aa77723740a09547197533146fba8355e86d6d9a4a1056b14"},
|
||||
{file = "Pillow-10.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0a026c188be3b443916179f5d04548092e253beb0c3e2ee0a4e2cdad72f66099"},
|
||||
{file = "Pillow-10.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:04f6f6149f266a100374ca3cc368b67fb27c4af9f1cc8cb6306d849dcdf12616"},
|
||||
{file = "Pillow-10.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb40c011447712d2e19cc261c82655f75f32cb724788df315ed992a4d65696bb"},
|
||||
{file = "Pillow-10.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a8413794b4ad9719346cd9306118450b7b00d9a15846451549314a58ac42219"},
|
||||
{file = "Pillow-10.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c9aeea7b63edb7884b031a35305629a7593272b54f429a9869a4f63a1bf04c34"},
|
||||
{file = "Pillow-10.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b4005fee46ed9be0b8fb42be0c20e79411533d1fd58edabebc0dd24626882cfd"},
|
||||
{file = "Pillow-10.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4d0152565c6aa6ebbfb1e5d8624140a440f2b99bf7afaafbdbf6430426497f28"},
|
||||
{file = "Pillow-10.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d921bc90b1defa55c9917ca6b6b71430e4286fc9e44c55ead78ca1a9f9eba5f2"},
|
||||
{file = "Pillow-10.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cfe96560c6ce2f4c07d6647af2d0f3c54cc33289894ebd88cfbb3bcd5391e256"},
|
||||
{file = "Pillow-10.1.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:937bdc5a7f5343d1c97dc98149a0be7eb9704e937fe3dc7140e229ae4fc572a7"},
|
||||
{file = "Pillow-10.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1c25762197144e211efb5f4e8ad656f36c8d214d390585d1d21281f46d556ba"},
|
||||
{file = "Pillow-10.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:afc8eef765d948543a4775f00b7b8c079b3321d6b675dde0d02afa2ee23000b4"},
|
||||
{file = "Pillow-10.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:883f216eac8712b83a63f41b76ddfb7b2afab1b74abbb413c5df6680f071a6b9"},
|
||||
{file = "Pillow-10.1.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b920e4d028f6442bea9a75b7491c063f0b9a3972520731ed26c83e254302eb1e"},
|
||||
{file = "Pillow-10.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c41d960babf951e01a49c9746f92c5a7e0d939d1652d7ba30f6b3090f27e412"},
|
||||
{file = "Pillow-10.1.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1fafabe50a6977ac70dfe829b2d5735fd54e190ab55259ec8aea4aaea412fa0b"},
|
||||
{file = "Pillow-10.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3b834f4b16173e5b92ab6566f0473bfb09f939ba14b23b8da1f54fa63e4b623f"},
|
||||
{file = "Pillow-10.1.0.tar.gz", hash = "sha256:e6bf8de6c36ed96c86ea3b6e1d5273c53f46ef518a062464cd7ef5dd2cf92e38"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"},
|
||||
{file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"},
|
||||
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"},
|
||||
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"},
|
||||
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"},
|
||||
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"},
|
||||
{file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"},
|
||||
{file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"},
|
||||
{file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"},
|
||||
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"},
|
||||
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"},
|
||||
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"},
|
||||
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"},
|
||||
{file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"},
|
||||
{file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
|
||||
fpx = ["olefile"]
|
||||
mic = ["olefile"]
|
||||
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
|
||||
typing = ["typing-extensions"]
|
||||
xmp = ["defusedxml"]
|
||||
|
||||
[[package]]
|
||||
name = "pkginfo"
|
||||
@@ -2475,13 +2479,13 @@ doc = ["Sphinx", "sphinx-rtd-theme"]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "1.35.0"
|
||||
version = "1.39.1"
|
||||
description = "Python client for Sentry (https://sentry.io)"
|
||||
optional = true
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "sentry-sdk-1.35.0.tar.gz", hash = "sha256:04e392db9a0d59bd49a51b9e3a92410ac5867556820465057c2ef89a38e953e9"},
|
||||
{file = "sentry_sdk-1.35.0-py2.py3-none-any.whl", hash = "sha256:a7865952701e46d38b41315c16c075367675c48d049b90a4cc2e41991ebc7efa"},
|
||||
{file = "sentry-sdk-1.39.1.tar.gz", hash = "sha256:320a55cdf9da9097a0bead239c35b7e61f53660ef9878861824fd6d9b2eaf3b5"},
|
||||
{file = "sentry_sdk-1.39.1-py2.py3-none-any.whl", hash = "sha256:81b5b9ffdd1a374e9eb0c053b5d2012155db9cbe76393a8585677b753bd5fdc1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2520,13 +2524,13 @@ tornado = ["tornado (>=5)"]
|
||||
|
||||
[[package]]
|
||||
name = "service-identity"
|
||||
version = "23.1.0"
|
||||
version = "24.1.0"
|
||||
description = "Service identity verification for pyOpenSSL & cryptography."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "service_identity-23.1.0-py3-none-any.whl", hash = "sha256:87415a691d52fcad954a500cb81f424d0273f8e7e3ee7d766128f4575080f383"},
|
||||
{file = "service_identity-23.1.0.tar.gz", hash = "sha256:ecb33cd96307755041e978ab14f8b14e13b40f1fbd525a4dc78f46d2b986431d"},
|
||||
{file = "service_identity-24.1.0-py3-none-any.whl", hash = "sha256:a28caf8130c8a5c1c7a6f5293faaf239bbfb7751e4862436920ee6f2616f568a"},
|
||||
{file = "service_identity-24.1.0.tar.gz", hash = "sha256:6829c9d62fb832c2e1c435629b0a8c476e1929881f28bee4d20bc24161009221"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2536,7 +2540,7 @@ pyasn1 = "*"
|
||||
pyasn1-modules = "*"
|
||||
|
||||
[package.extras]
|
||||
dev = ["pyopenssl", "service-identity[docs,idna,mypy,tests]"]
|
||||
dev = ["pyopenssl", "service-identity[idna,mypy,tests]"]
|
||||
docs = ["furo", "myst-parser", "pyopenssl", "sphinx", "sphinx-notfound-page"]
|
||||
idna = ["idna"]
|
||||
mypy = ["idna", "mypy", "types-pyopenssl"]
|
||||
@@ -3037,24 +3041,24 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "types-commonmark"
|
||||
version = "0.9.2.4"
|
||||
version = "0.9.2.20240106"
|
||||
description = "Typing stubs for commonmark"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "types-commonmark-0.9.2.4.tar.gz", hash = "sha256:2c6486f65735cf18215cca3e962b17787fa545be279306f79b801f64a5319959"},
|
||||
{file = "types_commonmark-0.9.2.4-py3-none-any.whl", hash = "sha256:d5090fa685c3e3c0ec3a5973ff842000baef6d86f762d52209b3c5e9fbd0b555"},
|
||||
{file = "types-commonmark-0.9.2.20240106.tar.gz", hash = "sha256:52a062b71766d6ab258fca2d8e19fb0853796e25ca9afa9d0f67a1e42c93479f"},
|
||||
{file = "types_commonmark-0.9.2.20240106-py3-none-any.whl", hash = "sha256:606d9de1e3a96cab0b1c0b6cccf4df099116148d1d864d115fde2e27ad6877c3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-jsonschema"
|
||||
version = "4.20.0.0"
|
||||
version = "4.20.0.20240105"
|
||||
description = "Typing stubs for jsonschema"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "types-jsonschema-4.20.0.0.tar.gz", hash = "sha256:0de1032d243f1d3dba8b745ad84efe8c1af71665a9deb1827636ac535dcb79c1"},
|
||||
{file = "types_jsonschema-4.20.0.0-py3-none-any.whl", hash = "sha256:e6d5df18aaca4412f0aae246a294761a92040e93d7bc840f002b7329a8b72d26"},
|
||||
{file = "types-jsonschema-4.20.0.20240105.tar.gz", hash = "sha256:4a71af7e904498e7ad055149f6dc1eee04153b59a99ad7dd17aa3769c9bc5982"},
|
||||
{file = "types_jsonschema-4.20.0.20240105-py3-none-any.whl", hash = "sha256:26706cd70a273e59e718074c4e756608a25ba61327a7f9a4493ebd11941e5ad4"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -3156,13 +3160,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.8.0"
|
||||
version = "4.9.0"
|
||||
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"},
|
||||
{file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"},
|
||||
{file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"},
|
||||
{file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+1
-1
@@ -96,7 +96,7 @@ module-name = "synapse.synapse_rust"
|
||||
|
||||
[tool.poetry]
|
||||
name = "matrix-synapse"
|
||||
version = "1.98.0"
|
||||
version = "1.99.0rc1"
|
||||
description = "Homeserver for the Matrix decentralised comms protocol"
|
||||
authors = ["Matrix.org Team and Contributors <packages@matrix.org>"]
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
/*
|
||||
* This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
*
|
||||
* Copyright (C) 2024 New Vector, Ltd
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* See the GNU Affero General Public License for more details:
|
||||
* <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
*
|
||||
* Originally licensed under the Apache License, Version 2.0:
|
||||
* <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*
|
||||
* [This file includes modifications made by New Vector Limited]
|
||||
*
|
||||
*/
|
||||
|
||||
//! Implements the internal metadata class attached to events.
|
||||
//!
|
||||
//! The internal metadata is a bit like a `TypedDict`, in that it is stored as a
|
||||
//! JSON dict in the DB. Most events have zero, or only a few, of these keys
|
||||
//! set. Therefore, since we care more about memory size than performance here,
|
||||
//! we store these fields in a mapping.
|
||||
//!
|
||||
//! We want to store (most) of the fields as Rust objects, so we implement the
|
||||
//! mapping by using a vec of enums. This is less efficient than using
|
||||
//! attributes, but for small number of keys is actually faster than using a
|
||||
//! hash or btree map.
|
||||
|
||||
use std::{num::NonZeroI64, ops::Deref};
|
||||
|
||||
use anyhow::Context;
|
||||
use log::warn;
|
||||
use pyo3::{
|
||||
exceptions::PyAttributeError,
|
||||
pyclass, pymethods,
|
||||
types::{PyDict, PyString},
|
||||
IntoPy, PyAny, PyObject, PyResult, Python,
|
||||
};
|
||||
|
||||
/// Definitions of the various fields of the internal metadata.
|
||||
#[derive(Clone)]
|
||||
enum EventInternalMetadataData {
|
||||
OutOfBandMembership(bool),
|
||||
SendOnBehalfOf(Box<str>),
|
||||
RecheckRedaction(bool),
|
||||
SoftFailed(bool),
|
||||
ProactivelySend(bool),
|
||||
Redacted(bool),
|
||||
TxnId(Box<str>),
|
||||
TokenId(i64),
|
||||
DeviceId(Box<str>),
|
||||
}
|
||||
|
||||
impl EventInternalMetadataData {
|
||||
/// Convert the field to its name and python object.
|
||||
fn to_python_pair<'a>(&self, py: Python<'a>) -> (&'a PyString, PyObject) {
|
||||
match self {
|
||||
EventInternalMetadataData::OutOfBandMembership(o) => {
|
||||
(pyo3::intern!(py, "out_of_band_membership"), o.into_py(py))
|
||||
}
|
||||
EventInternalMetadataData::SendOnBehalfOf(o) => {
|
||||
(pyo3::intern!(py, "send_on_behalf_of"), o.into_py(py))
|
||||
}
|
||||
EventInternalMetadataData::RecheckRedaction(o) => {
|
||||
(pyo3::intern!(py, "recheck_redaction"), o.into_py(py))
|
||||
}
|
||||
EventInternalMetadataData::SoftFailed(o) => {
|
||||
(pyo3::intern!(py, "soft_failed"), o.into_py(py))
|
||||
}
|
||||
EventInternalMetadataData::ProactivelySend(o) => {
|
||||
(pyo3::intern!(py, "proactively_send"), o.into_py(py))
|
||||
}
|
||||
EventInternalMetadataData::Redacted(o) => {
|
||||
(pyo3::intern!(py, "redacted"), o.into_py(py))
|
||||
}
|
||||
EventInternalMetadataData::TxnId(o) => (pyo3::intern!(py, "txn_id"), o.into_py(py)),
|
||||
EventInternalMetadataData::TokenId(o) => (pyo3::intern!(py, "token_id"), o.into_py(py)),
|
||||
EventInternalMetadataData::DeviceId(o) => {
|
||||
(pyo3::intern!(py, "device_id"), o.into_py(py))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts from python key/values to the field.
|
||||
///
|
||||
/// Returns `None` if the key is a valid but unrecognized string.
|
||||
fn from_python_pair(key: &PyAny, value: &PyAny) -> PyResult<Option<Self>> {
|
||||
let key_str: &str = key.extract()?;
|
||||
|
||||
let e = match key_str {
|
||||
"out_of_band_membership" => EventInternalMetadataData::OutOfBandMembership(
|
||||
value
|
||||
.extract()
|
||||
.with_context(|| format!("'{key_str}' has invalid type"))?,
|
||||
),
|
||||
|
||||
"send_on_behalf_of" => EventInternalMetadataData::SendOnBehalfOf(
|
||||
value
|
||||
.extract()
|
||||
.map(String::into_boxed_str)
|
||||
.with_context(|| format!("'{key_str}' has invalid type"))?,
|
||||
),
|
||||
"recheck_redaction" => EventInternalMetadataData::RecheckRedaction(
|
||||
value
|
||||
.extract()
|
||||
.with_context(|| format!("'{key_str}' has invalid type"))?,
|
||||
),
|
||||
"soft_failed" => EventInternalMetadataData::SoftFailed(
|
||||
value
|
||||
.extract()
|
||||
.with_context(|| format!("'{key_str}' has invalid type"))?,
|
||||
),
|
||||
"proactively_send" => EventInternalMetadataData::ProactivelySend(
|
||||
value
|
||||
.extract()
|
||||
.with_context(|| format!("'{key_str}' has invalid type"))?,
|
||||
),
|
||||
"redacted" => EventInternalMetadataData::Redacted(
|
||||
value
|
||||
.extract()
|
||||
.with_context(|| format!("'{key_str}' has invalid type"))?,
|
||||
),
|
||||
"txn_id" => EventInternalMetadataData::TxnId(
|
||||
value
|
||||
.extract()
|
||||
.map(String::into_boxed_str)
|
||||
.with_context(|| format!("'{key_str}' has invalid type"))?,
|
||||
),
|
||||
"token_id" => EventInternalMetadataData::TokenId(
|
||||
value
|
||||
.extract()
|
||||
.with_context(|| format!("'{key_str}' has invalid type"))?,
|
||||
),
|
||||
"device_id" => EventInternalMetadataData::DeviceId(
|
||||
value
|
||||
.extract()
|
||||
.map(String::into_boxed_str)
|
||||
.with_context(|| format!("'{key_str}' has invalid type"))?,
|
||||
),
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
Ok(Some(e))
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper macro to find the given field in internal metadata, returning None if
|
||||
/// not found.
|
||||
macro_rules! get_property_opt {
|
||||
($self:expr, $name:ident) => {
|
||||
$self.data.iter().find_map(|entry| {
|
||||
if let EventInternalMetadataData::$name(data) = entry {
|
||||
Some(data)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
/// Helper macro to find the given field in internal metadata, raising an
|
||||
/// attribute error if not found.
|
||||
macro_rules! get_property {
|
||||
($self:expr, $name:ident) => {
|
||||
get_property_opt!($self, $name).ok_or_else(|| {
|
||||
PyAttributeError::new_err(format!(
|
||||
"'EventInternalMetadata' has no attribute '{}'",
|
||||
stringify!($name),
|
||||
))
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
/// Helper macro to set the give field.
|
||||
macro_rules! set_property {
|
||||
($self:expr, $name:ident, $obj:expr) => {
|
||||
for entry in &mut $self.data {
|
||||
if let EventInternalMetadataData::$name(data) = entry {
|
||||
*data = $obj;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$self.data.push(EventInternalMetadataData::$name($obj))
|
||||
};
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
#[derive(Clone)]
|
||||
pub struct EventInternalMetadata {
|
||||
/// The fields of internal metadata. This functions as a mapping.
|
||||
data: Vec<EventInternalMetadataData>,
|
||||
|
||||
/// The stream ordering of this event. None, until it has been persisted.
|
||||
#[pyo3(get, set)]
|
||||
stream_ordering: Option<NonZeroI64>,
|
||||
|
||||
/// whether this event is an outlier (ie, whether we have the state at that
|
||||
/// point in the DAG)
|
||||
#[pyo3(get, set)]
|
||||
outlier: bool,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl EventInternalMetadata {
|
||||
#[new]
|
||||
fn new(dict: &PyDict) -> PyResult<Self> {
|
||||
let mut data = Vec::with_capacity(dict.len());
|
||||
|
||||
for (key, value) in dict.iter() {
|
||||
match EventInternalMetadataData::from_python_pair(key, value) {
|
||||
Ok(Some(entry)) => data.push(entry),
|
||||
Ok(None) => {}
|
||||
Err(err) => {
|
||||
warn!("Ignoring internal metadata field '{key}', as failed to convert to Rust due to {err}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data.shrink_to_fit();
|
||||
|
||||
Ok(EventInternalMetadata {
|
||||
data,
|
||||
stream_ordering: None,
|
||||
outlier: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn copy(&self) -> Self {
|
||||
self.clone()
|
||||
}
|
||||
|
||||
fn get_dict(&self, py: Python<'_>) -> PyResult<PyObject> {
|
||||
let dict = PyDict::new(py);
|
||||
|
||||
for entry in &self.data {
|
||||
let (key, value) = entry.to_python_pair(py);
|
||||
dict.set_item(key, value)?;
|
||||
}
|
||||
|
||||
Ok(dict.into())
|
||||
}
|
||||
|
||||
fn is_outlier(&self) -> bool {
|
||||
self.outlier
|
||||
}
|
||||
|
||||
/// Whether this event is an out-of-band membership.
|
||||
///
|
||||
/// OOB memberships are a special case of outlier events: they are
|
||||
/// membership events for federated rooms that we aren't full members of.
|
||||
/// Examples include invites received over federation, and rejections for
|
||||
/// such invites.
|
||||
///
|
||||
/// The concept of an OOB membership is needed because these events need to
|
||||
/// be processed as if they're new regular events (e.g. updating membership
|
||||
/// state in the database, relaying to clients via /sync, etc) despite being
|
||||
/// outliers.
|
||||
///
|
||||
/// See also
|
||||
/// https://element-hq.github.io/synapse/develop/development/room-dag-concepts.html#out-of-band-membership-events.
|
||||
///
|
||||
/// (Added in synapse 0.99.0, so may be unreliable for events received
|
||||
/// before that)
|
||||
fn is_out_of_band_membership(&self) -> bool {
|
||||
get_property_opt!(self, OutOfBandMembership)
|
||||
.copied()
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Whether this server should send the event on behalf of another server.
|
||||
/// This is used by the federation "send_join" API to forward the initial
|
||||
/// join event for a server in the room.
|
||||
///
|
||||
/// returns a str with the name of the server this event is sent on behalf
|
||||
/// of.
|
||||
fn get_send_on_behalf_of(&self) -> Option<&str> {
|
||||
let s = get_property_opt!(self, SendOnBehalfOf);
|
||||
s.map(|a| a.deref())
|
||||
}
|
||||
|
||||
/// Whether the redaction event needs to be rechecked when fetching
|
||||
/// from the database.
|
||||
///
|
||||
/// Starting in room v3 redaction events are accepted up front, and later
|
||||
/// checked to see if the redacter and redactee's domains match.
|
||||
///
|
||||
/// If the sender of the redaction event is allowed to redact any event
|
||||
/// due to auth rules, then this will always return false.
|
||||
fn need_to_check_redaction(&self) -> bool {
|
||||
get_property_opt!(self, RecheckRedaction)
|
||||
.copied()
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Whether the event has been soft failed.
|
||||
///
|
||||
/// Soft failed events should be handled as usual, except:
|
||||
/// 1. They should not go down sync or event streams, or generally sent to
|
||||
/// clients.
|
||||
/// 2. They should not be added to the forward extremities (and therefore
|
||||
/// not to current state).
|
||||
fn is_soft_failed(&self) -> bool {
|
||||
get_property_opt!(self, SoftFailed)
|
||||
.copied()
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Whether the event, if ours, should be sent to other clients and servers.
|
||||
///
|
||||
/// This is used for sending dummy events internally. Servers and clients
|
||||
/// can still explicitly fetch the event.
|
||||
fn should_proactively_send(&self) -> bool {
|
||||
get_property_opt!(self, ProactivelySend)
|
||||
.copied()
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
/// Whether the event has been redacted.
|
||||
///
|
||||
/// This is used for efficiently checking whether an event has been marked
|
||||
/// as redacted without needing to make another database call.
|
||||
fn is_redacted(&self) -> bool {
|
||||
get_property_opt!(self, Redacted).copied().unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Whether this event can trigger a push notification
|
||||
fn is_notifiable(&self) -> bool {
|
||||
!self.outlier || self.is_out_of_band_membership()
|
||||
}
|
||||
|
||||
// ** The following are the getters and setters of the various properties **
|
||||
|
||||
#[getter]
|
||||
fn get_out_of_band_membership(&self) -> PyResult<bool> {
|
||||
let bool = get_property!(self, OutOfBandMembership)?;
|
||||
Ok(*bool)
|
||||
}
|
||||
#[setter]
|
||||
fn set_out_of_band_membership(&mut self, obj: bool) {
|
||||
set_property!(self, OutOfBandMembership, obj);
|
||||
}
|
||||
|
||||
#[getter(send_on_behalf_of)]
|
||||
fn getter_send_on_behalf_of(&self) -> PyResult<&str> {
|
||||
let s = get_property!(self, SendOnBehalfOf)?;
|
||||
Ok(s)
|
||||
}
|
||||
#[setter]
|
||||
fn set_send_on_behalf_of(&mut self, obj: String) {
|
||||
set_property!(self, SendOnBehalfOf, obj.into_boxed_str());
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn get_recheck_redaction(&self) -> PyResult<bool> {
|
||||
let bool = get_property!(self, RecheckRedaction)?;
|
||||
Ok(*bool)
|
||||
}
|
||||
#[setter]
|
||||
fn set_recheck_redaction(&mut self, obj: bool) {
|
||||
set_property!(self, RecheckRedaction, obj);
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn get_soft_failed(&self) -> PyResult<bool> {
|
||||
let bool = get_property!(self, SoftFailed)?;
|
||||
Ok(*bool)
|
||||
}
|
||||
#[setter]
|
||||
fn set_soft_failed(&mut self, obj: bool) {
|
||||
set_property!(self, SoftFailed, obj);
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn get_proactively_send(&self) -> PyResult<bool> {
|
||||
let bool = get_property!(self, ProactivelySend)?;
|
||||
Ok(*bool)
|
||||
}
|
||||
#[setter]
|
||||
fn set_proactively_send(&mut self, obj: bool) {
|
||||
set_property!(self, ProactivelySend, obj);
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn get_redacted(&self) -> PyResult<bool> {
|
||||
let bool = get_property!(self, Redacted)?;
|
||||
Ok(*bool)
|
||||
}
|
||||
#[setter]
|
||||
fn set_redacted(&mut self, obj: bool) {
|
||||
set_property!(self, Redacted, obj);
|
||||
}
|
||||
|
||||
/// The transaction ID, if it was set when the event was created.
|
||||
#[getter]
|
||||
fn get_txn_id(&self) -> PyResult<&str> {
|
||||
let s = get_property!(self, TxnId)?;
|
||||
Ok(s)
|
||||
}
|
||||
#[setter]
|
||||
fn set_txn_id(&mut self, obj: String) {
|
||||
set_property!(self, TxnId, obj.into_boxed_str());
|
||||
}
|
||||
|
||||
/// The access token ID of the user who sent this event, if any.
|
||||
#[getter]
|
||||
fn get_token_id(&self) -> PyResult<i64> {
|
||||
let r = get_property!(self, TokenId)?;
|
||||
Ok(*r)
|
||||
}
|
||||
#[setter]
|
||||
fn set_token_id(&mut self, obj: i64) {
|
||||
set_property!(self, TokenId, obj);
|
||||
}
|
||||
|
||||
/// The device ID of the user who sent this event, if any.
|
||||
#[getter]
|
||||
fn get_device_id(&self) -> PyResult<&str> {
|
||||
let s = get_property!(self, DeviceId)?;
|
||||
Ok(s)
|
||||
}
|
||||
#[setter]
|
||||
fn set_device_id(&mut self, obj: String) {
|
||||
set_property!(self, DeviceId, obj.into_boxed_str());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
*
|
||||
* Copyright (C) 2024 New Vector, Ltd
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* See the GNU Affero General Public License for more details:
|
||||
* <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
*
|
||||
* Originally licensed under the Apache License, Version 2.0:
|
||||
* <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*
|
||||
* [This file includes modifications made by New Vector Limited]
|
||||
*
|
||||
*/
|
||||
|
||||
//! Classes for representing Events.
|
||||
|
||||
use pyo3::{types::PyModule, PyResult, Python};
|
||||
|
||||
mod internal_metadata;
|
||||
|
||||
/// Called when registering modules with python.
|
||||
pub fn register_module(py: Python<'_>, m: &PyModule) -> PyResult<()> {
|
||||
let child_module = PyModule::new(py, "events")?;
|
||||
child_module.add_class::<internal_metadata::EventInternalMetadata>()?;
|
||||
|
||||
m.add_submodule(child_module)?;
|
||||
|
||||
// We need to manually add the module to sys.modules to make `from
|
||||
// synapse.synapse_rust import events` work.
|
||||
py.import("sys")?
|
||||
.getattr("modules")?
|
||||
.set_item("synapse.synapse_rust.events", child_module)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -3,6 +3,7 @@ use pyo3::prelude::*;
|
||||
use pyo3_log::ResetHandle;
|
||||
|
||||
pub mod acl;
|
||||
pub mod events;
|
||||
pub mod push;
|
||||
|
||||
lazy_static! {
|
||||
@@ -41,6 +42,7 @@ fn synapse_rust(py: Python<'_>, m: &PyModule) -> PyResult<()> {
|
||||
|
||||
acl::register_module(py, m)?;
|
||||
push::register_module(py, m)?;
|
||||
events::register_module(py, m)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+14
-1
@@ -299,6 +299,19 @@ def _parse_oidc_config_dict(
|
||||
config_path + ("client_secret",),
|
||||
)
|
||||
|
||||
# If no client secret is specified then the auth method must be None
|
||||
client_auth_method = oidc_config.get("client_auth_method")
|
||||
if client_secret is None and client_secret_jwt_key is None:
|
||||
if client_auth_method is None:
|
||||
client_auth_method = "none"
|
||||
elif client_auth_method != "none":
|
||||
raise ConfigError(
|
||||
"No 'client_secret' is set in OIDC config, and 'client_auth_method' is not set to 'none'"
|
||||
)
|
||||
|
||||
if client_auth_method is None:
|
||||
client_auth_method = "client_secret_basic"
|
||||
|
||||
return OidcProviderConfig(
|
||||
idp_id=idp_id,
|
||||
idp_name=oidc_config.get("idp_name", "OIDC"),
|
||||
@@ -309,7 +322,7 @@ def _parse_oidc_config_dict(
|
||||
client_id=oidc_config["client_id"],
|
||||
client_secret=client_secret,
|
||||
client_secret_jwt_key=client_secret_jwt_key,
|
||||
client_auth_method=oidc_config.get("client_auth_method", "client_secret_basic"),
|
||||
client_auth_method=client_auth_method,
|
||||
pkce_method=oidc_config.get("pkce_method", "auto"),
|
||||
scopes=oidc_config.get("scopes", ["openid"]),
|
||||
authorization_endpoint=oidc_config.get("authorization_endpoint"),
|
||||
|
||||
+8
-122
@@ -42,7 +42,8 @@ from unpaddedbase64 import encode_base64
|
||||
|
||||
from synapse.api.constants import RelationTypes
|
||||
from synapse.api.room_versions import EventFormatVersions, RoomVersion, RoomVersions
|
||||
from synapse.types import JsonDict, RoomStreamToken, StrCollection
|
||||
from synapse.synapse_rust.events import EventInternalMetadata
|
||||
from synapse.types import JsonDict, StrCollection
|
||||
from synapse.util.caches import intern_dict
|
||||
from synapse.util.frozenutils import freeze
|
||||
from synapse.util.stringutils import strtobool
|
||||
@@ -74,7 +75,7 @@ T = TypeVar("T")
|
||||
#
|
||||
# Note that DictProperty/DefaultDictProperty cannot actually be used with
|
||||
# EventBuilder as it lacks a _dict property.
|
||||
_DictPropertyInstance = Union["_EventInternalMetadata", "EventBase", "EventBuilder"]
|
||||
_DictPropertyInstance = Union["EventBase", "EventBuilder"]
|
||||
|
||||
|
||||
class DictProperty(Generic[T]):
|
||||
@@ -111,7 +112,7 @@ class DictProperty(Generic[T]):
|
||||
if instance is None:
|
||||
return self
|
||||
try:
|
||||
assert isinstance(instance, (EventBase, _EventInternalMetadata))
|
||||
assert isinstance(instance, EventBase)
|
||||
return instance._dict[self.key]
|
||||
except KeyError as e1:
|
||||
# We want this to look like a regular attribute error (mostly so that
|
||||
@@ -127,11 +128,11 @@ class DictProperty(Generic[T]):
|
||||
) from e1.__context__
|
||||
|
||||
def __set__(self, instance: _DictPropertyInstance, v: T) -> None:
|
||||
assert isinstance(instance, (EventBase, _EventInternalMetadata))
|
||||
assert isinstance(instance, EventBase)
|
||||
instance._dict[self.key] = v
|
||||
|
||||
def __delete__(self, instance: _DictPropertyInstance) -> None:
|
||||
assert isinstance(instance, (EventBase, _EventInternalMetadata))
|
||||
assert isinstance(instance, EventBase)
|
||||
try:
|
||||
del instance._dict[self.key]
|
||||
except KeyError as e1:
|
||||
@@ -176,125 +177,10 @@ class DefaultDictProperty(DictProperty, Generic[T]):
|
||||
) -> Union[T, "DefaultDictProperty"]:
|
||||
if instance is None:
|
||||
return self
|
||||
assert isinstance(instance, (EventBase, _EventInternalMetadata))
|
||||
assert isinstance(instance, EventBase)
|
||||
return instance._dict.get(self.key, self.default)
|
||||
|
||||
|
||||
class _EventInternalMetadata:
|
||||
__slots__ = ["_dict", "stream_ordering", "outlier"]
|
||||
|
||||
def __init__(self, internal_metadata_dict: JsonDict):
|
||||
# we have to copy the dict, because it turns out that the same dict is
|
||||
# reused. TODO: fix that
|
||||
self._dict = dict(internal_metadata_dict)
|
||||
|
||||
# the stream ordering of this event. None, until it has been persisted.
|
||||
self.stream_ordering: Optional[int] = None
|
||||
|
||||
# whether this event is an outlier (ie, whether we have the state at that point
|
||||
# in the DAG)
|
||||
self.outlier = False
|
||||
|
||||
out_of_band_membership: DictProperty[bool] = DictProperty("out_of_band_membership")
|
||||
send_on_behalf_of: DictProperty[str] = DictProperty("send_on_behalf_of")
|
||||
recheck_redaction: DictProperty[bool] = DictProperty("recheck_redaction")
|
||||
soft_failed: DictProperty[bool] = DictProperty("soft_failed")
|
||||
proactively_send: DictProperty[bool] = DictProperty("proactively_send")
|
||||
redacted: DictProperty[bool] = DictProperty("redacted")
|
||||
|
||||
txn_id: DictProperty[str] = DictProperty("txn_id")
|
||||
"""The transaction ID, if it was set when the event was created."""
|
||||
|
||||
token_id: DictProperty[int] = DictProperty("token_id")
|
||||
"""The access token ID of the user who sent this event, if any."""
|
||||
|
||||
device_id: DictProperty[str] = DictProperty("device_id")
|
||||
"""The device ID of the user who sent this event, if any."""
|
||||
|
||||
# XXX: These are set by StreamWorkerStore._set_before_and_after.
|
||||
# I'm pretty sure that these are never persisted to the database, so shouldn't
|
||||
# be here
|
||||
before: DictProperty[RoomStreamToken] = DictProperty("before")
|
||||
after: DictProperty[RoomStreamToken] = DictProperty("after")
|
||||
order: DictProperty[Tuple[int, int]] = DictProperty("order")
|
||||
|
||||
def get_dict(self) -> JsonDict:
|
||||
return dict(self._dict)
|
||||
|
||||
def is_outlier(self) -> bool:
|
||||
return self.outlier
|
||||
|
||||
def is_out_of_band_membership(self) -> bool:
|
||||
"""Whether this event is an out-of-band membership.
|
||||
|
||||
OOB memberships are a special case of outlier events: they are membership events
|
||||
for federated rooms that we aren't full members of. Examples include invites
|
||||
received over federation, and rejections for such invites.
|
||||
|
||||
The concept of an OOB membership is needed because these events need to be
|
||||
processed as if they're new regular events (e.g. updating membership state in
|
||||
the database, relaying to clients via /sync, etc) despite being outliers.
|
||||
|
||||
See also https://element-hq.github.io/synapse/develop/development/room-dag-concepts.html#out-of-band-membership-events.
|
||||
|
||||
(Added in synapse 0.99.0, so may be unreliable for events received before that)
|
||||
"""
|
||||
return self._dict.get("out_of_band_membership", False)
|
||||
|
||||
def get_send_on_behalf_of(self) -> Optional[str]:
|
||||
"""Whether this server should send the event on behalf of another server.
|
||||
This is used by the federation "send_join" API to forward the initial join
|
||||
event for a server in the room.
|
||||
|
||||
returns a str with the name of the server this event is sent on behalf of.
|
||||
"""
|
||||
return self._dict.get("send_on_behalf_of")
|
||||
|
||||
def need_to_check_redaction(self) -> bool:
|
||||
"""Whether the redaction event needs to be rechecked when fetching
|
||||
from the database.
|
||||
|
||||
Starting in room v3 redaction events are accepted up front, and later
|
||||
checked to see if the redacter and redactee's domains match.
|
||||
|
||||
If the sender of the redaction event is allowed to redact any event
|
||||
due to auth rules, then this will always return false.
|
||||
"""
|
||||
return self._dict.get("recheck_redaction", False)
|
||||
|
||||
def is_soft_failed(self) -> bool:
|
||||
"""Whether the event has been soft failed.
|
||||
|
||||
Soft failed events should be handled as usual, except:
|
||||
1. They should not go down sync or event streams, or generally
|
||||
sent to clients.
|
||||
2. They should not be added to the forward extremities (and
|
||||
therefore not to current state).
|
||||
"""
|
||||
return self._dict.get("soft_failed", False)
|
||||
|
||||
def should_proactively_send(self) -> bool:
|
||||
"""Whether the event, if ours, should be sent to other clients and
|
||||
servers.
|
||||
|
||||
This is used for sending dummy events internally. Servers and clients
|
||||
can still explicitly fetch the event.
|
||||
"""
|
||||
return self._dict.get("proactively_send", True)
|
||||
|
||||
def is_redacted(self) -> bool:
|
||||
"""Whether the event has been redacted.
|
||||
|
||||
This is used for efficiently checking whether an event has been
|
||||
marked as redacted without needing to make another database call.
|
||||
"""
|
||||
return self._dict.get("redacted", False)
|
||||
|
||||
def is_notifiable(self) -> bool:
|
||||
"""Whether this event can trigger a push notification"""
|
||||
return not self.is_outlier() or self.is_out_of_band_membership()
|
||||
|
||||
|
||||
class EventBase(metaclass=abc.ABCMeta):
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
@@ -320,7 +206,7 @@ class EventBase(metaclass=abc.ABCMeta):
|
||||
|
||||
self._dict = event_dict
|
||||
|
||||
self.internal_metadata = _EventInternalMetadata(internal_metadata_dict)
|
||||
self.internal_metadata = EventInternalMetadata(internal_metadata_dict)
|
||||
|
||||
depth: DictProperty[int] = DictProperty("depth")
|
||||
content: DictProperty[JsonDict] = DictProperty("content")
|
||||
|
||||
@@ -31,9 +31,10 @@ from synapse.api.room_versions import (
|
||||
)
|
||||
from synapse.crypto.event_signing import add_hashes_and_signatures
|
||||
from synapse.event_auth import auth_types_for_event
|
||||
from synapse.events import EventBase, _EventInternalMetadata, make_event_from_dict
|
||||
from synapse.events import EventBase, make_event_from_dict
|
||||
from synapse.state import StateHandler
|
||||
from synapse.storage.databases.main import DataStore
|
||||
from synapse.synapse_rust.events import EventInternalMetadata
|
||||
from synapse.types import EventID, JsonDict, StrCollection
|
||||
from synapse.types.state import StateFilter
|
||||
from synapse.util import Clock
|
||||
@@ -93,8 +94,8 @@ class EventBuilder:
|
||||
_redacts: Optional[str] = None
|
||||
_origin_server_ts: Optional[int] = None
|
||||
|
||||
internal_metadata: _EventInternalMetadata = attr.Factory(
|
||||
lambda: _EventInternalMetadata({})
|
||||
internal_metadata: EventInternalMetadata = attr.Factory(
|
||||
lambda: EventInternalMetadata({})
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
+3
-11
@@ -404,7 +404,7 @@ _DEFAULT_SERIALIZE_EVENT_CONFIG = SerializeEventConfig()
|
||||
|
||||
|
||||
def serialize_event(
|
||||
e: Union[JsonDict, EventBase],
|
||||
e: EventBase,
|
||||
time_now_ms: int,
|
||||
*,
|
||||
config: SerializeEventConfig = _DEFAULT_SERIALIZE_EVENT_CONFIG,
|
||||
@@ -420,10 +420,6 @@ def serialize_event(
|
||||
The serialized event dictionary.
|
||||
"""
|
||||
|
||||
# FIXME(erikj): To handle the case of presence events and the like
|
||||
if not isinstance(e, EventBase):
|
||||
return e
|
||||
|
||||
time_now_ms = int(time_now_ms)
|
||||
|
||||
# Should this strip out None's?
|
||||
@@ -531,7 +527,7 @@ class EventClientSerializer:
|
||||
|
||||
async def serialize_event(
|
||||
self,
|
||||
event: Union[JsonDict, EventBase],
|
||||
event: EventBase,
|
||||
time_now: int,
|
||||
*,
|
||||
config: SerializeEventConfig = _DEFAULT_SERIALIZE_EVENT_CONFIG,
|
||||
@@ -549,10 +545,6 @@ class EventClientSerializer:
|
||||
Returns:
|
||||
The serialized event
|
||||
"""
|
||||
# To handle the case of presence events and the like
|
||||
if not isinstance(event, EventBase):
|
||||
return event
|
||||
|
||||
serialized_event = serialize_event(event, time_now, config=config)
|
||||
|
||||
new_unsigned = {}
|
||||
@@ -656,7 +648,7 @@ class EventClientSerializer:
|
||||
|
||||
async def serialize_events(
|
||||
self,
|
||||
events: Iterable[Union[JsonDict, EventBase]],
|
||||
events: Iterable[EventBase],
|
||||
time_now: int,
|
||||
*,
|
||||
config: SerializeEventConfig = _DEFAULT_SERIALIZE_EVENT_CONFIG,
|
||||
|
||||
@@ -1155,7 +1155,7 @@ class FederationClient(FederationBase):
|
||||
# NB: We *need* to copy to ensure that we don't have multiple
|
||||
# references being passed on, as that causes... issues.
|
||||
for s in signed_state:
|
||||
s.internal_metadata = copy.deepcopy(s.internal_metadata)
|
||||
s.internal_metadata = s.internal_metadata.copy()
|
||||
|
||||
# double-check that the auth chain doesn't include a different create event
|
||||
auth_chain_create_events = [
|
||||
|
||||
@@ -154,7 +154,10 @@ class PublicRoomList(BaseFederationServlet):
|
||||
limit = None
|
||||
|
||||
data = await self.handler.get_local_public_room_list(
|
||||
limit, since_token, network_tuple=network_tuple, from_federation=True
|
||||
limit,
|
||||
since_token,
|
||||
network_tuple=network_tuple,
|
||||
from_federation_origin=origin,
|
||||
)
|
||||
return 200, data
|
||||
|
||||
@@ -195,7 +198,7 @@ class PublicRoomList(BaseFederationServlet):
|
||||
since_token=since_token,
|
||||
search_filter=search_filter,
|
||||
network_tuple=network_tuple,
|
||||
from_federation=True,
|
||||
from_federation_origin=origin,
|
||||
)
|
||||
|
||||
return 200, data
|
||||
|
||||
@@ -208,7 +208,12 @@ class AdminHandler:
|
||||
if not events:
|
||||
break
|
||||
|
||||
from_key = events[-1].internal_metadata.after
|
||||
last_event = events[-1]
|
||||
assert last_event.internal_metadata.stream_ordering
|
||||
from_key = RoomStreamToken(
|
||||
stream=last_event.internal_metadata.stream_ordering,
|
||||
topological=last_event.depth,
|
||||
)
|
||||
|
||||
events = await filter_events_for_client(
|
||||
self._storage_controllers, user_id, events
|
||||
|
||||
+41
-36
@@ -20,7 +20,7 @@
|
||||
|
||||
import logging
|
||||
import random
|
||||
from typing import TYPE_CHECKING, Iterable, List, Optional
|
||||
from typing import TYPE_CHECKING, Iterable, List, Optional, cast
|
||||
|
||||
from synapse.api.constants import EduTypes, EventTypes, Membership, PresenceState
|
||||
from synapse.api.errors import AuthError, SynapseError
|
||||
@@ -29,7 +29,7 @@ from synapse.events.utils import SerializeEventConfig
|
||||
from synapse.handlers.presence import format_user_presence_state
|
||||
from synapse.storage.databases.main.events_worker import EventRedactBehaviour
|
||||
from synapse.streams.config import PaginationConfig
|
||||
from synapse.types import JsonDict, Requester, UserID
|
||||
from synapse.types import JsonDict, Requester, StreamKeyType, UserID
|
||||
from synapse.visibility import filter_events_for_client
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -93,49 +93,54 @@ class EventStreamHandler:
|
||||
is_guest=requester.is_guest,
|
||||
explicit_room_id=room_id,
|
||||
)
|
||||
events = stream_result.events
|
||||
events_by_source = stream_result.events_by_source
|
||||
|
||||
time_now = self.clock.time_msec()
|
||||
|
||||
# When the user joins a new room, or another user joins a currently
|
||||
# joined room, we need to send down presence for those users.
|
||||
to_add: List[JsonDict] = []
|
||||
for event in events:
|
||||
if not isinstance(event, EventBase):
|
||||
to_return: List[JsonDict] = []
|
||||
for keyname, source_events in events_by_source.items():
|
||||
if keyname != StreamKeyType.ROOM:
|
||||
e = cast(List[JsonDict], source_events)
|
||||
to_return.extend(e)
|
||||
continue
|
||||
if event.type == EventTypes.Member:
|
||||
if event.membership != Membership.JOIN:
|
||||
continue
|
||||
# Send down presence.
|
||||
if event.state_key == requester.user.to_string():
|
||||
# Send down presence for everyone in the room.
|
||||
users: Iterable[str] = await self.store.get_users_in_room(
|
||||
event.room_id
|
||||
|
||||
events = cast(List[EventBase], source_events)
|
||||
|
||||
serialized_events = await self._event_serializer.serialize_events(
|
||||
events,
|
||||
time_now,
|
||||
config=SerializeEventConfig(
|
||||
as_client_event=as_client_event, requester=requester
|
||||
),
|
||||
)
|
||||
to_return.extend(serialized_events)
|
||||
|
||||
for event in events:
|
||||
if event.type == EventTypes.Member:
|
||||
if event.membership != Membership.JOIN:
|
||||
continue
|
||||
# Send down presence.
|
||||
if event.state_key == requester.user.to_string():
|
||||
# Send down presence for everyone in the room.
|
||||
users: Iterable[str] = await self.store.get_users_in_room(
|
||||
event.room_id
|
||||
)
|
||||
else:
|
||||
users = [event.state_key]
|
||||
|
||||
states = await presence_handler.get_states(users)
|
||||
to_return.extend(
|
||||
{
|
||||
"type": EduTypes.PRESENCE,
|
||||
"content": format_user_presence_state(state, time_now),
|
||||
}
|
||||
for state in states
|
||||
)
|
||||
else:
|
||||
users = [event.state_key]
|
||||
|
||||
states = await presence_handler.get_states(users)
|
||||
to_add.extend(
|
||||
{
|
||||
"type": EduTypes.PRESENCE,
|
||||
"content": format_user_presence_state(state, time_now),
|
||||
}
|
||||
for state in states
|
||||
)
|
||||
|
||||
events.extend(to_add)
|
||||
|
||||
chunks = await self._event_serializer.serialize_events(
|
||||
events,
|
||||
time_now,
|
||||
config=SerializeEventConfig(
|
||||
as_client_event=as_client_event, requester=requester
|
||||
),
|
||||
)
|
||||
|
||||
chunk = {
|
||||
"chunk": chunks,
|
||||
"chunk": to_return,
|
||||
"start": await stream_result.start_token.to_string(self.store),
|
||||
"end": await stream_result.end_token.to_string(self.store),
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ from synapse.types import (
|
||||
)
|
||||
from synapse.types.state import StateFilter
|
||||
from synapse.util.async_helpers import Linearizer, concurrently_execute
|
||||
from synapse.util.iterutils import batch_iter, partition, sorted_topologically_batched
|
||||
from synapse.util.iterutils import batch_iter, partition, sorted_topologically
|
||||
from synapse.util.retryutils import NotRetryingDestination
|
||||
from synapse.util.stringutils import shortstr
|
||||
|
||||
@@ -1141,16 +1141,8 @@ class FederationEventHandler:
|
||||
partial_state_flags = await self._store.get_partial_state_events(seen)
|
||||
partial_state = any(partial_state_flags.values())
|
||||
|
||||
# Get the state of the events we know about
|
||||
ours = await self._state_storage_controller.get_state_groups_ids(
|
||||
room_id, seen, await_full_state=False
|
||||
)
|
||||
|
||||
# state_maps is a list of mappings from (type, state_key) to event_id
|
||||
state_maps: List[StateMap[str]] = list(ours.values())
|
||||
|
||||
# we don't need this any more, let's delete it.
|
||||
del ours
|
||||
state_maps: List[StateMap[str]] = []
|
||||
|
||||
# Ask the remote server for the states we don't
|
||||
# know about
|
||||
@@ -1169,6 +1161,17 @@ class FederationEventHandler:
|
||||
|
||||
state_maps.append(remote_state_map)
|
||||
|
||||
# Get the state of the events we know about. We do this *after*
|
||||
# trying to fetch missing state over federation as that might fail
|
||||
# and then we can skip loading the local state.
|
||||
ours = await self._state_storage_controller.get_state_groups_ids(
|
||||
room_id, seen, await_full_state=False
|
||||
)
|
||||
state_maps.extend(ours.values())
|
||||
|
||||
# we don't need this any more, let's delete it.
|
||||
del ours
|
||||
|
||||
room_version = await self._store.get_room_version_id(room_id)
|
||||
state_map = await self._state_resolution_handler.resolve_events_with_store(
|
||||
room_id,
|
||||
@@ -1678,57 +1681,36 @@ class FederationEventHandler:
|
||||
|
||||
# We need to persist an event's auth events before the event.
|
||||
auth_graph = {
|
||||
ev: [event_map[e_id] for e_id in ev.auth_event_ids() if e_id in event_map]
|
||||
ev.event_id: [e_id for e_id in ev.auth_event_ids() if e_id in event_map]
|
||||
for ev in event_map.values()
|
||||
}
|
||||
for roots in sorted_topologically_batched(event_map.values(), auth_graph):
|
||||
if not roots:
|
||||
# if *none* of the remaining events are ready, that means
|
||||
# we have a loop. This either means a bug in our logic, or that
|
||||
# somebody has managed to create a loop (which requires finding a
|
||||
# hash collision in room v2 and later).
|
||||
logger.warning(
|
||||
"Loop found in auth events while fetching missing state/auth "
|
||||
"events: %s",
|
||||
shortstr(event_map.keys()),
|
||||
)
|
||||
return
|
||||
sorted_auth_event_ids = sorted_topologically(event_map.keys(), auth_graph)
|
||||
sorted_auth_events = [event_map[e_id] for e_id in sorted_auth_event_ids]
|
||||
logger.info(
|
||||
"Persisting %i remaining outliers: %s",
|
||||
len(sorted_auth_events),
|
||||
shortstr(e.event_id for e in sorted_auth_events),
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Persisting %i of %i remaining outliers: %s",
|
||||
len(roots),
|
||||
len(event_map),
|
||||
shortstr(e.event_id for e in roots),
|
||||
)
|
||||
|
||||
await self._auth_and_persist_outliers_inner(room_id, roots)
|
||||
|
||||
async def _auth_and_persist_outliers_inner(
|
||||
self, room_id: str, fetched_events: Collection[EventBase]
|
||||
) -> None:
|
||||
"""Helper for _auth_and_persist_outliers
|
||||
|
||||
Persists a batch of events where we have (theoretically) already persisted all
|
||||
of their auth events.
|
||||
|
||||
Marks the events as outliers, auths them, persists them to the database, and,
|
||||
where appropriate (eg, an invite), awakes the notifier.
|
||||
|
||||
Params:
|
||||
origin: where the events came from
|
||||
room_id: the room that the events are meant to be in (though this has
|
||||
not yet been checked)
|
||||
fetched_events: the events to persist
|
||||
"""
|
||||
# get all the auth events for all the events in this batch. By now, they should
|
||||
# have been persisted.
|
||||
auth_events = {
|
||||
aid for event in fetched_events for aid in event.auth_event_ids()
|
||||
auth_event_ids = {
|
||||
aid for event in sorted_auth_events for aid in event.auth_event_ids()
|
||||
}
|
||||
persisted_events = await self._store.get_events(
|
||||
auth_events,
|
||||
allow_rejected=True,
|
||||
)
|
||||
auth_map = {
|
||||
ev.event_id: ev
|
||||
for ev in sorted_auth_events
|
||||
if ev.event_id in auth_event_ids
|
||||
}
|
||||
|
||||
missing_events = auth_event_ids.difference(auth_map)
|
||||
if missing_events:
|
||||
persisted_events = await self._store.get_events(
|
||||
missing_events,
|
||||
allow_rejected=True,
|
||||
redact_behaviour=EventRedactBehaviour.as_is,
|
||||
)
|
||||
auth_map.update(persisted_events)
|
||||
|
||||
events_and_contexts_to_persist: List[Tuple[EventBase, EventContext]] = []
|
||||
|
||||
@@ -1736,7 +1718,7 @@ class FederationEventHandler:
|
||||
with nested_logging_context(suffix=event.event_id):
|
||||
auth = []
|
||||
for auth_event_id in event.auth_event_ids():
|
||||
ae = persisted_events.get(auth_event_id)
|
||||
ae = auth_map.get(auth_event_id)
|
||||
if not ae:
|
||||
# the fact we can't find the auth event doesn't mean it doesn't
|
||||
# exist, which means it is premature to reject `event`. Instead we
|
||||
@@ -1755,7 +1737,9 @@ class FederationEventHandler:
|
||||
context = EventContext.for_outlier(self._storage_controllers)
|
||||
try:
|
||||
validate_event_for_room_version(event)
|
||||
await check_state_independent_auth_rules(self._store, event)
|
||||
await check_state_independent_auth_rules(
|
||||
self._store, event, batched_auth_events=auth_map
|
||||
)
|
||||
check_state_dependent_auth_rules(event, auth)
|
||||
except AuthError as e:
|
||||
logger.warning("Rejecting %r because %s", event, e)
|
||||
@@ -1772,7 +1756,7 @@ class FederationEventHandler:
|
||||
|
||||
events_and_contexts_to_persist.append((event, context))
|
||||
|
||||
for event in fetched_events:
|
||||
for event in sorted_auth_events:
|
||||
await prep(event)
|
||||
|
||||
await self.persist_events_and_notify(
|
||||
|
||||
@@ -180,6 +180,10 @@ class RelationsHandler:
|
||||
config=serialize_options,
|
||||
),
|
||||
}
|
||||
|
||||
if recurse:
|
||||
return_value["recursion_depth"] = 3
|
||||
|
||||
if include_original_event:
|
||||
# Do not bundle aggregations when retrieving the original event because
|
||||
# we want the content before relations are applied to it.
|
||||
|
||||
@@ -26,7 +26,17 @@ import random
|
||||
import string
|
||||
from collections import OrderedDict
|
||||
from http import HTTPStatus
|
||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional, Tuple
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
cast,
|
||||
)
|
||||
|
||||
import attr
|
||||
from typing_extensions import TypedDict
|
||||
@@ -1742,13 +1752,19 @@ class RoomEventSource(EventSource[RoomStreamToken, EventBase]):
|
||||
events = list(room_events)
|
||||
events.extend(e for evs, _ in room_to_events.values() for e in evs)
|
||||
|
||||
events.sort(key=lambda e: e.internal_metadata.order)
|
||||
# We know stream_ordering must be not None here, as its been
|
||||
# persisted, but mypy doesn't know that
|
||||
events.sort(key=lambda e: cast(int, e.internal_metadata.stream_ordering))
|
||||
|
||||
if limit:
|
||||
events[:] = events[:limit]
|
||||
|
||||
if events:
|
||||
end_key = events[-1].internal_metadata.after
|
||||
last_event = events[-1]
|
||||
assert last_event.internal_metadata.stream_ordering
|
||||
end_key = RoomStreamToken(
|
||||
stream=last_event.internal_metadata.stream_ordering,
|
||||
)
|
||||
else:
|
||||
end_key = to_key
|
||||
|
||||
|
||||
+128
-51
@@ -19,7 +19,7 @@
|
||||
#
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Optional, Tuple
|
||||
from typing import TYPE_CHECKING, Any, List, Optional, Tuple
|
||||
|
||||
import attr
|
||||
import msgpack
|
||||
@@ -54,6 +54,9 @@ REMOTE_ROOM_LIST_POLL_INTERVAL = 60 * 1000
|
||||
# This is used to indicate we should only return rooms published to the main list.
|
||||
EMPTY_THIRD_PARTY_ID = ThirdPartyInstanceID(None, None)
|
||||
|
||||
# Maximum number of local public rooms returned over the CS or SS API
|
||||
MAX_PUBLIC_ROOMS_IN_RESPONSE = 100
|
||||
|
||||
|
||||
class RoomListHandler:
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
@@ -74,7 +77,7 @@ class RoomListHandler:
|
||||
since_token: Optional[str] = None,
|
||||
search_filter: Optional[dict] = None,
|
||||
network_tuple: Optional[ThirdPartyInstanceID] = EMPTY_THIRD_PARTY_ID,
|
||||
from_federation: bool = False,
|
||||
from_federation_origin: Optional[str] = None,
|
||||
) -> JsonDict:
|
||||
"""Generate a local public room list.
|
||||
|
||||
@@ -89,7 +92,8 @@ class RoomListHandler:
|
||||
This can be (None, None) to indicate the main list, or a particular
|
||||
appservice and network id to use an appservice specific one.
|
||||
Setting to None returns all public rooms across all lists.
|
||||
from_federation: true iff the request comes from the federation API
|
||||
from_federation_origin: the server name of the requester, or None
|
||||
if the request is not from federation.
|
||||
"""
|
||||
if not self.enable_room_list_search:
|
||||
return {"chunk": [], "total_room_count_estimate": 0}
|
||||
@@ -102,36 +106,43 @@ class RoomListHandler:
|
||||
network_tuple,
|
||||
)
|
||||
|
||||
if search_filter:
|
||||
capped_limit: int = (
|
||||
MAX_PUBLIC_ROOMS_IN_RESPONSE
|
||||
if limit is None or limit > MAX_PUBLIC_ROOMS_IN_RESPONSE
|
||||
else limit
|
||||
)
|
||||
|
||||
if search_filter or from_federation_origin is not None:
|
||||
# We explicitly don't bother caching searches or requests for
|
||||
# appservice specific lists.
|
||||
logger.info("Bypassing cache as search request.")
|
||||
# We also don't bother caching requests from federated homeservers.
|
||||
logger.debug("Bypassing cache as search or federation request.")
|
||||
|
||||
return await self._get_public_room_list(
|
||||
limit,
|
||||
capped_limit,
|
||||
since_token,
|
||||
search_filter,
|
||||
network_tuple=network_tuple,
|
||||
from_federation=from_federation,
|
||||
from_federation_origin=from_federation_origin,
|
||||
)
|
||||
|
||||
key = (limit, since_token, network_tuple)
|
||||
key = (capped_limit, since_token, network_tuple)
|
||||
return await self.response_cache.wrap(
|
||||
key,
|
||||
self._get_public_room_list,
|
||||
limit,
|
||||
capped_limit,
|
||||
since_token,
|
||||
network_tuple=network_tuple,
|
||||
from_federation=from_federation,
|
||||
from_federation_origin=from_federation_origin,
|
||||
)
|
||||
|
||||
async def _get_public_room_list(
|
||||
self,
|
||||
limit: Optional[int] = None,
|
||||
limit: int,
|
||||
since_token: Optional[str] = None,
|
||||
search_filter: Optional[dict] = None,
|
||||
network_tuple: Optional[ThirdPartyInstanceID] = EMPTY_THIRD_PARTY_ID,
|
||||
from_federation: bool = False,
|
||||
from_federation_origin: Optional[str] = None,
|
||||
) -> JsonDict:
|
||||
"""Generate a public room list.
|
||||
Args:
|
||||
@@ -142,8 +153,8 @@ class RoomListHandler:
|
||||
This can be (None, None) to indicate the main list, or a particular
|
||||
appservice and network id to use an appservice specific one.
|
||||
Setting to None returns all public rooms across all lists.
|
||||
from_federation: Whether this request originated from a
|
||||
federating server or a client. Used for room filtering.
|
||||
from_federation_origin: the server name of the requester, or None
|
||||
if the request is not from federation.
|
||||
"""
|
||||
|
||||
# Pagination tokens work by storing the room ID sent in the last batch,
|
||||
@@ -165,8 +176,16 @@ class RoomListHandler:
|
||||
forwards = True
|
||||
has_batch_token = False
|
||||
|
||||
# we request one more than wanted to see if there are more pages to come
|
||||
probing_limit = limit + 1 if limit is not None else None
|
||||
if from_federation_origin is None:
|
||||
# Client-Server API:
|
||||
# we request one more than wanted to see if there are more pages to come
|
||||
probing_limit = limit + 1
|
||||
else:
|
||||
# Federation API:
|
||||
# we request a handful more in case any get filtered out by ACLs
|
||||
# as a best easy effort attempt to return the full number of entries
|
||||
# specified by `limit`.
|
||||
probing_limit = limit + 10
|
||||
|
||||
results = await self.store.get_largest_public_rooms(
|
||||
network_tuple,
|
||||
@@ -174,7 +193,7 @@ class RoomListHandler:
|
||||
probing_limit,
|
||||
bounds=bounds,
|
||||
forwards=forwards,
|
||||
ignore_non_federatable=from_federation,
|
||||
ignore_non_federatable=from_federation_origin is not None,
|
||||
)
|
||||
|
||||
def build_room_entry(room: LargestRoomStats) -> JsonDict:
|
||||
@@ -195,59 +214,117 @@ class RoomListHandler:
|
||||
# Filter out Nones – rather omit the field altogether
|
||||
return {k: v for k, v in entry.items() if v is not None}
|
||||
|
||||
response: JsonDict = {}
|
||||
num_results = len(results)
|
||||
if limit is not None:
|
||||
more_to_come = num_results == probing_limit
|
||||
# Build a list of up to `limit` entries.
|
||||
room_entries: List[JsonDict] = []
|
||||
rooms_iterator = results if forwards else reversed(results)
|
||||
|
||||
# Depending on direction we trim either the front or back.
|
||||
if forwards:
|
||||
results = results[:limit]
|
||||
# Track the first and last 'considered' rooms so that we can provide correct
|
||||
# next_batch/prev_batch tokens.
|
||||
# This is needed because the loop might finish early when
|
||||
# `len(room_entries) >= limit` and we might be left with rooms we didn't
|
||||
# 'consider' (iterate over) and we should save those rooms for the next
|
||||
# batch.
|
||||
first_considered_room: Optional[LargestRoomStats] = None
|
||||
last_considered_room: Optional[LargestRoomStats] = None
|
||||
cut_off_due_to_limit: bool = False
|
||||
|
||||
for room_result in rooms_iterator:
|
||||
if len(room_entries) >= limit:
|
||||
cut_off_due_to_limit = True
|
||||
break
|
||||
|
||||
if first_considered_room is None:
|
||||
first_considered_room = room_result
|
||||
last_considered_room = room_result
|
||||
|
||||
if from_federation_origin is not None:
|
||||
# If this is a federated request, apply server ACLs if the room has any set
|
||||
acl_evaluator = (
|
||||
await self._storage_controllers.state.get_server_acl_for_room(
|
||||
room_result.room_id
|
||||
)
|
||||
)
|
||||
|
||||
if acl_evaluator is not None:
|
||||
if not acl_evaluator.server_matches_acl_event(
|
||||
from_federation_origin
|
||||
):
|
||||
# the requesting server is ACL blocked by the room,
|
||||
# don't show in directory
|
||||
continue
|
||||
|
||||
room_entries.append(build_room_entry(room_result))
|
||||
|
||||
if not forwards:
|
||||
# If we are paginating backwards, we still return the chunk in
|
||||
# biggest-first order, so reverse again.
|
||||
room_entries.reverse()
|
||||
# Swap the order of first/last considered rooms.
|
||||
first_considered_room, last_considered_room = (
|
||||
last_considered_room,
|
||||
first_considered_room,
|
||||
)
|
||||
|
||||
response: JsonDict = {
|
||||
"chunk": room_entries,
|
||||
}
|
||||
num_results = len(results)
|
||||
|
||||
more_to_come_from_database = num_results == probing_limit
|
||||
|
||||
if forwards and has_batch_token:
|
||||
# If there was a token given then we assume that there
|
||||
# must be previous results, even if there were no results in this batch.
|
||||
if first_considered_room is not None:
|
||||
response["prev_batch"] = RoomListNextBatch(
|
||||
last_joined_members=first_considered_room.joined_members,
|
||||
last_room_id=first_considered_room.room_id,
|
||||
direction_is_forward=False,
|
||||
).to_token()
|
||||
else:
|
||||
results = results[-limit:]
|
||||
else:
|
||||
more_to_come = False
|
||||
# If we didn't find any results this time,
|
||||
# we don't have an actual room ID to put in the token.
|
||||
# But since `first_considered_room` is None, we know that we have
|
||||
# reached the end of the results.
|
||||
# So we can use a token of (0, empty room ID) to paginate from the end
|
||||
# next time.
|
||||
response["prev_batch"] = RoomListNextBatch(
|
||||
last_joined_members=0,
|
||||
last_room_id="",
|
||||
direction_is_forward=False,
|
||||
).to_token()
|
||||
|
||||
if num_results > 0:
|
||||
final_entry = results[-1]
|
||||
initial_entry = results[0]
|
||||
|
||||
assert first_considered_room is not None
|
||||
assert last_considered_room is not None
|
||||
if forwards:
|
||||
if has_batch_token:
|
||||
# If there was a token given then we assume that there
|
||||
# must be previous results.
|
||||
response["prev_batch"] = RoomListNextBatch(
|
||||
last_joined_members=initial_entry.joined_members,
|
||||
last_room_id=initial_entry.room_id,
|
||||
direction_is_forward=False,
|
||||
).to_token()
|
||||
|
||||
if more_to_come:
|
||||
if more_to_come_from_database or cut_off_due_to_limit:
|
||||
response["next_batch"] = RoomListNextBatch(
|
||||
last_joined_members=final_entry.joined_members,
|
||||
last_room_id=final_entry.room_id,
|
||||
last_joined_members=last_considered_room.joined_members,
|
||||
last_room_id=last_considered_room.room_id,
|
||||
direction_is_forward=True,
|
||||
).to_token()
|
||||
else:
|
||||
else: # backwards
|
||||
if has_batch_token:
|
||||
response["next_batch"] = RoomListNextBatch(
|
||||
last_joined_members=final_entry.joined_members,
|
||||
last_room_id=final_entry.room_id,
|
||||
last_joined_members=last_considered_room.joined_members,
|
||||
last_room_id=last_considered_room.room_id,
|
||||
direction_is_forward=True,
|
||||
).to_token()
|
||||
|
||||
if more_to_come:
|
||||
if more_to_come_from_database or cut_off_due_to_limit:
|
||||
response["prev_batch"] = RoomListNextBatch(
|
||||
last_joined_members=initial_entry.joined_members,
|
||||
last_room_id=initial_entry.room_id,
|
||||
last_joined_members=first_considered_room.joined_members,
|
||||
last_room_id=first_considered_room.room_id,
|
||||
direction_is_forward=False,
|
||||
).to_token()
|
||||
|
||||
response["chunk"] = [build_room_entry(r) for r in results]
|
||||
|
||||
# We can't efficiently count the total number of rooms that are not
|
||||
# blocked by ACLs, but this is just an estimate so that should be
|
||||
# good enough.
|
||||
response["total_room_count_estimate"] = await self.store.count_public_rooms(
|
||||
network_tuple,
|
||||
ignore_non_federatable=from_federation,
|
||||
ignore_non_federatable=from_federation_origin is not None,
|
||||
search_filter=search_filter,
|
||||
)
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ from synapse.api.ratelimiting import Ratelimiter
|
||||
from synapse.config.ratelimiting import RatelimitSettings
|
||||
from synapse.events import EventBase
|
||||
from synapse.types import JsonDict, Requester, StrCollection
|
||||
from synapse.types.state import StateFilter
|
||||
from synapse.util.caches.response_cache import ResponseCache
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -546,7 +547,16 @@ class RoomSummaryHandler:
|
||||
Returns:
|
||||
True if the room is accessible to the requesting user or server.
|
||||
"""
|
||||
state_ids = await self._storage_controllers.state.get_current_state_ids(room_id)
|
||||
event_types = [
|
||||
(EventTypes.JoinRules, ""),
|
||||
(EventTypes.RoomHistoryVisibility, ""),
|
||||
]
|
||||
if requester:
|
||||
event_types.append((EventTypes.Member, requester))
|
||||
|
||||
state_ids = await self._storage_controllers.state.get_current_state_ids(
|
||||
room_id, state_filter=StateFilter.from_types(event_types)
|
||||
)
|
||||
|
||||
# If there's no state for the room, it isn't known.
|
||||
if not state_ids:
|
||||
|
||||
@@ -583,10 +583,11 @@ class SyncHandler:
|
||||
# `recents`, so partial state is only a problem when a membership
|
||||
# event turns up in `recents` but has not made it into the current
|
||||
# state.
|
||||
current_state_ids_map = (
|
||||
await self.store.get_partial_current_state_ids(room_id)
|
||||
current_state_ids = (
|
||||
await self.store.check_if_events_in_current_state(
|
||||
{e.event_id for e in recents if e.is_state()}
|
||||
)
|
||||
)
|
||||
current_state_ids = frozenset(current_state_ids_map.values())
|
||||
|
||||
recents = await filter_events_for_client(
|
||||
self._storage_controllers,
|
||||
@@ -601,7 +602,10 @@ class SyncHandler:
|
||||
if not limited or block_all_timeline:
|
||||
prev_batch_token = upto_token
|
||||
if recents:
|
||||
room_key = recents[0].internal_metadata.before
|
||||
assert recents[0].internal_metadata.stream_ordering
|
||||
room_key = RoomStreamToken(
|
||||
stream=recents[0].internal_metadata.stream_ordering - 1
|
||||
)
|
||||
prev_batch_token = upto_token.copy_and_replace(
|
||||
StreamKeyType.ROOM, room_key
|
||||
)
|
||||
@@ -664,10 +668,11 @@ class SyncHandler:
|
||||
# `loaded_recents`, so partial state is only a problem when a
|
||||
# membership event turns up in `loaded_recents` but has not made it
|
||||
# into the current state.
|
||||
current_state_ids_map = (
|
||||
await self.store.get_partial_current_state_ids(room_id)
|
||||
current_state_ids = (
|
||||
await self.store.check_if_events_in_current_state(
|
||||
{e.event_id for e in loaded_recents if e.is_state()}
|
||||
)
|
||||
)
|
||||
current_state_ids = frozenset(current_state_ids_map.values())
|
||||
|
||||
loaded_recents = await filter_events_for_client(
|
||||
self._storage_controllers,
|
||||
@@ -689,7 +694,10 @@ class SyncHandler:
|
||||
if len(recents) > timeline_limit:
|
||||
limited = True
|
||||
recents = recents[-timeline_limit:]
|
||||
room_key = recents[0].internal_metadata.before
|
||||
assert recents[0].internal_metadata.stream_ordering
|
||||
room_key = RoomStreamToken(
|
||||
stream=recents[0].internal_metadata.stream_ordering - 1
|
||||
)
|
||||
|
||||
prev_batch_token = upto_token.copy_and_replace(StreamKeyType.ROOM, room_key)
|
||||
|
||||
|
||||
@@ -574,7 +574,7 @@ class DirectServeHtmlResource(_AsyncResource):
|
||||
assert isinstance(response_object, bytes)
|
||||
html_bytes = response_object
|
||||
|
||||
respond_with_html_bytes(request, 200, html_bytes)
|
||||
respond_with_html_bytes(request, code, html_bytes)
|
||||
|
||||
def _send_error_response(
|
||||
self,
|
||||
|
||||
+8
-6
@@ -198,12 +198,12 @@ class _NotifierUserStream:
|
||||
|
||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||
class EventStreamResult:
|
||||
events: List[Union[JsonDict, EventBase]]
|
||||
events_by_source: Dict[StreamKeyType, List[Union[JsonDict, EventBase]]]
|
||||
start_token: StreamToken
|
||||
end_token: StreamToken
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self.events)
|
||||
return any(bool(e) for e in self.events_by_source.values())
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||
@@ -694,12 +694,12 @@ class Notifier:
|
||||
before_token: StreamToken, after_token: StreamToken
|
||||
) -> EventStreamResult:
|
||||
if after_token == before_token:
|
||||
return EventStreamResult([], from_token, from_token)
|
||||
return EventStreamResult({}, from_token, from_token)
|
||||
|
||||
# The events fetched from each source are a JsonDict, EventBase, or
|
||||
# UserPresenceState, but see below for UserPresenceState being
|
||||
# converted to JsonDict.
|
||||
events: List[Union[JsonDict, EventBase]] = []
|
||||
events_by_source: Dict[StreamKeyType, List[Union[JsonDict, EventBase]]] = {}
|
||||
end_token = from_token
|
||||
|
||||
for keyname, source in self.event_sources.sources.get_sources():
|
||||
@@ -734,10 +734,12 @@ class Notifier:
|
||||
for event in new_events
|
||||
]
|
||||
|
||||
events.extend(new_events)
|
||||
if new_events:
|
||||
events_by_source.setdefault(keyname, []).extend(new_events)
|
||||
|
||||
end_token = end_token.copy_and_replace(keyname, new_key)
|
||||
|
||||
return EventStreamResult(events, from_token, end_token)
|
||||
return EventStreamResult(events_by_source, from_token, end_token)
|
||||
|
||||
user_id_for_stream = user.to_string()
|
||||
if is_peeking:
|
||||
|
||||
@@ -28,17 +28,11 @@ from synapse.storage.databases.main import DataStore
|
||||
|
||||
async def get_badge_count(store: DataStore, user_id: str, group_by_room: bool) -> int:
|
||||
invites = await store.get_invited_rooms_for_local_user(user_id)
|
||||
joins = await store.get_rooms_for_user(user_id)
|
||||
|
||||
badge = len(invites)
|
||||
|
||||
room_to_count = await store.get_unread_counts_by_room_for_user(user_id)
|
||||
for room_id, notify_count in room_to_count.items():
|
||||
# room_to_count may include rooms which the user has left,
|
||||
# ignore those.
|
||||
if room_id not in joins:
|
||||
continue
|
||||
|
||||
for _room_id, notify_count in room_to_count.items():
|
||||
if notify_count == 0:
|
||||
continue
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ class UsersRestServletV2(RestServlet):
|
||||
)
|
||||
|
||||
user_id = parse_string(request, "user_id")
|
||||
name = parse_string(request, "name")
|
||||
name = parse_string(request, "name", encoding="utf-8")
|
||||
|
||||
guests = parse_boolean(request, "guests", default=True)
|
||||
if self._msc3861_enabled and guests:
|
||||
@@ -412,15 +412,6 @@ class UserRestServletV2(RestServlet):
|
||||
target_user.to_string(), False, requester, by_admin=True
|
||||
)
|
||||
elif not deactivate and user["deactivated"]:
|
||||
if (
|
||||
"password" not in body
|
||||
and self.auth_handler.can_change_password()
|
||||
):
|
||||
raise SynapseError(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"Must provide a password to re-activate an account.",
|
||||
)
|
||||
|
||||
await self.deactivate_account_handler.activate_account(
|
||||
target_user.to_string()
|
||||
)
|
||||
|
||||
@@ -106,7 +106,7 @@ class PasswordResetSubmitTokenResource(DirectServeHtmlResource):
|
||||
return (
|
||||
302,
|
||||
(
|
||||
b'You are being redirected to <a src="%s">%s</a>.'
|
||||
b'You are being redirected to <a href="%s">%s</a>.'
|
||||
% (next_link_bytes, next_link_bytes)
|
||||
),
|
||||
)
|
||||
|
||||
@@ -245,33 +245,74 @@ class DeviceInboxWorkerStore(SQLBaseStore):
|
||||
* The last-processed stream ID. Subsequent calls of this function with the
|
||||
same device should pass this value as 'from_stream_id'.
|
||||
"""
|
||||
(
|
||||
user_id_device_id_to_messages,
|
||||
last_processed_stream_id,
|
||||
) = await self._get_device_messages(
|
||||
user_ids=[user_id],
|
||||
device_id=device_id,
|
||||
from_stream_id=from_stream_id,
|
||||
to_stream_id=to_stream_id,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
if not user_id_device_id_to_messages:
|
||||
if not self._device_inbox_stream_cache.has_entity_changed(
|
||||
user_id, from_stream_id
|
||||
):
|
||||
# There were no messages!
|
||||
return [], to_stream_id
|
||||
|
||||
# Extract the messages, no need to return the user and device ID again
|
||||
to_device_messages = user_id_device_id_to_messages.get((user_id, device_id), [])
|
||||
def get_device_messages_txn(
|
||||
txn: LoggingTransaction,
|
||||
) -> Tuple[List[JsonDict], int]:
|
||||
sql = """
|
||||
SELECT stream_id, message_json FROM device_inbox
|
||||
WHERE user_id = ? AND device_id = ?
|
||||
AND ? < stream_id AND stream_id <= ?
|
||||
ORDER BY stream_id ASC
|
||||
LIMIT ?
|
||||
"""
|
||||
txn.execute(sql, (user_id, device_id, from_stream_id, to_stream_id, limit))
|
||||
|
||||
return to_device_messages, last_processed_stream_id
|
||||
# Create and fill a dictionary of (user ID, device ID) -> list of messages
|
||||
# intended for each device.
|
||||
last_processed_stream_pos = to_stream_id
|
||||
to_device_messages: List[JsonDict] = []
|
||||
rowcount = 0
|
||||
for row in txn:
|
||||
rowcount += 1
|
||||
|
||||
last_processed_stream_pos = row[0]
|
||||
message_dict = db_to_json(row[1])
|
||||
|
||||
# Store the device details
|
||||
to_device_messages.append(message_dict)
|
||||
|
||||
# start a new span for each message, so that we can tag each separately
|
||||
with start_active_span("get_to_device_message"):
|
||||
set_tag(SynapseTags.TO_DEVICE_TYPE, message_dict["type"])
|
||||
set_tag(SynapseTags.TO_DEVICE_SENDER, message_dict["sender"])
|
||||
set_tag(SynapseTags.TO_DEVICE_RECIPIENT, user_id)
|
||||
set_tag(SynapseTags.TO_DEVICE_RECIPIENT_DEVICE, device_id)
|
||||
set_tag(
|
||||
SynapseTags.TO_DEVICE_MSGID,
|
||||
message_dict["content"].get(EventContentFields.TO_DEVICE_MSGID),
|
||||
)
|
||||
|
||||
if rowcount == limit:
|
||||
# We ended up bumping up against the message limit. There may be more messages
|
||||
# to retrieve. Return what we have, as well as the last stream position that
|
||||
# was processed.
|
||||
#
|
||||
# The caller is expected to set this as the lower (exclusive) bound
|
||||
# for the next query of this device.
|
||||
return to_device_messages, last_processed_stream_pos
|
||||
|
||||
# The limit was not reached, thus we know that recipient_device_to_messages
|
||||
# contains all to-device messages for the given device and stream id range.
|
||||
#
|
||||
# We return to_stream_id, which the caller should then provide as the lower
|
||||
# (exclusive) bound on the next query of this device.
|
||||
return to_device_messages, to_stream_id
|
||||
|
||||
return await self.db_pool.runInteraction(
|
||||
"get_messages_for_device", get_device_messages_txn
|
||||
)
|
||||
|
||||
async def _get_device_messages(
|
||||
self,
|
||||
user_ids: Collection[str],
|
||||
from_stream_id: int,
|
||||
to_stream_id: int,
|
||||
device_id: Optional[str] = None,
|
||||
limit: Optional[int] = None,
|
||||
) -> Tuple[Dict[Tuple[str, str], List[JsonDict]], int]:
|
||||
"""
|
||||
Retrieve pending to-device messages for a collection of user devices.
|
||||
@@ -291,11 +332,7 @@ class DeviceInboxWorkerStore(SQLBaseStore):
|
||||
user_ids: The user IDs to filter device messages by.
|
||||
from_stream_id: The lower boundary of stream id to filter with (exclusive).
|
||||
to_stream_id: The upper boundary of stream id to filter with (inclusive).
|
||||
device_id: A device ID to query to-device messages for. If not provided, to-device
|
||||
messages from all device IDs for the given user IDs will be queried. May not be
|
||||
provided if `user_ids` contains more than one entry.
|
||||
limit: The maximum number of to-device messages to return. Can only be used when
|
||||
passing a single user ID / device ID tuple.
|
||||
|
||||
|
||||
Returns:
|
||||
A tuple containing:
|
||||
@@ -308,30 +345,7 @@ class DeviceInboxWorkerStore(SQLBaseStore):
|
||||
logger.warning("No users provided upon querying for device IDs")
|
||||
return {}, to_stream_id
|
||||
|
||||
# Prevent a query for one user's device also retrieving another user's device with
|
||||
# the same device ID (device IDs are not unique across users).
|
||||
if len(user_ids) > 1 and device_id is not None:
|
||||
raise AssertionError(
|
||||
"Programming error: 'device_id' cannot be supplied to "
|
||||
"_get_device_messages when >1 user_id has been provided"
|
||||
)
|
||||
|
||||
# A limit can only be applied when querying for a single user ID / device ID tuple.
|
||||
# See the docstring of this function for more details.
|
||||
if limit is not None and device_id is None:
|
||||
raise AssertionError(
|
||||
"Programming error: _get_device_messages was passed 'limit' "
|
||||
"without a specific user_id/device_id"
|
||||
)
|
||||
|
||||
user_ids_to_query: Set[str] = set()
|
||||
device_ids_to_query: Set[str] = set()
|
||||
|
||||
# Note that a device ID could be an empty str
|
||||
if device_id is not None:
|
||||
# If a device ID was passed, use it to filter results.
|
||||
# Otherwise, device IDs will be derived from the given collection of user IDs.
|
||||
device_ids_to_query.add(device_id)
|
||||
|
||||
# Determine which users have devices with pending messages
|
||||
for user_id in user_ids:
|
||||
@@ -355,20 +369,20 @@ class DeviceInboxWorkerStore(SQLBaseStore):
|
||||
# hidden devices should not receive to-device messages.
|
||||
# Note that this is more efficient than just dropping `device_id` from the query,
|
||||
# since device_inbox has an index on `(user_id, device_id, stream_id)`
|
||||
if not device_ids_to_query:
|
||||
user_device_dicts = cast(
|
||||
List[Tuple[str]],
|
||||
self.db_pool.simple_select_many_txn(
|
||||
txn,
|
||||
table="devices",
|
||||
column="user_id",
|
||||
iterable=user_ids_to_query,
|
||||
keyvalues={"hidden": False},
|
||||
retcols=("device_id",),
|
||||
),
|
||||
)
|
||||
|
||||
device_ids_to_query.update({row[0] for row in user_device_dicts})
|
||||
user_device_dicts = cast(
|
||||
List[Tuple[str]],
|
||||
self.db_pool.simple_select_many_txn(
|
||||
txn,
|
||||
table="devices",
|
||||
column="user_id",
|
||||
iterable=user_ids_to_query,
|
||||
keyvalues={"hidden": False},
|
||||
retcols=("device_id",),
|
||||
),
|
||||
)
|
||||
|
||||
device_ids_to_query = {row[0] for row in user_device_dicts}
|
||||
|
||||
if not device_ids_to_query:
|
||||
# We've ended up with no devices to query.
|
||||
@@ -400,22 +414,15 @@ class DeviceInboxWorkerStore(SQLBaseStore):
|
||||
to_stream_id,
|
||||
)
|
||||
|
||||
# If a limit was provided, limit the data retrieved from the database
|
||||
if limit is not None:
|
||||
sql += "LIMIT ?"
|
||||
sql_args += (limit,)
|
||||
|
||||
txn.execute(sql, sql_args)
|
||||
|
||||
# Create and fill a dictionary of (user ID, device ID) -> list of messages
|
||||
# intended for each device.
|
||||
last_processed_stream_pos = to_stream_id
|
||||
recipient_device_to_messages: Dict[Tuple[str, str], List[JsonDict]] = {}
|
||||
rowcount = 0
|
||||
for row in txn:
|
||||
rowcount += 1
|
||||
|
||||
last_processed_stream_pos = row[0]
|
||||
recipient_user_id = row[1]
|
||||
recipient_device_id = row[2]
|
||||
message_dict = db_to_json(row[3])
|
||||
@@ -436,18 +443,6 @@ class DeviceInboxWorkerStore(SQLBaseStore):
|
||||
message_dict["content"].get(EventContentFields.TO_DEVICE_MSGID),
|
||||
)
|
||||
|
||||
if limit is not None and rowcount == limit:
|
||||
# We ended up bumping up against the message limit. There may be more messages
|
||||
# to retrieve. Return what we have, as well as the last stream position that
|
||||
# was processed.
|
||||
#
|
||||
# The caller is expected to set this as the lower (exclusive) bound
|
||||
# for the next query of this device.
|
||||
return recipient_device_to_messages, last_processed_stream_pos
|
||||
|
||||
# The limit was not reached, thus we know that recipient_device_to_messages
|
||||
# contains all to-device messages for the given device and stream id range.
|
||||
#
|
||||
# We return to_stream_id, which the caller should then provide as the lower
|
||||
# (exclusive) bound on the next query of this device.
|
||||
return recipient_device_to_messages, to_stream_id
|
||||
|
||||
@@ -1794,7 +1794,7 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore):
|
||||
device_ids: The IDs of the devices to delete
|
||||
"""
|
||||
|
||||
def _delete_devices_txn(txn: LoggingTransaction) -> None:
|
||||
def _delete_devices_txn(txn: LoggingTransaction, device_ids: List[str]) -> None:
|
||||
self.db_pool.simple_delete_many_txn(
|
||||
txn,
|
||||
table="devices",
|
||||
@@ -1811,7 +1811,11 @@ class DeviceStore(DeviceWorkerStore, DeviceBackgroundUpdateStore):
|
||||
keyvalues={"user_id": user_id},
|
||||
)
|
||||
|
||||
await self.db_pool.runInteraction("delete_devices", _delete_devices_txn)
|
||||
for batch in batch_iter(device_ids, 100):
|
||||
await self.db_pool.runInteraction(
|
||||
"delete_devices", _delete_devices_txn, batch
|
||||
)
|
||||
|
||||
for device_id in device_ids:
|
||||
self.device_id_exists_cache.invalidate((user_id, device_id))
|
||||
|
||||
|
||||
@@ -357,10 +357,6 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
This function is intentionally not cached because it is called to calculate the
|
||||
unread badge for push notifications and thus the result is expected to change.
|
||||
|
||||
Note that this function assumes the user is a member of the room. Because
|
||||
summary rows are not removed when a user leaves a room, the caller must
|
||||
filter out those results from the result.
|
||||
|
||||
Returns:
|
||||
A map of room ID to notification counts for the given user.
|
||||
"""
|
||||
@@ -373,127 +369,170 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
||||
def _get_unread_counts_by_room_for_user_txn(
|
||||
self, txn: LoggingTransaction, user_id: str
|
||||
) -> Dict[str, int]:
|
||||
receipt_types_clause, args = make_in_list_sql_clause(
|
||||
# To get the badge count of all rooms we need to make three queries:
|
||||
# 1. Fetch all counts from `event_push_summary`, discarding any stale
|
||||
# rooms.
|
||||
# 2. Fetch all notifications from `event_push_actions` that haven't
|
||||
# been rotated yet.
|
||||
# 3. Fetch all notifications from `event_push_actions` for the stale
|
||||
# rooms.
|
||||
#
|
||||
# The "stale room" scenario generally happens when there is a new read
|
||||
# receipt that hasn't yet been processed to update the
|
||||
# `event_push_summary` table. When that happens we ignore the
|
||||
# `event_push_summary` table for that room and calculate the count
|
||||
# manually from `event_push_actions`.
|
||||
|
||||
# We need to only take into account read receipts of these types.
|
||||
receipt_types_clause, receipt_types_args = make_in_list_sql_clause(
|
||||
self.database_engine,
|
||||
"receipt_type",
|
||||
(ReceiptTypes.READ, ReceiptTypes.READ_PRIVATE),
|
||||
)
|
||||
args.extend([user_id, user_id])
|
||||
|
||||
receipts_cte = f"""
|
||||
WITH all_receipts AS (
|
||||
SELECT room_id, thread_id, MAX(event_stream_ordering) AS max_receipt_stream_ordering
|
||||
FROM receipts_linearized
|
||||
LEFT JOIN events USING (room_id, event_id)
|
||||
WHERE
|
||||
{receipt_types_clause}
|
||||
AND user_id = ?
|
||||
GROUP BY room_id, thread_id
|
||||
)
|
||||
"""
|
||||
|
||||
receipts_joins = """
|
||||
LEFT JOIN (
|
||||
SELECT room_id, thread_id,
|
||||
max_receipt_stream_ordering AS threaded_receipt_stream_ordering
|
||||
FROM all_receipts
|
||||
WHERE thread_id IS NOT NULL
|
||||
) AS threaded_receipts USING (room_id, thread_id)
|
||||
LEFT JOIN (
|
||||
SELECT room_id, thread_id,
|
||||
max_receipt_stream_ordering AS unthreaded_receipt_stream_ordering
|
||||
FROM all_receipts
|
||||
WHERE thread_id IS NULL
|
||||
) AS unthreaded_receipts USING (room_id)
|
||||
"""
|
||||
|
||||
# First get summary counts by room / thread for the user. We use the max receipt
|
||||
# stream ordering of both threaded & unthreaded receipts to compare against the
|
||||
# summary table.
|
||||
#
|
||||
# PostgreSQL and SQLite differ in comparing scalar numerics.
|
||||
if isinstance(self.database_engine, PostgresEngine):
|
||||
# GREATEST ignores NULLs.
|
||||
max_clause = """GREATEST(
|
||||
threaded_receipt_stream_ordering,
|
||||
unthreaded_receipt_stream_ordering
|
||||
)"""
|
||||
else:
|
||||
# MAX returns NULL if any are NULL, so COALESCE to 0 first.
|
||||
max_clause = """MAX(
|
||||
COALESCE(threaded_receipt_stream_ordering, 0),
|
||||
COALESCE(unthreaded_receipt_stream_ordering, 0)
|
||||
)"""
|
||||
|
||||
# Step 1, fetch all counts from `event_push_summary` for the user. This
|
||||
# is slightly convoluted as we also need to pull out the stream ordering
|
||||
# of the most recent receipt of the user in the room (either a thread
|
||||
# aware receipt or thread unaware receipt) in order to determine
|
||||
# whether the row in `event_push_summary` is stale. Hence the outer
|
||||
# GROUP BY and odd join condition against `receipts_linearized`.
|
||||
sql = f"""
|
||||
{receipts_cte}
|
||||
SELECT eps.room_id, eps.thread_id, notif_count
|
||||
FROM event_push_summary AS eps
|
||||
{receipts_joins}
|
||||
WHERE user_id = ?
|
||||
AND notif_count != 0
|
||||
AND (
|
||||
(last_receipt_stream_ordering IS NULL AND stream_ordering > {max_clause})
|
||||
OR last_receipt_stream_ordering = {max_clause}
|
||||
SELECT room_id, notif_count, stream_ordering, thread_id, last_receipt_stream_ordering,
|
||||
MAX(receipt_stream_ordering)
|
||||
FROM (
|
||||
SELECT e.room_id, notif_count, e.stream_ordering, e.thread_id, last_receipt_stream_ordering,
|
||||
ev.stream_ordering AS receipt_stream_ordering
|
||||
FROM event_push_summary AS e
|
||||
INNER JOIN local_current_membership USING (user_id, room_id)
|
||||
LEFT JOIN receipts_linearized AS r ON (
|
||||
e.user_id = r.user_id
|
||||
AND e.room_id = r.room_id
|
||||
AND (e.thread_id = r.thread_id OR r.thread_id IS NULL)
|
||||
AND {receipt_types_clause}
|
||||
)
|
||||
LEFT JOIN events AS ev ON (r.event_id = ev.event_id)
|
||||
WHERE e.user_id = ? and notif_count > 0
|
||||
) AS es
|
||||
GROUP BY room_id, notif_count, stream_ordering, thread_id, last_receipt_stream_ordering
|
||||
"""
|
||||
txn.execute(sql, args)
|
||||
|
||||
seen_thread_ids = set()
|
||||
room_to_count: Dict[str, int] = defaultdict(int)
|
||||
|
||||
for room_id, thread_id, notif_count in txn:
|
||||
room_to_count[room_id] += notif_count
|
||||
seen_thread_ids.add(thread_id)
|
||||
|
||||
# Now get any event push actions that haven't been rotated using the same OR
|
||||
# join and filter by receipt and event push summary rotated up to stream ordering.
|
||||
sql = f"""
|
||||
{receipts_cte}
|
||||
SELECT epa.room_id, epa.thread_id, COUNT(CASE WHEN epa.notif = 1 THEN 1 END) AS notif_count
|
||||
FROM event_push_actions AS epa
|
||||
{receipts_joins}
|
||||
WHERE user_id = ?
|
||||
AND epa.notif = 1
|
||||
AND stream_ordering > (SELECT stream_ordering FROM event_push_summary_stream_ordering)
|
||||
AND (threaded_receipt_stream_ordering IS NULL OR stream_ordering > threaded_receipt_stream_ordering)
|
||||
AND (unthreaded_receipt_stream_ordering IS NULL OR stream_ordering > unthreaded_receipt_stream_ordering)
|
||||
GROUP BY epa.room_id, epa.thread_id
|
||||
"""
|
||||
txn.execute(sql, args)
|
||||
|
||||
for room_id, thread_id, notif_count in txn:
|
||||
# Note: only count push actions we have valid summaries for with up to date receipt.
|
||||
if thread_id not in seen_thread_ids:
|
||||
continue
|
||||
room_to_count[room_id] += notif_count
|
||||
|
||||
thread_id_clause, thread_ids_args = make_in_list_sql_clause(
|
||||
self.database_engine, "epa.thread_id", seen_thread_ids
|
||||
txn.execute(
|
||||
sql,
|
||||
receipt_types_args
|
||||
+ [
|
||||
user_id,
|
||||
],
|
||||
)
|
||||
|
||||
room_to_count: Dict[str, int] = defaultdict(int)
|
||||
stale_room_ids = set()
|
||||
for row in txn:
|
||||
room_id = row[0]
|
||||
notif_count = row[1]
|
||||
stream_ordering = row[2]
|
||||
_thread_id = row[3]
|
||||
last_receipt_stream_ordering = row[4]
|
||||
receipt_stream_ordering = row[5]
|
||||
|
||||
if last_receipt_stream_ordering is None:
|
||||
if receipt_stream_ordering is None:
|
||||
room_to_count[room_id] += notif_count
|
||||
elif stream_ordering > receipt_stream_ordering:
|
||||
room_to_count[room_id] += notif_count
|
||||
else:
|
||||
# The latest read receipt from the user is after all the rows for
|
||||
# this room in `event_push_summary`. We ignore them, and
|
||||
# calculate the count from `event_push_actions` in step 3.
|
||||
pass
|
||||
elif last_receipt_stream_ordering == receipt_stream_ordering:
|
||||
room_to_count[room_id] += notif_count
|
||||
else:
|
||||
# The row is stale if `last_receipt_stream_ordering` is set and
|
||||
# *doesn't* match the latest receipt from the user.
|
||||
stale_room_ids.add(room_id)
|
||||
|
||||
# Discard any stale rooms from `room_to_count`, as we will recalculate
|
||||
# them in step 3.
|
||||
for room_id in stale_room_ids:
|
||||
room_to_count.pop(room_id, None)
|
||||
|
||||
# Step 2, basically the same query, except against `event_push_actions`
|
||||
# and only fetching rows inserted since the last rotation.
|
||||
rotated_upto_stream_ordering = self.db_pool.simple_select_one_onecol_txn(
|
||||
txn,
|
||||
table="event_push_summary_stream_ordering",
|
||||
keyvalues={},
|
||||
retcol="stream_ordering",
|
||||
)
|
||||
|
||||
# Finally re-check event_push_actions for any rooms not in the summary, ignoring
|
||||
# the rotated up-to position. This handles the case where a read receipt has arrived
|
||||
# but not been rotated meaning the summary table is out of date, so we go back to
|
||||
# the push actions table.
|
||||
sql = f"""
|
||||
{receipts_cte}
|
||||
SELECT epa.room_id, COUNT(CASE WHEN epa.notif = 1 THEN 1 END) AS notif_count
|
||||
FROM event_push_actions AS epa
|
||||
{receipts_joins}
|
||||
WHERE user_id = ?
|
||||
AND NOT {thread_id_clause}
|
||||
AND epa.notif = 1
|
||||
AND (threaded_receipt_stream_ordering IS NULL OR stream_ordering > threaded_receipt_stream_ordering)
|
||||
AND (unthreaded_receipt_stream_ordering IS NULL OR stream_ordering > unthreaded_receipt_stream_ordering)
|
||||
GROUP BY epa.room_id
|
||||
SELECT room_id, thread_id
|
||||
FROM (
|
||||
SELECT e.room_id, e.stream_ordering, e.thread_id,
|
||||
ev.stream_ordering AS receipt_stream_ordering
|
||||
FROM event_push_actions AS e
|
||||
INNER JOIN local_current_membership USING (user_id, room_id)
|
||||
LEFT JOIN receipts_linearized AS r ON (
|
||||
e.user_id = r.user_id
|
||||
AND e.room_id = r.room_id
|
||||
AND (e.thread_id = r.thread_id OR r.thread_id IS NULL)
|
||||
AND {receipt_types_clause}
|
||||
)
|
||||
LEFT JOIN events AS ev ON (r.event_id = ev.event_id)
|
||||
WHERE e.user_id = ? and notif > 0
|
||||
AND e.stream_ordering > ?
|
||||
) AS es
|
||||
GROUP BY room_id, stream_ordering, thread_id
|
||||
HAVING stream_ordering > COALESCE(MAX(receipt_stream_ordering), 0)
|
||||
"""
|
||||
|
||||
args.extend(thread_ids_args)
|
||||
txn.execute(sql, args)
|
||||
txn.execute(
|
||||
sql,
|
||||
receipt_types_args + [user_id, rotated_upto_stream_ordering],
|
||||
)
|
||||
for room_id, _thread_id in txn:
|
||||
# Again, we ignore any stale rooms.
|
||||
if room_id not in stale_room_ids:
|
||||
# For event push actions it is one notification per row.
|
||||
room_to_count[room_id] += 1
|
||||
|
||||
for room_id, notif_count in txn:
|
||||
room_to_count[room_id] += notif_count
|
||||
# Step 3, if we have stale rooms then we need to recalculate the counts
|
||||
# from `event_push_actions`. Again, this is basically the same query as
|
||||
# above except without a lower bound on stream ordering and only against
|
||||
# a specific set of rooms.
|
||||
if stale_room_ids:
|
||||
room_id_clause, room_id_args = make_in_list_sql_clause(
|
||||
self.database_engine,
|
||||
"e.room_id",
|
||||
stale_room_ids,
|
||||
)
|
||||
|
||||
sql = f"""
|
||||
SELECT room_id, thread_id
|
||||
FROM (
|
||||
SELECT e.room_id, e.stream_ordering, e.thread_id,
|
||||
ev.stream_ordering AS receipt_stream_ordering
|
||||
FROM event_push_actions AS e
|
||||
INNER JOIN local_current_membership USING (user_id, room_id)
|
||||
LEFT JOIN receipts_linearized AS r ON (
|
||||
e.user_id = r.user_id
|
||||
AND e.room_id = r.room_id
|
||||
AND (e.thread_id = r.thread_id OR r.thread_id IS NULL)
|
||||
AND {receipt_types_clause}
|
||||
)
|
||||
LEFT JOIN events AS ev ON (r.event_id = ev.event_id)
|
||||
WHERE e.user_id = ? and notif > 0
|
||||
AND {room_id_clause}
|
||||
) AS es
|
||||
GROUP BY room_id, stream_ordering, thread_id
|
||||
HAVING stream_ordering > COALESCE(MAX(receipt_stream_ordering), 0)
|
||||
"""
|
||||
txn.execute(
|
||||
sql,
|
||||
receipt_types_args + [user_id] + room_id_args,
|
||||
)
|
||||
for room_id, _ in txn:
|
||||
room_to_count[room_id] += 1
|
||||
|
||||
return room_to_count
|
||||
|
||||
|
||||
@@ -1496,7 +1496,7 @@ class EventsWorkerStore(SQLBaseStore):
|
||||
room_version_id=row[5],
|
||||
rejected_reason=row[6],
|
||||
redactions=[],
|
||||
outlier=row[7],
|
||||
outlier=bool(row[7]), # This is an int in SQLite3
|
||||
)
|
||||
|
||||
# check for redactions
|
||||
|
||||
@@ -24,13 +24,17 @@ from typing import (
|
||||
Any,
|
||||
Collection,
|
||||
Dict,
|
||||
FrozenSet,
|
||||
Iterable,
|
||||
List,
|
||||
Mapping,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
|
||||
import attr
|
||||
@@ -52,7 +56,7 @@ from synapse.storage.database import (
|
||||
)
|
||||
from synapse.storage.databases.main.events_worker import EventsWorkerStore
|
||||
from synapse.storage.databases.main.roommember import RoomMemberWorkerStore
|
||||
from synapse.types import JsonDict, JsonMapping, StateMap
|
||||
from synapse.types import JsonDict, JsonMapping, StateKey, StateMap, StrCollection
|
||||
from synapse.types.state import StateFilter
|
||||
from synapse.util.caches import intern_string
|
||||
from synapse.util.caches.descriptors import cached, cachedList
|
||||
@@ -64,6 +68,8 @@ if TYPE_CHECKING:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
MAX_STATE_DELTA_HOPS = 100
|
||||
|
||||
@@ -318,6 +324,20 @@ class StateGroupWorkerStore(EventsWorkerStore, SQLBaseStore):
|
||||
"get_partial_current_state_ids", _get_current_state_ids_txn
|
||||
)
|
||||
|
||||
async def check_if_events_in_current_state(
|
||||
self, event_ids: StrCollection
|
||||
) -> FrozenSet[str]:
|
||||
"""Checks and returns which of the given events is part of the current state."""
|
||||
rows = await self.db_pool.simple_select_many_batch(
|
||||
table="current_state_events",
|
||||
column="event_id",
|
||||
iterable=event_ids,
|
||||
retcols=("event_id",),
|
||||
desc="check_if_events_in_current_state",
|
||||
)
|
||||
|
||||
return frozenset(event_id for event_id, in rows)
|
||||
|
||||
# FIXME: how should this be cached?
|
||||
@cancellable
|
||||
async def get_partial_filtered_current_state_ids(
|
||||
@@ -349,7 +369,8 @@ class StateGroupWorkerStore(EventsWorkerStore, SQLBaseStore):
|
||||
def _get_filtered_current_state_ids_txn(
|
||||
txn: LoggingTransaction,
|
||||
) -> StateMap[str]:
|
||||
results = {}
|
||||
results = StateMapWrapper(state_filter=state_filter or StateFilter.all())
|
||||
|
||||
sql = """
|
||||
SELECT type, state_key, event_id FROM current_state_events
|
||||
WHERE room_id = ?
|
||||
@@ -726,3 +747,41 @@ class StateStore(StateGroupWorkerStore, MainStateBackgroundUpdateStore):
|
||||
hs: "HomeServer",
|
||||
):
|
||||
super().__init__(database, db_conn, hs)
|
||||
|
||||
|
||||
@attr.s(auto_attribs=True, slots=True)
|
||||
class StateMapWrapper(Dict[StateKey, str]):
|
||||
"""A wrapper around a StateMap[str] to ensure that we only query for items
|
||||
that were not filtered out.
|
||||
|
||||
This is to help prevent bugs where we filter out state but other bits of the
|
||||
code expect the state to be there.
|
||||
"""
|
||||
|
||||
state_filter: StateFilter
|
||||
|
||||
def __getitem__(self, key: StateKey) -> str:
|
||||
if key not in self.state_filter:
|
||||
raise Exception("State map was filtered and doesn't include: %s", key)
|
||||
return super().__getitem__(key)
|
||||
|
||||
@overload
|
||||
def get(self, key: Tuple[str, str]) -> Optional[str]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def get(self, key: Tuple[str, str], default: Union[str, _T]) -> Union[str, _T]:
|
||||
...
|
||||
|
||||
def get(
|
||||
self, key: StateKey, default: Union[str, _T, None] = None
|
||||
) -> Union[str, _T, None]:
|
||||
if key not in self.state_filter:
|
||||
raise Exception("State map was filtered and doesn't include: %s", key)
|
||||
return super().get(key, default)
|
||||
|
||||
def __contains__(self, key: Any) -> bool:
|
||||
if key not in self.state_filter:
|
||||
raise Exception("State map was filtered and doesn't include: %s", key)
|
||||
|
||||
return super().__contains__(key)
|
||||
|
||||
@@ -705,8 +705,6 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore):
|
||||
[r.event_id for r in rows], get_prev_content=True
|
||||
)
|
||||
|
||||
self._set_before_and_after(ret, rows, topo_order=False)
|
||||
|
||||
if order.lower() == "desc":
|
||||
ret.reverse()
|
||||
|
||||
@@ -793,8 +791,6 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore):
|
||||
[r.event_id for r in rows], get_prev_content=True
|
||||
)
|
||||
|
||||
self._set_before_and_after(ret, rows, topo_order=False)
|
||||
|
||||
return ret
|
||||
|
||||
async def get_recent_events_for_room(
|
||||
@@ -820,8 +816,6 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore):
|
||||
[r.event_id for r in rows], get_prev_content=True
|
||||
)
|
||||
|
||||
self._set_before_and_after(events, rows)
|
||||
|
||||
return events, token
|
||||
|
||||
async def get_recent_event_ids_for_room(
|
||||
@@ -1094,31 +1088,6 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore):
|
||||
# `[(None,)]`
|
||||
return rows[0][0] if rows[0][0] is not None else 0
|
||||
|
||||
@staticmethod
|
||||
def _set_before_and_after(
|
||||
events: List[EventBase], rows: List[_EventDictReturn], topo_order: bool = True
|
||||
) -> None:
|
||||
"""Inserts ordering information to events' internal metadata from
|
||||
the DB rows.
|
||||
|
||||
Args:
|
||||
events
|
||||
rows
|
||||
topo_order: Whether the events were ordered topologically or by stream
|
||||
ordering. If true then all rows should have a non null
|
||||
topological_ordering.
|
||||
"""
|
||||
for event, row in zip(events, rows):
|
||||
stream = row.stream_ordering
|
||||
if topo_order and row.topological_ordering:
|
||||
topo: Optional[int] = row.topological_ordering
|
||||
else:
|
||||
topo = None
|
||||
internal = event.internal_metadata
|
||||
internal.before = RoomStreamToken(topological=topo, stream=stream - 1)
|
||||
internal.after = RoomStreamToken(topological=topo, stream=stream)
|
||||
internal.order = (int(topo) if topo else 0, int(stream))
|
||||
|
||||
async def get_events_around(
|
||||
self,
|
||||
room_id: str,
|
||||
@@ -1559,8 +1528,6 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore):
|
||||
[r.event_id for r in rows], get_prev_content=True
|
||||
)
|
||||
|
||||
self._set_before_and_after(events, rows)
|
||||
|
||||
return events, token
|
||||
|
||||
@cached()
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
#
|
||||
# Copyright (C) 2024 New Vector, Ltd
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# See the GNU Affero General Public License for more details:
|
||||
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from synapse.types import JsonDict
|
||||
|
||||
class EventInternalMetadata:
|
||||
def __init__(self, internal_metadata_dict: JsonDict): ...
|
||||
|
||||
stream_ordering: Optional[int]
|
||||
"""the stream ordering of this event. None, until it has been persisted."""
|
||||
|
||||
outlier: bool
|
||||
"""whether this event is an outlier (ie, whether we have the state at that
|
||||
point in the DAG)"""
|
||||
|
||||
out_of_band_membership: bool
|
||||
send_on_behalf_of: str
|
||||
recheck_redaction: bool
|
||||
soft_failed: bool
|
||||
proactively_send: bool
|
||||
redacted: bool
|
||||
|
||||
txn_id: str
|
||||
"""The transaction ID, if it was set when the event was created."""
|
||||
token_id: int
|
||||
"""The access token ID of the user who sent this event, if any."""
|
||||
device_id: str
|
||||
"""The device ID of the user who sent this event, if any."""
|
||||
|
||||
def get_dict(self) -> JsonDict: ...
|
||||
def is_outlier(self) -> bool: ...
|
||||
def copy(self) -> "EventInternalMetadata": ...
|
||||
def is_out_of_band_membership(self) -> bool:
|
||||
"""Whether this event is an out-of-band membership.
|
||||
|
||||
OOB memberships are a special case of outlier events: they are membership events
|
||||
for federated rooms that we aren't full members of. Examples include invites
|
||||
received over federation, and rejections for such invites.
|
||||
|
||||
The concept of an OOB membership is needed because these events need to be
|
||||
processed as if they're new regular events (e.g. updating membership state in
|
||||
the database, relaying to clients via /sync, etc) despite being outliers.
|
||||
|
||||
See also https://element-hq.github.io/synapse/develop/development/room-dag-concepts.html#out-of-band-membership-events.
|
||||
|
||||
(Added in synapse 0.99.0, so may be unreliable for events received before that)
|
||||
"""
|
||||
...
|
||||
def get_send_on_behalf_of(self) -> Optional[str]:
|
||||
"""Whether this server should send the event on behalf of another server.
|
||||
This is used by the federation "send_join" API to forward the initial join
|
||||
event for a server in the room.
|
||||
|
||||
returns a str with the name of the server this event is sent on behalf of.
|
||||
"""
|
||||
...
|
||||
def need_to_check_redaction(self) -> bool:
|
||||
"""Whether the redaction event needs to be rechecked when fetching
|
||||
from the database.
|
||||
|
||||
Starting in room v3 redaction events are accepted up front, and later
|
||||
checked to see if the redacter and redactee's domains match.
|
||||
|
||||
If the sender of the redaction event is allowed to redact any event
|
||||
due to auth rules, then this will always return false.
|
||||
"""
|
||||
...
|
||||
def is_soft_failed(self) -> bool:
|
||||
"""Whether the event has been soft failed.
|
||||
|
||||
Soft failed events should be handled as usual, except:
|
||||
1. They should not go down sync or event streams, or generally
|
||||
sent to clients.
|
||||
2. They should not be added to the forward extremities (and
|
||||
therefore not to current state).
|
||||
"""
|
||||
...
|
||||
def should_proactively_send(self) -> bool:
|
||||
"""Whether the event, if ours, should be sent to other clients and
|
||||
servers.
|
||||
|
||||
This is used for sending dummy events internally. Servers and clients
|
||||
can still explicitly fetch the event.
|
||||
"""
|
||||
...
|
||||
def is_redacted(self) -> bool:
|
||||
"""Whether the event has been redacted.
|
||||
|
||||
This is used for efficiently checking whether an event has been
|
||||
marked as redacted without needing to make another database call.
|
||||
"""
|
||||
...
|
||||
def is_notifiable(self) -> bool:
|
||||
"""Whether this event can trigger a push notification"""
|
||||
...
|
||||
@@ -20,6 +20,7 @@
|
||||
import logging
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Collection,
|
||||
Dict,
|
||||
@@ -584,6 +585,29 @@ class StateFilter:
|
||||
# local users only
|
||||
return False
|
||||
|
||||
def __contains__(self, key: Any) -> bool:
|
||||
if not isinstance(key, tuple) or len(key) != 2:
|
||||
raise TypeError(
|
||||
f"'in StateFilter' requires (str, str) as left operand, not {type(key).__name__}"
|
||||
)
|
||||
|
||||
typ, state_key = key
|
||||
|
||||
if not isinstance(typ, str) or not isinstance(state_key, str):
|
||||
raise TypeError(
|
||||
f"'in StateFilter' requires (str, str) as left operand, not ({type(typ).__name__}, {type(state_key).__name__})"
|
||||
)
|
||||
|
||||
if typ in self.types:
|
||||
state_keys = self.types[typ]
|
||||
if state_keys is None or state_key in state_keys:
|
||||
return True
|
||||
|
||||
elif self.include_others:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
_ALL_STATE_FILTER = StateFilter(types=immutabledict(), include_others=True)
|
||||
_ALL_NON_MEMBER_STATE_FILTER = StateFilter(
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
from http import HTTPStatus
|
||||
from typing import Optional, Set
|
||||
|
||||
from synapse.rest import admin
|
||||
from synapse.rest.client import directory, login, room
|
||||
from synapse.types import JsonDict
|
||||
|
||||
from tests import unittest
|
||||
|
||||
|
||||
class RoomListHandlerTestCase(unittest.HomeserverTestCase):
|
||||
servlets = [
|
||||
admin.register_servlets,
|
||||
login.register_servlets,
|
||||
room.register_servlets,
|
||||
directory.register_servlets,
|
||||
]
|
||||
|
||||
def _create_published_room(
|
||||
self, tok: str, extra_content: Optional[JsonDict] = None
|
||||
) -> str:
|
||||
room_id = self.helper.create_room_as(tok=tok, extra_content=extra_content)
|
||||
channel = self.make_request(
|
||||
"PUT",
|
||||
f"/_matrix/client/v3/directory/list/room/{room_id}?access_token={tok}",
|
||||
content={
|
||||
"visibility": "public",
|
||||
},
|
||||
)
|
||||
assert channel.code == HTTPStatus.OK, f"couldn't publish room: {channel.result}"
|
||||
return room_id
|
||||
|
||||
def test_acls_applied_to_room_directory_results(self) -> None:
|
||||
"""
|
||||
Creates 3 rooms. Room 2 has an ACL that only permits the homeservers
|
||||
`test` and `test2` to access it.
|
||||
|
||||
We then simulate `test2` and `test3` requesting the room directory and assert
|
||||
that `test3` does not see room 2, but `test2` sees all 3.
|
||||
"""
|
||||
self.register_user("u1", "p1")
|
||||
u1tok = self.login("u1", "p1")
|
||||
room1 = self._create_published_room(u1tok)
|
||||
|
||||
room2 = self._create_published_room(
|
||||
u1tok,
|
||||
extra_content={
|
||||
"initial_state": [
|
||||
{
|
||||
"type": "m.room.server_acl",
|
||||
"content": {
|
||||
"allow": ["test", "test2"],
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
room3 = self._create_published_room(u1tok)
|
||||
|
||||
room_list = self.get_success(
|
||||
self.hs.get_room_list_handler().get_local_public_room_list(
|
||||
limit=50, from_federation_origin="test2"
|
||||
)
|
||||
)
|
||||
room_ids_in_test2_list: Set[str] = {
|
||||
entry["room_id"] for entry in room_list["chunk"]
|
||||
}
|
||||
|
||||
room_list = self.get_success(
|
||||
self.hs.get_room_list_handler().get_local_public_room_list(
|
||||
limit=50, from_federation_origin="test3"
|
||||
)
|
||||
)
|
||||
room_ids_in_test3_list: Set[str] = {
|
||||
entry["room_id"] for entry in room_list["chunk"]
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
room_ids_in_test2_list,
|
||||
{room1, room2, room3},
|
||||
"test2 should be able to see all 3 rooms",
|
||||
)
|
||||
self.assertEqual(
|
||||
room_ids_in_test3_list,
|
||||
{room1, room3},
|
||||
"test3 should be able to see only 2 rooms",
|
||||
)
|
||||
@@ -1638,8 +1638,17 @@ class UserRestTestCase(unittest.HomeserverTestCase):
|
||||
)
|
||||
)
|
||||
|
||||
self.non_ascii_displayname = "ąćęłńóśżźäöüß中国日本"
|
||||
self.non_ascii_user = self.register_user(
|
||||
"nonascii", "nonascii", displayname=self.non_ascii_displayname
|
||||
)
|
||||
|
||||
self.url_prefix = "/_synapse/admin/v2/users/%s"
|
||||
self.url_other_user = self.url_prefix % self.other_user
|
||||
self.url_non_ascii_user = (
|
||||
"/_synapse/admin/v2/users?name=%s"
|
||||
% urllib.parse.quote(self.non_ascii_displayname)
|
||||
)
|
||||
|
||||
def test_requester_is_no_admin(self) -> None:
|
||||
"""
|
||||
@@ -1790,6 +1799,20 @@ class UserRestTestCase(unittest.HomeserverTestCase):
|
||||
self.assertEqual("User", channel.json_body["displayname"])
|
||||
self._check_fields(channel.json_body)
|
||||
|
||||
def test_get_user_nonascii_displayname(self) -> None:
|
||||
"""
|
||||
Test get user by non-ascii display name
|
||||
"""
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
self.url_non_ascii_user,
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
|
||||
users = {user["name"]: user for user in channel.json_body["users"]}
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
self.assertIn(self.non_ascii_user, users, channel.json_body["users"])
|
||||
|
||||
def test_create_server_admin(self) -> None:
|
||||
"""
|
||||
Check that a new admin user is created successfully.
|
||||
@@ -2747,7 +2770,7 @@ class UserRestTestCase(unittest.HomeserverTestCase):
|
||||
profile = self.get_success(self.store._get_user_in_directory(self.other_user))
|
||||
self.assertIsNone(profile)
|
||||
|
||||
def test_reactivate_user(self) -> None:
|
||||
def test_reactivate_user_with_password(self) -> None:
|
||||
"""
|
||||
Test reactivating another user.
|
||||
"""
|
||||
@@ -2755,16 +2778,7 @@ class UserRestTestCase(unittest.HomeserverTestCase):
|
||||
# Deactivate the user.
|
||||
self._deactivate_user("@user:test")
|
||||
|
||||
# Attempt to reactivate the user (without a password).
|
||||
channel = self.make_request(
|
||||
"PUT",
|
||||
self.url_other_user,
|
||||
access_token=self.admin_user_tok,
|
||||
content={"deactivated": False},
|
||||
)
|
||||
self.assertEqual(400, channel.code, msg=channel.json_body)
|
||||
|
||||
# Reactivate the user.
|
||||
# Reactivate the user with password.
|
||||
channel = self.make_request(
|
||||
"PUT",
|
||||
self.url_other_user,
|
||||
@@ -2779,6 +2793,30 @@ class UserRestTestCase(unittest.HomeserverTestCase):
|
||||
# This key was removed intentionally. Ensure it is not accidentally re-included.
|
||||
self.assertNotIn("password_hash", channel.json_body)
|
||||
|
||||
def test_reactivate_user_without_password(self) -> None:
|
||||
"""
|
||||
Test reactivating another user without a password.
|
||||
This can be using some local users and some user with SSO (password = `null`).
|
||||
"""
|
||||
|
||||
# Deactivate the user.
|
||||
self._deactivate_user("@user:test")
|
||||
|
||||
# Reactivate the user without a password.
|
||||
channel = self.make_request(
|
||||
"PUT",
|
||||
self.url_other_user,
|
||||
access_token=self.admin_user_tok,
|
||||
content={"deactivated": False},
|
||||
)
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
self.assertEqual("@user:test", channel.json_body["name"])
|
||||
self.assertFalse(channel.json_body["deactivated"])
|
||||
self._is_erased("@user:test", False)
|
||||
|
||||
# This key was removed intentionally. Ensure it is not accidentally re-included.
|
||||
self.assertNotIn("password_hash", channel.json_body)
|
||||
|
||||
@override_config({"password_config": {"localdb_enabled": False}})
|
||||
def test_reactivate_user_localdb_disabled(self) -> None:
|
||||
"""
|
||||
@@ -2788,7 +2826,7 @@ class UserRestTestCase(unittest.HomeserverTestCase):
|
||||
# Deactivate the user.
|
||||
self._deactivate_user("@user:test")
|
||||
|
||||
# Reactivate the user with a password
|
||||
# Reactivate the user with a password.
|
||||
channel = self.make_request(
|
||||
"PUT",
|
||||
self.url_other_user,
|
||||
@@ -2822,7 +2860,7 @@ class UserRestTestCase(unittest.HomeserverTestCase):
|
||||
# Deactivate the user.
|
||||
self._deactivate_user("@user:test")
|
||||
|
||||
# Reactivate the user with a password
|
||||
# Reactivate the user with a password.
|
||||
channel = self.make_request(
|
||||
"PUT",
|
||||
self.url_other_user,
|
||||
|
||||
@@ -328,16 +328,49 @@ class PasswordResetTestCase(unittest.HomeserverTestCase):
|
||||
|
||||
self.assertIsNotNone(session_id)
|
||||
|
||||
def test_password_reset_redirection(self) -> None:
|
||||
"""Test basic password reset flow"""
|
||||
old_password = "monkey"
|
||||
|
||||
user_id = self.register_user("kermit", old_password)
|
||||
self.login("kermit", old_password)
|
||||
|
||||
email = "test@example.com"
|
||||
|
||||
# Add a threepid
|
||||
self.get_success(
|
||||
self.store.user_add_threepid(
|
||||
user_id=user_id,
|
||||
medium="email",
|
||||
address=email,
|
||||
validated_at=0,
|
||||
added_at=0,
|
||||
)
|
||||
)
|
||||
|
||||
client_secret = "foobar"
|
||||
next_link = "http://example.com"
|
||||
self._request_token(email, client_secret, "127.0.0.1", next_link)
|
||||
|
||||
self.assertEqual(len(self.email_attempts), 1)
|
||||
link = self._get_link_from_email()
|
||||
|
||||
self._validate_token(link, next_link)
|
||||
|
||||
def _request_token(
|
||||
self,
|
||||
email: str,
|
||||
client_secret: str,
|
||||
ip: str = "127.0.0.1",
|
||||
next_link: Optional[str] = None,
|
||||
) -> str:
|
||||
body = {"client_secret": client_secret, "email": email, "send_attempt": 1}
|
||||
if next_link is not None:
|
||||
body["next_link"] = next_link
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
b"account/password/email/requestToken",
|
||||
{"client_secret": client_secret, "email": email, "send_attempt": 1},
|
||||
body,
|
||||
client_ip=ip,
|
||||
)
|
||||
|
||||
@@ -350,7 +383,7 @@ class PasswordResetTestCase(unittest.HomeserverTestCase):
|
||||
|
||||
return channel.json_body["sid"]
|
||||
|
||||
def _validate_token(self, link: str) -> None:
|
||||
def _validate_token(self, link: str, next_link: Optional[str] = None) -> None:
|
||||
# Remove the host
|
||||
path = link.replace("https://example.com", "")
|
||||
|
||||
@@ -378,7 +411,11 @@ class PasswordResetTestCase(unittest.HomeserverTestCase):
|
||||
shorthand=False,
|
||||
content_is_form=True,
|
||||
)
|
||||
self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
|
||||
self.assertEqual(
|
||||
HTTPStatus.OK if next_link is None else HTTPStatus.FOUND,
|
||||
channel.code,
|
||||
channel.result,
|
||||
)
|
||||
|
||||
def _get_link_from_email(self) -> str:
|
||||
assert self.email_attempts, "No emails have been sent"
|
||||
|
||||
@@ -324,7 +324,7 @@ class DatabaseOutageTestCase(unittest.HomeserverTestCase):
|
||||
)
|
||||
|
||||
self.event_ids: List[str] = []
|
||||
for idx in range(20):
|
||||
for idx in range(1, 21): # Stream ordering starts at 1.
|
||||
event_json = {
|
||||
"type": f"test {idx}",
|
||||
"room_id": self.room_id,
|
||||
|
||||
@@ -44,12 +44,13 @@ from synapse.api.room_versions import (
|
||||
EventFormatVersions,
|
||||
RoomVersion,
|
||||
)
|
||||
from synapse.events import EventBase, _EventInternalMetadata
|
||||
from synapse.events import EventBase
|
||||
from synapse.rest import admin
|
||||
from synapse.rest.client import login, room
|
||||
from synapse.server import HomeServer
|
||||
from synapse.storage.database import LoggingTransaction
|
||||
from synapse.storage.types import Cursor
|
||||
from synapse.synapse_rust.events import EventInternalMetadata
|
||||
from synapse.types import JsonDict
|
||||
from synapse.util import Clock, json_encoder
|
||||
|
||||
@@ -1209,7 +1210,7 @@ class FakeEvent:
|
||||
type = "foo"
|
||||
state_key = "foo"
|
||||
|
||||
internal_metadata = _EventInternalMetadata({})
|
||||
internal_metadata = EventInternalMetadata({})
|
||||
|
||||
def auth_event_ids(self) -> List[str]:
|
||||
return self.auth_events
|
||||
|
||||
@@ -25,9 +25,10 @@ from twisted.test.proto_helpers import MemoryReactor
|
||||
|
||||
from synapse.api.constants import EventTypes, Membership
|
||||
from synapse.api.room_versions import RoomVersions
|
||||
from synapse.events import EventBase, _EventInternalMetadata
|
||||
from synapse.events import EventBase
|
||||
from synapse.events.builder import EventBuilder
|
||||
from synapse.server import HomeServer
|
||||
from synapse.synapse_rust.events import EventInternalMetadata
|
||||
from synapse.types import JsonDict, RoomID, UserID
|
||||
from synapse.util import Clock
|
||||
|
||||
@@ -268,7 +269,7 @@ class RedactionTestCase(unittest.HomeserverTestCase):
|
||||
return self._base_builder.type
|
||||
|
||||
@property
|
||||
def internal_metadata(self) -> _EventInternalMetadata:
|
||||
def internal_metadata(self) -> EventInternalMetadata:
|
||||
return self._base_builder.internal_metadata
|
||||
|
||||
event_1, unpersisted_context_1 = self.get_success(
|
||||
|
||||
Reference in New Issue
Block a user