Merge remote-tracking branch 'origin/develop' into clokep/complement-workers
This commit is contained in:
commit
3cdb39d1f0
33
CHANGES.md
33
CHANGES.md
|
@ -1,3 +1,36 @@
|
|||
Users will stop receiving message updates via email for addresses that were previously linked to their account
|
||||
|
||||
Synapse 1.41.1 (2021-08-31)
|
||||
===========================
|
||||
|
||||
Due to the two security issues highlighted below, server administrators are encouraged to update Synapse. We are not aware of these vulnerabilities being exploited in the wild.
|
||||
|
||||
Security advisory
|
||||
-----------------
|
||||
|
||||
The following issues are fixed in v1.41.1.
|
||||
|
||||
- **[GHSA-3x4c-pq33-4w3q](https://github.com/matrix-org/synapse/security/advisories/GHSA-3x4c-pq33-4w3q) / [CVE-2021-39164](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-39164): Enumerating a private room's list of members and their display names.**
|
||||
|
||||
If an unauthorized user both knows the Room ID of a private room *and* that room's history visibility is set to `shared`, then they may be able to enumerate the room's members, including their display names.
|
||||
|
||||
The unauthorized user must be on the same homeserver as a user who is a member of the target room.
|
||||
|
||||
Fixed by [52c7a51cf](https://github.com/matrix-org/synapse/commit/52c7a51cf).
|
||||
|
||||
- **[GHSA-jj53-8fmw-f2w2](https://github.com/matrix-org/synapse/security/advisories/GHSA-jj53-8fmw-f2w2) / [CVE-2021-39163](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-39163): Disclosing a private room's name, avatar, topic, and number of members.**
|
||||
|
||||
If an unauthorized user knows the Room ID of a private room, then its name, avatar, topic, and number of members may be disclosed through Group / Community features.
|
||||
|
||||
The unauthorized user must be on the same homeserver as a user who is a member of the target room, and their homeserver must allow non-administrators to create groups (`enable_group_creation` in the Synapse configuration; off by default).
|
||||
|
||||
Fixed by [cb35df940a](https://github.com/matrix-org/synapse/commit/cb35df940a), [\#10723](https://github.com/matrix-org/synapse/issues/10723).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix a regression introduced in Synapse 1.41 which broke email transmission on systems using older versions of the Twisted library. ([\#10713](https://github.com/matrix-org/synapse/issues/10713))
|
||||
|
||||
Synapse 1.41.0 (2021-08-24)
|
||||
===========================
|
||||
|
||||
|
|
444
CONTRIBUTING.md
444
CONTRIBUTING.md
|
@ -1,443 +1,3 @@
|
|||
Welcome to Synapse
|
||||
# Welcome to Synapse
|
||||
|
||||
This document aims to get you started with contributing to this repo!
|
||||
|
||||
- [1. Who can contribute to Synapse?](#1-who-can-contribute-to-synapse)
|
||||
- [2. What do I need?](#2-what-do-i-need)
|
||||
- [3. Get the source.](#3-get-the-source)
|
||||
- [4. Install the dependencies](#4-install-the-dependencies)
|
||||
* [Under Unix (macOS, Linux, BSD, ...)](#under-unix-macos-linux-bsd-)
|
||||
* [Under Windows](#under-windows)
|
||||
- [5. Get in touch.](#5-get-in-touch)
|
||||
- [6. Pick an issue.](#6-pick-an-issue)
|
||||
- [7. Turn coffee and documentation into code and documentation!](#7-turn-coffee-and-documentation-into-code-and-documentation)
|
||||
- [8. Test, test, test!](#8-test-test-test)
|
||||
* [Run the linters.](#run-the-linters)
|
||||
* [Run the unit tests.](#run-the-unit-tests-twisted-trial)
|
||||
* [Run the integration tests (SyTest).](#run-the-integration-tests-sytest)
|
||||
* [Run the integration tests (Complement).](#run-the-integration-tests-complement)
|
||||
- [9. Submit your patch.](#9-submit-your-patch)
|
||||
* [Changelog](#changelog)
|
||||
+ [How do I know what to call the changelog file before I create the PR?](#how-do-i-know-what-to-call-the-changelog-file-before-i-create-the-pr)
|
||||
+ [Debian changelog](#debian-changelog)
|
||||
* [Sign off](#sign-off)
|
||||
- [10. Turn feedback into better code.](#10-turn-feedback-into-better-code)
|
||||
- [11. Find a new issue.](#11-find-a-new-issue)
|
||||
- [Notes for maintainers on merging PRs etc](#notes-for-maintainers-on-merging-prs-etc)
|
||||
- [Conclusion](#conclusion)
|
||||
|
||||
# 1. Who can contribute to Synapse?
|
||||
|
||||
Everyone is welcome to contribute code to [matrix.org
|
||||
projects](https://github.com/matrix-org), 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](LICENSE)).
|
||||
|
||||
# 2. What do I need?
|
||||
|
||||
The code of Synapse is written in Python 3. To do pretty much anything, you'll need [a recent version of Python 3](https://wiki.python.org/moin/BeginnersGuide/Download).
|
||||
|
||||
The source code of Synapse is hosted on GitHub. You will also need [a recent version of git](https://github.com/git-guides/install-git).
|
||||
|
||||
For some tests, you will need [a recent version of Docker](https://docs.docker.com/get-docker/).
|
||||
|
||||
|
||||
# 3. Get the source.
|
||||
|
||||
The preferred and easiest way to contribute changes is to fork the relevant
|
||||
project on GitHub, and then [create a pull request](
|
||||
https://help.github.com/articles/using-pull-requests/) to ask us to pull your
|
||||
changes into our repo.
|
||||
|
||||
Please base your changes on the `develop` branch.
|
||||
|
||||
```sh
|
||||
git clone git@github.com:YOUR_GITHUB_USER_NAME/synapse.git
|
||||
git checkout develop
|
||||
```
|
||||
|
||||
If you need help getting started with git, this is beyond the scope of the document, but you
|
||||
can find many good git tutorials on the web.
|
||||
|
||||
# 4. Install the dependencies
|
||||
|
||||
## Under Unix (macOS, Linux, BSD, ...)
|
||||
|
||||
Once you have installed Python 3 and added the source, please open a terminal and
|
||||
setup a *virtualenv*, as follows:
|
||||
|
||||
```sh
|
||||
cd path/where/you/have/cloned/the/repository
|
||||
python3 -m venv ./env
|
||||
source ./env/bin/activate
|
||||
pip install -e ".[all,lint,mypy,test]"
|
||||
pip install tox
|
||||
```
|
||||
|
||||
This will install the developer dependencies for the project.
|
||||
|
||||
## Under Windows
|
||||
|
||||
TBD
|
||||
|
||||
|
||||
# 5. Get in touch.
|
||||
|
||||
Join our developer community on Matrix: #synapse-dev:matrix.org !
|
||||
|
||||
|
||||
# 6. Pick an issue.
|
||||
|
||||
Fix your favorite problem or perhaps find a [Good First Issue](https://github.com/matrix-org/synapse/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+First+Issue%22)
|
||||
to work on.
|
||||
|
||||
|
||||
# 7. Turn coffee and documentation into code and documentation!
|
||||
|
||||
Synapse's code style is documented [here](docs/code_style.md). Please follow
|
||||
it, including the conventions for the [sample configuration
|
||||
file](docs/code_style.md#configuration-file-format).
|
||||
|
||||
There is a growing amount of documentation located in the [docs](docs)
|
||||
directory. This documentation is intended primarily for sysadmins running their
|
||||
own Synapse instance, as well as developers interacting externally with
|
||||
Synapse. [docs/dev](docs/dev) exists primarily to house documentation for
|
||||
Synapse developers. [docs/admin_api](docs/admin_api) houses documentation
|
||||
regarding Synapse's Admin API, which is used mostly by sysadmins and external
|
||||
service developers.
|
||||
|
||||
If you add new files added to either of these folders, please use [GitHub-Flavoured
|
||||
Markdown](https://guides.github.com/features/mastering-markdown/).
|
||||
|
||||
Some documentation also exists in [Synapse's GitHub
|
||||
Wiki](https://github.com/matrix-org/synapse/wiki), although this is primarily
|
||||
contributed to by community authors.
|
||||
|
||||
|
||||
# 8. Test, test, test!
|
||||
<a name="test-test-test"></a>
|
||||
|
||||
While you're developing and before submitting a patch, you'll
|
||||
want to test your code.
|
||||
|
||||
## Run the linters.
|
||||
|
||||
The linters look at your code and do two things:
|
||||
|
||||
- ensure that your code follows the coding style adopted by the project;
|
||||
- catch a number of errors in your code.
|
||||
|
||||
They're pretty fast, don't hesitate!
|
||||
|
||||
```sh
|
||||
source ./env/bin/activate
|
||||
./scripts-dev/lint.sh
|
||||
```
|
||||
|
||||
Note that this script *will modify your files* to fix styling errors.
|
||||
Make sure that you have saved all your files.
|
||||
|
||||
If you wish to restrict the linters to only the files changed since the last commit
|
||||
(much faster!), you can instead run:
|
||||
|
||||
```sh
|
||||
source ./env/bin/activate
|
||||
./scripts-dev/lint.sh -d
|
||||
```
|
||||
|
||||
Or if you know exactly which files you wish to lint, you can instead run:
|
||||
|
||||
```sh
|
||||
source ./env/bin/activate
|
||||
./scripts-dev/lint.sh path/to/file1.py path/to/file2.py path/to/folder
|
||||
```
|
||||
|
||||
## Run the unit tests (Twisted trial).
|
||||
|
||||
The unit tests run parts of Synapse, including your changes, to see if anything
|
||||
was broken. They are slower than the linters but will typically catch more errors.
|
||||
|
||||
```sh
|
||||
source ./env/bin/activate
|
||||
trial tests
|
||||
```
|
||||
|
||||
If you wish to only run *some* unit tests, you may specify
|
||||
another module instead of `tests` - or a test class or a method:
|
||||
|
||||
```sh
|
||||
source ./env/bin/activate
|
||||
trial tests.rest.admin.test_room tests.handlers.test_admin.ExfiltrateData.test_invite
|
||||
```
|
||||
|
||||
If your tests fail, you may wish to look at the logs (the default log level is `ERROR`):
|
||||
|
||||
```sh
|
||||
less _trial_temp/test.log
|
||||
```
|
||||
|
||||
To increase the log level for the tests, set `SYNAPSE_TEST_LOG_LEVEL`:
|
||||
|
||||
```sh
|
||||
SYNAPSE_TEST_LOG_LEVEL=DEBUG trial tests
|
||||
```
|
||||
|
||||
|
||||
## Run the integration tests ([Sytest](https://github.com/matrix-org/sytest)).
|
||||
|
||||
The integration tests are a more comprehensive suite of tests. They
|
||||
run a full version of Synapse, including your changes, to check if
|
||||
anything was broken. They are slower than the unit tests but will
|
||||
typically catch more errors.
|
||||
|
||||
The following command will let you run the integration test with the most common
|
||||
configuration:
|
||||
|
||||
```sh
|
||||
$ docker run --rm -it -v /path/where/you/have/cloned/the/repository\:/src:ro -v /path/to/where/you/want/logs\:/logs matrixdotorg/sytest-synapse:buster
|
||||
```
|
||||
|
||||
This configuration should generally cover your needs. For more details about other configurations, see [documentation in the SyTest repo](https://github.com/matrix-org/sytest/blob/develop/docker/README.md).
|
||||
|
||||
|
||||
## Run the integration tests ([Complement](https://github.com/matrix-org/complement)).
|
||||
|
||||
[Complement](https://github.com/matrix-org/complement) is a suite of black box tests that can be run on any homeserver implementation. It can also be thought of as end-to-end (e2e) tests.
|
||||
|
||||
It's often nice to develop on Synapse and write Complement tests at the same time.
|
||||
Here is how to run your local Synapse checkout against your local Complement checkout.
|
||||
|
||||
(checkout [`complement`](https://github.com/matrix-org/complement) alongside your `synapse` checkout)
|
||||
```sh
|
||||
COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh
|
||||
```
|
||||
|
||||
To run a specific test file, you can pass the test name at the end of the command. The name passed comes from the naming structure in your Complement tests. If you're unsure of the name, you can do a full run and copy it from the test output:
|
||||
|
||||
```sh
|
||||
COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh TestBackfillingHistory
|
||||
```
|
||||
|
||||
To run a specific test, you can specify the whole name structure:
|
||||
|
||||
```sh
|
||||
COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh TestBackfillingHistory/parallel/Backfilled_historical_events_resolve_with_proper_state_in_correct_order
|
||||
```
|
||||
|
||||
|
||||
### Access database for homeserver after Complement test runs.
|
||||
|
||||
If you're curious what the database looks like after you run some tests, here are some steps to get you going in Synapse:
|
||||
|
||||
1. In your Complement test comment out `defer deployment.Destroy(t)` and replace with `defer time.Sleep(2 * time.Hour)` to keep the homeserver running after the tests complete
|
||||
1. Start the Complement tests
|
||||
1. Find the name of the container, `docker ps -f name=complement_` (this will filter for just the Compelement related Docker containers)
|
||||
1. Access the container replacing the name with what you found in the previous step: `docker exec -it complement_1_hs_with_application_service.hs1_2 /bin/bash`
|
||||
1. Install sqlite (database driver), `apt-get update && apt-get install -y sqlite3`
|
||||
1. Then run `sqlite3` and open the database `.open /conf/homeserver.db` (this db path comes from the Synapse homeserver.yaml)
|
||||
|
||||
|
||||
# 9. Submit your patch.
|
||||
|
||||
Once you're happy with your patch, it's time to prepare a Pull Request.
|
||||
|
||||
To prepare a Pull Request, please:
|
||||
|
||||
1. verify that [all the tests pass](#test-test-test), including the coding style;
|
||||
2. [sign off](#sign-off) your contribution;
|
||||
3. `git push` your commit to your fork of Synapse;
|
||||
4. on GitHub, [create the Pull Request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request);
|
||||
5. add a [changelog entry](#changelog) and push it to your Pull Request;
|
||||
6. for most contributors, that's all - however, if you are a member of the organization `matrix-org`, on GitHub, please request a review from `matrix.org / Synapse Core`.
|
||||
7. if you need to update your PR, please avoid rebasing and just add new commits to your branch.
|
||||
|
||||
|
||||
## Changelog
|
||||
|
||||
All changes, even minor ones, need a corresponding changelog / newsfragment
|
||||
entry. These are managed by [Towncrier](https://github.com/hawkowl/towncrier).
|
||||
|
||||
To create a changelog entry, make a new file in the `changelog.d` directory named
|
||||
in the format of `PRnumber.type`. The type can be one of the following:
|
||||
|
||||
* `feature`
|
||||
* `bugfix`
|
||||
* `docker` (for updates to the Docker image)
|
||||
* `doc` (for updates to the documentation)
|
||||
* `removal` (also used for deprecations)
|
||||
* `misc` (for internal-only changes)
|
||||
|
||||
This file will become part of our [changelog](
|
||||
https://github.com/matrix-org/synapse/blob/master/CHANGES.md) at the next
|
||||
release, so the content of the file should be a short description of your
|
||||
change in the same style as the rest of the changelog. The file can contain Markdown
|
||||
formatting, and should end with a full stop (.) or an exclamation mark (!) for
|
||||
consistency.
|
||||
|
||||
Adding credits to the changelog is encouraged, we value your
|
||||
contributions and would like to have you shouted out in the release notes!
|
||||
|
||||
For example, a fix in PR #1234 would have its changelog entry in
|
||||
`changelog.d/1234.bugfix`, and contain content like:
|
||||
|
||||
> The security levels of Florbs are now validated when received
|
||||
> via the `/federation/florb` endpoint. Contributed by Jane Matrix.
|
||||
|
||||
If there are multiple pull requests involved in a single bugfix/feature/etc,
|
||||
then the content for each `changelog.d` file should be the same. Towncrier will
|
||||
merge the matching files together into a single changelog entry when we come to
|
||||
release.
|
||||
|
||||
### How do I know what to call the changelog file before I create the PR?
|
||||
|
||||
Obviously, you don't know if you should call your newsfile
|
||||
`1234.bugfix` or `5678.bugfix` until you create the PR, which leads to a
|
||||
chicken-and-egg problem.
|
||||
|
||||
There are two options for solving this:
|
||||
|
||||
1. Open the PR without a changelog file, see what number you got, and *then*
|
||||
add the changelog file to your branch (see [Updating your pull
|
||||
request](#updating-your-pull-request)), or:
|
||||
|
||||
1. Look at the [list of all
|
||||
issues/PRs](https://github.com/matrix-org/synapse/issues?q=), add one to the
|
||||
highest number you see, and quickly open the PR before somebody else claims
|
||||
your number.
|
||||
|
||||
[This
|
||||
script](https://github.com/richvdh/scripts/blob/master/next_github_number.sh)
|
||||
might be helpful if you find yourself doing this a lot.
|
||||
|
||||
Sorry, we know it's a bit fiddly, but it's *really* helpful for us when we come
|
||||
to put together a release!
|
||||
|
||||
### Debian changelog
|
||||
|
||||
Changes which affect the debian packaging files (in `debian`) are an
|
||||
exception to the rule that all changes require a `changelog.d` file.
|
||||
|
||||
In this case, you will need to add an entry to the debian changelog for the
|
||||
next release. For this, run the following command:
|
||||
|
||||
```
|
||||
dch
|
||||
```
|
||||
|
||||
This will make up a new version number (if there isn't already an unreleased
|
||||
version in flight), and open an editor where you can add a new changelog entry.
|
||||
(Our release process will ensure that the version number and maintainer name is
|
||||
corrected for the release.)
|
||||
|
||||
If your change affects both the debian packaging *and* files outside the debian
|
||||
directory, you will need both a regular newsfragment *and* an entry in the
|
||||
debian changelog. (Though typically such changes should be submitted as two
|
||||
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:
|
||||
|
||||
```
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
```
|
||||
|
||||
If you agree to this for your contribution, then all that's needed is to
|
||||
include the line in your commit or pull request comment:
|
||||
|
||||
```
|
||||
Signed-off-by: Your Name <your@email.example.org>
|
||||
```
|
||||
|
||||
We accept contributions under a legally identifiable name, such as
|
||||
your name on government documentation or common-law names (names
|
||||
claimed by legitimate usage or repute). Unfortunately, we cannot
|
||||
accept anonymous contributions at this time.
|
||||
|
||||
Git allows you to add this signoff automatically when using the `-s`
|
||||
flag to `git commit`, which uses the name and email set in your
|
||||
`user.name` and `user.email` git configs.
|
||||
|
||||
|
||||
# 10. Turn feedback into better code.
|
||||
|
||||
Once the Pull Request is opened, you will see a few things:
|
||||
|
||||
1. our automated CI (Continuous Integration) pipeline will run (again) the linters, the unit tests, the integration tests and more;
|
||||
2. one or more of the developers will take a look at your Pull Request and offer feedback.
|
||||
|
||||
From this point, you should:
|
||||
|
||||
1. Look at the results of the CI pipeline.
|
||||
- If there is any error, fix the error.
|
||||
2. If a developer has requested changes, make these changes and let us know if it is ready for a developer to review again.
|
||||
3. Create a new commit with the changes.
|
||||
- Please do NOT overwrite the history. New commits make the reviewer's life easier.
|
||||
- Push this commits to your Pull Request.
|
||||
4. Back to 1.
|
||||
|
||||
Once both the CI and the developers are happy, the patch will be merged into Synapse and released shortly!
|
||||
|
||||
# 11. Find a new issue.
|
||||
|
||||
By now, you know the drill!
|
||||
|
||||
# Notes for maintainers on merging PRs etc
|
||||
|
||||
There are some notes for those with commit access to the project on how we
|
||||
manage git [here](docs/development/git.md).
|
||||
|
||||
# Conclusion
|
||||
|
||||
That's it! Matrix is a very open and collaborative project as you might expect
|
||||
given our obsession with open communication. If we're going to successfully
|
||||
matrix together all the fragmented communication technologies out there we are
|
||||
reliant on contributions and collaboration from the community to do so. So
|
||||
please get involved - and we hope you have as much fun hacking on Matrix as we
|
||||
do!
|
||||
Please see the [contributors' guide](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html) in our rendered documentation.
|
||||
|
|
37
README.rst
37
README.rst
|
@ -1,6 +1,6 @@
|
|||
=========================================================
|
||||
Synapse |support| |development| |license| |pypi| |python|
|
||||
=========================================================
|
||||
=========================================================================
|
||||
Synapse |support| |development| |documentation| |license| |pypi| |python|
|
||||
=========================================================================
|
||||
|
||||
.. contents::
|
||||
|
||||
|
@ -85,9 +85,14 @@ For support installing or managing Synapse, please join |room|_ (from a matrix.o
|
|||
account if necessary) and ask questions there. We do not use GitHub issues for
|
||||
support requests, only for bug reports and feature requests.
|
||||
|
||||
Synapse's documentation is `nicely rendered on GitHub Pages <https://matrix-org.github.io/synapse>`_,
|
||||
with its source available in |docs|_.
|
||||
|
||||
.. |room| replace:: ``#synapse:matrix.org``
|
||||
.. _room: https://matrix.to/#/#synapse:matrix.org
|
||||
|
||||
.. |docs| replace:: ``docs``
|
||||
.. _docs: docs
|
||||
|
||||
Synapse Installation
|
||||
====================
|
||||
|
@ -263,7 +268,23 @@ Then update the ``users`` table in the database::
|
|||
Synapse Development
|
||||
===================
|
||||
|
||||
Join our developer community on Matrix: `#synapse-dev:matrix.org <https://matrix.to/#/#synapse-dev:matrix.org>`_
|
||||
The best place to get started is our
|
||||
`guide for contributors <https://matrix-org.github.io/synapse/latest/development/contributing_guide.html>`_.
|
||||
This is part of our larger `documentation <https://matrix-org.github.io/synapse/latest>`_, which includes
|
||||
information for synapse developers as well as synapse administrators.
|
||||
|
||||
Developers might be particularly interested in:
|
||||
|
||||
* `Synapse's database schema <https://matrix-org.github.io/synapse/latest/development/database_schema.html>`_,
|
||||
* `notes on Synapse's implementation details <https://matrix-org.github.io/synapse/latest/development/internal_documentation/index.html>`_, and
|
||||
* `how we use git <https://matrix-org.github.io/synapse/latest/development/git.html>`_.
|
||||
|
||||
Alongside all that, join our developer community on Matrix:
|
||||
`#synapse-dev:matrix.org <https://matrix.to/#/#synapse-dev:matrix.org>`_, featuring real humans!
|
||||
|
||||
|
||||
Quick start
|
||||
-----------
|
||||
|
||||
Before setting up a development environment for synapse, make sure you have the
|
||||
system dependencies (such as the python header files) installed - see
|
||||
|
@ -308,7 +329,7 @@ If you just want to start a single instance of the app and run it directly::
|
|||
|
||||
|
||||
Running the unit tests
|
||||
======================
|
||||
----------------------
|
||||
|
||||
After getting up and running, you may wish to run Synapse's unit tests to
|
||||
check that everything is installed correctly::
|
||||
|
@ -327,7 +348,7 @@ to see the logging output, see the `CONTRIBUTING doc <CONTRIBUTING.md#run-the-un
|
|||
|
||||
|
||||
Running the Integration Tests
|
||||
=============================
|
||||
-----------------------------
|
||||
|
||||
Synapse is accompanied by `SyTest <https://github.com/matrix-org/sytest>`_,
|
||||
a Matrix homeserver integration testing suite, which uses HTTP requests to
|
||||
|
@ -445,6 +466,10 @@ This is normally caused by a misconfiguration in your reverse-proxy. See
|
|||
:alt: (discuss development on #synapse-dev:matrix.org)
|
||||
:target: https://matrix.to/#/#synapse-dev:matrix.org
|
||||
|
||||
.. |documentation| image:: https://img.shields.io/badge/documentation-%E2%9C%93-success
|
||||
:alt: (Rendered documentation on GitHub Pages)
|
||||
:target: https://matrix-org.github.io/synapse/latest/
|
||||
|
||||
.. |license| image:: https://img.shields.io/github/license/matrix-org/synapse
|
||||
:alt: (check license in LICENSE file)
|
||||
:target: LICENSE
|
||||
|
|
1
changelog.d/10232.bugfix
Normal file
1
changelog.d/10232.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Validate new `m.room.power_levels` events. Contributed by @aaronraimist.
|
1
changelog.d/10581.bugfix
Normal file
1
changelog.d/10581.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Remove pushers when deleting a 3pid from an account. Pushers for old unlinked emails will also be deleted.
|
1
changelog.d/10595.doc
Normal file
1
changelog.d/10595.doc
Normal file
|
@ -0,0 +1 @@
|
|||
Advertise https://matrix-org.github.io/synapse docs in README and CONTRIBUTING files.
|
1
changelog.d/10645.misc
Normal file
1
changelog.d/10645.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Make `backfill` and `get_missing_events` use the same codepath.
|
1
changelog.d/10679.bugfix
Normal file
1
changelog.d/10679.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Improve ServerNoticeServlet to avoid duplicate requests and add unit tests.
|
1
changelog.d/10692.misc
Normal file
1
changelog.d/10692.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Split the event-processing methods in `FederationHandler` into a separate `FederationEventHandler`.
|
1
changelog.d/10703.bugfix
Normal file
1
changelog.d/10703.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Fix a regression introduced in v1.41.0 which affected the performance of concurrent fetches of large sets of events, in extreme cases causing the process to hang.
|
1
changelog.d/10706.misc
Normal file
1
changelog.d/10706.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Remove unused `compare_digest` function.
|
1
changelog.d/10707.misc
Normal file
1
changelog.d/10707.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Add missing type hints to REST servlets.
|
1
changelog.d/10708.doc
Normal file
1
changelog.d/10708.doc
Normal file
|
@ -0,0 +1 @@
|
|||
Minor clarifications to the documentation for reverse proxies.
|
1
changelog.d/10711.doc
Normal file
1
changelog.d/10711.doc
Normal file
|
@ -0,0 +1 @@
|
|||
Removed table of contents from the top of installation and contributing documentation pages.
|
1
changelog.d/10712.feature
Normal file
1
changelog.d/10712.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Skip final GC at shutdown to improve restart performance.
|
1
changelog.d/10713.bugfix
Normal file
1
changelog.d/10713.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Fix a regression introduced in Synapse 1.41 which broke email transmission on Systems using older versions of the Twisted library.
|
1
changelog.d/10714.feature
Normal file
1
changelog.d/10714.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Allow configuration of the oEmbed URLs used for URL previews.
|
1
changelog.d/10723.bugfix
Normal file
1
changelog.d/10723.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Fix unauthorised exposure of room metadata to communities.
|
1
changelog.d/10725.feature
Normal file
1
changelog.d/10725.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Add pagination to the spaces summary based on updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946).
|
6
debian/changelog
vendored
6
debian/changelog
vendored
|
@ -1,3 +1,9 @@
|
|||
matrix-synapse-py3 (1.41.1) stable; urgency=high
|
||||
|
||||
* New synapse release 1.41.1.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Tue, 31 Aug 2021 12:59:10 +0100
|
||||
|
||||
matrix-synapse-py3 (1.41.0) stable; urgency=medium
|
||||
|
||||
* New synapse release 1.41.0.
|
||||
|
|
|
@ -1,7 +1,427 @@
|
|||
<!--
|
||||
Include the contents of CONTRIBUTING.md from the project root (where GitHub likes it
|
||||
to be)
|
||||
-->
|
||||
# Contributing
|
||||
|
||||
{{#include ../../CONTRIBUTING.md}}
|
||||
This document aims to get you started with contributing to Synapse!
|
||||
|
||||
# 1. Who can contribute to Synapse?
|
||||
|
||||
Everyone is welcome to contribute code to [matrix.org
|
||||
projects](https://github.com/matrix-org), 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/matrix-org/synapse/blob/develop/LICENSE)).
|
||||
|
||||
# 2. What do I need?
|
||||
|
||||
The code of Synapse is written in Python 3. To do pretty much anything, you'll need [a recent version of Python 3](https://wiki.python.org/moin/BeginnersGuide/Download).
|
||||
|
||||
The source code of Synapse is hosted on GitHub. You will also need [a recent version of git](https://github.com/git-guides/install-git).
|
||||
|
||||
For some tests, you will need [a recent version of Docker](https://docs.docker.com/get-docker/).
|
||||
|
||||
|
||||
# 3. Get the source.
|
||||
|
||||
The preferred and easiest way to contribute changes is to fork the relevant
|
||||
project on GitHub, and then [create a pull request](
|
||||
https://help.github.com/articles/using-pull-requests/) to ask us to pull your
|
||||
changes into our repo.
|
||||
|
||||
Please base your changes on the `develop` branch.
|
||||
|
||||
```sh
|
||||
git clone git@github.com:YOUR_GITHUB_USER_NAME/synapse.git
|
||||
git checkout develop
|
||||
```
|
||||
|
||||
If you need help getting started with git, this is beyond the scope of the document, but you
|
||||
can find many good git tutorials on the web.
|
||||
|
||||
# 4. Install the dependencies
|
||||
|
||||
## Under Unix (macOS, Linux, BSD, ...)
|
||||
|
||||
Once you have installed Python 3 and added the source, please open a terminal and
|
||||
setup a *virtualenv*, as follows:
|
||||
|
||||
```sh
|
||||
cd path/where/you/have/cloned/the/repository
|
||||
python3 -m venv ./env
|
||||
source ./env/bin/activate
|
||||
pip install -e ".[all,lint,mypy,test]"
|
||||
pip install tox
|
||||
```
|
||||
|
||||
This will install the developer dependencies for the project.
|
||||
|
||||
## Under Windows
|
||||
|
||||
TBD
|
||||
|
||||
|
||||
# 5. Get in touch.
|
||||
|
||||
Join our developer community on Matrix: #synapse-dev:matrix.org !
|
||||
|
||||
|
||||
# 6. Pick an issue.
|
||||
|
||||
Fix your favorite problem or perhaps find a [Good First Issue](https://github.com/matrix-org/synapse/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+First+Issue%22)
|
||||
to work on.
|
||||
|
||||
|
||||
# 7. Turn coffee into code and documentation!
|
||||
|
||||
There is a growing amount of documentation located in the
|
||||
[`docs`](https://github.com/matrix-org/synapse/tree/develop/docs)
|
||||
directory, with a rendered version [available online](https://matrix-org.github.io/synapse).
|
||||
This documentation is intended primarily for sysadmins running their
|
||||
own Synapse instance, as well as developers interacting externally with
|
||||
Synapse.
|
||||
[`docs/development`](https://github.com/matrix-org/synapse/tree/develop/docs/development)
|
||||
exists primarily to house documentation for
|
||||
Synapse developers.
|
||||
[`docs/admin_api`](https://github.com/matrix-org/synapse/tree/develop/docs/admin_api) houses documentation
|
||||
regarding Synapse's Admin API, which is used mostly by sysadmins and external
|
||||
service developers.
|
||||
|
||||
Synapse's code style is documented [here](../code_style.md). Please follow
|
||||
it, including the conventions for the [sample configuration
|
||||
file](../code_style.md#configuration-file-format).
|
||||
|
||||
We welcome improvements and additions to our documentation itself! When
|
||||
writing new pages, please
|
||||
[build `docs` to a book](https://github.com/matrix-org/synapse/tree/develop/docs#adding-to-the-documentation)
|
||||
to check that your contributions render correctly. The docs are written in
|
||||
[GitHub-Flavoured Markdown](https://guides.github.com/features/mastering-markdown/).
|
||||
|
||||
Some documentation also exists in [Synapse's GitHub
|
||||
Wiki](https://github.com/matrix-org/synapse/wiki), although this is primarily
|
||||
contributed to by community authors.
|
||||
|
||||
|
||||
# 8. Test, test, test!
|
||||
<a name="test-test-test"></a>
|
||||
|
||||
While you're developing and before submitting a patch, you'll
|
||||
want to test your code.
|
||||
|
||||
## Run the linters.
|
||||
|
||||
The linters look at your code and do two things:
|
||||
|
||||
- ensure that your code follows the coding style adopted by the project;
|
||||
- catch a number of errors in your code.
|
||||
|
||||
They're pretty fast, don't hesitate!
|
||||
|
||||
```sh
|
||||
source ./env/bin/activate
|
||||
./scripts-dev/lint.sh
|
||||
```
|
||||
|
||||
Note that this script *will modify your files* to fix styling errors.
|
||||
Make sure that you have saved all your files.
|
||||
|
||||
If you wish to restrict the linters to only the files changed since the last commit
|
||||
(much faster!), you can instead run:
|
||||
|
||||
```sh
|
||||
source ./env/bin/activate
|
||||
./scripts-dev/lint.sh -d
|
||||
```
|
||||
|
||||
Or if you know exactly which files you wish to lint, you can instead run:
|
||||
|
||||
```sh
|
||||
source ./env/bin/activate
|
||||
./scripts-dev/lint.sh path/to/file1.py path/to/file2.py path/to/folder
|
||||
```
|
||||
|
||||
## Run the unit tests (Twisted trial).
|
||||
|
||||
The unit tests run parts of Synapse, including your changes, to see if anything
|
||||
was broken. They are slower than the linters but will typically catch more errors.
|
||||
|
||||
```sh
|
||||
source ./env/bin/activate
|
||||
trial tests
|
||||
```
|
||||
|
||||
If you wish to only run *some* unit tests, you may specify
|
||||
another module instead of `tests` - or a test class or a method:
|
||||
|
||||
```sh
|
||||
source ./env/bin/activate
|
||||
trial tests.rest.admin.test_room tests.handlers.test_admin.ExfiltrateData.test_invite
|
||||
```
|
||||
|
||||
If your tests fail, you may wish to look at the logs (the default log level is `ERROR`):
|
||||
|
||||
```sh
|
||||
less _trial_temp/test.log
|
||||
```
|
||||
|
||||
To increase the log level for the tests, set `SYNAPSE_TEST_LOG_LEVEL`:
|
||||
|
||||
```sh
|
||||
SYNAPSE_TEST_LOG_LEVEL=DEBUG trial tests
|
||||
```
|
||||
|
||||
|
||||
## Run the integration tests ([Sytest](https://github.com/matrix-org/sytest)).
|
||||
|
||||
The integration tests are a more comprehensive suite of tests. They
|
||||
run a full version of Synapse, including your changes, to check if
|
||||
anything was broken. They are slower than the unit tests but will
|
||||
typically catch more errors.
|
||||
|
||||
The following command will let you run the integration test with the most common
|
||||
configuration:
|
||||
|
||||
```sh
|
||||
$ docker run --rm -it -v /path/where/you/have/cloned/the/repository\:/src:ro -v /path/to/where/you/want/logs\:/logs matrixdotorg/sytest-synapse:buster
|
||||
```
|
||||
|
||||
This configuration should generally cover your needs. For more details about other configurations, see [documentation in the SyTest repo](https://github.com/matrix-org/sytest/blob/develop/docker/README.md).
|
||||
|
||||
|
||||
## Run the integration tests ([Complement](https://github.com/matrix-org/complement)).
|
||||
|
||||
[Complement](https://github.com/matrix-org/complement) is a suite of black box tests that can be run on any homeserver implementation. It can also be thought of as end-to-end (e2e) tests.
|
||||
|
||||
It's often nice to develop on Synapse and write Complement tests at the same time.
|
||||
Here is how to run your local Synapse checkout against your local Complement checkout.
|
||||
|
||||
(checkout [`complement`](https://github.com/matrix-org/complement) alongside your `synapse` checkout)
|
||||
```sh
|
||||
COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh
|
||||
```
|
||||
|
||||
To run a specific test file, you can pass the test name at the end of the command. The name passed comes from the naming structure in your Complement tests. If you're unsure of the name, you can do a full run and copy it from the test output:
|
||||
|
||||
```sh
|
||||
COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh TestBackfillingHistory
|
||||
```
|
||||
|
||||
To run a specific test, you can specify the whole name structure:
|
||||
|
||||
```sh
|
||||
COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh TestBackfillingHistory/parallel/Backfilled_historical_events_resolve_with_proper_state_in_correct_order
|
||||
```
|
||||
|
||||
|
||||
### Access database for homeserver after Complement test runs.
|
||||
|
||||
If you're curious what the database looks like after you run some tests, here are some steps to get you going in Synapse:
|
||||
|
||||
1. In your Complement test comment out `defer deployment.Destroy(t)` and replace with `defer time.Sleep(2 * time.Hour)` to keep the homeserver running after the tests complete
|
||||
1. Start the Complement tests
|
||||
1. Find the name of the container, `docker ps -f name=complement_` (this will filter for just the Compelement related Docker containers)
|
||||
1. Access the container replacing the name with what you found in the previous step: `docker exec -it complement_1_hs_with_application_service.hs1_2 /bin/bash`
|
||||
1. Install sqlite (database driver), `apt-get update && apt-get install -y sqlite3`
|
||||
1. Then run `sqlite3` and open the database `.open /conf/homeserver.db` (this db path comes from the Synapse homeserver.yaml)
|
||||
|
||||
|
||||
# 9. Submit your patch.
|
||||
|
||||
Once you're happy with your patch, it's time to prepare a Pull Request.
|
||||
|
||||
To prepare a Pull Request, please:
|
||||
|
||||
1. verify that [all the tests pass](#test-test-test), including the coding style;
|
||||
2. [sign off](#sign-off) your contribution;
|
||||
3. `git push` your commit to your fork of Synapse;
|
||||
4. on GitHub, [create the Pull Request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request);
|
||||
5. add a [changelog entry](#changelog) and push it to your Pull Request;
|
||||
6. for most contributors, that's all - however, if you are a member of the organization `matrix-org`, on GitHub, please request a review from `matrix.org / Synapse Core`.
|
||||
7. if you need to update your PR, please avoid rebasing and just add new commits to your branch.
|
||||
|
||||
|
||||
## Changelog
|
||||
|
||||
All changes, even minor ones, need a corresponding changelog / newsfragment
|
||||
entry. These are managed by [Towncrier](https://github.com/hawkowl/towncrier).
|
||||
|
||||
To create a changelog entry, make a new file in the `changelog.d` directory named
|
||||
in the format of `PRnumber.type`. The type can be one of the following:
|
||||
|
||||
* `feature`
|
||||
* `bugfix`
|
||||
* `docker` (for updates to the Docker image)
|
||||
* `doc` (for updates to the documentation)
|
||||
* `removal` (also used for deprecations)
|
||||
* `misc` (for internal-only changes)
|
||||
|
||||
This file will become part of our [changelog](
|
||||
https://github.com/matrix-org/synapse/blob/master/CHANGES.md) at the next
|
||||
release, so the content of the file should be a short description of your
|
||||
change in the same style as the rest of the changelog. The file can contain Markdown
|
||||
formatting, and should end with a full stop (.) or an exclamation mark (!) for
|
||||
consistency.
|
||||
|
||||
Adding credits to the changelog is encouraged, we value your
|
||||
contributions and would like to have you shouted out in the release notes!
|
||||
|
||||
For example, a fix in PR #1234 would have its changelog entry in
|
||||
`changelog.d/1234.bugfix`, and contain content like:
|
||||
|
||||
> The security levels of Florbs are now validated when received
|
||||
> via the `/federation/florb` endpoint. Contributed by Jane Matrix.
|
||||
|
||||
If there are multiple pull requests involved in a single bugfix/feature/etc,
|
||||
then the content for each `changelog.d` file should be the same. Towncrier will
|
||||
merge the matching files together into a single changelog entry when we come to
|
||||
release.
|
||||
|
||||
### How do I know what to call the changelog file before I create the PR?
|
||||
|
||||
Obviously, you don't know if you should call your newsfile
|
||||
`1234.bugfix` or `5678.bugfix` until you create the PR, which leads to a
|
||||
chicken-and-egg problem.
|
||||
|
||||
There are two options for solving this:
|
||||
|
||||
1. Open the PR without a changelog file, see what number you got, and *then*
|
||||
add the changelog file to your branch (see [Updating your pull
|
||||
request](#updating-your-pull-request)), or:
|
||||
|
||||
1. Look at the [list of all
|
||||
issues/PRs](https://github.com/matrix-org/synapse/issues?q=), add one to the
|
||||
highest number you see, and quickly open the PR before somebody else claims
|
||||
your number.
|
||||
|
||||
[This
|
||||
script](https://github.com/richvdh/scripts/blob/master/next_github_number.sh)
|
||||
might be helpful if you find yourself doing this a lot.
|
||||
|
||||
Sorry, we know it's a bit fiddly, but it's *really* helpful for us when we come
|
||||
to put together a release!
|
||||
|
||||
### Debian changelog
|
||||
|
||||
Changes which affect the debian packaging files (in `debian`) are an
|
||||
exception to the rule that all changes require a `changelog.d` file.
|
||||
|
||||
In this case, you will need to add an entry to the debian changelog for the
|
||||
next release. For this, run the following command:
|
||||
|
||||
```
|
||||
dch
|
||||
```
|
||||
|
||||
This will make up a new version number (if there isn't already an unreleased
|
||||
version in flight), and open an editor where you can add a new changelog entry.
|
||||
(Our release process will ensure that the version number and maintainer name is
|
||||
corrected for the release.)
|
||||
|
||||
If your change affects both the debian packaging *and* files outside the debian
|
||||
directory, you will need both a regular newsfragment *and* an entry in the
|
||||
debian changelog. (Though typically such changes should be submitted as two
|
||||
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:
|
||||
|
||||
```
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
```
|
||||
|
||||
If you agree to this for your contribution, then all that's needed is to
|
||||
include the line in your commit or pull request comment:
|
||||
|
||||
```
|
||||
Signed-off-by: Your Name <your@email.example.org>
|
||||
```
|
||||
|
||||
We accept contributions under a legally identifiable name, such as
|
||||
your name on government documentation or common-law names (names
|
||||
claimed by legitimate usage or repute). Unfortunately, we cannot
|
||||
accept anonymous contributions at this time.
|
||||
|
||||
Git allows you to add this signoff automatically when using the `-s`
|
||||
flag to `git commit`, which uses the name and email set in your
|
||||
`user.name` and `user.email` git configs.
|
||||
|
||||
|
||||
# 10. Turn feedback into better code.
|
||||
|
||||
Once the Pull Request is opened, you will see a few things:
|
||||
|
||||
1. our automated CI (Continuous Integration) pipeline will run (again) the linters, the unit tests, the integration tests and more;
|
||||
2. one or more of the developers will take a look at your Pull Request and offer feedback.
|
||||
|
||||
From this point, you should:
|
||||
|
||||
1. Look at the results of the CI pipeline.
|
||||
- If there is any error, fix the error.
|
||||
2. If a developer has requested changes, make these changes and let us know if it is ready for a developer to review again.
|
||||
3. Create a new commit with the changes.
|
||||
- Please do NOT overwrite the history. New commits make the reviewer's life easier.
|
||||
- Push this commits to your Pull Request.
|
||||
4. Back to 1.
|
||||
|
||||
Once both the CI and the developers are happy, the patch will be merged into Synapse and released shortly!
|
||||
|
||||
# 11. Find a new issue.
|
||||
|
||||
By now, you know the drill!
|
||||
|
||||
# Notes for maintainers on merging PRs etc
|
||||
|
||||
There are some notes for those with commit access to the project on how we
|
||||
manage git [here](git.md).
|
||||
|
||||
# Conclusion
|
||||
|
||||
That's it! Matrix is a very open and collaborative project as you might expect
|
||||
given our obsession with open communication. If we're going to successfully
|
||||
matrix together all the fragmented communication technologies out there we are
|
||||
reliant on contributions and collaboration from the community to do so. So
|
||||
please get involved - and we hope you have as much fun hacking on Matrix as we
|
||||
do!
|
||||
|
|
|
@ -64,6 +64,9 @@ server {
|
|||
server_name matrix.example.com;
|
||||
|
||||
location ~* ^(\/_matrix|\/_synapse\/client) {
|
||||
# note: do not add a path (even a single /) after the port in `proxy_pass`,
|
||||
# otherwise nginx will canonicalise the URI and cause signature verification
|
||||
# errors.
|
||||
proxy_pass http://localhost:8008;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
@ -76,10 +79,7 @@ server {
|
|||
}
|
||||
```
|
||||
|
||||
**NOTE**: Do not add a path after the port in `proxy_pass`, otherwise nginx will
|
||||
canonicalise/normalise the URI.
|
||||
|
||||
### Caddy 1
|
||||
### Caddy v1
|
||||
|
||||
```
|
||||
matrix.example.com {
|
||||
|
@ -99,7 +99,7 @@ example.com:8448 {
|
|||
}
|
||||
```
|
||||
|
||||
### Caddy 2
|
||||
### Caddy v2
|
||||
|
||||
```
|
||||
matrix.example.com {
|
||||
|
|
|
@ -1075,6 +1075,27 @@ url_preview_accept_language:
|
|||
# - en
|
||||
|
||||
|
||||
# oEmbed allows for easier embedding content from a website. It can be
|
||||
# used for generating URLs previews of services which support it.
|
||||
#
|
||||
oembed:
|
||||
# A default list of oEmbed providers is included with Synapse.
|
||||
#
|
||||
# Uncomment the following to disable using these default oEmbed URLs.
|
||||
# Defaults to 'false'.
|
||||
#
|
||||
#disable_default_providers: true
|
||||
|
||||
# Additional files with oEmbed configuration (each should be in the
|
||||
# form of providers.json).
|
||||
#
|
||||
# By default, this list is empty (so only the default providers.json
|
||||
# is used).
|
||||
#
|
||||
#additional_providers:
|
||||
# - oembed/my_providers.json
|
||||
|
||||
|
||||
## Captcha ##
|
||||
# See docs/CAPTCHA_SETUP.md for full details of configuring this.
|
||||
|
||||
|
|
|
@ -1,44 +1,5 @@
|
|||
# Installation Instructions
|
||||
|
||||
There are 3 steps to follow under **Installation Instructions**.
|
||||
|
||||
- [Installation Instructions](#installation-instructions)
|
||||
- [Choosing your server name](#choosing-your-server-name)
|
||||
- [Installing Synapse](#installing-synapse)
|
||||
- [Installing from source](#installing-from-source)
|
||||
- [Platform-specific prerequisites](#platform-specific-prerequisites)
|
||||
- [Debian/Ubuntu/Raspbian](#debianubunturaspbian)
|
||||
- [ArchLinux](#archlinux)
|
||||
- [CentOS/Fedora](#centosfedora)
|
||||
- [macOS](#macos)
|
||||
- [OpenSUSE](#opensuse)
|
||||
- [OpenBSD](#openbsd)
|
||||
- [Windows](#windows)
|
||||
- [Prebuilt packages](#prebuilt-packages)
|
||||
- [Docker images and Ansible playbooks](#docker-images-and-ansible-playbooks)
|
||||
- [Debian/Ubuntu](#debianubuntu)
|
||||
- [Matrix.org packages](#matrixorg-packages)
|
||||
- [Downstream Debian packages](#downstream-debian-packages)
|
||||
- [Downstream Ubuntu packages](#downstream-ubuntu-packages)
|
||||
- [Fedora](#fedora)
|
||||
- [OpenSUSE](#opensuse-1)
|
||||
- [SUSE Linux Enterprise Server](#suse-linux-enterprise-server)
|
||||
- [ArchLinux](#archlinux-1)
|
||||
- [Void Linux](#void-linux)
|
||||
- [FreeBSD](#freebsd)
|
||||
- [OpenBSD](#openbsd-1)
|
||||
- [NixOS](#nixos)
|
||||
- [Setting up Synapse](#setting-up-synapse)
|
||||
- [Using PostgreSQL](#using-postgresql)
|
||||
- [TLS certificates](#tls-certificates)
|
||||
- [Client Well-Known URI](#client-well-known-uri)
|
||||
- [Email](#email)
|
||||
- [Registering a user](#registering-a-user)
|
||||
- [Setting up a TURN server](#setting-up-a-turn-server)
|
||||
- [URL previews](#url-previews)
|
||||
- [Troubleshooting Installation](#troubleshooting-installation)
|
||||
|
||||
|
||||
## Choosing your server name
|
||||
|
||||
It is important to choose the name for your server before you install Synapse,
|
||||
|
|
|
@ -107,6 +107,11 @@ This may affect you if you make use of custom HTML templates for the
|
|||
The template is now provided an `error` variable if the authentication
|
||||
process failed. See the default templates linked above for an example.
|
||||
|
||||
# Upgrading to v1.42.0
|
||||
|
||||
## Removal of out-of-date email pushers
|
||||
Users will stop receiving message updates via email for addresses that were
|
||||
once, but not still, linked to their account.
|
||||
|
||||
# Upgrading to v1.41.0
|
||||
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
:root {
|
||||
--pagetoc-width: 250px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width:1439px) {
|
||||
.sidetoc {
|
||||
display: none;
|
||||
|
@ -8,6 +12,7 @@
|
|||
main {
|
||||
position: relative;
|
||||
margin-left: 100px !important;
|
||||
margin-right: var(--pagetoc-width) !important;
|
||||
}
|
||||
.sidetoc {
|
||||
margin-left: auto;
|
||||
|
@ -18,7 +23,7 @@
|
|||
}
|
||||
.pagetoc {
|
||||
position: fixed;
|
||||
width: 250px;
|
||||
width: var(--pagetoc-width);
|
||||
overflow: auto;
|
||||
right: 20px;
|
||||
height: calc(100% - var(--menu-bar-height));
|
||||
|
|
1
mypy.ini
1
mypy.ini
|
@ -91,6 +91,7 @@ files =
|
|||
tests/test_utils,
|
||||
tests/handlers/test_password_providers.py,
|
||||
tests/handlers/test_room_summary.py,
|
||||
tests/handlers/test_send_email.py,
|
||||
tests/handlers/test_sync.py,
|
||||
tests/rest/client/test_login.py,
|
||||
tests/rest/client/test_auth.py,
|
||||
|
|
|
@ -47,7 +47,7 @@ try:
|
|||
except ImportError:
|
||||
pass
|
||||
|
||||
__version__ = "1.41.0"
|
||||
__version__ = "1.41.1"
|
||||
|
||||
if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)):
|
||||
# We import here so that we don't have to install a bunch of deps when
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import atexit
|
||||
import gc
|
||||
import logging
|
||||
import os
|
||||
|
@ -403,6 +404,12 @@ async def start(hs: "HomeServer"):
|
|||
gc.collect()
|
||||
gc.freeze()
|
||||
|
||||
# Speed up shutdowns by freezing all allocated objects. This moves everything
|
||||
# into the permanent generation and excludes them from the final GC.
|
||||
# Unfortunately only works on Python 3.7
|
||||
if platform.python_implementation() == "CPython" and sys.version_info >= (3, 7):
|
||||
atexit.register(gc.freeze)
|
||||
|
||||
|
||||
def setup_sentry(hs):
|
||||
"""Enable sentry integration, if enabled in configuration
|
||||
|
|
|
@ -30,6 +30,7 @@ from .key import KeyConfig
|
|||
from .logger import LoggingConfig
|
||||
from .metrics import MetricsConfig
|
||||
from .modules import ModulesConfig
|
||||
from .oembed import OembedConfig
|
||||
from .oidc import OIDCConfig
|
||||
from .password_auth_providers import PasswordAuthProviderConfig
|
||||
from .push import PushConfig
|
||||
|
@ -65,6 +66,7 @@ class HomeServerConfig(RootConfig):
|
|||
LoggingConfig,
|
||||
RatelimitConfig,
|
||||
ContentRepositoryConfig,
|
||||
OembedConfig,
|
||||
CaptchaConfig,
|
||||
VoipConfig,
|
||||
RegistrationConfig,
|
||||
|
|
180
synapse/config/oembed.py
Normal file
180
synapse/config/oembed.py
Normal file
|
@ -0,0 +1,180 @@
|
|||
# Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import json
|
||||
import re
|
||||
from typing import Any, Dict, Iterable, List, Pattern
|
||||
from urllib import parse as urlparse
|
||||
|
||||
import attr
|
||||
import pkg_resources
|
||||
|
||||
from synapse.types import JsonDict
|
||||
|
||||
from ._base import Config, ConfigError
|
||||
from ._util import validate_config
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||
class OEmbedEndpointConfig:
|
||||
# The API endpoint to fetch.
|
||||
api_endpoint: str
|
||||
# The patterns to match.
|
||||
url_patterns: List[Pattern]
|
||||
|
||||
|
||||
class OembedConfig(Config):
|
||||
"""oEmbed Configuration"""
|
||||
|
||||
section = "oembed"
|
||||
|
||||
def read_config(self, config, **kwargs):
|
||||
oembed_config: Dict[str, Any] = config.get("oembed") or {}
|
||||
|
||||
# A list of patterns which will be used.
|
||||
self.oembed_patterns: List[OEmbedEndpointConfig] = list(
|
||||
self._parse_and_validate_providers(oembed_config)
|
||||
)
|
||||
|
||||
def _parse_and_validate_providers(
|
||||
self, oembed_config: dict
|
||||
) -> Iterable[OEmbedEndpointConfig]:
|
||||
"""Extract and parse the oEmbed providers from the given JSON file.
|
||||
|
||||
Returns a generator which yields the OidcProviderConfig objects
|
||||
"""
|
||||
# Whether to use the packaged providers.json file.
|
||||
if not oembed_config.get("disable_default_providers") or False:
|
||||
providers = json.load(
|
||||
pkg_resources.resource_stream("synapse", "res/providers.json")
|
||||
)
|
||||
yield from self._parse_and_validate_provider(
|
||||
providers, config_path=("oembed",)
|
||||
)
|
||||
|
||||
# The JSON files which includes additional provider information.
|
||||
for i, file in enumerate(oembed_config.get("additional_providers") or []):
|
||||
# TODO Error checking.
|
||||
with open(file) as f:
|
||||
providers = json.load(f)
|
||||
|
||||
yield from self._parse_and_validate_provider(
|
||||
providers,
|
||||
config_path=(
|
||||
"oembed",
|
||||
"additional_providers",
|
||||
f"<item {i}>",
|
||||
),
|
||||
)
|
||||
|
||||
def _parse_and_validate_provider(
|
||||
self, providers: List[JsonDict], config_path: Iterable[str]
|
||||
) -> Iterable[OEmbedEndpointConfig]:
|
||||
# Ensure it is the proper form.
|
||||
validate_config(
|
||||
_OEMBED_PROVIDER_SCHEMA,
|
||||
providers,
|
||||
config_path=config_path,
|
||||
)
|
||||
|
||||
# Parse it and yield each result.
|
||||
for provider in providers:
|
||||
# Each provider might have multiple API endpoints, each which
|
||||
# might have multiple patterns to match.
|
||||
for endpoint in provider["endpoints"]:
|
||||
api_endpoint = endpoint["url"]
|
||||
patterns = [
|
||||
self._glob_to_pattern(glob, config_path)
|
||||
for glob in endpoint["schemes"]
|
||||
]
|
||||
yield OEmbedEndpointConfig(api_endpoint, patterns)
|
||||
|
||||
def _glob_to_pattern(self, glob: str, config_path: Iterable[str]) -> Pattern:
|
||||
"""
|
||||
Convert the glob into a sane regular expression to match against. The
|
||||
rules followed will be slightly different for the domain portion vs.
|
||||
the rest.
|
||||
|
||||
1. The scheme must be one of HTTP / HTTPS (and have no globs).
|
||||
2. The domain can have globs, but we limit it to characters that can
|
||||
reasonably be a domain part.
|
||||
TODO: This does not attempt to handle Unicode domain names.
|
||||
TODO: The domain should not allow wildcard TLDs.
|
||||
3. Other parts allow a glob to be any one, or more, characters.
|
||||
"""
|
||||
results = urlparse.urlparse(glob)
|
||||
|
||||
# Ensure the scheme does not have wildcards (and is a sane scheme).
|
||||
if results.scheme not in {"http", "https"}:
|
||||
raise ConfigError(f"Insecure oEmbed scheme: {results.scheme}", config_path)
|
||||
|
||||
pattern = urlparse.urlunparse(
|
||||
[
|
||||
results.scheme,
|
||||
re.escape(results.netloc).replace("\\*", "[a-zA-Z0-9_-]+"),
|
||||
]
|
||||
+ [re.escape(part).replace("\\*", ".+") for part in results[2:]]
|
||||
)
|
||||
return re.compile(pattern)
|
||||
|
||||
def generate_config_section(self, **kwargs):
|
||||
return """\
|
||||
# oEmbed allows for easier embedding content from a website. It can be
|
||||
# used for generating URLs previews of services which support it.
|
||||
#
|
||||
oembed:
|
||||
# A default list of oEmbed providers is included with Synapse.
|
||||
#
|
||||
# Uncomment the following to disable using these default oEmbed URLs.
|
||||
# Defaults to 'false'.
|
||||
#
|
||||
#disable_default_providers: true
|
||||
|
||||
# Additional files with oEmbed configuration (each should be in the
|
||||
# form of providers.json).
|
||||
#
|
||||
# By default, this list is empty (so only the default providers.json
|
||||
# is used).
|
||||
#
|
||||
#additional_providers:
|
||||
# - oembed/my_providers.json
|
||||
"""
|
||||
|
||||
|
||||
_OEMBED_PROVIDER_SCHEMA = {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider_name": {"type": "string"},
|
||||
"provider_url": {"type": "string"},
|
||||
"endpoints": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"schemes": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"url": {"type": "string"},
|
||||
"formats": {"type": "array", "items": {"type": "string"}},
|
||||
"discovery": {"type": "boolean"},
|
||||
},
|
||||
"required": ["schemes", "url"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["provider_name", "provider_url", "endpoints"],
|
||||
},
|
||||
}
|
|
@ -32,6 +32,9 @@ from . import EventBase
|
|||
# the literal fields "foo\" and "bar" but will instead be treated as "foo\\.bar"
|
||||
SPLIT_FIELD_REGEX = re.compile(r"(?<!\\)\.")
|
||||
|
||||
CANONICALJSON_MAX_INT = (2 ** 53) - 1
|
||||
CANONICALJSON_MIN_INT = -CANONICALJSON_MAX_INT
|
||||
|
||||
|
||||
def prune_event(event: EventBase) -> EventBase:
|
||||
"""Returns a pruned version of the given event, which removes all keys we
|
||||
|
@ -505,7 +508,7 @@ def validate_canonicaljson(value: Any):
|
|||
* NaN, Infinity, -Infinity
|
||||
"""
|
||||
if isinstance(value, int):
|
||||
if value <= -(2 ** 53) or 2 ** 53 <= value:
|
||||
if value < CANONICALJSON_MIN_INT or CANONICALJSON_MAX_INT < value:
|
||||
raise SynapseError(400, "JSON integer out of range", Codes.BAD_JSON)
|
||||
|
||||
elif isinstance(value, float):
|
||||
|
|
|
@ -11,16 +11,22 @@
|
|||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import collections.abc
|
||||
from typing import Union
|
||||
|
||||
import jsonschema
|
||||
|
||||
from synapse.api.constants import MAX_ALIAS_LENGTH, EventTypes, Membership
|
||||
from synapse.api.errors import Codes, SynapseError
|
||||
from synapse.api.room_versions import EventFormatVersions
|
||||
from synapse.config.homeserver import HomeServerConfig
|
||||
from synapse.events import EventBase
|
||||
from synapse.events.builder import EventBuilder
|
||||
from synapse.events.utils import validate_canonicaljson
|
||||
from synapse.events.utils import (
|
||||
CANONICALJSON_MAX_INT,
|
||||
CANONICALJSON_MIN_INT,
|
||||
validate_canonicaljson,
|
||||
)
|
||||
from synapse.federation.federation_server import server_matches_acl_event
|
||||
from synapse.types import EventID, RoomID, UserID
|
||||
|
||||
|
@ -87,6 +93,29 @@ class EventValidator:
|
|||
400, "Can't create an ACL event that denies the local server"
|
||||
)
|
||||
|
||||
if event.type == EventTypes.PowerLevels:
|
||||
try:
|
||||
jsonschema.validate(
|
||||
instance=event.content,
|
||||
schema=POWER_LEVELS_SCHEMA,
|
||||
cls=plValidator,
|
||||
)
|
||||
except jsonschema.ValidationError as e:
|
||||
if e.path:
|
||||
# example: "users_default": '0' is not of type 'integer'
|
||||
message = '"' + e.path[-1] + '": ' + e.message # noqa: B306
|
||||
# jsonschema.ValidationError.message is a valid attribute
|
||||
else:
|
||||
# example: '0' is not of type 'integer'
|
||||
message = e.message # noqa: B306
|
||||
# jsonschema.ValidationError.message is a valid attribute
|
||||
|
||||
raise SynapseError(
|
||||
code=400,
|
||||
msg=message,
|
||||
errcode=Codes.BAD_JSON,
|
||||
)
|
||||
|
||||
def _validate_retention(self, event: EventBase):
|
||||
"""Checks that an event that defines the retention policy for a room respects the
|
||||
format enforced by the spec.
|
||||
|
@ -185,3 +214,47 @@ class EventValidator:
|
|||
def _ensure_state_event(self, event):
|
||||
if not event.is_state():
|
||||
raise SynapseError(400, "'%s' must be state events" % (event.type,))
|
||||
|
||||
|
||||
POWER_LEVELS_SCHEMA = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ban": {"$ref": "#/definitions/int"},
|
||||
"events": {"$ref": "#/definitions/objectOfInts"},
|
||||
"events_default": {"$ref": "#/definitions/int"},
|
||||
"invite": {"$ref": "#/definitions/int"},
|
||||
"kick": {"$ref": "#/definitions/int"},
|
||||
"notifications": {"$ref": "#/definitions/objectOfInts"},
|
||||
"redact": {"$ref": "#/definitions/int"},
|
||||
"state_default": {"$ref": "#/definitions/int"},
|
||||
"users": {"$ref": "#/definitions/objectOfInts"},
|
||||
"users_default": {"$ref": "#/definitions/int"},
|
||||
},
|
||||
"definitions": {
|
||||
"int": {
|
||||
"type": "integer",
|
||||
"minimum": CANONICALJSON_MIN_INT,
|
||||
"maximum": CANONICALJSON_MAX_INT,
|
||||
},
|
||||
"objectOfInts": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"$ref": "#/definitions/int"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _create_power_level_validator():
|
||||
validator = jsonschema.validators.validator_for(POWER_LEVELS_SCHEMA)
|
||||
|
||||
# by default jsonschema does not consider a frozendict to be an object so
|
||||
# we need to use a custom type checker
|
||||
# https://python-jsonschema.readthedocs.io/en/stable/validate/?highlight=object#validating-with-additional-types
|
||||
type_checker = validator.TYPE_CHECKER.redefine(
|
||||
"object", lambda checker, thing: isinstance(thing, collections.abc.Mapping)
|
||||
)
|
||||
|
||||
return jsonschema.validators.extend(validator, type_checker=type_checker)
|
||||
|
||||
|
||||
plValidator = _create_power_level_validator()
|
||||
|
|
|
@ -110,6 +110,7 @@ class FederationServer(FederationBase):
|
|||
super().__init__(hs)
|
||||
|
||||
self.handler = hs.get_federation_handler()
|
||||
self._federation_event_handler = hs.get_federation_event_handler()
|
||||
self.state = hs.get_state_handler()
|
||||
self._event_auth_handler = hs.get_event_auth_handler()
|
||||
|
||||
|
@ -787,7 +788,9 @@ class FederationServer(FederationBase):
|
|||
|
||||
event = await self._check_sigs_and_hash(room_version, event)
|
||||
|
||||
return await self.handler.on_send_membership_event(origin, event)
|
||||
return await self._federation_event_handler.on_send_membership_event(
|
||||
origin, event
|
||||
)
|
||||
|
||||
async def on_event_auth(
|
||||
self, origin: str, room_id: str, event_id: str
|
||||
|
@ -1005,7 +1008,7 @@ class FederationServer(FederationBase):
|
|||
async with lock:
|
||||
logger.info("handling received PDU: %s", event)
|
||||
try:
|
||||
await self.handler.on_receive_pdu(origin, event)
|
||||
await self._federation_event_handler.on_receive_pdu(origin, event)
|
||||
except FederationError as e:
|
||||
# XXX: Ideally we'd inform the remote we failed to process
|
||||
# the event, but we can't return an error in the transaction
|
||||
|
|
|
@ -332,6 +332,13 @@ class GroupsServerWorkerHandler:
|
|||
requester_user_id, group_id
|
||||
)
|
||||
|
||||
# Note! room_results["is_public"] is about whether the room is considered
|
||||
# public from the group's point of view. (i.e. whether non-group members
|
||||
# should be able to see the room is in the group).
|
||||
# This is not the same as whether the room itself is public (in the sense
|
||||
# of being visible in the room directory).
|
||||
# As such, room_results["is_public"] itself is not sufficient to determine
|
||||
# whether any given user is permitted to see the room's metadata.
|
||||
room_results = await self.store.get_rooms_in_group(
|
||||
group_id, include_private=is_user_in_group
|
||||
)
|
||||
|
@ -341,8 +348,15 @@ class GroupsServerWorkerHandler:
|
|||
room_id = room_result["room_id"]
|
||||
|
||||
joined_users = await self.store.get_users_in_room(room_id)
|
||||
|
||||
# check the user is actually allowed to see the room before showing it to them
|
||||
allow_private = requester_user_id in joined_users
|
||||
|
||||
entry = await self.room_list_handler.generate_room_entry(
|
||||
room_id, len(joined_users), with_alias=False, allow_private=True
|
||||
room_id,
|
||||
len(joined_users),
|
||||
with_alias=False,
|
||||
allow_private=allow_private,
|
||||
)
|
||||
|
||||
if not entry:
|
||||
|
@ -354,7 +368,7 @@ class GroupsServerWorkerHandler:
|
|||
|
||||
chunk.sort(key=lambda e: -e["num_joined_members"])
|
||||
|
||||
return {"chunk": chunk, "total_room_count_estimate": len(room_results)}
|
||||
return {"chunk": chunk, "total_room_count_estimate": len(chunk)}
|
||||
|
||||
|
||||
class GroupsServerHandler(GroupsServerWorkerHandler):
|
||||
|
|
|
@ -1464,6 +1464,10 @@ class AuthHandler(BaseHandler):
|
|||
)
|
||||
|
||||
await self.store.user_delete_threepid(user_id, medium, address)
|
||||
if medium == "email":
|
||||
await self.store.delete_pusher_by_app_id_pushkey_user_id(
|
||||
app_id="m.email", pushkey=address, user_id=user_id
|
||||
)
|
||||
return result
|
||||
|
||||
async def hash(self, password: str) -> str:
|
||||
|
@ -1732,7 +1736,6 @@ class AuthHandler(BaseHandler):
|
|||
|
||||
@attr.s(slots=True)
|
||||
class MacaroonGenerator:
|
||||
|
||||
hs = attr.ib()
|
||||
|
||||
def generate_guest_access_token(self, user_id: str) -> str:
|
||||
|
|
File diff suppressed because it is too large
Load diff
1825
synapse/handlers/federation_event.py
Normal file
1825
synapse/handlers/federation_event.py
Normal file
File diff suppressed because it is too large
Load diff
|
@ -183,20 +183,37 @@ class MessageHandler:
|
|||
|
||||
if not last_events:
|
||||
raise NotFoundError("Can't find event for token %s" % (at_token,))
|
||||
last_event = last_events[0]
|
||||
|
||||
# check whether the user is in the room at that time to determine
|
||||
# whether they should be treated as peeking.
|
||||
state_map = await self.state_store.get_state_for_event(
|
||||
last_event.event_id,
|
||||
StateFilter.from_types([(EventTypes.Member, user_id)]),
|
||||
)
|
||||
|
||||
joined = False
|
||||
membership_event = state_map.get((EventTypes.Member, user_id))
|
||||
if membership_event:
|
||||
joined = membership_event.membership == Membership.JOIN
|
||||
|
||||
is_peeking = not joined
|
||||
|
||||
visible_events = await filter_events_for_client(
|
||||
self.storage,
|
||||
user_id,
|
||||
last_events,
|
||||
filter_send_to_client=False,
|
||||
is_peeking=is_peeking,
|
||||
)
|
||||
|
||||
event = last_events[0]
|
||||
if visible_events:
|
||||
room_state_events = await self.state_store.get_state_for_events(
|
||||
[event.event_id], state_filter=state_filter
|
||||
[last_event.event_id], state_filter=state_filter
|
||||
)
|
||||
room_state: Mapping[Any, EventBase] = room_state_events[event.event_id]
|
||||
room_state: Mapping[Any, EventBase] = room_state_events[
|
||||
last_event.event_id
|
||||
]
|
||||
else:
|
||||
raise AuthError(
|
||||
403,
|
||||
|
|
|
@ -19,9 +19,12 @@ from email.mime.text import MIMEText
|
|||
from io import BytesIO
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from pkg_resources import parse_version
|
||||
|
||||
import twisted
|
||||
from twisted.internet.defer import Deferred
|
||||
from twisted.internet.interfaces import IReactorTCP
|
||||
from twisted.mail.smtp import ESMTPSenderFactory
|
||||
from twisted.internet.interfaces import IOpenSSLContextFactory, IReactorTCP
|
||||
from twisted.mail.smtp import ESMTPSender, ESMTPSenderFactory
|
||||
|
||||
from synapse.logging.context import make_deferred_yieldable
|
||||
|
||||
|
@ -30,6 +33,19 @@ if TYPE_CHECKING:
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_is_old_twisted = parse_version(twisted.__version__) < parse_version("21")
|
||||
|
||||
|
||||
class _NoTLSESMTPSender(ESMTPSender):
|
||||
"""Extend ESMTPSender to disable TLS
|
||||
|
||||
Unfortunately, before Twisted 21.2, ESMTPSender doesn't give an easy way to disable
|
||||
TLS, so we override its internal method which it uses to generate a context factory.
|
||||
"""
|
||||
|
||||
def _getContextFactory(self) -> Optional[IOpenSSLContextFactory]:
|
||||
return None
|
||||
|
||||
|
||||
async def _sendmail(
|
||||
reactor: IReactorTCP,
|
||||
|
@ -42,7 +58,7 @@ async def _sendmail(
|
|||
password: Optional[bytes] = None,
|
||||
require_auth: bool = False,
|
||||
require_tls: bool = False,
|
||||
tls_hostname: Optional[str] = None,
|
||||
enable_tls: bool = True,
|
||||
) -> None:
|
||||
"""A simple wrapper around ESMTPSenderFactory, to allow substitution in tests
|
||||
|
||||
|
@ -57,24 +73,37 @@ async def _sendmail(
|
|||
password: password to give when authenticating
|
||||
require_auth: if auth is not offered, fail the request
|
||||
require_tls: if TLS is not offered, fail the reqest
|
||||
tls_hostname: TLS hostname to check for. None to disable TLS.
|
||||
enable_tls: True to enable TLS. If this is False and require_tls is True,
|
||||
the request will fail.
|
||||
"""
|
||||
msg = BytesIO(msg_bytes)
|
||||
|
||||
d: "Deferred[object]" = Deferred()
|
||||
|
||||
factory = ESMTPSenderFactory(
|
||||
username,
|
||||
password,
|
||||
from_addr,
|
||||
to_addr,
|
||||
msg,
|
||||
d,
|
||||
heloFallback=True,
|
||||
requireAuthentication=require_auth,
|
||||
requireTransportSecurity=require_tls,
|
||||
hostname=tls_hostname,
|
||||
)
|
||||
def build_sender_factory(**kwargs) -> ESMTPSenderFactory:
|
||||
return ESMTPSenderFactory(
|
||||
username,
|
||||
password,
|
||||
from_addr,
|
||||
to_addr,
|
||||
msg,
|
||||
d,
|
||||
heloFallback=True,
|
||||
requireAuthentication=require_auth,
|
||||
requireTransportSecurity=require_tls,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
if _is_old_twisted:
|
||||
# before twisted 21.2, we have to override the ESMTPSender protocol to disable
|
||||
# TLS
|
||||
factory = build_sender_factory()
|
||||
|
||||
if not enable_tls:
|
||||
factory.protocol = _NoTLSESMTPSender
|
||||
else:
|
||||
# for twisted 21.2 and later, there is a 'hostname' parameter which we should
|
||||
# set to enable TLS.
|
||||
factory = build_sender_factory(hostname=smtphost if enable_tls else None)
|
||||
|
||||
# the IReactorTCP interface claims host has to be a bytes, which seems to be wrong
|
||||
reactor.connectTCP(smtphost, smtpport, factory, timeout=30, bindAddress=None) # type: ignore[arg-type]
|
||||
|
@ -154,5 +183,5 @@ class SendEmailHandler:
|
|||
password=self._smtp_pass,
|
||||
require_auth=self._smtp_user is not None,
|
||||
require_tls=self._require_transport_security,
|
||||
tls_hostname=self._smtp_host if self._enable_tls else None,
|
||||
enable_tls=self._enable_tls,
|
||||
)
|
||||
|
|
|
@ -48,7 +48,8 @@ logger = logging.getLogger(__name__)
|
|||
# [1] https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers.
|
||||
|
||||
REQUIREMENTS = [
|
||||
"jsonschema>=2.5.1",
|
||||
# we use the TYPE_CHECKER.redefine method added in jsonschema 3.0.0
|
||||
"jsonschema>=3.0.0",
|
||||
"frozendict>=1",
|
||||
"unpaddedbase64>=1.1.0",
|
||||
"canonicaljson>=1.4.0",
|
||||
|
|
|
@ -62,7 +62,7 @@ class ReplicationFederationSendEventsRestServlet(ReplicationEndpoint):
|
|||
self.store = hs.get_datastore()
|
||||
self.storage = hs.get_storage()
|
||||
self.clock = hs.get_clock()
|
||||
self.federation_handler = hs.get_federation_handler()
|
||||
self.federation_event_handler = hs.get_federation_event_handler()
|
||||
|
||||
@staticmethod
|
||||
async def _serialize_payload(store, room_id, event_and_contexts, backfilled):
|
||||
|
@ -127,7 +127,7 @@ class ReplicationFederationSendEventsRestServlet(ReplicationEndpoint):
|
|||
|
||||
logger.info("Got %d events from federation", len(event_and_contexts))
|
||||
|
||||
max_stream_id = await self.federation_handler.persist_events_and_notify(
|
||||
max_stream_id = await self.federation_event_handler.persist_events_and_notify(
|
||||
room_id, event_and_contexts, backfilled
|
||||
)
|
||||
|
||||
|
|
17
synapse/res/providers.json
Normal file
17
synapse/res/providers.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
[
|
||||
{
|
||||
"provider_name": "Twitter",
|
||||
"provider_url": "http://www.twitter.com/",
|
||||
"endpoints": [
|
||||
{
|
||||
"schemes": [
|
||||
"https://twitter.com/*/status/*",
|
||||
"https://*.twitter.com/*/status/*",
|
||||
"https://twitter.com/*/moments/*",
|
||||
"https://*.twitter.com/*/moments/*"
|
||||
],
|
||||
"url": "https://publish.twitter.com/oembed"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -223,7 +223,6 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
|||
RoomMembersRestServlet(hs).register(http_server)
|
||||
DeleteRoomRestServlet(hs).register(http_server)
|
||||
JoinRoomAliasServlet(hs).register(http_server)
|
||||
SendServerNoticeServlet(hs).register(http_server)
|
||||
VersionServlet(hs).register(http_server)
|
||||
UserAdminServlet(hs).register(http_server)
|
||||
UserMembershipRestServlet(hs).register(http_server)
|
||||
|
@ -247,6 +246,10 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
|||
NewRegistrationTokenRestServlet(hs).register(http_server)
|
||||
RegistrationTokenRestServlet(hs).register(http_server)
|
||||
|
||||
# Some servlets only get registered for the main process.
|
||||
if hs.config.worker_app is None:
|
||||
SendServerNoticeServlet(hs).register(http_server)
|
||||
|
||||
|
||||
def register_servlets_for_client_rest_resource(
|
||||
hs: "HomeServer", http_server: HttpServer
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
from typing import TYPE_CHECKING, Optional, Tuple
|
||||
|
||||
from synapse.api.constants import EventTypes
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.api.errors import NotFoundError, SynapseError
|
||||
from synapse.http.server import HttpServer
|
||||
from synapse.http.servlet import (
|
||||
RestServlet,
|
||||
|
@ -53,6 +53,8 @@ class SendServerNoticeServlet(RestServlet):
|
|||
def __init__(self, hs: "HomeServer"):
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.server_notices_manager = hs.get_server_notices_manager()
|
||||
self.admin_handler = hs.get_admin_handler()
|
||||
self.txns = HttpTransactionCache(hs)
|
||||
|
||||
def register(self, json_resource: HttpServer):
|
||||
|
@ -79,19 +81,22 @@ class SendServerNoticeServlet(RestServlet):
|
|||
# We grab the server notices manager here as its initialisation has a check for worker processes,
|
||||
# but worker processes still need to initialise SendServerNoticeServlet (as it is part of the
|
||||
# admin api).
|
||||
if not self.hs.get_server_notices_manager().is_enabled():
|
||||
if not self.server_notices_manager.is_enabled():
|
||||
raise SynapseError(400, "Server notices are not enabled on this server")
|
||||
|
||||
user_id = body["user_id"]
|
||||
UserID.from_string(user_id)
|
||||
if not self.hs.is_mine_id(user_id):
|
||||
target_user = UserID.from_string(body["user_id"])
|
||||
if not self.hs.is_mine(target_user):
|
||||
raise SynapseError(400, "Server notices can only be sent to local users")
|
||||
|
||||
event = await self.hs.get_server_notices_manager().send_notice(
|
||||
user_id=body["user_id"],
|
||||
if not await self.admin_handler.get_user(target_user):
|
||||
raise NotFoundError("User not found")
|
||||
|
||||
event = await self.server_notices_manager.send_notice(
|
||||
user_id=target_user.to_string(),
|
||||
type=event_type,
|
||||
state_key=state_key,
|
||||
event_content=body["content"],
|
||||
txn_id=txn_id,
|
||||
)
|
||||
|
||||
return 200, {"event_id": event.event_id}
|
||||
|
|
|
@ -13,12 +13,19 @@
|
|||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Tuple
|
||||
|
||||
from synapse.api.errors import AuthError, NotFoundError, SynapseError
|
||||
from synapse.http.server import HttpServer
|
||||
from synapse.http.servlet import RestServlet, parse_json_object_from_request
|
||||
from synapse.http.site import SynapseRequest
|
||||
from synapse.types import JsonDict
|
||||
|
||||
from ._base import client_patterns
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -32,13 +39,15 @@ class AccountDataServlet(RestServlet):
|
|||
"/user/(?P<user_id>[^/]*)/account_data/(?P<account_data_type>[^/]*)"
|
||||
)
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.auth = hs.get_auth()
|
||||
self.store = hs.get_datastore()
|
||||
self.handler = hs.get_account_data_handler()
|
||||
|
||||
async def on_PUT(self, request, user_id, account_data_type):
|
||||
async def on_PUT(
|
||||
self, request: SynapseRequest, user_id: str, account_data_type: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
if user_id != requester.user.to_string():
|
||||
raise AuthError(403, "Cannot add account data for other users.")
|
||||
|
@ -49,7 +58,9 @@ class AccountDataServlet(RestServlet):
|
|||
|
||||
return 200, {}
|
||||
|
||||
async def on_GET(self, request, user_id, account_data_type):
|
||||
async def on_GET(
|
||||
self, request: SynapseRequest, user_id: str, account_data_type: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
if user_id != requester.user.to_string():
|
||||
raise AuthError(403, "Cannot get account data for other users.")
|
||||
|
@ -76,13 +87,19 @@ class RoomAccountDataServlet(RestServlet):
|
|||
"/account_data/(?P<account_data_type>[^/]*)"
|
||||
)
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.auth = hs.get_auth()
|
||||
self.store = hs.get_datastore()
|
||||
self.handler = hs.get_account_data_handler()
|
||||
|
||||
async def on_PUT(self, request, user_id, room_id, account_data_type):
|
||||
async def on_PUT(
|
||||
self,
|
||||
request: SynapseRequest,
|
||||
user_id: str,
|
||||
room_id: str,
|
||||
account_data_type: str,
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
if user_id != requester.user.to_string():
|
||||
raise AuthError(403, "Cannot add account data for other users.")
|
||||
|
@ -102,7 +119,13 @@ class RoomAccountDataServlet(RestServlet):
|
|||
|
||||
return 200, {}
|
||||
|
||||
async def on_GET(self, request, user_id, room_id, account_data_type):
|
||||
async def on_GET(
|
||||
self,
|
||||
request: SynapseRequest,
|
||||
user_id: str,
|
||||
room_id: str,
|
||||
account_data_type: str,
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
if user_id != requester.user.to_string():
|
||||
raise AuthError(403, "Cannot get account data for other users.")
|
||||
|
@ -117,6 +140,6 @@ class RoomAccountDataServlet(RestServlet):
|
|||
return 200, event
|
||||
|
||||
|
||||
def register_servlets(hs, http_server):
|
||||
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||
AccountDataServlet(hs).register(http_server)
|
||||
RoomAccountDataServlet(hs).register(http_server)
|
||||
|
|
|
@ -156,7 +156,7 @@ class GroupSummaryRoomsCatServlet(RestServlet):
|
|||
group_id: str,
|
||||
category_id: Optional[str],
|
||||
room_id: str,
|
||||
):
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
requester_user_id = requester.user.to_string()
|
||||
|
||||
|
@ -188,7 +188,7 @@ class GroupSummaryRoomsCatServlet(RestServlet):
|
|||
@_validate_group_id
|
||||
async def on_DELETE(
|
||||
self, request: SynapseRequest, group_id: str, category_id: str, room_id: str
|
||||
):
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
requester_user_id = requester.user.to_string()
|
||||
|
||||
|
@ -451,7 +451,7 @@ class GroupSummaryUsersRoleServlet(RestServlet):
|
|||
@_validate_group_id
|
||||
async def on_DELETE(
|
||||
self, request: SynapseRequest, group_id: str, role_id: str, user_id: str
|
||||
):
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
requester_user_id = requester.user.to_string()
|
||||
|
||||
|
@ -674,7 +674,7 @@ class GroupAdminRoomsConfigServlet(RestServlet):
|
|||
@_validate_group_id
|
||||
async def on_PUT(
|
||||
self, request: SynapseRequest, group_id: str, room_id: str, config_key: str
|
||||
):
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
requester_user_id = requester.user.to_string()
|
||||
|
||||
|
@ -706,7 +706,7 @@ class GroupAdminUsersInviteServlet(RestServlet):
|
|||
|
||||
@_validate_group_id
|
||||
async def on_PUT(
|
||||
self, request: SynapseRequest, group_id, user_id
|
||||
self, request: SynapseRequest, group_id: str, user_id: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
requester_user_id = requester.user.to_string()
|
||||
|
@ -738,7 +738,7 @@ class GroupAdminUsersKickServlet(RestServlet):
|
|||
|
||||
@_validate_group_id
|
||||
async def on_PUT(
|
||||
self, request: SynapseRequest, group_id, user_id
|
||||
self, request: SynapseRequest, group_id: str, user_id: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
requester_user_id = requester.user.to_string()
|
||||
|
|
|
@ -13,13 +13,20 @@
|
|||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Tuple
|
||||
|
||||
from synapse.api.constants import ReadReceiptEventFields
|
||||
from synapse.api.errors import Codes, SynapseError
|
||||
from synapse.http.server import HttpServer
|
||||
from synapse.http.servlet import RestServlet, parse_json_object_from_request
|
||||
from synapse.http.site import SynapseRequest
|
||||
from synapse.types import JsonDict
|
||||
|
||||
from ._base import client_patterns
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -30,14 +37,16 @@ class ReceiptRestServlet(RestServlet):
|
|||
"/(?P<event_id>[^/]*)$"
|
||||
)
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.receipts_handler = hs.get_receipts_handler()
|
||||
self.presence_handler = hs.get_presence_handler()
|
||||
|
||||
async def on_POST(self, request, room_id, receipt_type, event_id):
|
||||
async def on_POST(
|
||||
self, request: SynapseRequest, room_id: str, receipt_type: str, event_id: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
|
||||
if receipt_type != "m.read":
|
||||
|
@ -67,5 +76,5 @@ class ReceiptRestServlet(RestServlet):
|
|||
return 200, {}
|
||||
|
||||
|
||||
def register_servlets(hs, http_server):
|
||||
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||
ReceiptRestServlet(hs).register(http_server)
|
||||
|
|
|
@ -12,10 +12,11 @@
|
|||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import hmac
|
||||
import logging
|
||||
import random
|
||||
from typing import List, Union
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
|
||||
from twisted.web.server import Request
|
||||
|
||||
import synapse
|
||||
import synapse.api.auth
|
||||
|
@ -30,15 +31,13 @@ from synapse.api.errors import (
|
|||
)
|
||||
from synapse.api.ratelimiting import Ratelimiter
|
||||
from synapse.config import ConfigError
|
||||
from synapse.config.captcha import CaptchaConfig
|
||||
from synapse.config.consent import ConsentConfig
|
||||
from synapse.config.emailconfig import ThreepidBehaviour
|
||||
from synapse.config.homeserver import HomeServerConfig
|
||||
from synapse.config.ratelimiting import FederationRateLimitConfig
|
||||
from synapse.config.registration import RegistrationConfig
|
||||
from synapse.config.server import is_threepid_reserved
|
||||
from synapse.handlers.auth import AuthHandler
|
||||
from synapse.handlers.ui_auth import UIAuthSessionDataConstants
|
||||
from synapse.http.server import finish_request, respond_with_html
|
||||
from synapse.http.server import HttpServer, finish_request, respond_with_html
|
||||
from synapse.http.servlet import (
|
||||
RestServlet,
|
||||
assert_params_in_dict,
|
||||
|
@ -46,6 +45,7 @@ from synapse.http.servlet import (
|
|||
parse_json_object_from_request,
|
||||
parse_string,
|
||||
)
|
||||
from synapse.http.site import SynapseRequest
|
||||
from synapse.metrics import threepid_send_requests
|
||||
from synapse.push.mailer import Mailer
|
||||
from synapse.types import JsonDict
|
||||
|
@ -60,17 +60,8 @@ from synapse.util.threepids import (
|
|||
|
||||
from ._base import client_patterns, interactive_auth_handler
|
||||
|
||||
# We ought to be using hmac.compare_digest() but on older pythons it doesn't
|
||||
# exist. It's a _really minor_ security flaw to use plain string comparison
|
||||
# because the timing attack is so obscured by all the other code here it's
|
||||
# unlikely to make much difference
|
||||
if hasattr(hmac, "compare_digest"):
|
||||
compare_digest = hmac.compare_digest
|
||||
else:
|
||||
|
||||
def compare_digest(a, b):
|
||||
return a == b
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -78,11 +69,7 @@ logger = logging.getLogger(__name__)
|
|||
class EmailRegisterRequestTokenRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/register/email/requestToken$")
|
||||
|
||||
def __init__(self, hs):
|
||||
"""
|
||||
Args:
|
||||
hs (synapse.server.HomeServer): server
|
||||
"""
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.hs = hs
|
||||
self.identity_handler = hs.get_identity_handler()
|
||||
|
@ -96,7 +83,7 @@ class EmailRegisterRequestTokenRestServlet(RestServlet):
|
|||
template_text=self.config.email_registration_template_text,
|
||||
)
|
||||
|
||||
async def on_POST(self, request):
|
||||
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
|
||||
if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.OFF:
|
||||
if self.hs.config.local_threepid_handling_disabled_due_to_email_config:
|
||||
logger.warning(
|
||||
|
@ -184,16 +171,12 @@ class EmailRegisterRequestTokenRestServlet(RestServlet):
|
|||
class MsisdnRegisterRequestTokenRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/register/msisdn/requestToken$")
|
||||
|
||||
def __init__(self, hs):
|
||||
"""
|
||||
Args:
|
||||
hs (synapse.server.HomeServer): server
|
||||
"""
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.hs = hs
|
||||
self.identity_handler = hs.get_identity_handler()
|
||||
|
||||
async def on_POST(self, request):
|
||||
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
|
||||
body = parse_json_object_from_request(request)
|
||||
|
||||
assert_params_in_dict(
|
||||
|
@ -268,11 +251,7 @@ class RegistrationSubmitTokenServlet(RestServlet):
|
|||
"/registration/(?P<medium>[^/]*)/submit_token$", releases=(), unstable=True
|
||||
)
|
||||
|
||||
def __init__(self, hs):
|
||||
"""
|
||||
Args:
|
||||
hs (synapse.server.HomeServer): server
|
||||
"""
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
|
@ -285,7 +264,7 @@ class RegistrationSubmitTokenServlet(RestServlet):
|
|||
self.config.email_registration_template_failure_html
|
||||
)
|
||||
|
||||
async def on_GET(self, request, medium):
|
||||
async def on_GET(self, request: Request, medium: str) -> None:
|
||||
if medium != "email":
|
||||
raise SynapseError(
|
||||
400, "This medium is currently not supported for registration"
|
||||
|
@ -339,11 +318,7 @@ class RegistrationSubmitTokenServlet(RestServlet):
|
|||
class UsernameAvailabilityRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/register/available")
|
||||
|
||||
def __init__(self, hs):
|
||||
"""
|
||||
Args:
|
||||
hs (synapse.server.HomeServer): server
|
||||
"""
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.hs = hs
|
||||
self.registration_handler = hs.get_registration_handler()
|
||||
|
@ -363,7 +338,7 @@ class UsernameAvailabilityRestServlet(RestServlet):
|
|||
),
|
||||
)
|
||||
|
||||
async def on_GET(self, request):
|
||||
async def on_GET(self, request: Request) -> Tuple[int, JsonDict]:
|
||||
if not self.hs.config.enable_registration:
|
||||
raise SynapseError(
|
||||
403, "Registration has been disabled", errcode=Codes.FORBIDDEN
|
||||
|
@ -432,11 +407,7 @@ class RegistrationTokenValidityRestServlet(RestServlet):
|
|||
class RegisterRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/register$")
|
||||
|
||||
def __init__(self, hs):
|
||||
"""
|
||||
Args:
|
||||
hs (synapse.server.HomeServer): server
|
||||
"""
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
|
||||
self.hs = hs
|
||||
|
@ -458,23 +429,21 @@ class RegisterRestServlet(RestServlet):
|
|||
)
|
||||
|
||||
@interactive_auth_handler
|
||||
async def on_POST(self, request):
|
||||
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
|
||||
body = parse_json_object_from_request(request)
|
||||
|
||||
client_addr = request.getClientIP()
|
||||
|
||||
await self.ratelimiter.ratelimit(None, client_addr, update=False)
|
||||
|
||||
kind = b"user"
|
||||
if b"kind" in request.args:
|
||||
kind = request.args[b"kind"][0]
|
||||
kind = parse_string(request, "kind", default="user")
|
||||
|
||||
if kind == b"guest":
|
||||
if kind == "guest":
|
||||
ret = await self._do_guest_registration(body, address=client_addr)
|
||||
return ret
|
||||
elif kind != b"user":
|
||||
elif kind != "user":
|
||||
raise UnrecognizedRequestError(
|
||||
"Do not understand membership kind: %s" % (kind.decode("utf8"),)
|
||||
f"Do not understand membership kind: {kind}",
|
||||
)
|
||||
|
||||
if self._msc2918_enabled:
|
||||
|
@ -762,7 +731,7 @@ class RegisterRestServlet(RestServlet):
|
|||
|
||||
async def _do_appservice_registration(
|
||||
self, username, as_token, body, should_issue_refresh_token: bool = False
|
||||
):
|
||||
) -> JsonDict:
|
||||
user_id = await self.registration_handler.appservice_register(
|
||||
username, as_token
|
||||
)
|
||||
|
@ -779,7 +748,7 @@ class RegisterRestServlet(RestServlet):
|
|||
params: JsonDict,
|
||||
is_appservice_ghost: bool = False,
|
||||
should_issue_refresh_token: bool = False,
|
||||
):
|
||||
) -> JsonDict:
|
||||
"""Complete registration of newly-registered user
|
||||
|
||||
Allocates device_id if one was not given; also creates access_token.
|
||||
|
@ -823,7 +792,9 @@ class RegisterRestServlet(RestServlet):
|
|||
|
||||
return result
|
||||
|
||||
async def _do_guest_registration(self, params, address=None):
|
||||
async def _do_guest_registration(
|
||||
self, params: JsonDict, address: Optional[str] = None
|
||||
) -> Tuple[int, JsonDict]:
|
||||
if not self.hs.config.allow_guest_access:
|
||||
raise SynapseError(403, "Guest access is disabled")
|
||||
user_id = await self.registration_handler.register_user(
|
||||
|
@ -861,9 +832,7 @@ class RegisterRestServlet(RestServlet):
|
|||
|
||||
|
||||
def _calculate_registration_flows(
|
||||
# technically `config` has to provide *all* of these interfaces, not just one
|
||||
config: Union[RegistrationConfig, ConsentConfig, CaptchaConfig],
|
||||
auth_handler: AuthHandler,
|
||||
config: HomeServerConfig, auth_handler: AuthHandler
|
||||
) -> List[List[str]]:
|
||||
"""Get a suitable flows list for registration
|
||||
|
||||
|
@ -942,7 +911,7 @@ def _calculate_registration_flows(
|
|||
return flows
|
||||
|
||||
|
||||
def register_servlets(hs, http_server):
|
||||
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||
EmailRegisterRequestTokenRestServlet(hs).register(http_server)
|
||||
MsisdnRegisterRequestTokenRestServlet(hs).register(http_server)
|
||||
UsernameAvailabilityRestServlet(hs).register(http_server)
|
||||
|
|
|
@ -19,25 +19,32 @@ any time to reflect changes in the MSC.
|
|||
"""
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Awaitable, Optional, Tuple
|
||||
|
||||
from synapse.api.constants import EventTypes, RelationTypes
|
||||
from synapse.api.errors import ShadowBanError, SynapseError
|
||||
from synapse.http.server import HttpServer
|
||||
from synapse.http.servlet import (
|
||||
RestServlet,
|
||||
parse_integer,
|
||||
parse_json_object_from_request,
|
||||
parse_string,
|
||||
)
|
||||
from synapse.http.site import SynapseRequest
|
||||
from synapse.rest.client.transactions import HttpTransactionCache
|
||||
from synapse.storage.relations import (
|
||||
AggregationPaginationToken,
|
||||
PaginationChunk,
|
||||
RelationPaginationToken,
|
||||
)
|
||||
from synapse.types import JsonDict
|
||||
from synapse.util.stringutils import random_string
|
||||
|
||||
from ._base import client_patterns
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -59,13 +66,13 @@ class RelationSendServlet(RestServlet):
|
|||
"/(?P<parent_id>[^/]*)/(?P<relation_type>[^/]*)/(?P<event_type>[^/]*)"
|
||||
)
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.auth = hs.get_auth()
|
||||
self.event_creation_handler = hs.get_event_creation_handler()
|
||||
self.txns = HttpTransactionCache(hs)
|
||||
|
||||
def register(self, http_server):
|
||||
def register(self, http_server: HttpServer) -> None:
|
||||
http_server.register_paths(
|
||||
"POST",
|
||||
client_patterns(self.PATTERN + "$", releases=()),
|
||||
|
@ -79,14 +86,35 @@ class RelationSendServlet(RestServlet):
|
|||
self.__class__.__name__,
|
||||
)
|
||||
|
||||
def on_PUT(self, request, *args, **kwargs):
|
||||
def on_PUT(
|
||||
self,
|
||||
request: SynapseRequest,
|
||||
room_id: str,
|
||||
parent_id: str,
|
||||
relation_type: str,
|
||||
event_type: str,
|
||||
txn_id: Optional[str] = None,
|
||||
) -> Awaitable[Tuple[int, JsonDict]]:
|
||||
return self.txns.fetch_or_execute_request(
|
||||
request, self.on_PUT_or_POST, request, *args, **kwargs
|
||||
request,
|
||||
self.on_PUT_or_POST,
|
||||
request,
|
||||
room_id,
|
||||
parent_id,
|
||||
relation_type,
|
||||
event_type,
|
||||
txn_id,
|
||||
)
|
||||
|
||||
async def on_PUT_or_POST(
|
||||
self, request, room_id, parent_id, relation_type, event_type, txn_id=None
|
||||
):
|
||||
self,
|
||||
request: SynapseRequest,
|
||||
room_id: str,
|
||||
parent_id: str,
|
||||
relation_type: str,
|
||||
event_type: str,
|
||||
txn_id: Optional[str] = None,
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request, allow_guest=True)
|
||||
|
||||
if event_type == EventTypes.Member:
|
||||
|
@ -136,7 +164,7 @@ class RelationPaginationServlet(RestServlet):
|
|||
releases=(),
|
||||
)
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.auth = hs.get_auth()
|
||||
self.store = hs.get_datastore()
|
||||
|
@ -145,8 +173,13 @@ class RelationPaginationServlet(RestServlet):
|
|||
self.event_handler = hs.get_event_handler()
|
||||
|
||||
async def on_GET(
|
||||
self, request, room_id, parent_id, relation_type=None, event_type=None
|
||||
):
|
||||
self,
|
||||
request: SynapseRequest,
|
||||
room_id: str,
|
||||
parent_id: str,
|
||||
relation_type: Optional[str] = None,
|
||||
event_type: Optional[str] = None,
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request, allow_guest=True)
|
||||
|
||||
await self.auth.check_user_in_room_or_world_readable(
|
||||
|
@ -156,6 +189,8 @@ class RelationPaginationServlet(RestServlet):
|
|||
# This gets the original event and checks that a) the event exists and
|
||||
# b) the user is allowed to view it.
|
||||
event = await self.event_handler.get_event(requester.user, room_id, parent_id)
|
||||
if event is None:
|
||||
raise SynapseError(404, "Unknown parent event.")
|
||||
|
||||
limit = parse_integer(request, "limit", default=5)
|
||||
from_token_str = parse_string(request, "from")
|
||||
|
@ -233,15 +268,20 @@ class RelationAggregationPaginationServlet(RestServlet):
|
|||
releases=(),
|
||||
)
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.auth = hs.get_auth()
|
||||
self.store = hs.get_datastore()
|
||||
self.event_handler = hs.get_event_handler()
|
||||
|
||||
async def on_GET(
|
||||
self, request, room_id, parent_id, relation_type=None, event_type=None
|
||||
):
|
||||
self,
|
||||
request: SynapseRequest,
|
||||
room_id: str,
|
||||
parent_id: str,
|
||||
relation_type: Optional[str] = None,
|
||||
event_type: Optional[str] = None,
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request, allow_guest=True)
|
||||
|
||||
await self.auth.check_user_in_room_or_world_readable(
|
||||
|
@ -253,6 +293,8 @@ class RelationAggregationPaginationServlet(RestServlet):
|
|||
# This checks that a) the event exists and b) the user is allowed to
|
||||
# view it.
|
||||
event = await self.event_handler.get_event(requester.user, room_id, parent_id)
|
||||
if event is None:
|
||||
raise SynapseError(404, "Unknown parent event.")
|
||||
|
||||
if relation_type not in (RelationTypes.ANNOTATION, None):
|
||||
raise SynapseError(400, "Relation type must be 'annotation'")
|
||||
|
@ -315,7 +357,7 @@ class RelationAggregationGroupPaginationServlet(RestServlet):
|
|||
releases=(),
|
||||
)
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.auth = hs.get_auth()
|
||||
self.store = hs.get_datastore()
|
||||
|
@ -323,7 +365,15 @@ class RelationAggregationGroupPaginationServlet(RestServlet):
|
|||
self._event_serializer = hs.get_event_client_serializer()
|
||||
self.event_handler = hs.get_event_handler()
|
||||
|
||||
async def on_GET(self, request, room_id, parent_id, relation_type, event_type, key):
|
||||
async def on_GET(
|
||||
self,
|
||||
request: SynapseRequest,
|
||||
room_id: str,
|
||||
parent_id: str,
|
||||
relation_type: str,
|
||||
event_type: str,
|
||||
key: str,
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request, allow_guest=True)
|
||||
|
||||
await self.auth.check_user_in_room_or_world_readable(
|
||||
|
@ -374,7 +424,7 @@ class RelationAggregationGroupPaginationServlet(RestServlet):
|
|||
return 200, return_value
|
||||
|
||||
|
||||
def register_servlets(hs, http_server):
|
||||
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||
RelationSendServlet(hs).register(http_server)
|
||||
RelationPaginationServlet(hs).register(http_server)
|
||||
RelationAggregationPaginationServlet(hs).register(http_server)
|
||||
|
|
|
@ -16,9 +16,11 @@
|
|||
""" This module contains REST servlets to do with rooms: /rooms/<paths> """
|
||||
import logging
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
||||
from typing import TYPE_CHECKING, Awaitable, Dict, List, Optional, Tuple
|
||||
from urllib import parse as urlparse
|
||||
|
||||
from twisted.web.server import Request
|
||||
|
||||
from synapse.api.constants import EventTypes, Membership
|
||||
from synapse.api.errors import (
|
||||
AuthError,
|
||||
|
@ -30,6 +32,7 @@ from synapse.api.errors import (
|
|||
)
|
||||
from synapse.api.filtering import Filter
|
||||
from synapse.events.utils import format_event_for_client_v2
|
||||
from synapse.http.server import HttpServer
|
||||
from synapse.http.servlet import (
|
||||
ResolveRoomIdMixin,
|
||||
RestServlet,
|
||||
|
@ -57,7 +60,7 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class TransactionRestServlet(RestServlet):
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.txns = HttpTransactionCache(hs)
|
||||
|
||||
|
@ -65,20 +68,22 @@ class TransactionRestServlet(RestServlet):
|
|||
class RoomCreateRestServlet(TransactionRestServlet):
|
||||
# No PATTERN; we have custom dispatch rules here
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__(hs)
|
||||
self._room_creation_handler = hs.get_room_creation_handler()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
def register(self, http_server):
|
||||
def register(self, http_server: HttpServer) -> None:
|
||||
PATTERNS = "/createRoom"
|
||||
register_txn_path(self, PATTERNS, http_server)
|
||||
|
||||
def on_PUT(self, request, txn_id):
|
||||
def on_PUT(
|
||||
self, request: SynapseRequest, txn_id: str
|
||||
) -> Awaitable[Tuple[int, JsonDict]]:
|
||||
set_tag("txn_id", txn_id)
|
||||
return self.txns.fetch_or_execute_request(request, self.on_POST, request)
|
||||
|
||||
async def on_POST(self, request):
|
||||
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
|
||||
info, _ = await self._room_creation_handler.create_room(
|
||||
|
@ -87,21 +92,21 @@ class RoomCreateRestServlet(TransactionRestServlet):
|
|||
|
||||
return 200, info
|
||||
|
||||
def get_room_config(self, request):
|
||||
def get_room_config(self, request: Request) -> JsonDict:
|
||||
user_supplied_config = parse_json_object_from_request(request)
|
||||
return user_supplied_config
|
||||
|
||||
|
||||
# TODO: Needs unit testing for generic events
|
||||
class RoomStateEventRestServlet(TransactionRestServlet):
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__(hs)
|
||||
self.event_creation_handler = hs.get_event_creation_handler()
|
||||
self.room_member_handler = hs.get_room_member_handler()
|
||||
self.message_handler = hs.get_message_handler()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
def register(self, http_server):
|
||||
def register(self, http_server: HttpServer) -> None:
|
||||
# /room/$roomid/state/$eventtype
|
||||
no_state_key = "/rooms/(?P<room_id>[^/]*)/state/(?P<event_type>[^/]*)$"
|
||||
|
||||
|
@ -136,13 +141,19 @@ class RoomStateEventRestServlet(TransactionRestServlet):
|
|||
self.__class__.__name__,
|
||||
)
|
||||
|
||||
def on_GET_no_state_key(self, request, room_id, event_type):
|
||||
def on_GET_no_state_key(
|
||||
self, request: SynapseRequest, room_id: str, event_type: str
|
||||
) -> Awaitable[Tuple[int, JsonDict]]:
|
||||
return self.on_GET(request, room_id, event_type, "")
|
||||
|
||||
def on_PUT_no_state_key(self, request, room_id, event_type):
|
||||
def on_PUT_no_state_key(
|
||||
self, request: SynapseRequest, room_id: str, event_type: str
|
||||
) -> Awaitable[Tuple[int, JsonDict]]:
|
||||
return self.on_PUT(request, room_id, event_type, "")
|
||||
|
||||
async def on_GET(self, request, room_id, event_type, state_key):
|
||||
async def on_GET(
|
||||
self, request: SynapseRequest, room_id: str, event_type: str, state_key: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request, allow_guest=True)
|
||||
format = parse_string(
|
||||
request, "format", default="content", allowed_values=["content", "event"]
|
||||
|
@ -165,7 +176,17 @@ class RoomStateEventRestServlet(TransactionRestServlet):
|
|||
elif format == "content":
|
||||
return 200, data.get_dict()["content"]
|
||||
|
||||
async def on_PUT(self, request, room_id, event_type, state_key, txn_id=None):
|
||||
# Format must be event or content, per the parse_string call above.
|
||||
raise RuntimeError(f"Unknown format: {format:r}.")
|
||||
|
||||
async def on_PUT(
|
||||
self,
|
||||
request: SynapseRequest,
|
||||
room_id: str,
|
||||
event_type: str,
|
||||
state_key: str,
|
||||
txn_id: Optional[str] = None,
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
|
||||
if txn_id:
|
||||
|
@ -211,27 +232,35 @@ class RoomStateEventRestServlet(TransactionRestServlet):
|
|||
|
||||
# TODO: Needs unit testing for generic events + feedback
|
||||
class RoomSendEventRestServlet(TransactionRestServlet):
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__(hs)
|
||||
self.event_creation_handler = hs.get_event_creation_handler()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
def register(self, http_server):
|
||||
def register(self, http_server: HttpServer) -> None:
|
||||
# /rooms/$roomid/send/$event_type[/$txn_id]
|
||||
PATTERNS = "/rooms/(?P<room_id>[^/]*)/send/(?P<event_type>[^/]*)"
|
||||
register_txn_path(self, PATTERNS, http_server, with_get=True)
|
||||
|
||||
async def on_POST(self, request, room_id, event_type, txn_id=None):
|
||||
async def on_POST(
|
||||
self,
|
||||
request: SynapseRequest,
|
||||
room_id: str,
|
||||
event_type: str,
|
||||
txn_id: Optional[str] = None,
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request, allow_guest=True)
|
||||
content = parse_json_object_from_request(request)
|
||||
|
||||
event_dict = {
|
||||
event_dict: JsonDict = {
|
||||
"type": event_type,
|
||||
"content": content,
|
||||
"room_id": room_id,
|
||||
"sender": requester.user.to_string(),
|
||||
}
|
||||
|
||||
# Twisted will have processed the args by now.
|
||||
assert request.args is not None
|
||||
if b"ts" in request.args and requester.app_service:
|
||||
event_dict["origin_server_ts"] = parse_integer(request, "ts", 0)
|
||||
|
||||
|
@ -249,10 +278,14 @@ class RoomSendEventRestServlet(TransactionRestServlet):
|
|||
set_tag("event_id", event_id)
|
||||
return 200, {"event_id": event_id}
|
||||
|
||||
def on_GET(self, request, room_id, event_type, txn_id):
|
||||
def on_GET(
|
||||
self, request: SynapseRequest, room_id: str, event_type: str, txn_id: str
|
||||
) -> Tuple[int, str]:
|
||||
return 200, "Not implemented"
|
||||
|
||||
def on_PUT(self, request, room_id, event_type, txn_id):
|
||||
def on_PUT(
|
||||
self, request: SynapseRequest, room_id: str, event_type: str, txn_id: str
|
||||
) -> Awaitable[Tuple[int, JsonDict]]:
|
||||
set_tag("txn_id", txn_id)
|
||||
|
||||
return self.txns.fetch_or_execute_request(
|
||||
|
@ -262,12 +295,12 @@ class RoomSendEventRestServlet(TransactionRestServlet):
|
|||
|
||||
# TODO: Needs unit testing for room ID + alias joins
|
||||
class JoinRoomAliasServlet(ResolveRoomIdMixin, TransactionRestServlet):
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__(hs)
|
||||
super(ResolveRoomIdMixin, self).__init__(hs) # ensure the Mixin is set up
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
def register(self, http_server):
|
||||
def register(self, http_server: HttpServer) -> None:
|
||||
# /join/$room_identifier[/$txn_id]
|
||||
PATTERNS = "/join/(?P<room_identifier>[^/]*)"
|
||||
register_txn_path(self, PATTERNS, http_server)
|
||||
|
@ -277,7 +310,7 @@ class JoinRoomAliasServlet(ResolveRoomIdMixin, TransactionRestServlet):
|
|||
request: SynapseRequest,
|
||||
room_identifier: str,
|
||||
txn_id: Optional[str] = None,
|
||||
):
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request, allow_guest=True)
|
||||
|
||||
try:
|
||||
|
@ -308,7 +341,9 @@ class JoinRoomAliasServlet(ResolveRoomIdMixin, TransactionRestServlet):
|
|||
|
||||
return 200, {"room_id": room_id}
|
||||
|
||||
def on_PUT(self, request, room_identifier, txn_id):
|
||||
def on_PUT(
|
||||
self, request: SynapseRequest, room_identifier: str, txn_id: str
|
||||
) -> Awaitable[Tuple[int, JsonDict]]:
|
||||
set_tag("txn_id", txn_id)
|
||||
|
||||
return self.txns.fetch_or_execute_request(
|
||||
|
@ -320,12 +355,12 @@ class JoinRoomAliasServlet(ResolveRoomIdMixin, TransactionRestServlet):
|
|||
class PublicRoomListRestServlet(TransactionRestServlet):
|
||||
PATTERNS = client_patterns("/publicRooms$", v1=True)
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__(hs)
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
async def on_GET(self, request):
|
||||
async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
|
||||
server = parse_string(request, "server")
|
||||
|
||||
try:
|
||||
|
@ -374,7 +409,7 @@ class PublicRoomListRestServlet(TransactionRestServlet):
|
|||
|
||||
return 200, data
|
||||
|
||||
async def on_POST(self, request):
|
||||
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
|
||||
await self.auth.get_user_by_req(request, allow_guest=True)
|
||||
|
||||
server = parse_string(request, "server")
|
||||
|
@ -438,13 +473,15 @@ class PublicRoomListRestServlet(TransactionRestServlet):
|
|||
class RoomMemberListRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/rooms/(?P<room_id>[^/]*)/members$", v1=True)
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.message_handler = hs.get_message_handler()
|
||||
self.auth = hs.get_auth()
|
||||
self.store = hs.get_datastore()
|
||||
|
||||
async def on_GET(self, request, room_id):
|
||||
async def on_GET(
|
||||
self, request: SynapseRequest, room_id: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
# TODO support Pagination stream API (limit/tokens)
|
||||
requester = await self.auth.get_user_by_req(request, allow_guest=True)
|
||||
handler = self.message_handler
|
||||
|
@ -490,12 +527,14 @@ class RoomMemberListRestServlet(RestServlet):
|
|||
class JoinedRoomMemberListRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/rooms/(?P<room_id>[^/]*)/joined_members$", v1=True)
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.message_handler = hs.get_message_handler()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
async def on_GET(self, request, room_id):
|
||||
async def on_GET(
|
||||
self, request: SynapseRequest, room_id: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
|
||||
users_with_profile = await self.message_handler.get_joined_members(
|
||||
|
@ -509,17 +548,21 @@ class JoinedRoomMemberListRestServlet(RestServlet):
|
|||
class RoomMessageListRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/rooms/(?P<room_id>[^/]*)/messages$", v1=True)
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.pagination_handler = hs.get_pagination_handler()
|
||||
self.auth = hs.get_auth()
|
||||
self.store = hs.get_datastore()
|
||||
|
||||
async def on_GET(self, request, room_id):
|
||||
async def on_GET(
|
||||
self, request: SynapseRequest, room_id: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request, allow_guest=True)
|
||||
pagination_config = await PaginationConfig.from_request(
|
||||
self.store, request, default_limit=10
|
||||
)
|
||||
# Twisted will have processed the args by now.
|
||||
assert request.args is not None
|
||||
as_client_event = b"raw" not in request.args
|
||||
filter_str = parse_string(request, "filter", encoding="utf-8")
|
||||
if filter_str:
|
||||
|
@ -549,12 +592,14 @@ class RoomMessageListRestServlet(RestServlet):
|
|||
class RoomStateRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/rooms/(?P<room_id>[^/]*)/state$", v1=True)
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.message_handler = hs.get_message_handler()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
async def on_GET(self, request, room_id):
|
||||
async def on_GET(
|
||||
self, request: SynapseRequest, room_id: str
|
||||
) -> Tuple[int, List[JsonDict]]:
|
||||
requester = await self.auth.get_user_by_req(request, allow_guest=True)
|
||||
# Get all the current state for this room
|
||||
events = await self.message_handler.get_state_events(
|
||||
|
@ -569,13 +614,15 @@ class RoomStateRestServlet(RestServlet):
|
|||
class RoomInitialSyncRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/rooms/(?P<room_id>[^/]*)/initialSync$", v1=True)
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.initial_sync_handler = hs.get_initial_sync_handler()
|
||||
self.auth = hs.get_auth()
|
||||
self.store = hs.get_datastore()
|
||||
|
||||
async def on_GET(self, request, room_id):
|
||||
async def on_GET(
|
||||
self, request: SynapseRequest, room_id: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request, allow_guest=True)
|
||||
pagination_config = await PaginationConfig.from_request(self.store, request)
|
||||
content = await self.initial_sync_handler.room_initial_sync(
|
||||
|
@ -589,14 +636,16 @@ class RoomEventServlet(RestServlet):
|
|||
"/rooms/(?P<room_id>[^/]*)/event/(?P<event_id>[^/]*)$", v1=True
|
||||
)
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.clock = hs.get_clock()
|
||||
self.event_handler = hs.get_event_handler()
|
||||
self._event_serializer = hs.get_event_client_serializer()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
async def on_GET(self, request, room_id, event_id):
|
||||
async def on_GET(
|
||||
self, request: SynapseRequest, room_id: str, event_id: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request, allow_guest=True)
|
||||
try:
|
||||
event = await self.event_handler.get_event(
|
||||
|
@ -610,10 +659,10 @@ class RoomEventServlet(RestServlet):
|
|||
|
||||
time_now = self.clock.time_msec()
|
||||
if event:
|
||||
event = await self._event_serializer.serialize_event(event, time_now)
|
||||
return 200, event
|
||||
event_dict = await self._event_serializer.serialize_event(event, time_now)
|
||||
return 200, event_dict
|
||||
|
||||
return SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND)
|
||||
raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND)
|
||||
|
||||
|
||||
class RoomEventContextServlet(RestServlet):
|
||||
|
@ -621,14 +670,16 @@ class RoomEventContextServlet(RestServlet):
|
|||
"/rooms/(?P<room_id>[^/]*)/context/(?P<event_id>[^/]*)$", v1=True
|
||||
)
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.clock = hs.get_clock()
|
||||
self.room_context_handler = hs.get_room_context_handler()
|
||||
self._event_serializer = hs.get_event_client_serializer()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
async def on_GET(self, request, room_id, event_id):
|
||||
async def on_GET(
|
||||
self, request: SynapseRequest, room_id: str, event_id: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request, allow_guest=True)
|
||||
|
||||
limit = parse_integer(request, "limit", default=10)
|
||||
|
@ -669,23 +720,27 @@ class RoomEventContextServlet(RestServlet):
|
|||
|
||||
|
||||
class RoomForgetRestServlet(TransactionRestServlet):
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__(hs)
|
||||
self.room_member_handler = hs.get_room_member_handler()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
def register(self, http_server):
|
||||
def register(self, http_server: HttpServer) -> None:
|
||||
PATTERNS = "/rooms/(?P<room_id>[^/]*)/forget"
|
||||
register_txn_path(self, PATTERNS, http_server)
|
||||
|
||||
async def on_POST(self, request, room_id, txn_id=None):
|
||||
async def on_POST(
|
||||
self, request: SynapseRequest, room_id: str, txn_id: Optional[str] = None
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request, allow_guest=False)
|
||||
|
||||
await self.room_member_handler.forget(user=requester.user, room_id=room_id)
|
||||
|
||||
return 200, {}
|
||||
|
||||
def on_PUT(self, request, room_id, txn_id):
|
||||
def on_PUT(
|
||||
self, request: SynapseRequest, room_id: str, txn_id: str
|
||||
) -> Awaitable[Tuple[int, JsonDict]]:
|
||||
set_tag("txn_id", txn_id)
|
||||
|
||||
return self.txns.fetch_or_execute_request(
|
||||
|
@ -695,12 +750,12 @@ class RoomForgetRestServlet(TransactionRestServlet):
|
|||
|
||||
# TODO: Needs unit testing
|
||||
class RoomMembershipRestServlet(TransactionRestServlet):
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__(hs)
|
||||
self.room_member_handler = hs.get_room_member_handler()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
def register(self, http_server):
|
||||
def register(self, http_server: HttpServer) -> None:
|
||||
# /rooms/$roomid/[invite|join|leave]
|
||||
PATTERNS = (
|
||||
"/rooms/(?P<room_id>[^/]*)/"
|
||||
|
@ -708,7 +763,13 @@ class RoomMembershipRestServlet(TransactionRestServlet):
|
|||
)
|
||||
register_txn_path(self, PATTERNS, http_server)
|
||||
|
||||
async def on_POST(self, request, room_id, membership_action, txn_id=None):
|
||||
async def on_POST(
|
||||
self,
|
||||
request: SynapseRequest,
|
||||
room_id: str,
|
||||
membership_action: str,
|
||||
txn_id: Optional[str] = None,
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request, allow_guest=True)
|
||||
|
||||
if requester.is_guest and membership_action not in {
|
||||
|
@ -771,13 +832,15 @@ class RoomMembershipRestServlet(TransactionRestServlet):
|
|||
|
||||
return 200, return_value
|
||||
|
||||
def _has_3pid_invite_keys(self, content):
|
||||
def _has_3pid_invite_keys(self, content: JsonDict) -> bool:
|
||||
for key in {"id_server", "medium", "address"}:
|
||||
if key not in content:
|
||||
return False
|
||||
return True
|
||||
|
||||
def on_PUT(self, request, room_id, membership_action, txn_id):
|
||||
def on_PUT(
|
||||
self, request: SynapseRequest, room_id: str, membership_action: str, txn_id: str
|
||||
) -> Awaitable[Tuple[int, JsonDict]]:
|
||||
set_tag("txn_id", txn_id)
|
||||
|
||||
return self.txns.fetch_or_execute_request(
|
||||
|
@ -786,16 +849,22 @@ class RoomMembershipRestServlet(TransactionRestServlet):
|
|||
|
||||
|
||||
class RoomRedactEventRestServlet(TransactionRestServlet):
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__(hs)
|
||||
self.event_creation_handler = hs.get_event_creation_handler()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
def register(self, http_server):
|
||||
def register(self, http_server: HttpServer) -> None:
|
||||
PATTERNS = "/rooms/(?P<room_id>[^/]*)/redact/(?P<event_id>[^/]*)"
|
||||
register_txn_path(self, PATTERNS, http_server)
|
||||
|
||||
async def on_POST(self, request, room_id, event_id, txn_id=None):
|
||||
async def on_POST(
|
||||
self,
|
||||
request: SynapseRequest,
|
||||
room_id: str,
|
||||
event_id: str,
|
||||
txn_id: Optional[str] = None,
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
content = parse_json_object_from_request(request)
|
||||
|
||||
|
@ -821,7 +890,9 @@ class RoomRedactEventRestServlet(TransactionRestServlet):
|
|||
set_tag("event_id", event_id)
|
||||
return 200, {"event_id": event_id}
|
||||
|
||||
def on_PUT(self, request, room_id, event_id, txn_id):
|
||||
def on_PUT(
|
||||
self, request: SynapseRequest, room_id: str, event_id: str, txn_id: str
|
||||
) -> Awaitable[Tuple[int, JsonDict]]:
|
||||
set_tag("txn_id", txn_id)
|
||||
|
||||
return self.txns.fetch_or_execute_request(
|
||||
|
@ -846,7 +917,9 @@ class RoomTypingRestServlet(RestServlet):
|
|||
hs.config.worker.writers.typing == hs.get_instance_name()
|
||||
)
|
||||
|
||||
async def on_PUT(self, request, room_id, user_id):
|
||||
async def on_PUT(
|
||||
self, request: SynapseRequest, room_id: str, user_id: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
|
||||
if not self._is_typing_writer:
|
||||
|
@ -897,7 +970,9 @@ class RoomAliasListServlet(RestServlet):
|
|||
self.auth = hs.get_auth()
|
||||
self.directory_handler = hs.get_directory_handler()
|
||||
|
||||
async def on_GET(self, request, room_id):
|
||||
async def on_GET(
|
||||
self, request: SynapseRequest, room_id: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
|
||||
alias_list = await self.directory_handler.get_aliases_for_room(
|
||||
|
@ -910,12 +985,12 @@ class RoomAliasListServlet(RestServlet):
|
|||
class SearchRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/search$", v1=True)
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.search_handler = hs.get_search_handler()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
async def on_POST(self, request):
|
||||
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
|
||||
content = parse_json_object_from_request(request)
|
||||
|
@ -929,19 +1004,24 @@ class SearchRestServlet(RestServlet):
|
|||
class JoinedRoomsRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/joined_rooms$", v1=True)
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.store = hs.get_datastore()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
async def on_GET(self, request):
|
||||
async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request, allow_guest=True)
|
||||
|
||||
room_ids = await self.store.get_rooms_for_user(requester.user.to_string())
|
||||
return 200, {"joined_rooms": list(room_ids)}
|
||||
|
||||
|
||||
def register_txn_path(servlet, regex_string, http_server, with_get=False):
|
||||
def register_txn_path(
|
||||
servlet: RestServlet,
|
||||
regex_string: str,
|
||||
http_server: HttpServer,
|
||||
with_get: bool = False,
|
||||
) -> None:
|
||||
"""Registers a transaction-based path.
|
||||
|
||||
This registers two paths:
|
||||
|
@ -949,28 +1029,37 @@ def register_txn_path(servlet, regex_string, http_server, with_get=False):
|
|||
POST regex_string
|
||||
|
||||
Args:
|
||||
regex_string (str): The regex string to register. Must NOT have a
|
||||
trailing $ as this string will be appended to.
|
||||
http_server : The http_server to register paths with.
|
||||
regex_string: The regex string to register. Must NOT have a
|
||||
trailing $ as this string will be appended to.
|
||||
http_server: The http_server to register paths with.
|
||||
with_get: True to also register respective GET paths for the PUTs.
|
||||
"""
|
||||
on_POST = getattr(servlet, "on_POST", None)
|
||||
on_PUT = getattr(servlet, "on_PUT", None)
|
||||
if on_POST is None or on_PUT is None:
|
||||
raise RuntimeError("on_POST and on_PUT must exist when using register_txn_path")
|
||||
http_server.register_paths(
|
||||
"POST",
|
||||
client_patterns(regex_string + "$", v1=True),
|
||||
servlet.on_POST,
|
||||
on_POST,
|
||||
servlet.__class__.__name__,
|
||||
)
|
||||
http_server.register_paths(
|
||||
"PUT",
|
||||
client_patterns(regex_string + "/(?P<txn_id>[^/]*)$", v1=True),
|
||||
servlet.on_PUT,
|
||||
on_PUT,
|
||||
servlet.__class__.__name__,
|
||||
)
|
||||
on_GET = getattr(servlet, "on_GET", None)
|
||||
if with_get:
|
||||
if on_GET is None:
|
||||
raise RuntimeError(
|
||||
"register_txn_path called with with_get = True, but no on_GET method exists"
|
||||
)
|
||||
http_server.register_paths(
|
||||
"GET",
|
||||
client_patterns(regex_string + "/(?P<txn_id>[^/]*)$", v1=True),
|
||||
servlet.on_GET,
|
||||
on_GET,
|
||||
servlet.__class__.__name__,
|
||||
)
|
||||
|
||||
|
@ -1120,7 +1209,9 @@ class RoomSummaryRestServlet(ResolveRoomIdMixin, RestServlet):
|
|||
)
|
||||
|
||||
|
||||
def register_servlets(hs: "HomeServer", http_server, is_worker=False):
|
||||
def register_servlets(
|
||||
hs: "HomeServer", http_server: HttpServer, is_worker: bool = False
|
||||
) -> None:
|
||||
RoomStateEventRestServlet(hs).register(http_server)
|
||||
RoomMemberListRestServlet(hs).register(http_server)
|
||||
JoinedRoomMemberListRestServlet(hs).register(http_server)
|
||||
|
@ -1148,5 +1239,5 @@ def register_servlets(hs: "HomeServer", http_server, is_worker=False):
|
|||
RoomForgetRestServlet(hs).register(http_server)
|
||||
|
||||
|
||||
def register_deprecated_servlets(hs, http_server):
|
||||
def register_deprecated_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||
RoomInitialSyncRestServlet(hs).register(http_server)
|
||||
|
|
135
synapse/rest/media/v1/oembed.py
Normal file
135
synapse/rest/media/v1/oembed.py
Normal file
|
@ -0,0 +1,135 @@
|
|||
# Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import attr
|
||||
|
||||
from synapse.http.client import SimpleHttpClient
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@attr.s(slots=True, auto_attribs=True)
|
||||
class OEmbedResult:
|
||||
# Either HTML content or URL must be provided.
|
||||
html: Optional[str]
|
||||
url: Optional[str]
|
||||
title: Optional[str]
|
||||
# Number of seconds to cache the content.
|
||||
cache_age: int
|
||||
|
||||
|
||||
class OEmbedError(Exception):
|
||||
"""An error occurred processing the oEmbed object."""
|
||||
|
||||
|
||||
class OEmbedProvider:
|
||||
"""
|
||||
A helper for accessing oEmbed content.
|
||||
|
||||
It can be used to check if a URL should be accessed via oEmbed and for
|
||||
requesting/parsing oEmbed content.
|
||||
"""
|
||||
|
||||
def __init__(self, hs: "HomeServer", client: SimpleHttpClient):
|
||||
self._oembed_patterns = {}
|
||||
for oembed_endpoint in hs.config.oembed.oembed_patterns:
|
||||
for pattern in oembed_endpoint.url_patterns:
|
||||
self._oembed_patterns[pattern] = oembed_endpoint.api_endpoint
|
||||
self._client = client
|
||||
|
||||
def get_oembed_url(self, url: str) -> Optional[str]:
|
||||
"""
|
||||
Check whether the URL should be downloaded as oEmbed content instead.
|
||||
|
||||
Args:
|
||||
url: The URL to check.
|
||||
|
||||
Returns:
|
||||
A URL to use instead or None if the original URL should be used.
|
||||
"""
|
||||
for url_pattern, endpoint in self._oembed_patterns.items():
|
||||
if url_pattern.fullmatch(url):
|
||||
return endpoint
|
||||
|
||||
# No match.
|
||||
return None
|
||||
|
||||
async def get_oembed_content(self, endpoint: str, url: str) -> OEmbedResult:
|
||||
"""
|
||||
Request content from an oEmbed endpoint.
|
||||
|
||||
Args:
|
||||
endpoint: The oEmbed API endpoint.
|
||||
url: The URL to pass to the API.
|
||||
|
||||
Returns:
|
||||
An object representing the metadata returned.
|
||||
|
||||
Raises:
|
||||
OEmbedError if fetching or parsing of the oEmbed information fails.
|
||||
"""
|
||||
try:
|
||||
logger.debug("Trying to get oEmbed content for url '%s'", url)
|
||||
result = await self._client.get_json(
|
||||
endpoint,
|
||||
# TODO Specify max height / width.
|
||||
# Note that only the JSON format is supported.
|
||||
args={"url": url},
|
||||
)
|
||||
|
||||
# Ensure there's a version of 1.0.
|
||||
if result.get("version") != "1.0":
|
||||
raise OEmbedError("Invalid version: %s" % (result.get("version"),))
|
||||
|
||||
oembed_type = result.get("type")
|
||||
|
||||
# Ensure the cache age is None or an int.
|
||||
cache_age = result.get("cache_age")
|
||||
if cache_age:
|
||||
cache_age = int(cache_age)
|
||||
|
||||
oembed_result = OEmbedResult(None, None, result.get("title"), cache_age)
|
||||
|
||||
# HTML content.
|
||||
if oembed_type == "rich":
|
||||
oembed_result.html = result.get("html")
|
||||
return oembed_result
|
||||
|
||||
if oembed_type == "photo":
|
||||
oembed_result.url = result.get("url")
|
||||
return oembed_result
|
||||
|
||||
# TODO Handle link and video types.
|
||||
|
||||
if "thumbnail_url" in result:
|
||||
oembed_result.url = result.get("thumbnail_url")
|
||||
return oembed_result
|
||||
|
||||
raise OEmbedError("Incompatible oEmbed information.")
|
||||
|
||||
except OEmbedError as e:
|
||||
# Trap OEmbedErrors first so we can directly re-raise them.
|
||||
logger.warning("Error parsing oEmbed metadata from %s: %r", url, e)
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
# Trap any exception and let the code follow as usual.
|
||||
# FIXME: pass through 404s and other error messages nicely
|
||||
logger.warning("Error downloading oEmbed metadata from %s: %r", url, e)
|
||||
raise OEmbedError() from e
|
|
@ -25,8 +25,6 @@ import traceback
|
|||
from typing import TYPE_CHECKING, Any, Dict, Generator, Iterable, Optional, Union
|
||||
from urllib import parse as urlparse
|
||||
|
||||
import attr
|
||||
|
||||
from twisted.internet.error import DNSLookupError
|
||||
from twisted.web.server import Request
|
||||
|
||||
|
@ -43,6 +41,7 @@ from synapse.logging.context import make_deferred_yieldable, run_in_background
|
|||
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||
from synapse.rest.media.v1._base import get_filename_from_headers
|
||||
from synapse.rest.media.v1.media_storage import MediaStorage
|
||||
from synapse.rest.media.v1.oembed import OEmbedError, OEmbedProvider
|
||||
from synapse.util import json_encoder
|
||||
from synapse.util.async_helpers import ObservableDeferred
|
||||
from synapse.util.caches.expiringcache import ExpiringCache
|
||||
|
@ -71,63 +70,6 @@ OG_TAG_VALUE_MAXLEN = 1000
|
|||
|
||||
ONE_HOUR = 60 * 60 * 1000
|
||||
|
||||
# A map of globs to API endpoints.
|
||||
_oembed_globs = {
|
||||
# Twitter.
|
||||
"https://publish.twitter.com/oembed": [
|
||||
"https://twitter.com/*/status/*",
|
||||
"https://*.twitter.com/*/status/*",
|
||||
"https://twitter.com/*/moments/*",
|
||||
"https://*.twitter.com/*/moments/*",
|
||||
# Include the HTTP versions too.
|
||||
"http://twitter.com/*/status/*",
|
||||
"http://*.twitter.com/*/status/*",
|
||||
"http://twitter.com/*/moments/*",
|
||||
"http://*.twitter.com/*/moments/*",
|
||||
],
|
||||
}
|
||||
# Convert the globs to regular expressions.
|
||||
_oembed_patterns = {}
|
||||
for endpoint, globs in _oembed_globs.items():
|
||||
for glob in globs:
|
||||
# Convert the glob into a sane regular expression to match against. The
|
||||
# rules followed will be slightly different for the domain portion vs.
|
||||
# the rest.
|
||||
#
|
||||
# 1. The scheme must be one of HTTP / HTTPS (and have no globs).
|
||||
# 2. The domain can have globs, but we limit it to characters that can
|
||||
# reasonably be a domain part.
|
||||
# TODO: This does not attempt to handle Unicode domain names.
|
||||
# 3. Other parts allow a glob to be any one, or more, characters.
|
||||
results = urlparse.urlparse(glob)
|
||||
|
||||
# Ensure the scheme does not have wildcards (and is a sane scheme).
|
||||
if results.scheme not in {"http", "https"}:
|
||||
raise ValueError("Insecure oEmbed glob scheme: %s" % (results.scheme,))
|
||||
|
||||
pattern = urlparse.urlunparse(
|
||||
[
|
||||
results.scheme,
|
||||
re.escape(results.netloc).replace("\\*", "[a-zA-Z0-9_-]+"),
|
||||
]
|
||||
+ [re.escape(part).replace("\\*", ".+") for part in results[2:]]
|
||||
)
|
||||
_oembed_patterns[re.compile(pattern)] = endpoint
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class OEmbedResult:
|
||||
# Either HTML content or URL must be provided.
|
||||
html = attr.ib(type=Optional[str])
|
||||
url = attr.ib(type=Optional[str])
|
||||
title = attr.ib(type=Optional[str])
|
||||
# Number of seconds to cache the content.
|
||||
cache_age = attr.ib(type=int)
|
||||
|
||||
|
||||
class OEmbedError(Exception):
|
||||
"""An error occurred processing the oEmbed object."""
|
||||
|
||||
|
||||
class PreviewUrlResource(DirectServeJsonResource):
|
||||
isLeaf = True
|
||||
|
@ -157,6 +99,8 @@ class PreviewUrlResource(DirectServeJsonResource):
|
|||
self.primary_base_path = media_repo.primary_base_path
|
||||
self.media_storage = media_storage
|
||||
|
||||
self._oembed = OEmbedProvider(hs, self.client)
|
||||
|
||||
# We run the background jobs if we're the instance specified (or no
|
||||
# instance is specified, where we assume there is only one instance
|
||||
# serving media).
|
||||
|
@ -367,87 +311,6 @@ class PreviewUrlResource(DirectServeJsonResource):
|
|||
|
||||
return jsonog.encode("utf8")
|
||||
|
||||
def _get_oembed_url(self, url: str) -> Optional[str]:
|
||||
"""
|
||||
Check whether the URL should be downloaded as oEmbed content instead.
|
||||
|
||||
Args:
|
||||
url: The URL to check.
|
||||
|
||||
Returns:
|
||||
A URL to use instead or None if the original URL should be used.
|
||||
"""
|
||||
for url_pattern, endpoint in _oembed_patterns.items():
|
||||
if url_pattern.fullmatch(url):
|
||||
return endpoint
|
||||
|
||||
# No match.
|
||||
return None
|
||||
|
||||
async def _get_oembed_content(self, endpoint: str, url: str) -> OEmbedResult:
|
||||
"""
|
||||
Request content from an oEmbed endpoint.
|
||||
|
||||
Args:
|
||||
endpoint: The oEmbed API endpoint.
|
||||
url: The URL to pass to the API.
|
||||
|
||||
Returns:
|
||||
An object representing the metadata returned.
|
||||
|
||||
Raises:
|
||||
OEmbedError if fetching or parsing of the oEmbed information fails.
|
||||
"""
|
||||
try:
|
||||
logger.debug("Trying to get oEmbed content for url '%s'", url)
|
||||
result = await self.client.get_json(
|
||||
endpoint,
|
||||
# TODO Specify max height / width.
|
||||
# Note that only the JSON format is supported.
|
||||
args={"url": url},
|
||||
)
|
||||
|
||||
# Ensure there's a version of 1.0.
|
||||
if result.get("version") != "1.0":
|
||||
raise OEmbedError("Invalid version: %s" % (result.get("version"),))
|
||||
|
||||
oembed_type = result.get("type")
|
||||
|
||||
# Ensure the cache age is None or an int.
|
||||
cache_age = result.get("cache_age")
|
||||
if cache_age:
|
||||
cache_age = int(cache_age)
|
||||
|
||||
oembed_result = OEmbedResult(None, None, result.get("title"), cache_age)
|
||||
|
||||
# HTML content.
|
||||
if oembed_type == "rich":
|
||||
oembed_result.html = result.get("html")
|
||||
return oembed_result
|
||||
|
||||
if oembed_type == "photo":
|
||||
oembed_result.url = result.get("url")
|
||||
return oembed_result
|
||||
|
||||
# TODO Handle link and video types.
|
||||
|
||||
if "thumbnail_url" in result:
|
||||
oembed_result.url = result.get("thumbnail_url")
|
||||
return oembed_result
|
||||
|
||||
raise OEmbedError("Incompatible oEmbed information.")
|
||||
|
||||
except OEmbedError as e:
|
||||
# Trap OEmbedErrors first so we can directly re-raise them.
|
||||
logger.warning("Error parsing oEmbed metadata from %s: %r", url, e)
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
# Trap any exception and let the code follow as usual.
|
||||
# FIXME: pass through 404s and other error messages nicely
|
||||
logger.warning("Error downloading oEmbed metadata from %s: %r", url, e)
|
||||
raise OEmbedError() from e
|
||||
|
||||
async def _download_url(self, url: str, user: str) -> Dict[str, Any]:
|
||||
# TODO: we should probably honour robots.txt... except in practice
|
||||
# we're most likely being explicitly triggered by a human rather than a
|
||||
|
@ -459,11 +322,11 @@ class PreviewUrlResource(DirectServeJsonResource):
|
|||
|
||||
# If this URL can be accessed via oEmbed, use that instead.
|
||||
url_to_download: Optional[str] = url
|
||||
oembed_url = self._get_oembed_url(url)
|
||||
oembed_url = self._oembed.get_oembed_url(url)
|
||||
if oembed_url:
|
||||
# The result might be a new URL to download, or it might be HTML content.
|
||||
try:
|
||||
oembed_result = await self._get_oembed_content(oembed_url, url)
|
||||
oembed_result = await self._oembed.get_oembed_content(oembed_url, url)
|
||||
if oembed_result.url:
|
||||
url_to_download = oembed_result.url
|
||||
elif oembed_result.html:
|
||||
|
|
|
@ -76,6 +76,7 @@ from synapse.handlers.e2e_room_keys import E2eRoomKeysHandler
|
|||
from synapse.handlers.event_auth import EventAuthHandler
|
||||
from synapse.handlers.events import EventHandler, EventStreamHandler
|
||||
from synapse.handlers.federation import FederationHandler
|
||||
from synapse.handlers.federation_event import FederationEventHandler
|
||||
from synapse.handlers.groups_local import GroupsLocalHandler, GroupsLocalWorkerHandler
|
||||
from synapse.handlers.identity import IdentityHandler
|
||||
from synapse.handlers.initial_sync import InitialSyncHandler
|
||||
|
@ -546,6 +547,10 @@ class HomeServer(metaclass=abc.ABCMeta):
|
|||
def get_federation_handler(self) -> FederationHandler:
|
||||
return FederationHandler(self)
|
||||
|
||||
@cache_in_self
|
||||
def get_federation_event_handler(self) -> FederationEventHandler:
|
||||
return FederationEventHandler(self)
|
||||
|
||||
@cache_in_self
|
||||
def get_identity_handler(self) -> IdentityHandler:
|
||||
return IdentityHandler(self)
|
||||
|
|
|
@ -12,26 +12,23 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import logging
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from synapse.api.constants import EventTypes, Membership, RoomCreationPreset
|
||||
from synapse.events import EventBase
|
||||
from synapse.types import UserID, create_requester
|
||||
from synapse.util.caches.descriptors import cached
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SERVER_NOTICE_ROOM_TAG = "m.server_notice"
|
||||
|
||||
|
||||
class ServerNoticesManager:
|
||||
def __init__(self, hs):
|
||||
"""
|
||||
|
||||
Args:
|
||||
hs (synapse.server.HomeServer):
|
||||
"""
|
||||
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
self._store = hs.get_datastore()
|
||||
self._config = hs.config
|
||||
self._account_data_handler = hs.get_account_data_handler()
|
||||
|
@ -58,6 +55,7 @@ class ServerNoticesManager:
|
|||
event_content: dict,
|
||||
type: str = EventTypes.Message,
|
||||
state_key: Optional[str] = None,
|
||||
txn_id: Optional[str] = None,
|
||||
) -> EventBase:
|
||||
"""Send a notice to the given user
|
||||
|
||||
|
@ -68,6 +66,7 @@ class ServerNoticesManager:
|
|||
event_content: content of event to send
|
||||
type: type of event
|
||||
is_state_event: Is the event a state event
|
||||
txn_id: The transaction ID.
|
||||
"""
|
||||
room_id = await self.get_or_create_notice_room_for_user(user_id)
|
||||
await self.maybe_invite_user_to_room(user_id, room_id)
|
||||
|
@ -90,7 +89,7 @@ class ServerNoticesManager:
|
|||
event_dict["state_key"] = state_key
|
||||
|
||||
event, _ = await self._event_creation_handler.create_and_send_nonmember_event(
|
||||
requester, event_dict, ratelimit=False
|
||||
requester, event_dict, ratelimit=False, txn_id=txn_id
|
||||
)
|
||||
return event
|
||||
|
||||
|
|
|
@ -520,16 +520,26 @@ class EventsWorkerStore(SQLBaseStore):
|
|||
# We now look up if we're already fetching some of the events in the DB,
|
||||
# if so we wait for those lookups to finish instead of pulling the same
|
||||
# events out of the DB multiple times.
|
||||
already_fetching: Dict[str, defer.Deferred] = {}
|
||||
#
|
||||
# Note: we might get the same `ObservableDeferred` back for multiple
|
||||
# events we're already fetching, so we deduplicate the deferreds to
|
||||
# avoid extraneous work (if we don't do this we can end up in a n^2 mode
|
||||
# when we wait on the same Deferred N times, then try and merge the
|
||||
# same dict into itself N times).
|
||||
already_fetching_ids: Set[str] = set()
|
||||
already_fetching_deferreds: Set[
|
||||
ObservableDeferred[Dict[str, _EventCacheEntry]]
|
||||
] = set()
|
||||
|
||||
for event_id in missing_events_ids:
|
||||
deferred = self._current_event_fetches.get(event_id)
|
||||
if deferred is not None:
|
||||
# We're already pulling the event out of the DB. Add the deferred
|
||||
# to the collection of deferreds to wait on.
|
||||
already_fetching[event_id] = deferred.observe()
|
||||
already_fetching_ids.add(event_id)
|
||||
already_fetching_deferreds.add(deferred)
|
||||
|
||||
missing_events_ids.difference_update(already_fetching)
|
||||
missing_events_ids.difference_update(already_fetching_ids)
|
||||
|
||||
if missing_events_ids:
|
||||
log_ctx = current_context()
|
||||
|
@ -569,18 +579,25 @@ class EventsWorkerStore(SQLBaseStore):
|
|||
with PreserveLoggingContext():
|
||||
fetching_deferred.callback(missing_events)
|
||||
|
||||
if already_fetching:
|
||||
if already_fetching_deferreds:
|
||||
# Wait for the other event requests to finish and add their results
|
||||
# to ours.
|
||||
results = await make_deferred_yieldable(
|
||||
defer.gatherResults(
|
||||
already_fetching.values(),
|
||||
(d.observe() for d in already_fetching_deferreds),
|
||||
consumeErrors=True,
|
||||
)
|
||||
).addErrback(unwrapFirstError)
|
||||
|
||||
for result in results:
|
||||
event_entry_map.update(result)
|
||||
# We filter out events that we haven't asked for as we might get
|
||||
# a *lot* of superfluous events back, and there is no point
|
||||
# going through and inserting them all (which can take time).
|
||||
event_entry_map.update(
|
||||
(event_id, entry)
|
||||
for event_id, entry in result.items()
|
||||
if event_id in already_fetching_ids
|
||||
)
|
||||
|
||||
if not allow_rejected:
|
||||
event_entry_map = {
|
||||
|
|
|
@ -295,6 +295,7 @@ class PurgeEventsStore(StateGroupWorkerStore, CacheInvalidationWorkerStore):
|
|||
self._invalidate_cache_and_stream(
|
||||
txn, self.have_seen_event, (room_id, event_id)
|
||||
)
|
||||
self._invalidate_get_event_cache(event_id)
|
||||
|
||||
logger.info("[purge] done")
|
||||
|
||||
|
|
|
@ -48,6 +48,11 @@ class PusherWorkerStore(SQLBaseStore):
|
|||
self._remove_stale_pushers,
|
||||
)
|
||||
|
||||
self.db_pool.updates.register_background_update_handler(
|
||||
"remove_deleted_email_pushers",
|
||||
self._remove_deleted_email_pushers,
|
||||
)
|
||||
|
||||
def _decode_pushers_rows(self, rows: Iterable[dict]) -> Iterator[PusherConfig]:
|
||||
"""JSON-decode the data in the rows returned from the `pushers` table
|
||||
|
||||
|
@ -388,6 +393,73 @@ class PusherWorkerStore(SQLBaseStore):
|
|||
|
||||
return number_deleted
|
||||
|
||||
async def _remove_deleted_email_pushers(
|
||||
self, progress: dict, batch_size: int
|
||||
) -> int:
|
||||
"""A background update that deletes all pushers for deleted email addresses.
|
||||
|
||||
In previous versions of synapse, when users deleted their email address, it didn't
|
||||
also delete all the pushers for that email address. This background update removes
|
||||
those to prevent unwanted emails. This should only need to be run once (when users
|
||||
upgrade to v1.42.0
|
||||
|
||||
Args:
|
||||
progress: dict used to store progress of this background update
|
||||
batch_size: the maximum number of rows to retrieve in a single select query
|
||||
|
||||
Returns:
|
||||
The number of deleted rows
|
||||
"""
|
||||
|
||||
last_pusher = progress.get("last_pusher", 0)
|
||||
|
||||
def _delete_pushers(txn) -> int:
|
||||
|
||||
sql = """
|
||||
SELECT p.id, p.user_name, p.app_id, p.pushkey
|
||||
FROM pushers AS p
|
||||
LEFT JOIN user_threepids AS t
|
||||
ON t.user_id = p.user_name
|
||||
AND t.medium = 'email'
|
||||
AND t.address = p.pushkey
|
||||
WHERE t.user_id is NULL
|
||||
AND p.app_id = 'm.email'
|
||||
AND p.id > ?
|
||||
ORDER BY p.id ASC
|
||||
LIMIT ?
|
||||
"""
|
||||
|
||||
txn.execute(sql, (last_pusher, batch_size))
|
||||
|
||||
last = None
|
||||
num_deleted = 0
|
||||
for row in txn:
|
||||
last = row[0]
|
||||
num_deleted += 1
|
||||
self.db_pool.simple_delete_txn(
|
||||
txn,
|
||||
"pushers",
|
||||
{"user_name": row[1], "app_id": row[2], "pushkey": row[3]},
|
||||
)
|
||||
|
||||
if last is not None:
|
||||
self.db_pool.updates._background_update_progress_txn(
|
||||
txn, "remove_deleted_email_pushers", {"last_pusher": last}
|
||||
)
|
||||
|
||||
return num_deleted
|
||||
|
||||
number_deleted = await self.db_pool.runInteraction(
|
||||
"_remove_deleted_email_pushers", _delete_pushers
|
||||
)
|
||||
|
||||
if number_deleted < batch_size:
|
||||
await self.db_pool.updates._end_background_update(
|
||||
"remove_deleted_email_pushers"
|
||||
)
|
||||
|
||||
return number_deleted
|
||||
|
||||
|
||||
class PusherStore(PusherWorkerStore):
|
||||
def get_pushers_stream_token(self) -> int:
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/* Copyright 2021 The Matrix.org Foundation C.I.C
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
-- We may not have deleted all pushers for emails that are no longer linked
|
||||
-- to an account, so we set up a background job to delete them.
|
||||
INSERT INTO background_updates (ordering, update_name, progress_json) VALUES
|
||||
(6302, 'remove_deleted_email_pushers', '{}');
|
|
@ -208,7 +208,7 @@ class FederationKnockingTestCase(
|
|||
async def _check_event_auth(origin, event, context, *args, **kwargs):
|
||||
return context
|
||||
|
||||
homeserver.get_federation_handler()._check_event_auth = _check_event_auth
|
||||
homeserver.get_federation_event_handler()._check_event_auth = _check_event_auth
|
||||
|
||||
return super().prepare(reactor, clock, homeserver)
|
||||
|
||||
|
|
|
@ -130,7 +130,9 @@ class FederationTestCase(unittest.HomeserverTestCase):
|
|||
)
|
||||
|
||||
with LoggingContext("send_rejected"):
|
||||
d = run_in_background(self.handler.on_receive_pdu, OTHER_SERVER, ev)
|
||||
d = run_in_background(
|
||||
self.hs.get_federation_event_handler().on_receive_pdu, OTHER_SERVER, ev
|
||||
)
|
||||
self.get_success(d)
|
||||
|
||||
# that should have been rejected
|
||||
|
@ -182,7 +184,9 @@ class FederationTestCase(unittest.HomeserverTestCase):
|
|||
)
|
||||
|
||||
with LoggingContext("send_rejected"):
|
||||
d = run_in_background(self.handler.on_receive_pdu, OTHER_SERVER, ev)
|
||||
d = run_in_background(
|
||||
self.hs.get_federation_event_handler().on_receive_pdu, OTHER_SERVER, ev
|
||||
)
|
||||
self.get_success(d)
|
||||
|
||||
# that should have been rejected
|
||||
|
@ -311,7 +315,9 @@ class FederationTestCase(unittest.HomeserverTestCase):
|
|||
with LoggingContext("receive_pdu"):
|
||||
# Fake the OTHER_SERVER federating the message event over to our local homeserver
|
||||
d = run_in_background(
|
||||
self.handler.on_receive_pdu, OTHER_SERVER, message_event
|
||||
self.hs.get_federation_event_handler().on_receive_pdu,
|
||||
OTHER_SERVER,
|
||||
message_event,
|
||||
)
|
||||
self.get_success(d)
|
||||
|
||||
|
@ -382,7 +388,9 @@ class FederationTestCase(unittest.HomeserverTestCase):
|
|||
join_event.signatures[other_server] = {"x": "y"}
|
||||
with LoggingContext("send_join"):
|
||||
d = run_in_background(
|
||||
self.handler.on_send_membership_event, other_server, join_event
|
||||
self.hs.get_federation_event_handler().on_send_membership_event,
|
||||
other_server,
|
||||
join_event,
|
||||
)
|
||||
self.get_success(d)
|
||||
|
||||
|
|
|
@ -885,7 +885,7 @@ class PresenceJoinTestCase(unittest.HomeserverTestCase):
|
|||
def prepare(self, reactor, clock, hs):
|
||||
self.federation_sender = hs.get_federation_sender()
|
||||
self.event_builder_factory = hs.get_event_builder_factory()
|
||||
self.federation_handler = hs.get_federation_handler()
|
||||
self.federation_event_handler = hs.get_federation_event_handler()
|
||||
self.presence_handler = hs.get_presence_handler()
|
||||
|
||||
# self.event_builder_for_2 = EventBuilderFactory(hs)
|
||||
|
@ -1026,7 +1026,7 @@ class PresenceJoinTestCase(unittest.HomeserverTestCase):
|
|||
builder.build(prev_event_ids=prev_event_ids, auth_event_ids=None)
|
||||
)
|
||||
|
||||
self.get_success(self.federation_handler.on_receive_pdu(hostname, event))
|
||||
self.get_success(self.federation_event_handler.on_receive_pdu(hostname, event))
|
||||
|
||||
# Check that it was successfully persisted.
|
||||
self.get_success(self.store.get_event(event.event_id))
|
||||
|
|
112
tests/handlers/test_send_email.py
Normal file
112
tests/handlers/test_send_email.py
Normal file
|
@ -0,0 +1,112 @@
|
|||
# Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
from typing import List, Tuple
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.internet.address import IPv4Address
|
||||
from twisted.internet.defer import ensureDeferred
|
||||
from twisted.mail import interfaces, smtp
|
||||
|
||||
from tests.server import FakeTransport
|
||||
from tests.unittest import HomeserverTestCase
|
||||
|
||||
|
||||
@implementer(interfaces.IMessageDelivery)
|
||||
class _DummyMessageDelivery:
|
||||
def __init__(self):
|
||||
# (recipient, message) tuples
|
||||
self.messages: List[Tuple[smtp.Address, bytes]] = []
|
||||
|
||||
def receivedHeader(self, helo, origin, recipients):
|
||||
return None
|
||||
|
||||
def validateFrom(self, helo, origin):
|
||||
return origin
|
||||
|
||||
def record_message(self, recipient: smtp.Address, message: bytes):
|
||||
self.messages.append((recipient, message))
|
||||
|
||||
def validateTo(self, user: smtp.User):
|
||||
return lambda: _DummyMessage(self, user)
|
||||
|
||||
|
||||
@implementer(interfaces.IMessageSMTP)
|
||||
class _DummyMessage:
|
||||
"""IMessageSMTP implementation which saves the message delivered to it
|
||||
to the _DummyMessageDelivery object.
|
||||
"""
|
||||
|
||||
def __init__(self, delivery: _DummyMessageDelivery, user: smtp.User):
|
||||
self._delivery = delivery
|
||||
self._user = user
|
||||
self._buffer: List[bytes] = []
|
||||
|
||||
def lineReceived(self, line):
|
||||
self._buffer.append(line)
|
||||
|
||||
def eomReceived(self):
|
||||
message = b"\n".join(self._buffer) + b"\n"
|
||||
self._delivery.record_message(self._user.dest, message)
|
||||
return defer.succeed(b"saved")
|
||||
|
||||
def connectionLost(self):
|
||||
pass
|
||||
|
||||
|
||||
class SendEmailHandlerTestCase(HomeserverTestCase):
|
||||
def test_send_email(self):
|
||||
"""Happy-path test that we can send email to a non-TLS server."""
|
||||
h = self.hs.get_send_email_handler()
|
||||
d = ensureDeferred(
|
||||
h.send_email(
|
||||
"foo@bar.com", "test subject", "Tests", "HTML content", "Text content"
|
||||
)
|
||||
)
|
||||
# there should be an attempt to connect to localhost:25
|
||||
self.assertEqual(len(self.reactor.tcpClients), 1)
|
||||
(host, port, client_factory, _timeout, _bindAddress) = self.reactor.tcpClients[
|
||||
0
|
||||
]
|
||||
self.assertEqual(host, "localhost")
|
||||
self.assertEqual(port, 25)
|
||||
|
||||
# wire it up to an SMTP server
|
||||
message_delivery = _DummyMessageDelivery()
|
||||
server_protocol = smtp.ESMTP()
|
||||
server_protocol.delivery = message_delivery
|
||||
# make sure that the server uses the test reactor to set timeouts
|
||||
server_protocol.callLater = self.reactor.callLater # type: ignore[assignment]
|
||||
|
||||
client_protocol = client_factory.buildProtocol(None)
|
||||
client_protocol.makeConnection(FakeTransport(server_protocol, self.reactor))
|
||||
server_protocol.makeConnection(
|
||||
FakeTransport(
|
||||
client_protocol,
|
||||
self.reactor,
|
||||
peer_address=IPv4Address("TCP", "127.0.0.1", 1234),
|
||||
)
|
||||
)
|
||||
|
||||
# the message should now get delivered
|
||||
self.get_success(d, by=0.1)
|
||||
|
||||
# check it arrived
|
||||
self.assertEqual(len(message_delivery.messages), 1)
|
||||
user, msg = message_delivery.messages.pop()
|
||||
self.assertEqual(str(user), "foo@bar.com")
|
||||
self.assertIn(b"Subject: test subject", msg)
|
|
@ -125,6 +125,8 @@ class EmailPusherTests(HomeserverTestCase):
|
|||
)
|
||||
)
|
||||
|
||||
self.auth_handler = hs.get_auth_handler()
|
||||
|
||||
def test_need_validated_email(self):
|
||||
"""Test that we can only add an email pusher if the user has validated
|
||||
their email.
|
||||
|
@ -305,6 +307,43 @@ class EmailPusherTests(HomeserverTestCase):
|
|||
# We should get emailed about that message
|
||||
self._check_for_mail()
|
||||
|
||||
def test_no_email_sent_after_removed(self):
|
||||
# Create a simple room with two users
|
||||
room = self.helper.create_room_as(self.user_id, tok=self.access_token)
|
||||
self.helper.invite(
|
||||
room=room,
|
||||
src=self.user_id,
|
||||
tok=self.access_token,
|
||||
targ=self.others[0].id,
|
||||
)
|
||||
self.helper.join(
|
||||
room=room,
|
||||
user=self.others[0].id,
|
||||
tok=self.others[0].token,
|
||||
)
|
||||
|
||||
# The other user sends a single message.
|
||||
self.helper.send(room, body="Hi!", tok=self.others[0].token)
|
||||
|
||||
# We should get emailed about that message
|
||||
self._check_for_mail()
|
||||
|
||||
# disassociate the user's email address
|
||||
self.get_success(
|
||||
self.auth_handler.delete_threepid(
|
||||
user_id=self.user_id,
|
||||
medium="email",
|
||||
address="a@example.com",
|
||||
)
|
||||
)
|
||||
|
||||
# check that the pusher for that email address has been deleted
|
||||
pushers = self.get_success(
|
||||
self.hs.get_datastore().get_pushers_by({"user_name": self.user_id})
|
||||
)
|
||||
pushers = list(pushers)
|
||||
self.assertEqual(len(pushers), 0)
|
||||
|
||||
def _check_for_mail(self):
|
||||
"""Check that the user receives an email notification"""
|
||||
|
||||
|
|
|
@ -205,7 +205,7 @@ class FederationSenderTestCase(BaseMultiWorkerStreamTestCase):
|
|||
def create_room_with_remote_server(self, user, token, remote_server="other_server"):
|
||||
room = self.helper.create_room_as(user, tok=token)
|
||||
store = self.hs.get_datastore()
|
||||
federation = self.hs.get_federation_handler()
|
||||
federation = self.hs.get_federation_event_handler()
|
||||
|
||||
prev_event_ids = self.get_success(store.get_latest_event_ids_in_room(room))
|
||||
room_version = self.get_success(store.get_room_version(room))
|
||||
|
|
450
tests/rest/admin/test_server_notice.py
Normal file
450
tests/rest/admin/test_server_notice.py
Normal file
|
@ -0,0 +1,450 @@
|
|||
# Copyright 2021 Dirk Klimpel
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from typing import List
|
||||
|
||||
import synapse.rest.admin
|
||||
from synapse.api.errors import Codes
|
||||
from synapse.rest.client import login, room, sync
|
||||
from synapse.storage.roommember import RoomsForUser
|
||||
from synapse.types import JsonDict
|
||||
|
||||
from tests import unittest
|
||||
from tests.unittest import override_config
|
||||
|
||||
|
||||
class ServerNoticeTestCase(unittest.HomeserverTestCase):
|
||||
|
||||
servlets = [
|
||||
synapse.rest.admin.register_servlets,
|
||||
login.register_servlets,
|
||||
room.register_servlets,
|
||||
sync.register_servlets,
|
||||
]
|
||||
|
||||
def prepare(self, reactor, clock, hs):
|
||||
self.store = hs.get_datastore()
|
||||
self.room_shutdown_handler = hs.get_room_shutdown_handler()
|
||||
self.pagination_handler = hs.get_pagination_handler()
|
||||
self.server_notices_manager = self.hs.get_server_notices_manager()
|
||||
|
||||
# Create user
|
||||
self.admin_user = self.register_user("admin", "pass", admin=True)
|
||||
self.admin_user_tok = self.login("admin", "pass")
|
||||
|
||||
self.other_user = self.register_user("user", "pass")
|
||||
self.other_user_token = self.login("user", "pass")
|
||||
|
||||
self.url = "/_synapse/admin/v1/send_server_notice"
|
||||
|
||||
def test_no_auth(self):
|
||||
"""Try to send a server notice without authentication."""
|
||||
channel = self.make_request("POST", self.url)
|
||||
|
||||
self.assertEqual(401, int(channel.result["code"]), msg=channel.result["body"])
|
||||
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"])
|
||||
|
||||
def test_requester_is_no_admin(self):
|
||||
"""If the user is not a server admin, an error is returned."""
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
self.url,
|
||||
access_token=self.other_user_token,
|
||||
)
|
||||
|
||||
self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"])
|
||||
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
|
||||
|
||||
@override_config({"server_notices": {"system_mxid_localpart": "notices"}})
|
||||
def test_user_does_not_exist(self):
|
||||
"""Tests that a lookup for a user that does not exist returns a 404"""
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
self.url,
|
||||
access_token=self.admin_user_tok,
|
||||
content={"user_id": "@unknown_person:test", "content": ""},
|
||||
)
|
||||
|
||||
self.assertEqual(404, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"])
|
||||
|
||||
@override_config({"server_notices": {"system_mxid_localpart": "notices"}})
|
||||
def test_user_is_not_local(self):
|
||||
"""
|
||||
Tests that a lookup for a user that is not a local returns a 400
|
||||
"""
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
self.url,
|
||||
access_token=self.admin_user_tok,
|
||||
content={
|
||||
"user_id": "@unknown_person:unknown_domain",
|
||||
"content": "",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(400, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(
|
||||
"Server notices can only be sent to local users", channel.json_body["error"]
|
||||
)
|
||||
|
||||
@override_config({"server_notices": {"system_mxid_localpart": "notices"}})
|
||||
def test_invalid_parameter(self):
|
||||
"""If parameters are invalid, an error is returned."""
|
||||
|
||||
# no content, no user
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
self.url,
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
|
||||
self.assertEqual(400, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(Codes.NOT_JSON, channel.json_body["errcode"])
|
||||
|
||||
# no content
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
self.url,
|
||||
access_token=self.admin_user_tok,
|
||||
content={"user_id": self.other_user},
|
||||
)
|
||||
|
||||
self.assertEqual(400, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(Codes.MISSING_PARAM, channel.json_body["errcode"])
|
||||
|
||||
# no body
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
self.url,
|
||||
access_token=self.admin_user_tok,
|
||||
content={"user_id": self.other_user, "content": ""},
|
||||
)
|
||||
|
||||
self.assertEqual(400, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"])
|
||||
self.assertEqual("'body' not in content", channel.json_body["error"])
|
||||
|
||||
# no msgtype
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
self.url,
|
||||
access_token=self.admin_user_tok,
|
||||
content={"user_id": self.other_user, "content": {"body": ""}},
|
||||
)
|
||||
|
||||
self.assertEqual(400, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"])
|
||||
self.assertEqual("'msgtype' not in content", channel.json_body["error"])
|
||||
|
||||
def test_server_notice_disabled(self):
|
||||
"""Tests that server returns error if server notice is disabled"""
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
self.url,
|
||||
access_token=self.admin_user_tok,
|
||||
content={
|
||||
"user_id": self.other_user,
|
||||
"content": "",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(400, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"])
|
||||
self.assertEqual(
|
||||
"Server notices are not enabled on this server", channel.json_body["error"]
|
||||
)
|
||||
|
||||
@override_config({"server_notices": {"system_mxid_localpart": "notices"}})
|
||||
def test_send_server_notice(self):
|
||||
"""
|
||||
Tests that sending two server notices is successfully,
|
||||
the server uses the same room and do not send messages twice.
|
||||
"""
|
||||
# user has no room memberships
|
||||
self._check_invite_and_join_status(self.other_user, 0, 0)
|
||||
|
||||
# send first message
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
self.url,
|
||||
access_token=self.admin_user_tok,
|
||||
content={
|
||||
"user_id": self.other_user,
|
||||
"content": {"msgtype": "m.text", "body": "test msg one"},
|
||||
},
|
||||
)
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
|
||||
# user has one invite
|
||||
invited_rooms = self._check_invite_and_join_status(self.other_user, 1, 0)
|
||||
room_id = invited_rooms[0].room_id
|
||||
|
||||
# user joins the room and is member now
|
||||
self.helper.join(room=room_id, user=self.other_user, tok=self.other_user_token)
|
||||
self._check_invite_and_join_status(self.other_user, 0, 1)
|
||||
|
||||
# get messages
|
||||
messages = self._sync_and_get_messages(room_id, self.other_user_token)
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertEqual(messages[0]["content"]["body"], "test msg one")
|
||||
self.assertEqual(messages[0]["sender"], "@notices:test")
|
||||
|
||||
# invalidate cache of server notices room_ids
|
||||
self.get_success(
|
||||
self.server_notices_manager.get_or_create_notice_room_for_user.invalidate_all()
|
||||
)
|
||||
|
||||
# send second message
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
self.url,
|
||||
access_token=self.admin_user_tok,
|
||||
content={
|
||||
"user_id": self.other_user,
|
||||
"content": {"msgtype": "m.text", "body": "test msg two"},
|
||||
},
|
||||
)
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
|
||||
# user has no new invites or memberships
|
||||
self._check_invite_and_join_status(self.other_user, 0, 1)
|
||||
|
||||
# get messages
|
||||
messages = self._sync_and_get_messages(room_id, self.other_user_token)
|
||||
|
||||
self.assertEqual(len(messages), 2)
|
||||
self.assertEqual(messages[0]["content"]["body"], "test msg one")
|
||||
self.assertEqual(messages[0]["sender"], "@notices:test")
|
||||
self.assertEqual(messages[1]["content"]["body"], "test msg two")
|
||||
self.assertEqual(messages[1]["sender"], "@notices:test")
|
||||
|
||||
@override_config({"server_notices": {"system_mxid_localpart": "notices"}})
|
||||
def test_send_server_notice_leave_room(self):
|
||||
"""
|
||||
Tests that sending a server notices is successfully.
|
||||
The user leaves the room and the second message appears
|
||||
in a new room.
|
||||
"""
|
||||
# user has no room memberships
|
||||
self._check_invite_and_join_status(self.other_user, 0, 0)
|
||||
|
||||
# send first message
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
self.url,
|
||||
access_token=self.admin_user_tok,
|
||||
content={
|
||||
"user_id": self.other_user,
|
||||
"content": {"msgtype": "m.text", "body": "test msg one"},
|
||||
},
|
||||
)
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
|
||||
# user has one invite
|
||||
invited_rooms = self._check_invite_and_join_status(self.other_user, 1, 0)
|
||||
first_room_id = invited_rooms[0].room_id
|
||||
|
||||
# user joins the room and is member now
|
||||
self.helper.join(
|
||||
room=first_room_id, user=self.other_user, tok=self.other_user_token
|
||||
)
|
||||
self._check_invite_and_join_status(self.other_user, 0, 1)
|
||||
|
||||
# get messages
|
||||
messages = self._sync_and_get_messages(first_room_id, self.other_user_token)
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertEqual(messages[0]["content"]["body"], "test msg one")
|
||||
self.assertEqual(messages[0]["sender"], "@notices:test")
|
||||
|
||||
# user leaves the romm
|
||||
self.helper.leave(
|
||||
room=first_room_id, user=self.other_user, tok=self.other_user_token
|
||||
)
|
||||
|
||||
# user is not member anymore
|
||||
self._check_invite_and_join_status(self.other_user, 0, 0)
|
||||
|
||||
# invalidate cache of server notices room_ids
|
||||
# if server tries to send to a cached room_id the user gets the message
|
||||
# in old room
|
||||
self.get_success(
|
||||
self.server_notices_manager.get_or_create_notice_room_for_user.invalidate_all()
|
||||
)
|
||||
|
||||
# send second message
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
self.url,
|
||||
access_token=self.admin_user_tok,
|
||||
content={
|
||||
"user_id": self.other_user,
|
||||
"content": {"msgtype": "m.text", "body": "test msg two"},
|
||||
},
|
||||
)
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
|
||||
# user has one invite
|
||||
invited_rooms = self._check_invite_and_join_status(self.other_user, 1, 0)
|
||||
second_room_id = invited_rooms[0].room_id
|
||||
|
||||
# user joins the room and is member now
|
||||
self.helper.join(
|
||||
room=second_room_id, user=self.other_user, tok=self.other_user_token
|
||||
)
|
||||
self._check_invite_and_join_status(self.other_user, 0, 1)
|
||||
|
||||
# get messages
|
||||
messages = self._sync_and_get_messages(second_room_id, self.other_user_token)
|
||||
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertEqual(messages[0]["content"]["body"], "test msg two")
|
||||
self.assertEqual(messages[0]["sender"], "@notices:test")
|
||||
# room has the same id
|
||||
self.assertNotEqual(first_room_id, second_room_id)
|
||||
|
||||
@override_config({"server_notices": {"system_mxid_localpart": "notices"}})
|
||||
def test_send_server_notice_delete_room(self):
|
||||
"""
|
||||
Tests that the user get server notice in a new room
|
||||
after the first server notice room was deleted.
|
||||
"""
|
||||
# user has no room memberships
|
||||
self._check_invite_and_join_status(self.other_user, 0, 0)
|
||||
|
||||
# send first message
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
self.url,
|
||||
access_token=self.admin_user_tok,
|
||||
content={
|
||||
"user_id": self.other_user,
|
||||
"content": {"msgtype": "m.text", "body": "test msg one"},
|
||||
},
|
||||
)
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
|
||||
# user has one invite
|
||||
invited_rooms = self._check_invite_and_join_status(self.other_user, 1, 0)
|
||||
first_room_id = invited_rooms[0].room_id
|
||||
|
||||
# user joins the room and is member now
|
||||
self.helper.join(
|
||||
room=first_room_id, user=self.other_user, tok=self.other_user_token
|
||||
)
|
||||
self._check_invite_and_join_status(self.other_user, 0, 1)
|
||||
|
||||
# get messages
|
||||
messages = self._sync_and_get_messages(first_room_id, self.other_user_token)
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertEqual(messages[0]["content"]["body"], "test msg one")
|
||||
self.assertEqual(messages[0]["sender"], "@notices:test")
|
||||
|
||||
# shut down and purge room
|
||||
self.get_success(
|
||||
self.room_shutdown_handler.shutdown_room(first_room_id, self.admin_user)
|
||||
)
|
||||
self.get_success(self.pagination_handler.purge_room(first_room_id))
|
||||
|
||||
# user is not member anymore
|
||||
self._check_invite_and_join_status(self.other_user, 0, 0)
|
||||
|
||||
# It doesn't really matter what API we use here, we just want to assert
|
||||
# that the room doesn't exist.
|
||||
summary = self.get_success(self.store.get_room_summary(first_room_id))
|
||||
# The summary should be empty since the room doesn't exist.
|
||||
self.assertEqual(summary, {})
|
||||
|
||||
# invalidate cache of server notices room_ids
|
||||
# if server tries to send to a cached room_id it gives an error
|
||||
self.get_success(
|
||||
self.server_notices_manager.get_or_create_notice_room_for_user.invalidate_all()
|
||||
)
|
||||
|
||||
# send second message
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
self.url,
|
||||
access_token=self.admin_user_tok,
|
||||
content={
|
||||
"user_id": self.other_user,
|
||||
"content": {"msgtype": "m.text", "body": "test msg two"},
|
||||
},
|
||||
)
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
|
||||
# user has one invite
|
||||
invited_rooms = self._check_invite_and_join_status(self.other_user, 1, 0)
|
||||
second_room_id = invited_rooms[0].room_id
|
||||
|
||||
# user joins the room and is member now
|
||||
self.helper.join(
|
||||
room=second_room_id, user=self.other_user, tok=self.other_user_token
|
||||
)
|
||||
self._check_invite_and_join_status(self.other_user, 0, 1)
|
||||
|
||||
# get message
|
||||
messages = self._sync_and_get_messages(second_room_id, self.other_user_token)
|
||||
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertEqual(messages[0]["content"]["body"], "test msg two")
|
||||
self.assertEqual(messages[0]["sender"], "@notices:test")
|
||||
# second room has new ID
|
||||
self.assertNotEqual(first_room_id, second_room_id)
|
||||
|
||||
def _check_invite_and_join_status(
|
||||
self, user_id: str, expected_invites: int, expected_memberships: int
|
||||
) -> RoomsForUser:
|
||||
"""Check invite and room membership status of a user.
|
||||
|
||||
Args
|
||||
user_id: user to check
|
||||
expected_invites: number of expected invites of this user
|
||||
expected_memberships: number of expected room memberships of this user
|
||||
Returns
|
||||
room_ids from the rooms that the user is invited
|
||||
"""
|
||||
|
||||
invited_rooms = self.get_success(
|
||||
self.store.get_invited_rooms_for_local_user(user_id)
|
||||
)
|
||||
self.assertEqual(expected_invites, len(invited_rooms))
|
||||
|
||||
room_ids = self.get_success(self.store.get_rooms_for_user(user_id))
|
||||
self.assertEqual(expected_memberships, len(room_ids))
|
||||
|
||||
return invited_rooms
|
||||
|
||||
def _sync_and_get_messages(self, room_id: str, token: str) -> List[JsonDict]:
|
||||
"""
|
||||
Do a sync and get messages of a room.
|
||||
|
||||
Args
|
||||
room_id: room that contains the messages
|
||||
token: access token of user
|
||||
|
||||
Returns
|
||||
list of messages contained in the room
|
||||
"""
|
||||
channel = self.make_request(
|
||||
"GET", "/_matrix/client/r0/sync", access_token=token
|
||||
)
|
||||
self.assertEqual(channel.code, 200)
|
||||
|
||||
# Get the messages
|
||||
room = channel.json_body["rooms"]["join"][room_id]
|
||||
messages = [
|
||||
x for x in room["timeline"]["events"] if x["type"] == "m.room.message"
|
||||
]
|
||||
return messages
|
56
tests/rest/client/test_groups.py
Normal file
56
tests/rest/client/test_groups.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
# Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from synapse.rest.client import groups, room
|
||||
|
||||
from tests import unittest
|
||||
from tests.unittest import override_config
|
||||
|
||||
|
||||
class GroupsTestCase(unittest.HomeserverTestCase):
|
||||
user_id = "@alice:test"
|
||||
room_creator_user_id = "@bob:test"
|
||||
|
||||
servlets = [room.register_servlets, groups.register_servlets]
|
||||
|
||||
@override_config({"enable_group_creation": True})
|
||||
def test_rooms_limited_by_visibility(self):
|
||||
group_id = "+spqr:test"
|
||||
|
||||
# Alice creates a group
|
||||
channel = self.make_request("POST", "/create_group", {"localpart": "spqr"})
|
||||
self.assertEquals(channel.code, 200, msg=channel.text_body)
|
||||
self.assertEquals(channel.json_body, {"group_id": group_id})
|
||||
|
||||
# Bob creates a private room
|
||||
room_id = self.helper.create_room_as(self.room_creator_user_id, is_public=False)
|
||||
self.helper.auth_user_id = self.room_creator_user_id
|
||||
self.helper.send_state(
|
||||
room_id, "m.room.name", {"name": "bob's secret room"}, tok=None
|
||||
)
|
||||
self.helper.auth_user_id = self.user_id
|
||||
|
||||
# Alice adds the room to her group.
|
||||
channel = self.make_request(
|
||||
"PUT", f"/groups/{group_id}/admin/rooms/{room_id}", {}
|
||||
)
|
||||
self.assertEquals(channel.code, 200, msg=channel.text_body)
|
||||
self.assertEquals(channel.json_body, {})
|
||||
|
||||
# Alice now tries to retrieve the room list of the space.
|
||||
channel = self.make_request("GET", f"/groups/{group_id}/rooms")
|
||||
self.assertEquals(channel.code, 200, msg=channel.text_body)
|
||||
self.assertEquals(
|
||||
channel.json_body, {"chunk": [], "total_room_count_estimate": 0}
|
||||
)
|
|
@ -12,6 +12,8 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from synapse.api.errors import Codes
|
||||
from synapse.events.utils import CANONICALJSON_MAX_INT, CANONICALJSON_MIN_INT
|
||||
from synapse.rest import admin
|
||||
from synapse.rest.client import login, room, sync
|
||||
|
||||
|
@ -203,3 +205,79 @@ class PowerLevelsTestCase(HomeserverTestCase):
|
|||
tok=self.admin_access_token,
|
||||
expect_code=200, # expect success
|
||||
)
|
||||
|
||||
def test_cannot_set_string_power_levels(self):
|
||||
room_power_levels = self.helper.get_state(
|
||||
self.room_id,
|
||||
"m.room.power_levels",
|
||||
tok=self.admin_access_token,
|
||||
)
|
||||
|
||||
# Update existing power levels with user at PL "0"
|
||||
room_power_levels["users"].update({self.user_user_id: "0"})
|
||||
|
||||
body = self.helper.send_state(
|
||||
self.room_id,
|
||||
"m.room.power_levels",
|
||||
room_power_levels,
|
||||
tok=self.admin_access_token,
|
||||
expect_code=400, # expect failure
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["errcode"],
|
||||
Codes.BAD_JSON,
|
||||
body,
|
||||
)
|
||||
|
||||
def test_cannot_set_unsafe_large_power_levels(self):
|
||||
room_power_levels = self.helper.get_state(
|
||||
self.room_id,
|
||||
"m.room.power_levels",
|
||||
tok=self.admin_access_token,
|
||||
)
|
||||
|
||||
# Update existing power levels with user at PL above the max safe integer
|
||||
room_power_levels["users"].update(
|
||||
{self.user_user_id: CANONICALJSON_MAX_INT + 1}
|
||||
)
|
||||
|
||||
body = self.helper.send_state(
|
||||
self.room_id,
|
||||
"m.room.power_levels",
|
||||
room_power_levels,
|
||||
tok=self.admin_access_token,
|
||||
expect_code=400, # expect failure
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["errcode"],
|
||||
Codes.BAD_JSON,
|
||||
body,
|
||||
)
|
||||
|
||||
def test_cannot_set_unsafe_small_power_levels(self):
|
||||
room_power_levels = self.helper.get_state(
|
||||
self.room_id,
|
||||
"m.room.power_levels",
|
||||
tok=self.admin_access_token,
|
||||
)
|
||||
|
||||
# Update existing power levels with user at PL below the minimum safe integer
|
||||
room_power_levels["users"].update(
|
||||
{self.user_user_id: CANONICALJSON_MIN_INT - 1}
|
||||
)
|
||||
|
||||
body = self.helper.send_state(
|
||||
self.room_id,
|
||||
"m.room.power_levels",
|
||||
room_power_levels,
|
||||
tok=self.admin_access_token,
|
||||
expect_code=400, # expect failure
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["errcode"],
|
||||
Codes.BAD_JSON,
|
||||
body,
|
||||
)
|
||||
|
|
|
@ -29,7 +29,7 @@ from synapse.api.constants import EventContentFields, EventTypes, Membership
|
|||
from synapse.api.errors import HttpResponseException
|
||||
from synapse.handlers.pagination import PurgeStatus
|
||||
from synapse.rest import admin
|
||||
from synapse.rest.client import account, directory, login, profile, room
|
||||
from synapse.rest.client import account, directory, login, profile, room, sync
|
||||
from synapse.types import JsonDict, RoomAlias, UserID, create_requester
|
||||
from synapse.util.stringutils import random_string
|
||||
|
||||
|
@ -381,6 +381,8 @@ class RoomPermissionsTestCase(RoomBase):
|
|||
class RoomsMemberListTestCase(RoomBase):
|
||||
"""Tests /rooms/$room_id/members/list REST events."""
|
||||
|
||||
servlets = RoomBase.servlets + [sync.register_servlets]
|
||||
|
||||
user_id = "@sid1:red"
|
||||
|
||||
def test_get_member_list(self):
|
||||
|
@ -397,6 +399,86 @@ class RoomsMemberListTestCase(RoomBase):
|
|||
channel = self.make_request("GET", "/rooms/%s/members" % room_id)
|
||||
self.assertEquals(403, channel.code, msg=channel.result["body"])
|
||||
|
||||
def test_get_member_list_no_permission_with_at_token(self):
|
||||
"""
|
||||
Tests that a stranger to the room cannot get the member list
|
||||
(in the case that they use an at token).
|
||||
"""
|
||||
room_id = self.helper.create_room_as("@someone.else:red")
|
||||
|
||||
# first sync to get an at token
|
||||
channel = self.make_request("GET", "/sync")
|
||||
self.assertEquals(200, channel.code)
|
||||
sync_token = channel.json_body["next_batch"]
|
||||
|
||||
# check that permission is denied for @sid1:red to get the
|
||||
# memberships of @someone.else:red's room.
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
f"/rooms/{room_id}/members?at={sync_token}",
|
||||
)
|
||||
self.assertEquals(403, channel.code, msg=channel.result["body"])
|
||||
|
||||
def test_get_member_list_no_permission_former_member(self):
|
||||
"""
|
||||
Tests that a former member of the room can not get the member list.
|
||||
"""
|
||||
# create a room, invite the user and the user joins
|
||||
room_id = self.helper.create_room_as("@alice:red")
|
||||
self.helper.invite(room_id, "@alice:red", self.user_id)
|
||||
self.helper.join(room_id, self.user_id)
|
||||
|
||||
# check that the user can see the member list to start with
|
||||
channel = self.make_request("GET", "/rooms/%s/members" % room_id)
|
||||
self.assertEquals(200, channel.code, msg=channel.result["body"])
|
||||
|
||||
# ban the user
|
||||
self.helper.change_membership(room_id, "@alice:red", self.user_id, "ban")
|
||||
|
||||
# check the user can no longer see the member list
|
||||
channel = self.make_request("GET", "/rooms/%s/members" % room_id)
|
||||
self.assertEquals(403, channel.code, msg=channel.result["body"])
|
||||
|
||||
def test_get_member_list_no_permission_former_member_with_at_token(self):
|
||||
"""
|
||||
Tests that a former member of the room can not get the member list
|
||||
(in the case that they use an at token).
|
||||
"""
|
||||
# create a room, invite the user and the user joins
|
||||
room_id = self.helper.create_room_as("@alice:red")
|
||||
self.helper.invite(room_id, "@alice:red", self.user_id)
|
||||
self.helper.join(room_id, self.user_id)
|
||||
|
||||
# sync to get an at token
|
||||
channel = self.make_request("GET", "/sync")
|
||||
self.assertEquals(200, channel.code)
|
||||
sync_token = channel.json_body["next_batch"]
|
||||
|
||||
# check that the user can see the member list to start with
|
||||
channel = self.make_request(
|
||||
"GET", "/rooms/%s/members?at=%s" % (room_id, sync_token)
|
||||
)
|
||||
self.assertEquals(200, channel.code, msg=channel.result["body"])
|
||||
|
||||
# ban the user (Note: the user is actually allowed to see this event and
|
||||
# state so that they know they're banned!)
|
||||
self.helper.change_membership(room_id, "@alice:red", self.user_id, "ban")
|
||||
|
||||
# invite a third user and let them join
|
||||
self.helper.invite(room_id, "@alice:red", "@bob:red")
|
||||
self.helper.join(room_id, "@bob:red")
|
||||
|
||||
# now, with the original user, sync again to get a new at token
|
||||
channel = self.make_request("GET", "/sync")
|
||||
self.assertEquals(200, channel.code)
|
||||
sync_token = channel.json_body["next_batch"]
|
||||
|
||||
# check the user can no longer see the updated member list
|
||||
channel = self.make_request(
|
||||
"GET", "/rooms/%s/members?at=%s" % (room_id, sync_token)
|
||||
)
|
||||
self.assertEquals(403, channel.code, msg=channel.result["body"])
|
||||
|
||||
def test_get_member_list_mixed_memberships(self):
|
||||
room_creator = "@some_other_guy:red"
|
||||
room_id = self.helper.create_room_as(room_creator)
|
||||
|
|
|
@ -14,13 +14,14 @@
|
|||
import json
|
||||
import os
|
||||
import re
|
||||
from unittest.mock import patch
|
||||
|
||||
from twisted.internet._resolver import HostResolution
|
||||
from twisted.internet.address import IPv4Address, IPv6Address
|
||||
from twisted.internet.error import DNSLookupError
|
||||
from twisted.test.proto_helpers import AccumulatingProtocol
|
||||
|
||||
from synapse.config.oembed import OEmbedEndpointConfig
|
||||
|
||||
from tests import unittest
|
||||
from tests.server import FakeTransport
|
||||
|
||||
|
@ -81,6 +82,19 @@ class URLPreviewTests(unittest.HomeserverTestCase):
|
|||
|
||||
hs = self.setup_test_homeserver(config=config)
|
||||
|
||||
# After the hs is created, modify the parsed oEmbed config (to avoid
|
||||
# messing with files).
|
||||
#
|
||||
# Note that HTTP URLs are used to avoid having to deal with TLS in tests.
|
||||
hs.config.oembed.oembed_patterns = [
|
||||
OEmbedEndpointConfig(
|
||||
api_endpoint="http://publish.twitter.com/oembed",
|
||||
url_patterns=[
|
||||
re.compile(r"http://twitter\.com/.+/status/.+"),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
return hs
|
||||
|
||||
def prepare(self, reactor, clock, hs):
|
||||
|
@ -544,123 +558,101 @@ class URLPreviewTests(unittest.HomeserverTestCase):
|
|||
|
||||
def test_oembed_photo(self):
|
||||
"""Test an oEmbed endpoint which returns a 'photo' type which redirects the preview to a new URL."""
|
||||
# Route the HTTP version to an HTTP endpoint so that the tests work.
|
||||
with patch.dict(
|
||||
"synapse.rest.media.v1.preview_url_resource._oembed_patterns",
|
||||
{
|
||||
re.compile(
|
||||
r"http://twitter\.com/.+/status/.+"
|
||||
): "http://publish.twitter.com/oembed",
|
||||
},
|
||||
clear=True,
|
||||
):
|
||||
self.lookups["publish.twitter.com"] = [(IPv4Address, "10.1.2.3")]
|
||||
self.lookups["cdn.twitter.com"] = [(IPv4Address, "10.1.2.3")]
|
||||
|
||||
self.lookups["publish.twitter.com"] = [(IPv4Address, "10.1.2.3")]
|
||||
self.lookups["cdn.twitter.com"] = [(IPv4Address, "10.1.2.3")]
|
||||
result = {
|
||||
"version": "1.0",
|
||||
"type": "photo",
|
||||
"url": "http://cdn.twitter.com/matrixdotorg",
|
||||
}
|
||||
oembed_content = json.dumps(result).encode("utf-8")
|
||||
|
||||
result = {
|
||||
"version": "1.0",
|
||||
"type": "photo",
|
||||
"url": "http://cdn.twitter.com/matrixdotorg",
|
||||
}
|
||||
oembed_content = json.dumps(result).encode("utf-8")
|
||||
end_content = (
|
||||
b"<html><head>"
|
||||
b"<title>Some Title</title>"
|
||||
b'<meta property="og:description" content="hi" />'
|
||||
b"</head></html>"
|
||||
)
|
||||
|
||||
end_content = (
|
||||
b"<html><head>"
|
||||
b"<title>Some Title</title>"
|
||||
b'<meta property="og:description" content="hi" />'
|
||||
b"</head></html>"
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
"preview_url?url=http://twitter.com/matrixdotorg/status/12345",
|
||||
shorthand=False,
|
||||
await_result=False,
|
||||
)
|
||||
self.pump()
|
||||
|
||||
client = self.reactor.tcpClients[0][2].buildProtocol(None)
|
||||
server = AccumulatingProtocol()
|
||||
server.makeConnection(FakeTransport(client, self.reactor))
|
||||
client.makeConnection(FakeTransport(server, self.reactor))
|
||||
client.dataReceived(
|
||||
(
|
||||
b"HTTP/1.0 200 OK\r\nContent-Length: %d\r\n"
|
||||
b'Content-Type: application/json; charset="utf8"\r\n\r\n'
|
||||
)
|
||||
% (len(oembed_content),)
|
||||
+ oembed_content
|
||||
)
|
||||
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
"preview_url?url=http://twitter.com/matrixdotorg/status/12345",
|
||||
shorthand=False,
|
||||
await_result=False,
|
||||
self.pump()
|
||||
|
||||
client = self.reactor.tcpClients[1][2].buildProtocol(None)
|
||||
server = AccumulatingProtocol()
|
||||
server.makeConnection(FakeTransport(client, self.reactor))
|
||||
client.makeConnection(FakeTransport(server, self.reactor))
|
||||
client.dataReceived(
|
||||
(
|
||||
b"HTTP/1.0 200 OK\r\nContent-Length: %d\r\n"
|
||||
b'Content-Type: text/html; charset="utf8"\r\n\r\n'
|
||||
)
|
||||
self.pump()
|
||||
% (len(end_content),)
|
||||
+ end_content
|
||||
)
|
||||
|
||||
client = self.reactor.tcpClients[0][2].buildProtocol(None)
|
||||
server = AccumulatingProtocol()
|
||||
server.makeConnection(FakeTransport(client, self.reactor))
|
||||
client.makeConnection(FakeTransport(server, self.reactor))
|
||||
client.dataReceived(
|
||||
(
|
||||
b"HTTP/1.0 200 OK\r\nContent-Length: %d\r\n"
|
||||
b'Content-Type: application/json; charset="utf8"\r\n\r\n'
|
||||
)
|
||||
% (len(oembed_content),)
|
||||
+ oembed_content
|
||||
)
|
||||
self.pump()
|
||||
|
||||
self.pump()
|
||||
|
||||
client = self.reactor.tcpClients[1][2].buildProtocol(None)
|
||||
server = AccumulatingProtocol()
|
||||
server.makeConnection(FakeTransport(client, self.reactor))
|
||||
client.makeConnection(FakeTransport(server, self.reactor))
|
||||
client.dataReceived(
|
||||
(
|
||||
b"HTTP/1.0 200 OK\r\nContent-Length: %d\r\n"
|
||||
b'Content-Type: text/html; charset="utf8"\r\n\r\n'
|
||||
)
|
||||
% (len(end_content),)
|
||||
+ end_content
|
||||
)
|
||||
|
||||
self.pump()
|
||||
|
||||
self.assertEqual(channel.code, 200)
|
||||
self.assertEqual(
|
||||
channel.json_body, {"og:title": "Some Title", "og:description": "hi"}
|
||||
)
|
||||
self.assertEqual(channel.code, 200)
|
||||
self.assertEqual(
|
||||
channel.json_body, {"og:title": "Some Title", "og:description": "hi"}
|
||||
)
|
||||
|
||||
def test_oembed_rich(self):
|
||||
"""Test an oEmbed endpoint which returns HTML content via the 'rich' type."""
|
||||
# Route the HTTP version to an HTTP endpoint so that the tests work.
|
||||
with patch.dict(
|
||||
"synapse.rest.media.v1.preview_url_resource._oembed_patterns",
|
||||
{
|
||||
re.compile(
|
||||
r"http://twitter\.com/.+/status/.+"
|
||||
): "http://publish.twitter.com/oembed",
|
||||
},
|
||||
clear=True,
|
||||
):
|
||||
self.lookups["publish.twitter.com"] = [(IPv4Address, "10.1.2.3")]
|
||||
|
||||
self.lookups["publish.twitter.com"] = [(IPv4Address, "10.1.2.3")]
|
||||
result = {
|
||||
"version": "1.0",
|
||||
"type": "rich",
|
||||
"html": "<div>Content Preview</div>",
|
||||
}
|
||||
end_content = json.dumps(result).encode("utf-8")
|
||||
|
||||
result = {
|
||||
"version": "1.0",
|
||||
"type": "rich",
|
||||
"html": "<div>Content Preview</div>",
|
||||
}
|
||||
end_content = json.dumps(result).encode("utf-8")
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
"preview_url?url=http://twitter.com/matrixdotorg/status/12345",
|
||||
shorthand=False,
|
||||
await_result=False,
|
||||
)
|
||||
self.pump()
|
||||
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
"preview_url?url=http://twitter.com/matrixdotorg/status/12345",
|
||||
shorthand=False,
|
||||
await_result=False,
|
||||
client = self.reactor.tcpClients[0][2].buildProtocol(None)
|
||||
server = AccumulatingProtocol()
|
||||
server.makeConnection(FakeTransport(client, self.reactor))
|
||||
client.makeConnection(FakeTransport(server, self.reactor))
|
||||
client.dataReceived(
|
||||
(
|
||||
b"HTTP/1.0 200 OK\r\nContent-Length: %d\r\n"
|
||||
b'Content-Type: application/json; charset="utf8"\r\n\r\n'
|
||||
)
|
||||
self.pump()
|
||||
% (len(end_content),)
|
||||
+ end_content
|
||||
)
|
||||
|
||||
client = self.reactor.tcpClients[0][2].buildProtocol(None)
|
||||
server = AccumulatingProtocol()
|
||||
server.makeConnection(FakeTransport(client, self.reactor))
|
||||
client.makeConnection(FakeTransport(server, self.reactor))
|
||||
client.dataReceived(
|
||||
(
|
||||
b"HTTP/1.0 200 OK\r\nContent-Length: %d\r\n"
|
||||
b'Content-Type: application/json; charset="utf8"\r\n\r\n'
|
||||
)
|
||||
% (len(end_content),)
|
||||
+ end_content
|
||||
)
|
||||
|
||||
self.pump()
|
||||
self.assertEqual(channel.code, 200)
|
||||
self.assertEqual(
|
||||
channel.json_body,
|
||||
{"og:title": None, "og:description": "Content Preview"},
|
||||
)
|
||||
self.pump()
|
||||
self.assertEqual(channel.code, 200)
|
||||
self.assertEqual(
|
||||
channel.json_body,
|
||||
{"og:title": None, "og:description": "Content Preview"},
|
||||
)
|
||||
|
|
|
@ -10,9 +10,10 @@ from zope.interface import implementer
|
|||
|
||||
from twisted.internet import address, threads, udp
|
||||
from twisted.internet._resolver import SimpleResolverComplexifier
|
||||
from twisted.internet.defer import Deferred, fail, succeed
|
||||
from twisted.internet.defer import Deferred, fail, maybeDeferred, succeed
|
||||
from twisted.internet.error import DNSLookupError
|
||||
from twisted.internet.interfaces import (
|
||||
IAddress,
|
||||
IHostnameResolver,
|
||||
IProtocol,
|
||||
IPullProducer,
|
||||
|
@ -511,6 +512,9 @@ class FakeTransport:
|
|||
will get called back for connectionLost() notifications etc.
|
||||
"""
|
||||
|
||||
_peer_address: Optional[IAddress] = attr.ib(default=None)
|
||||
"""The value to be returend by getPeer"""
|
||||
|
||||
disconnecting = False
|
||||
disconnected = False
|
||||
connected = True
|
||||
|
@ -519,7 +523,7 @@ class FakeTransport:
|
|||
autoflush = attr.ib(default=True)
|
||||
|
||||
def getPeer(self):
|
||||
return None
|
||||
return self._peer_address
|
||||
|
||||
def getHost(self):
|
||||
return None
|
||||
|
@ -572,7 +576,12 @@ class FakeTransport:
|
|||
self.producerStreaming = streaming
|
||||
|
||||
def _produce():
|
||||
d = self.producer.resumeProducing()
|
||||
if not self.producer:
|
||||
# we've been unregistered
|
||||
return
|
||||
# some implementations of IProducer (for example, FileSender)
|
||||
# don't return a deferred.
|
||||
d = maybeDeferred(self.producer.resumeProducing)
|
||||
d.addCallback(lambda x: self._reactor.callLater(0.1, _produce))
|
||||
|
||||
if not streaming:
|
||||
|
|
|
@ -75,7 +75,8 @@ class MessageAcceptTests(unittest.HomeserverTestCase):
|
|||
)
|
||||
|
||||
self.handler = self.homeserver.get_federation_handler()
|
||||
self.handler._check_event_auth = lambda origin, event, context, state, claimed_auth_event_map, backfilled: succeed(
|
||||
federation_event_handler = self.homeserver.get_federation_event_handler()
|
||||
federation_event_handler._check_event_auth = lambda origin, event, context, state, claimed_auth_event_map, backfilled: succeed(
|
||||
context
|
||||
)
|
||||
self.client = self.homeserver.get_federation_client()
|
||||
|
@ -85,7 +86,9 @@ class MessageAcceptTests(unittest.HomeserverTestCase):
|
|||
|
||||
# Send the join, it should return None (which is not an error)
|
||||
self.assertEqual(
|
||||
self.get_success(self.handler.on_receive_pdu("test.serv", join_event)),
|
||||
self.get_success(
|
||||
federation_event_handler.on_receive_pdu("test.serv", join_event)
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
|
@ -129,9 +132,10 @@ class MessageAcceptTests(unittest.HomeserverTestCase):
|
|||
}
|
||||
)
|
||||
|
||||
federation_event_handler = self.homeserver.get_federation_event_handler()
|
||||
with LoggingContext("test-context"):
|
||||
failure = self.get_failure(
|
||||
self.handler.on_receive_pdu("test.serv", lying_event),
|
||||
federation_event_handler.on_receive_pdu("test.serv", lying_event),
|
||||
FederationError,
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in a new issue