Compare commits
163 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ec57c5d16 | |||
| 1bcd521f6e | |||
| aab115360f | |||
| f45030d8d7 | |||
| 5fb7028c7a | |||
| 7c110e43ee | |||
| 588c18bb5b | |||
| 78b155649c | |||
| 9af2b55f76 | |||
| bd57760163 | |||
| fc7b6584a1 | |||
| 721b159789 | |||
| 1a9ef1d400 | |||
| 499ff39b12 | |||
| 8510939011 | |||
| a112d28963 | |||
| 3a7f6b65a2 | |||
| 097d2652f7 | |||
| 2502f5f48b | |||
| 4bd4659ba3 | |||
| 4f810e5726 | |||
| fc45a323ee | |||
| d66e5aacb7 | |||
| 94f6c674df | |||
| 75538813fc | |||
| 95ab2eb4a1 | |||
| d53faa40e9 | |||
| 4650526b5e | |||
| 26713515de | |||
| bc3d6b918b | |||
| 14f13babb0 | |||
| 2615c6bd9e | |||
| 016af01598 | |||
| aa530e6800 | |||
| dae224a73f | |||
| b4189b112f | |||
| f6dd12d1e2 | |||
| 2f62e1f6ff | |||
| d1d38081a7 | |||
| 1cc5fc1f6c | |||
| ac3cc32367 | |||
| df9c100542 | |||
| 4d08b8f30c | |||
| cb683d3e3c | |||
| 5bdb189f86 | |||
| b2b90b7d34 | |||
| a3f2d000e0 | |||
| c5d60eadd5 | |||
| def5ea4062 | |||
| dce6e9e0c1 | |||
| 06a1f3e207 | |||
| fec2dcb1a5 | |||
| 0a56966f7d | |||
| 0d67a8cd9d | |||
| fe2294ec8d | |||
| 4bd67db100 | |||
| fa4b54aca5 | |||
| 862b2f9ad5 | |||
| 2889b05554 | |||
| fde37e4e98 | |||
| 220a733d73 | |||
| d828d1dc57 | |||
| 93003aa172 | |||
| d16c6375fe | |||
| 37b165620d | |||
| 3600f5568b | |||
| 58cce39f3a | |||
| c605da97bf | |||
| fe79b5e521 | |||
| 2ae3cc287e | |||
| e975b15101 | |||
| 4d794dae21 | |||
| e9981d58ca | |||
| 31d44ec4bd | |||
| 39bbf6a4a5 | |||
| 5037326d66 | |||
| 6bfc5ad3a1 | |||
| 0c2362861e | |||
| 847b9dcd1c | |||
| 3e1af5109c | |||
| 8ea2f756a9 | |||
| a82c96b87f | |||
| 099829d5a9 | |||
| 99113e40ba | |||
| 0b6bc36402 | |||
| 8824325b82 | |||
| 57b3751918 | |||
| 5ac75fc9a2 | |||
| e2c46ed851 | |||
| 04710cc2d7 | |||
| 54d50fbfdf | |||
| 06675db684 | |||
| 6cdfb0207e | |||
| e9e5d3392d | |||
| cb967e2346 | |||
| 45f5d8f3fd | |||
| 468bd090ff | |||
| 5c1ece0ffc | |||
| 640fcbb07f | |||
| 123918b739 | |||
| 8d92329214 | |||
| 3dcf2feba8 | |||
| 8541db741a | |||
| 46c8f7a517 | |||
| 67e0631f8f | |||
| d7add713a8 | |||
| 532b825ed9 | |||
| 7e8e683754 | |||
| d79c9994f4 | |||
| 30858ff461 | |||
| 58c8ed5b0d | |||
| f76d407ef3 | |||
| 7ddbbc45b7 | |||
| 0729ef01f8 | |||
| ecaa299cab | |||
| 2ec2809460 | |||
| f795595e95 | |||
| 878b00c395 | |||
| 9b6f72663e | |||
| 540f40f0cd | |||
| 5726378ece | |||
| 7e1c7cc274 | |||
| 4aba561c65 | |||
| 52839886d6 | |||
| a97d4e218a | |||
| ddd30f44a0 | |||
| ba17de7fbc | |||
| 119c9c10b0 | |||
| d0bba35197 | |||
| 4ccdbfcdb1 | |||
| bc4b2ecf70 | |||
| 0b4f4cb0b4 | |||
| 338dca58c0 | |||
| 6dac0e738c | |||
| 2d4853039f | |||
| 56f07d980a | |||
| fa1b293da2 | |||
| cbcfd642a0 | |||
| b825d1c800 | |||
| dd64b9dbdd | |||
| dba9152d15 | |||
| d16f5574b6 | |||
| 4cb577c23f | |||
| 8c41c04ee4 | |||
| 753b1270da | |||
| 6368150a74 | |||
| ec24108cc2 | |||
| 895b79ac2e | |||
| b75537beaf | |||
| 84660d91b2 | |||
| cc187f9337 | |||
| 2e052110ee | |||
| 85d1e03b9d | |||
| 1a94de60e8 | |||
| 73f1de31d1 | |||
| 62388a1e44 | |||
| ae5521be9c | |||
| 66b75e2d81 | |||
| 2dfbeea66f | |||
| b898a5600a | |||
| 4a30e4acb4 | |||
| ef13dc4846 | |||
| de7672b78f |
+24
-1
@@ -5,6 +5,7 @@
|
||||
* [Prebuilt packages](#prebuilt-packages)
|
||||
* [Setting up Synapse](#setting-up-synapse)
|
||||
* [TLS certificates](#tls-certificates)
|
||||
* [Email](#email)
|
||||
* [Registering a user](#registering-a-user)
|
||||
* [Setting up a TURN server](#setting-up-a-turn-server)
|
||||
* [URL previews](#url-previews)
|
||||
@@ -394,9 +395,31 @@ To configure Synapse to expose an HTTPS port, you will need to edit
|
||||
instance, if using certbot, use `fullchain.pem` as your certificate, not
|
||||
`cert.pem`).
|
||||
|
||||
For those of you upgrading your TLS certificate in readiness for Synapse 1.0,
|
||||
For those of you upgrading your TLS certificate for Synapse 1.0 compliance,
|
||||
please take a look at [our guide](docs/MSC1711_certificates_FAQ.md#configuring-certificates-for-compatibility-with-synapse-100).
|
||||
|
||||
## Email
|
||||
|
||||
It is desirable for Synapse to have the capability to send email. For example,
|
||||
this is required to support the 'password reset' feature.
|
||||
|
||||
To configure an SMTP server for Synapse, modify the configuration section
|
||||
headed ``email``, and be sure to have at least the ``smtp_host``, ``smtp_port``
|
||||
and ``notif_from`` fields filled out. You may also need to set ``smtp_user``,
|
||||
``smtp_pass``, and ``require_transport_security``..
|
||||
|
||||
If Synapse is not configured with an SMTP server, password reset via email will
|
||||
be disabled by default.
|
||||
|
||||
Alternatively it is possible delegate the sending of email to the server's
|
||||
identity server. Doing so is convenient but not recommended, since a malicious
|
||||
or compromised identity server could theoretically hijack a given user's
|
||||
account by redirecting mail.
|
||||
|
||||
If you are absolutely certain that you wish to use the server's identity server
|
||||
for password resets, set ``trust_identity_server_for_password_resets`` to
|
||||
``true`` under the ``email:`` configuration section.
|
||||
|
||||
## Registering a user
|
||||
|
||||
You will need at least one user on your server in order to use a Matrix
|
||||
|
||||
+49
@@ -49,6 +49,55 @@ returned by the Client-Server API:
|
||||
# configured on port 443.
|
||||
curl -kv https://<host.name>/_matrix/client/versions 2>&1 | grep "Server:"
|
||||
|
||||
Upgrading to v1.0
|
||||
=================
|
||||
|
||||
Validation of TLS certificates
|
||||
------------------------------
|
||||
|
||||
Synapse v1.0 is the first release to enforce
|
||||
validation of TLS certificates for the federation API. It is therefore
|
||||
essential that your certificates are correctly configured. See the `FAQ
|
||||
<docs/MSC1711_certificates_FAQ.md>`_ for more information.
|
||||
|
||||
Note, v1.0 installations will also no longer be able to federate with servers
|
||||
that have not correctly configured their certificates.
|
||||
|
||||
In rare cases, it may be desirable to disable certificate checking: for
|
||||
example, it might be essential to be able to federate with a given legacy
|
||||
server in a closed federation. This can be done in one of two ways:-
|
||||
|
||||
* Configure the global switch ``federation_verify_certificates`` to ``false``.
|
||||
* Configure a whitelist of server domains to trust via ``federation_certificate_verification_whitelist``.
|
||||
|
||||
See the `sample configuration file <docs/sample_config.yaml>`_
|
||||
for more details on these settings.
|
||||
|
||||
Email
|
||||
-----
|
||||
When a user requests a password reset, Synapse will send an email to the
|
||||
user to confirm the request.
|
||||
|
||||
Previous versions of Synapse delegated the job of sending this email to an
|
||||
identity server. If the identity server was somehow malicious or became
|
||||
compromised, it would be theoretically possible to hijack an account through
|
||||
this means.
|
||||
|
||||
Therefore, by default, Synapse v1.0 will send the confirmation email itself. If
|
||||
Synapse is not configured with an SMTP server, password reset via email will be
|
||||
disabled.
|
||||
|
||||
To configure an SMTP server for Synapse, modify the configuration section
|
||||
headed ``email``, and be sure to have at least the ``smtp_host``, ``smtp_port``
|
||||
and ``notif_from`` fields filled out. You may also need to set ``smtp_user``,
|
||||
``smtp_pass``, and ``require_transport_security``.
|
||||
|
||||
If you are absolutely certain that you wish to continue using an identity
|
||||
server for password resets, set ``trust_identity_server_for_password_resets`` to ``true``.
|
||||
|
||||
See the `sample configuration file <docs/sample_config.yaml>`_
|
||||
for more details on these settings.
|
||||
|
||||
Upgrading to v0.99.0
|
||||
====================
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Synapse now more efficiently collates room statistics.
|
||||
@@ -0,0 +1 @@
|
||||
Fix worker registration bug caused by ClientReaderSlavedStore being unable to see get_profileinfo.
|
||||
@@ -0,0 +1 @@
|
||||
Synapse will now serve the experimental "room complexity" API endpoint.
|
||||
@@ -0,0 +1 @@
|
||||
Add experimental support for relations (aka reactions and edits).
|
||||
@@ -0,0 +1 @@
|
||||
Ability to configure default room version.
|
||||
@@ -0,0 +1 @@
|
||||
The base classes for the v1 and v2_alpha REST APIs have been unified.
|
||||
@@ -0,0 +1 @@
|
||||
Simplifications and comments in do_auth.
|
||||
@@ -0,0 +1 @@
|
||||
Remove urllib3 pin as requests 2.22.0 has been released supporting urllib3 1.25.2.
|
||||
@@ -0,0 +1 @@
|
||||
Preparatory work for key-validity features.
|
||||
@@ -0,0 +1 @@
|
||||
Fix appservice timestamp massaging.
|
||||
@@ -0,0 +1 @@
|
||||
Preparatory work for key-validity features.
|
||||
@@ -0,0 +1 @@
|
||||
Preparatory work for key-validity features.
|
||||
@@ -0,0 +1 @@
|
||||
Preparatory work for key-validity features.
|
||||
@@ -0,0 +1 @@
|
||||
Preparatory work for key-validity features.
|
||||
@@ -0,0 +1 @@
|
||||
Preparatory work for key-validity features.
|
||||
@@ -0,0 +1 @@
|
||||
Ability to configure default room version.
|
||||
@@ -0,0 +1 @@
|
||||
Preparatory work for key-validity features.
|
||||
@@ -0,0 +1 @@
|
||||
Ensure that server_keys fetched via a notary server are correctly signed.
|
||||
@@ -0,0 +1 @@
|
||||
Show the correct error when logging out and access token is missing.
|
||||
@@ -0,0 +1 @@
|
||||
Fix error code when there is an invalid parameter on /_matrix/client/r0/publicRooms
|
||||
@@ -0,0 +1 @@
|
||||
Fix error when downloading thumbnail with missing width/height parameter.
|
||||
@@ -0,0 +1 @@
|
||||
Synapse now more efficiently collates room statistics.
|
||||
@@ -0,0 +1 @@
|
||||
Fix schema update for account validity.
|
||||
@@ -0,0 +1 @@
|
||||
Fix bug where we leaked extremities when we soft failed events, leading to performance degradation.
|
||||
@@ -0,0 +1 @@
|
||||
Fix "db txn 'update_presence' from sentinel context" log messages.
|
||||
@@ -0,0 +1 @@
|
||||
Allow configuring a range for the account validity startup job.
|
||||
@@ -0,0 +1 @@
|
||||
Fix dropped logcontexts during high outbound traffic.
|
||||
@@ -0,0 +1 @@
|
||||
Fix bug where we leaked extremities when we soft failed events, leading to performance degradation.
|
||||
@@ -0,0 +1 @@
|
||||
Fix docs on resetting the user directory.
|
||||
@@ -0,0 +1 @@
|
||||
Specify the type of reCAPTCHA key to use.
|
||||
@@ -0,0 +1 @@
|
||||
Improve sample config for monthly active user blocking.
|
||||
@@ -0,0 +1 @@
|
||||
CAS login will now hit the r0 API, not the deprecated v1 one.
|
||||
@@ -0,0 +1 @@
|
||||
Remove spurious debug from MatrixFederationHttpClient.get_json.
|
||||
@@ -0,0 +1 @@
|
||||
Improve logging for logcontext leaks.
|
||||
@@ -0,0 +1 @@
|
||||
Fix bug where we leaked extremities when we soft failed events, leading to performance degradation.
|
||||
@@ -0,0 +1 @@
|
||||
Fix a bug where it is not possible to get events in the federation format with the request `GET /_matrix/client/r0/rooms/{roomId}/messages`.
|
||||
@@ -0,0 +1 @@
|
||||
Fix performance problems with the rooms stats background update.
|
||||
@@ -0,0 +1 @@
|
||||
Preparatory work for key-validity features.
|
||||
@@ -0,0 +1 @@
|
||||
Preparatory work for key-validity features.
|
||||
@@ -0,0 +1 @@
|
||||
Fix noisy 'no key for server' logs.
|
||||
@@ -0,0 +1 @@
|
||||
Clarify that the admin change password API logs the user out.
|
||||
@@ -0,0 +1 @@
|
||||
Fix bug where a notary server would sometimes forget old keys.
|
||||
@@ -0,0 +1 @@
|
||||
Prevent users from setting huge displaynames and avatar URLs.
|
||||
@@ -0,0 +1 @@
|
||||
Fix handling of failures when processing incoming events where calling `/event_auth` on remote server fails.
|
||||
@@ -0,0 +1 @@
|
||||
Ensure that we have an up-to-date copy of the signing key when validating incoming federation requests.
|
||||
@@ -0,0 +1 @@
|
||||
Synapse now more efficiently collates room statistics.
|
||||
@@ -0,0 +1 @@
|
||||
The base classes for the v1 and v2_alpha REST APIs have been unified.
|
||||
@@ -0,0 +1 @@
|
||||
Improve docstrings on MatrixFederationClient.
|
||||
@@ -0,0 +1 @@
|
||||
Fix various problems which made the signing-key notary server time out for some requests.
|
||||
@@ -0,0 +1 @@
|
||||
Fix bug which would make certain operations (such as room joins) block for 20 minutes while attemoting to fetch verification keys.
|
||||
@@ -0,0 +1 @@
|
||||
Fix a bug where we could rapidly mark a server as unreachable even though it was only down for a few minutes.
|
||||
@@ -0,0 +1,2 @@
|
||||
Fix a bug where we could rapidly mark a server as unreachable even though it was only down for a few minutes.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Fix a bug where account validity renewal emails could only be sent when email notifs were enabled.
|
||||
@@ -0,0 +1 @@
|
||||
Fix failure when fetching batches of events during backfill, etc.
|
||||
@@ -0,0 +1 @@
|
||||
Preparatory work for key-validity features.
|
||||
@@ -0,0 +1 @@
|
||||
Clean up FederationClient.get_events for clarity.
|
||||
@@ -0,0 +1 @@
|
||||
Preparatory work for key-validity features.
|
||||
@@ -0,0 +1 @@
|
||||
Add a new room version where the timestamps on events are checked against the validity periods on signing keys.
|
||||
@@ -0,0 +1 @@
|
||||
Fix room stats and presence background updates to correctly handle missing events.
|
||||
@@ -0,0 +1 @@
|
||||
Preparatory work for key-validity features.
|
||||
@@ -0,0 +1 @@
|
||||
Fix notes about ACME in the MSC1711 faq.
|
||||
@@ -0,0 +1 @@
|
||||
Update /_matrix/client/versions to reference support for r0.5.0.
|
||||
@@ -0,0 +1 @@
|
||||
Update upgrade and installation guides ahead of 1.0.
|
||||
+1
-1
@@ -161,7 +161,7 @@ specify values for `SYNAPSE_CONFIG_PATH`, `SYNAPSE_SERVER_NAME` and
|
||||
example:
|
||||
|
||||
```
|
||||
docker run -it --rm
|
||||
docker run -it --rm \
|
||||
--mount type=volume,src=synapse-data,dst=/data \
|
||||
-e SYNAPSE_CONFIG_PATH=/data/homeserver.yaml \
|
||||
-e SYNAPSE_SERVER_NAME=my.matrix.host \
|
||||
|
||||
@@ -7,6 +7,7 @@ Requires a public/private key pair from:
|
||||
|
||||
https://developers.google.com/recaptcha/
|
||||
|
||||
Must be a reCAPTCHA v2 key using the "I'm not a robot" Checkbox option
|
||||
|
||||
Setting ReCaptcha Keys
|
||||
----------------------
|
||||
|
||||
@@ -68,16 +68,14 @@ Admins should upgrade and configure a valid CA cert. Homeservers that require a
|
||||
.well-known entry (see below), should retain their SRV record and use it
|
||||
alongside their .well-known record.
|
||||
|
||||
**>= 5th March 2019 - Synapse 1.0.0 is released**
|
||||
**10th June 2019 - Synapse 1.0.0 is released**
|
||||
|
||||
1.0.0 will land no sooner than 1 month after 0.99.0, leaving server admins one
|
||||
month after 5th February to upgrade to 0.99.0 and deploy their certificates. In
|
||||
1.0.0 is scheduled for release on 10th June. In
|
||||
accordance with the the [S2S spec](https://matrix.org/docs/spec/server_server/r0.1.0.html)
|
||||
1.0.0 will enforce certificate validity. This means that any homeserver without a
|
||||
valid certificate after this point will no longer be able to federate with
|
||||
1.0.0 servers.
|
||||
|
||||
|
||||
## Configuring certificates for compatibility with Synapse 1.0.0
|
||||
|
||||
### If you do not currently have an SRV record
|
||||
@@ -145,12 +143,11 @@ You can do this with a `.well-known` file as follows:
|
||||
1. Keep the SRV record in place - it is needed for backwards compatibility
|
||||
with Synapse 0.34 and earlier.
|
||||
|
||||
2. Give synapse a certificate corresponding to the target domain
|
||||
(`customer.example.net` in the above example). Currently Synapse's ACME
|
||||
support [does not support
|
||||
this](https://github.com/matrix-org/synapse/issues/4552), so you will have
|
||||
to acquire a certificate yourself and give it to Synapse via
|
||||
`tls_certificate_path` and `tls_private_key_path`.
|
||||
2. Give Synapse a certificate corresponding to the target domain
|
||||
(`customer.example.net` in the above example). You can either use Synapse's
|
||||
built-in [ACME support](./ACME.md) for this (via the `domain` parameter in
|
||||
the `acme` section), or acquire a certificate yourself and give it to
|
||||
Synapse via `tls_certificate_path` and `tls_private_key_path`.
|
||||
|
||||
3. Restart Synapse to ensure the new certificate is loaded.
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ An empty body may be passed for backwards compatibility.
|
||||
Reset password
|
||||
==============
|
||||
|
||||
Changes the password of another user.
|
||||
Changes the password of another user. This will automatically log the user out of all their devices.
|
||||
|
||||
The api is::
|
||||
|
||||
|
||||
+48
-4
@@ -83,6 +83,16 @@ pid_file: DATADIR/homeserver.pid
|
||||
#
|
||||
#restrict_public_rooms_to_local_users: true
|
||||
|
||||
# The default room version for newly created rooms.
|
||||
#
|
||||
# Known room versions are listed here:
|
||||
# https://matrix.org/docs/spec/#complete-list-of-room-versions
|
||||
#
|
||||
# For example, for room version 1, default_room_version should be set
|
||||
# to "1".
|
||||
#
|
||||
#default_room_version: "1"
|
||||
|
||||
# The GC threshold parameters to pass to `gc.set_threshold`, if defined
|
||||
#
|
||||
#gc_thresholds: [700, 10, 10]
|
||||
@@ -251,6 +261,22 @@ listeners:
|
||||
|
||||
# Monthly Active User Blocking
|
||||
#
|
||||
# Used in cases where the admin or server owner wants to limit to the
|
||||
# number of monthly active users.
|
||||
#
|
||||
# 'limit_usage_by_mau' disables/enables monthly active user blocking. When
|
||||
# anabled and a limit is reached the server returns a 'ResourceLimitError'
|
||||
# with error type Codes.RESOURCE_LIMIT_EXCEEDED
|
||||
#
|
||||
# 'max_mau_value' is the hard limit of monthly active users above which
|
||||
# the server will start blocking user actions.
|
||||
#
|
||||
# 'mau_trial_days' is a means to add a grace period for active users. It
|
||||
# means that users must be active for this number of days before they
|
||||
# can be considered active and guards against the case where lots of users
|
||||
# sign up in a short space of time never to return after their initial
|
||||
# session.
|
||||
#
|
||||
#limit_usage_by_mau: False
|
||||
#max_mau_value: 50
|
||||
#mau_trial_days: 2
|
||||
@@ -753,7 +779,9 @@ uploads_path: "DATADIR/uploads"
|
||||
# This means that, if a validity period is set, and Synapse is restarted (it will
|
||||
# then derive an expiration date from the current validity period), and some time
|
||||
# after that the validity period changes and Synapse is restarted, the users'
|
||||
# expiration dates won't be updated unless their account is manually renewed.
|
||||
# expiration dates won't be updated unless their account is manually renewed. This
|
||||
# date will be randomly selected within a range [now + period - d ; now + period],
|
||||
# where d is equal to 10% of the validity period.
|
||||
#
|
||||
#account_validity:
|
||||
# enabled: True
|
||||
@@ -1093,9 +1121,9 @@ password_config:
|
||||
#
|
||||
# 'search_all_users' defines whether to search all users visible to your HS
|
||||
# when searching the user directory, rather than limiting to users visible
|
||||
# in public rooms. Defaults to false. If you set it True, you'll have to run
|
||||
# UPDATE user_directory_stream_pos SET stream_id = NULL;
|
||||
# on your database to tell it to rebuild the user_directory search indexes.
|
||||
# in public rooms. Defaults to false. If you set it True, you'll have to
|
||||
# rebuild the user_directory search indexes, see
|
||||
# https://github.com/matrix-org/synapse/blob/master/docs/user_directory.md
|
||||
#
|
||||
#user_directory:
|
||||
# enabled: true
|
||||
@@ -1153,6 +1181,22 @@ password_config:
|
||||
#
|
||||
|
||||
|
||||
|
||||
# Local statistics collection. Used in populating the room directory.
|
||||
#
|
||||
# 'bucket_size' controls how large each statistics timeslice is. It can
|
||||
# be defined in a human readable short form -- e.g. "1d", "1y".
|
||||
#
|
||||
# 'retention' controls how long historical statistics will be kept for.
|
||||
# It can be defined in a human readable short form -- e.g. "1d", "1y".
|
||||
#
|
||||
#
|
||||
#stats:
|
||||
# enabled: true
|
||||
# bucket_size: 1d
|
||||
# retention: 1y
|
||||
|
||||
|
||||
# Server Notices room configuration
|
||||
#
|
||||
# Uncomment this section to enable a room which can be used to send notices
|
||||
|
||||
@@ -7,11 +7,7 @@ who are present in a publicly viewable room present on the server.
|
||||
|
||||
The directory info is stored in various tables, which can (typically after
|
||||
DB corruption) get stale or out of sync. If this happens, for now the
|
||||
quickest solution to fix it is:
|
||||
|
||||
```
|
||||
UPDATE user_directory_stream_pos SET stream_id = NULL;
|
||||
```
|
||||
|
||||
and restart the synapse, which should then start a background task to
|
||||
solution to fix it is to execute the SQL here
|
||||
https://github.com/matrix-org/synapse/blob/master/synapse/storage/schema/delta/53/user_dir_populate.sql
|
||||
and then restart synapse. This should then start a background task to
|
||||
flush the current tables and regenerate the directory.
|
||||
|
||||
@@ -20,9 +20,7 @@ class CallVisitor(ast.NodeVisitor):
|
||||
else:
|
||||
return
|
||||
|
||||
if name == "client_path_patterns":
|
||||
PATTERNS_V1.append(node.args[0].s)
|
||||
elif name == "client_v2_patterns":
|
||||
if name == "client_patterns":
|
||||
PATTERNS_V2.append(node.args[0].s)
|
||||
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ class EventTypes(object):
|
||||
|
||||
RoomHistoryVisibility = "m.room.history_visibility"
|
||||
CanonicalAlias = "m.room.canonical_alias"
|
||||
Encryption = "m.room.encryption"
|
||||
RoomAvatar = "m.room.avatar"
|
||||
RoomEncryption = "m.room.encryption"
|
||||
GuestAccess = "m.room.guest_access"
|
||||
|
||||
@@ -50,6 +50,7 @@ class RoomVersion(object):
|
||||
disposition = attr.ib() # str; one of the RoomDispositions
|
||||
event_format = attr.ib() # int; one of the EventFormatVersions
|
||||
state_res = attr.ib() # int; one of the StateResolutionVersions
|
||||
enforce_key_validity = attr.ib() # bool
|
||||
|
||||
|
||||
class RoomVersions(object):
|
||||
@@ -58,35 +59,36 @@ class RoomVersions(object):
|
||||
RoomDisposition.STABLE,
|
||||
EventFormatVersions.V1,
|
||||
StateResolutionVersions.V1,
|
||||
)
|
||||
STATE_V2_TEST = RoomVersion(
|
||||
"state-v2-test",
|
||||
RoomDisposition.UNSTABLE,
|
||||
EventFormatVersions.V1,
|
||||
StateResolutionVersions.V2,
|
||||
enforce_key_validity=False,
|
||||
)
|
||||
V2 = RoomVersion(
|
||||
"2",
|
||||
RoomDisposition.STABLE,
|
||||
EventFormatVersions.V1,
|
||||
StateResolutionVersions.V2,
|
||||
enforce_key_validity=False,
|
||||
)
|
||||
V3 = RoomVersion(
|
||||
"3",
|
||||
RoomDisposition.STABLE,
|
||||
EventFormatVersions.V2,
|
||||
StateResolutionVersions.V2,
|
||||
enforce_key_validity=False,
|
||||
)
|
||||
V4 = RoomVersion(
|
||||
"4",
|
||||
RoomDisposition.STABLE,
|
||||
EventFormatVersions.V3,
|
||||
StateResolutionVersions.V2,
|
||||
enforce_key_validity=False,
|
||||
)
|
||||
VDH_TEST_KEY_VALIDITY = RoomVersion(
|
||||
"vdh-test-key-validity",
|
||||
RoomDisposition.UNSTABLE,
|
||||
EventFormatVersions.V3,
|
||||
StateResolutionVersions.V2,
|
||||
enforce_key_validity=False,
|
||||
)
|
||||
|
||||
|
||||
# the version we will give rooms which are created on this server
|
||||
DEFAULT_ROOM_VERSION = RoomVersions.V1
|
||||
|
||||
|
||||
KNOWN_ROOM_VERSIONS = {
|
||||
@@ -94,7 +96,7 @@ KNOWN_ROOM_VERSIONS = {
|
||||
RoomVersions.V1,
|
||||
RoomVersions.V2,
|
||||
RoomVersions.V3,
|
||||
RoomVersions.STATE_V2_TEST,
|
||||
RoomVersions.V4,
|
||||
RoomVersions.VDH_TEST_KEY_VALIDITY,
|
||||
)
|
||||
} # type: dict[str, RoomVersion]
|
||||
|
||||
@@ -26,6 +26,7 @@ CLIENT_API_PREFIX = "/_matrix/client"
|
||||
FEDERATION_PREFIX = "/_matrix/federation"
|
||||
FEDERATION_V1_PREFIX = FEDERATION_PREFIX + "/v1"
|
||||
FEDERATION_V2_PREFIX = FEDERATION_PREFIX + "/v2"
|
||||
FEDERATION_UNSTABLE_PREFIX = FEDERATION_PREFIX + "/unstable"
|
||||
STATIC_PREFIX = "/_matrix/static"
|
||||
WEB_CLIENT_PREFIX = "/_matrix/client"
|
||||
CONTENT_REPO_PREFIX = "/_matrix/content"
|
||||
|
||||
+13
-7
@@ -344,15 +344,21 @@ class _LimitedHostnameResolver(object):
|
||||
|
||||
def resolveHostName(self, resolutionReceiver, hostName, portNumber=0,
|
||||
addressTypes=None, transportSemantics='TCP'):
|
||||
# Note this is happening deep within the reactor, so we don't need to
|
||||
# worry about log contexts.
|
||||
|
||||
# We need this function to return `resolutionReceiver` so we do all the
|
||||
# actual logic involving deferreds in a separate function.
|
||||
self._resolve(
|
||||
resolutionReceiver, hostName, portNumber,
|
||||
addressTypes, transportSemantics,
|
||||
)
|
||||
|
||||
# even though this is happening within the depths of twisted, we need to drop
|
||||
# our logcontext before starting _resolve, otherwise: (a) _resolve will drop
|
||||
# the logcontext if it returns an incomplete deferred; (b) _resolve will
|
||||
# call the resolutionReceiver *with* a logcontext, which it won't be expecting.
|
||||
with PreserveLoggingContext():
|
||||
self._resolve(
|
||||
resolutionReceiver,
|
||||
hostName,
|
||||
portNumber,
|
||||
addressTypes,
|
||||
transportSemantics,
|
||||
)
|
||||
|
||||
return resolutionReceiver
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ from synapse.replication.slave.storage.devices import SlavedDeviceStore
|
||||
from synapse.replication.slave.storage.directory import DirectoryStore
|
||||
from synapse.replication.slave.storage.events import SlavedEventStore
|
||||
from synapse.replication.slave.storage.keys import SlavedKeyStore
|
||||
from synapse.replication.slave.storage.profile import SlavedProfileStore
|
||||
from synapse.replication.slave.storage.push_rule import SlavedPushRuleStore
|
||||
from synapse.replication.slave.storage.receipts import SlavedReceiptsStore
|
||||
from synapse.replication.slave.storage.registration import SlavedRegistrationStore
|
||||
@@ -81,6 +82,7 @@ class ClientReaderSlavedStore(
|
||||
SlavedApplicationServiceStore,
|
||||
SlavedRegistrationStore,
|
||||
SlavedTransactionStore,
|
||||
SlavedProfileStore,
|
||||
SlavedClientIpStore,
|
||||
BaseSlavedStore,
|
||||
):
|
||||
|
||||
@@ -37,8 +37,7 @@ from synapse.replication.slave.storage.client_ips import SlavedClientIpStore
|
||||
from synapse.replication.slave.storage.devices import SlavedDeviceStore
|
||||
from synapse.replication.slave.storage.registration import SlavedRegistrationStore
|
||||
from synapse.replication.tcp.client import ReplicationClientHandler
|
||||
from synapse.rest.client.v1.base import ClientV1RestServlet, client_path_patterns
|
||||
from synapse.rest.client.v2_alpha._base import client_v2_patterns
|
||||
from synapse.rest.client.v2_alpha._base import client_patterns
|
||||
from synapse.server import HomeServer
|
||||
from synapse.storage.engines import create_engine
|
||||
from synapse.util.httpresourcetree import create_resource_tree
|
||||
@@ -49,11 +48,11 @@ from synapse.util.versionstring import get_version_string
|
||||
logger = logging.getLogger("synapse.app.frontend_proxy")
|
||||
|
||||
|
||||
class PresenceStatusStubServlet(ClientV1RestServlet):
|
||||
PATTERNS = client_path_patterns("/presence/(?P<user_id>[^/]*)/status")
|
||||
class PresenceStatusStubServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/presence/(?P<user_id>[^/]*)/status")
|
||||
|
||||
def __init__(self, hs):
|
||||
super(PresenceStatusStubServlet, self).__init__(hs)
|
||||
super(PresenceStatusStubServlet, self).__init__()
|
||||
self.http_client = hs.get_simple_http_client()
|
||||
self.auth = hs.get_auth()
|
||||
self.main_uri = hs.config.worker_main_http_uri
|
||||
@@ -84,7 +83,7 @@ class PresenceStatusStubServlet(ClientV1RestServlet):
|
||||
|
||||
|
||||
class KeyUploadServlet(RestServlet):
|
||||
PATTERNS = client_v2_patterns("/keys/upload(/(?P<device_id>[^/]+))?$")
|
||||
PATTERNS = client_patterns("/keys/upload(/(?P<device_id>[^/]+))?$")
|
||||
|
||||
def __init__(self, hs):
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2015, 2016 OpenMarket Ltd
|
||||
# Copyright 2015-2016 OpenMarket Ltd
|
||||
# Copyright 2017-2018 New Vector Ltd
|
||||
# Copyright 2019 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.
|
||||
@@ -29,12 +31,50 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class EmailConfig(Config):
|
||||
def read_config(self, config):
|
||||
# TODO: We should separate better the email configuration from the notification
|
||||
# and account validity config.
|
||||
|
||||
self.email_enable_notifs = False
|
||||
|
||||
email_config = config.get("email", {})
|
||||
self.email_enable_notifs = email_config.get("enable_notifs", False)
|
||||
|
||||
if self.email_enable_notifs:
|
||||
self.email_smtp_host = email_config.get("smtp_host", None)
|
||||
self.email_smtp_port = email_config.get("smtp_port", None)
|
||||
self.email_smtp_user = email_config.get("smtp_user", None)
|
||||
self.email_smtp_pass = email_config.get("smtp_pass", None)
|
||||
self.require_transport_security = email_config.get(
|
||||
"require_transport_security", False
|
||||
)
|
||||
if "app_name" in email_config:
|
||||
self.email_app_name = email_config["app_name"]
|
||||
else:
|
||||
self.email_app_name = "Matrix"
|
||||
|
||||
self.email_notif_from = email_config.get("notif_from", None)
|
||||
if self.email_notif_from is not None:
|
||||
# make sure it's valid
|
||||
parsed = email.utils.parseaddr(self.email_notif_from)
|
||||
if parsed[1] == '':
|
||||
raise RuntimeError("Invalid notif_from address")
|
||||
|
||||
template_dir = email_config.get("template_dir")
|
||||
# we need an absolute path, because we change directory after starting (and
|
||||
# we don't yet know what auxilliary templates like mail.css we will need).
|
||||
# (Note that loading as package_resources with jinja.PackageLoader doesn't
|
||||
# work for the same reason.)
|
||||
if not template_dir:
|
||||
template_dir = pkg_resources.resource_filename(
|
||||
'synapse', 'res/templates'
|
||||
)
|
||||
|
||||
self.email_template_dir = os.path.abspath(template_dir)
|
||||
|
||||
self.email_enable_notifs = email_config.get("enable_notifs", False)
|
||||
account_validity_renewal_enabled = config.get(
|
||||
"account_validity", {},
|
||||
).get("renew_at")
|
||||
|
||||
if self.email_enable_notifs or account_validity_renewal_enabled:
|
||||
# make sure we can import the required deps
|
||||
import jinja2
|
||||
import bleach
|
||||
@@ -42,6 +82,7 @@ class EmailConfig(Config):
|
||||
jinja2
|
||||
bleach
|
||||
|
||||
if self.email_enable_notifs:
|
||||
required = [
|
||||
"smtp_host",
|
||||
"smtp_port",
|
||||
@@ -66,34 +107,13 @@ class EmailConfig(Config):
|
||||
"email.enable_notifs is True but no public_baseurl is set"
|
||||
)
|
||||
|
||||
self.email_smtp_host = email_config["smtp_host"]
|
||||
self.email_smtp_port = email_config["smtp_port"]
|
||||
self.email_notif_from = email_config["notif_from"]
|
||||
self.email_notif_template_html = email_config["notif_template_html"]
|
||||
self.email_notif_template_text = email_config["notif_template_text"]
|
||||
self.email_expiry_template_html = email_config.get(
|
||||
"expiry_template_html", "notice_expiry.html",
|
||||
)
|
||||
self.email_expiry_template_text = email_config.get(
|
||||
"expiry_template_text", "notice_expiry.txt",
|
||||
)
|
||||
|
||||
template_dir = email_config.get("template_dir")
|
||||
# we need an absolute path, because we change directory after starting (and
|
||||
# we don't yet know what auxilliary templates like mail.css we will need).
|
||||
# (Note that loading as package_resources with jinja.PackageLoader doesn't
|
||||
# work for the same reason.)
|
||||
if not template_dir:
|
||||
template_dir = pkg_resources.resource_filename(
|
||||
'synapse', 'res/templates'
|
||||
)
|
||||
template_dir = os.path.abspath(template_dir)
|
||||
|
||||
for f in self.email_notif_template_text, self.email_notif_template_html:
|
||||
p = os.path.join(template_dir, f)
|
||||
p = os.path.join(self.email_template_dir, f)
|
||||
if not os.path.isfile(p):
|
||||
raise ConfigError("Unable to find email template file %s" % (p, ))
|
||||
self.email_template_dir = template_dir
|
||||
|
||||
self.email_notif_for_new_users = email_config.get(
|
||||
"notif_for_new_users", True
|
||||
@@ -101,29 +121,24 @@ class EmailConfig(Config):
|
||||
self.email_riot_base_url = email_config.get(
|
||||
"riot_base_url", None
|
||||
)
|
||||
self.email_smtp_user = email_config.get(
|
||||
"smtp_user", None
|
||||
)
|
||||
self.email_smtp_pass = email_config.get(
|
||||
"smtp_pass", None
|
||||
)
|
||||
self.require_transport_security = email_config.get(
|
||||
"require_transport_security", False
|
||||
)
|
||||
if "app_name" in email_config:
|
||||
self.email_app_name = email_config["app_name"]
|
||||
else:
|
||||
self.email_app_name = "Matrix"
|
||||
|
||||
# make sure it's valid
|
||||
parsed = email.utils.parseaddr(self.email_notif_from)
|
||||
if parsed[1] == '':
|
||||
raise RuntimeError("Invalid notif_from address")
|
||||
else:
|
||||
self.email_enable_notifs = False
|
||||
# Not much point setting defaults for the rest: it would be an
|
||||
# error for them to be used.
|
||||
|
||||
if account_validity_renewal_enabled:
|
||||
self.email_expiry_template_html = email_config.get(
|
||||
"expiry_template_html", "notice_expiry.html",
|
||||
)
|
||||
self.email_expiry_template_text = email_config.get(
|
||||
"expiry_template_text", "notice_expiry.txt",
|
||||
)
|
||||
|
||||
for f in self.email_expiry_template_text, self.email_expiry_template_html:
|
||||
p = os.path.join(self.email_template_dir, f)
|
||||
if not os.path.isfile(p):
|
||||
raise ConfigError("Unable to find email template file %s" % (p, ))
|
||||
|
||||
def default_config(self, config_dir_path, server_name, **kwargs):
|
||||
return """
|
||||
# Enable sending emails for notification events or expiry notices
|
||||
|
||||
@@ -13,6 +13,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.
|
||||
|
||||
from .api import ApiConfig
|
||||
from .appservice import AppServiceConfig
|
||||
from .captcha import CaptchaConfig
|
||||
@@ -36,20 +37,41 @@ from .saml2_config import SAML2Config
|
||||
from .server import ServerConfig
|
||||
from .server_notices_config import ServerNoticesConfig
|
||||
from .spam_checker import SpamCheckerConfig
|
||||
from .stats import StatsConfig
|
||||
from .tls import TlsConfig
|
||||
from .user_directory import UserDirectoryConfig
|
||||
from .voip import VoipConfig
|
||||
from .workers import WorkerConfig
|
||||
|
||||
|
||||
class HomeServerConfig(ServerConfig, TlsConfig, DatabaseConfig, LoggingConfig,
|
||||
RatelimitConfig, ContentRepositoryConfig, CaptchaConfig,
|
||||
VoipConfig, RegistrationConfig, MetricsConfig, ApiConfig,
|
||||
AppServiceConfig, KeyConfig, SAML2Config, CasConfig,
|
||||
JWTConfig, PasswordConfig, EmailConfig,
|
||||
WorkerConfig, PasswordAuthProviderConfig, PushConfig,
|
||||
SpamCheckerConfig, GroupsConfig, UserDirectoryConfig,
|
||||
ConsentConfig,
|
||||
ServerNoticesConfig, RoomDirectoryConfig,
|
||||
):
|
||||
class HomeServerConfig(
|
||||
ServerConfig,
|
||||
TlsConfig,
|
||||
DatabaseConfig,
|
||||
LoggingConfig,
|
||||
RatelimitConfig,
|
||||
ContentRepositoryConfig,
|
||||
CaptchaConfig,
|
||||
VoipConfig,
|
||||
RegistrationConfig,
|
||||
MetricsConfig,
|
||||
ApiConfig,
|
||||
AppServiceConfig,
|
||||
KeyConfig,
|
||||
SAML2Config,
|
||||
CasConfig,
|
||||
JWTConfig,
|
||||
PasswordConfig,
|
||||
EmailConfig,
|
||||
WorkerConfig,
|
||||
PasswordAuthProviderConfig,
|
||||
PushConfig,
|
||||
SpamCheckerConfig,
|
||||
GroupsConfig,
|
||||
UserDirectoryConfig,
|
||||
ConsentConfig,
|
||||
StatsConfig,
|
||||
ServerNoticesConfig,
|
||||
RoomDirectoryConfig,
|
||||
):
|
||||
pass
|
||||
|
||||
@@ -39,6 +39,8 @@ class AccountValidityConfig(Config):
|
||||
else:
|
||||
self.renew_email_subject = "Renew your %(app)s account"
|
||||
|
||||
self.startup_job_max_delta = self.period * 10. / 100.
|
||||
|
||||
if self.renew_by_email_enabled and "public_baseurl" not in synapse_config:
|
||||
raise ConfigError("Can't send renewal emails without 'public_baseurl'")
|
||||
|
||||
@@ -129,7 +131,9 @@ class RegistrationConfig(Config):
|
||||
# This means that, if a validity period is set, and Synapse is restarted (it will
|
||||
# then derive an expiration date from the current validity period), and some time
|
||||
# after that the validity period changes and Synapse is restarted, the users'
|
||||
# expiration dates won't be updated unless their account is manually renewed.
|
||||
# expiration dates won't be updated unless their account is manually renewed. This
|
||||
# date will be randomly selected within a range [now + period - d ; now + period],
|
||||
# where d is equal to 10%% of the validity period.
|
||||
#
|
||||
#account_validity:
|
||||
# enabled: True
|
||||
|
||||
@@ -20,6 +20,7 @@ import os.path
|
||||
|
||||
from netaddr import IPSet
|
||||
|
||||
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
|
||||
from synapse.http.endpoint import parse_and_validate_server_name
|
||||
from synapse.python_dependencies import DependencyException, check_requirements
|
||||
|
||||
@@ -35,6 +36,8 @@ logger = logging.Logger(__name__)
|
||||
# in the list.
|
||||
DEFAULT_BIND_ADDRESSES = ['::', '0.0.0.0']
|
||||
|
||||
DEFAULT_ROOM_VERSION = "1"
|
||||
|
||||
|
||||
class ServerConfig(Config):
|
||||
|
||||
@@ -88,6 +91,22 @@ class ServerConfig(Config):
|
||||
"restrict_public_rooms_to_local_users", False,
|
||||
)
|
||||
|
||||
default_room_version = config.get(
|
||||
"default_room_version", DEFAULT_ROOM_VERSION,
|
||||
)
|
||||
|
||||
# Ensure room version is a str
|
||||
default_room_version = str(default_room_version)
|
||||
|
||||
if default_room_version not in KNOWN_ROOM_VERSIONS:
|
||||
raise ConfigError(
|
||||
"Unknown default_room_version: %s, known room versions: %s" %
|
||||
(default_room_version, list(KNOWN_ROOM_VERSIONS.keys()))
|
||||
)
|
||||
|
||||
# Get the actual room version object rather than just the identifier
|
||||
self.default_room_version = KNOWN_ROOM_VERSIONS[default_room_version]
|
||||
|
||||
# whether to enable search. If disabled, new entries will not be inserted
|
||||
# into the search tables and they will not be indexed. Users will receive
|
||||
# errors when attempting to search for messages.
|
||||
@@ -310,6 +329,10 @@ class ServerConfig(Config):
|
||||
unsecure_port = 8008
|
||||
|
||||
pid_file = os.path.join(data_dir_path, "homeserver.pid")
|
||||
|
||||
# Bring DEFAULT_ROOM_VERSION into the local-scope for use in the
|
||||
# default config string
|
||||
default_room_version = DEFAULT_ROOM_VERSION
|
||||
return """\
|
||||
## Server ##
|
||||
|
||||
@@ -384,6 +407,16 @@ class ServerConfig(Config):
|
||||
#
|
||||
#restrict_public_rooms_to_local_users: true
|
||||
|
||||
# The default room version for newly created rooms.
|
||||
#
|
||||
# Known room versions are listed here:
|
||||
# https://matrix.org/docs/spec/#complete-list-of-room-versions
|
||||
#
|
||||
# For example, for room version 1, default_room_version should be set
|
||||
# to "1".
|
||||
#
|
||||
#default_room_version: "%(default_room_version)s"
|
||||
|
||||
# The GC threshold parameters to pass to `gc.set_threshold`, if defined
|
||||
#
|
||||
#gc_thresholds: [700, 10, 10]
|
||||
@@ -552,6 +585,22 @@ class ServerConfig(Config):
|
||||
|
||||
# Monthly Active User Blocking
|
||||
#
|
||||
# Used in cases where the admin or server owner wants to limit to the
|
||||
# number of monthly active users.
|
||||
#
|
||||
# 'limit_usage_by_mau' disables/enables monthly active user blocking. When
|
||||
# anabled and a limit is reached the server returns a 'ResourceLimitError'
|
||||
# with error type Codes.RESOURCE_LIMIT_EXCEEDED
|
||||
#
|
||||
# 'max_mau_value' is the hard limit of monthly active users above which
|
||||
# the server will start blocking user actions.
|
||||
#
|
||||
# 'mau_trial_days' is a means to add a grace period for active users. It
|
||||
# means that users must be active for this number of days before they
|
||||
# can be considered active and guards against the case where lots of users
|
||||
# sign up in a short space of time never to return after their initial
|
||||
# session.
|
||||
#
|
||||
#limit_usage_by_mau: False
|
||||
#max_mau_value: 50
|
||||
#mau_trial_days: 2
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2018 New Vector Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import division
|
||||
|
||||
import sys
|
||||
|
||||
from ._base import Config
|
||||
|
||||
|
||||
class StatsConfig(Config):
|
||||
"""Stats Configuration
|
||||
Configuration for the behaviour of synapse's stats engine
|
||||
"""
|
||||
|
||||
def read_config(self, config):
|
||||
self.stats_enabled = True
|
||||
self.stats_bucket_size = 86400
|
||||
self.stats_retention = sys.maxsize
|
||||
stats_config = config.get("stats", None)
|
||||
if stats_config:
|
||||
self.stats_enabled = stats_config.get("enabled", self.stats_enabled)
|
||||
self.stats_bucket_size = (
|
||||
self.parse_duration(stats_config.get("bucket_size", "1d")) / 1000
|
||||
)
|
||||
self.stats_retention = (
|
||||
self.parse_duration(
|
||||
stats_config.get("retention", "%ds" % (sys.maxsize,))
|
||||
)
|
||||
/ 1000
|
||||
)
|
||||
|
||||
def default_config(self, config_dir_path, server_name, **kwargs):
|
||||
return """
|
||||
# Local statistics collection. Used in populating the room directory.
|
||||
#
|
||||
# 'bucket_size' controls how large each statistics timeslice is. It can
|
||||
# be defined in a human readable short form -- e.g. "1d", "1y".
|
||||
#
|
||||
# 'retention' controls how long historical statistics will be kept for.
|
||||
# It can be defined in a human readable short form -- e.g. "1d", "1y".
|
||||
#
|
||||
#
|
||||
#stats:
|
||||
# enabled: true
|
||||
# bucket_size: 1d
|
||||
# retention: 1y
|
||||
"""
|
||||
@@ -43,9 +43,9 @@ class UserDirectoryConfig(Config):
|
||||
#
|
||||
# 'search_all_users' defines whether to search all users visible to your HS
|
||||
# when searching the user directory, rather than limiting to users visible
|
||||
# in public rooms. Defaults to false. If you set it True, you'll have to run
|
||||
# UPDATE user_directory_stream_pos SET stream_id = NULL;
|
||||
# on your database to tell it to rebuild the user_directory search indexes.
|
||||
# in public rooms. Defaults to false. If you set it True, you'll have to
|
||||
# rebuild the user_directory search indexes, see
|
||||
# https://github.com/matrix-org/synapse/blob/master/docs/user_directory.md
|
||||
#
|
||||
#user_directory:
|
||||
# enabled: true
|
||||
|
||||
@@ -31,7 +31,11 @@ logger = logging.getLogger(__name__)
|
||||
def check_event_content_hash(event, hash_algorithm=hashlib.sha256):
|
||||
"""Check whether the hash for this PDU matches the contents"""
|
||||
name, expected_hash = compute_content_hash(event.get_pdu_json(), hash_algorithm)
|
||||
logger.debug("Expecting hash: %s", encode_base64(expected_hash))
|
||||
logger.debug(
|
||||
"Verifying content hash on %s (expecting: %s)",
|
||||
event.event_id,
|
||||
encode_base64(expected_hash),
|
||||
)
|
||||
|
||||
# some malformed events lack a 'hashes'. Protect against it being missing
|
||||
# or a weird type by basically treating it the same as an unhashed event.
|
||||
|
||||
+550
-419
File diff suppressed because it is too large
Load Diff
@@ -76,6 +76,7 @@ class EventBuilder(object):
|
||||
# someone tries to get them when they don't exist.
|
||||
_state_key = attr.ib(default=None)
|
||||
_redacts = attr.ib(default=None)
|
||||
_origin_server_ts = attr.ib(default=None)
|
||||
|
||||
internal_metadata = attr.ib(default=attr.Factory(lambda: _EventInternalMetadata({})))
|
||||
|
||||
@@ -142,6 +143,9 @@ class EventBuilder(object):
|
||||
if self._redacts is not None:
|
||||
event_dict["redacts"] = self._redacts
|
||||
|
||||
if self._origin_server_ts is not None:
|
||||
event_dict["origin_server_ts"] = self._origin_server_ts
|
||||
|
||||
defer.returnValue(
|
||||
create_local_event_from_event_dict(
|
||||
clock=self._clock,
|
||||
@@ -209,6 +213,7 @@ class EventBuilderFactory(object):
|
||||
content=key_values.get("content", {}),
|
||||
unsigned=key_values.get("unsigned", {}),
|
||||
redacts=key_values.get("redacts", None),
|
||||
origin_server_ts=key_values.get("origin_server_ts", None),
|
||||
)
|
||||
|
||||
|
||||
@@ -245,7 +250,7 @@ def create_local_event_from_event_dict(clock, hostname, signing_key,
|
||||
event_dict["event_id"] = _create_event_id(clock, hostname)
|
||||
|
||||
event_dict["origin"] = hostname
|
||||
event_dict["origin_server_ts"] = time_now
|
||||
event_dict.setdefault("origin_server_ts", time_now)
|
||||
|
||||
event_dict.setdefault("unsigned", {})
|
||||
age = event_dict["unsigned"].pop("age", 0)
|
||||
|
||||
@@ -330,12 +330,13 @@ class EventClientSerializer(object):
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def serialize_event(self, event, time_now, **kwargs):
|
||||
def serialize_event(self, event, time_now, bundle_aggregations=True, **kwargs):
|
||||
"""Serializes a single event.
|
||||
|
||||
Args:
|
||||
event (EventBase)
|
||||
time_now (int): The current time in milliseconds
|
||||
bundle_aggregations (bool): Whether to bundle in related events
|
||||
**kwargs: Arguments to pass to `serialize_event`
|
||||
|
||||
Returns:
|
||||
@@ -350,7 +351,7 @@ class EventClientSerializer(object):
|
||||
|
||||
# If MSC1849 is enabled then we need to look if thre are any relations
|
||||
# we need to bundle in with the event
|
||||
if self.experimental_msc1849_support_enabled:
|
||||
if self.experimental_msc1849_support_enabled and bundle_aggregations:
|
||||
annotations = yield self.store.get_aggregation_groups_for_event(
|
||||
event_id,
|
||||
)
|
||||
|
||||
@@ -223,9 +223,6 @@ def _check_sigs_on_pdus(keyring, room_version, pdus):
|
||||
the signatures are valid, or fail (with a SynapseError) if not.
|
||||
"""
|
||||
|
||||
# (currently this is written assuming the v1 room structure; we'll probably want a
|
||||
# separate function for checking v2 rooms)
|
||||
|
||||
# we want to check that the event is signed by:
|
||||
#
|
||||
# (a) the sender's server
|
||||
@@ -257,6 +254,10 @@ def _check_sigs_on_pdus(keyring, room_version, pdus):
|
||||
for p in pdus
|
||||
]
|
||||
|
||||
v = KNOWN_ROOM_VERSIONS.get(room_version)
|
||||
if not v:
|
||||
raise RuntimeError("Unrecognized room version %s" % (room_version,))
|
||||
|
||||
# First we check that the sender event is signed by the sender's domain
|
||||
# (except if its a 3pid invite, in which case it may be sent by any server)
|
||||
pdus_to_check_sender = [
|
||||
@@ -264,10 +265,16 @@ def _check_sigs_on_pdus(keyring, room_version, pdus):
|
||||
if not _is_invite_via_3pid(p.pdu)
|
||||
]
|
||||
|
||||
more_deferreds = keyring.verify_json_objects_for_server([
|
||||
(p.sender_domain, p.redacted_pdu_json)
|
||||
for p in pdus_to_check_sender
|
||||
])
|
||||
more_deferreds = keyring.verify_json_objects_for_server(
|
||||
[
|
||||
(
|
||||
p.sender_domain,
|
||||
p.redacted_pdu_json,
|
||||
p.pdu.origin_server_ts if v.enforce_key_validity else 0,
|
||||
)
|
||||
for p in pdus_to_check_sender
|
||||
]
|
||||
)
|
||||
|
||||
def sender_err(e, pdu_to_check):
|
||||
errmsg = "event id %s: unable to verify signature for sender %s: %s" % (
|
||||
@@ -287,20 +294,22 @@ def _check_sigs_on_pdus(keyring, room_version, pdus):
|
||||
# event id's domain (normally only the case for joins/leaves), and add additional
|
||||
# checks. Only do this if the room version has a concept of event ID domain
|
||||
# (ie, the room version uses old-style non-hash event IDs).
|
||||
v = KNOWN_ROOM_VERSIONS.get(room_version)
|
||||
if not v:
|
||||
raise RuntimeError("Unrecognized room version %s" % (room_version,))
|
||||
|
||||
if v.event_format == EventFormatVersions.V1:
|
||||
pdus_to_check_event_id = [
|
||||
p for p in pdus_to_check
|
||||
if p.sender_domain != get_domain_from_id(p.pdu.event_id)
|
||||
]
|
||||
|
||||
more_deferreds = keyring.verify_json_objects_for_server([
|
||||
(get_domain_from_id(p.pdu.event_id), p.redacted_pdu_json)
|
||||
for p in pdus_to_check_event_id
|
||||
])
|
||||
more_deferreds = keyring.verify_json_objects_for_server(
|
||||
[
|
||||
(
|
||||
get_domain_from_id(p.pdu.event_id),
|
||||
p.redacted_pdu_json,
|
||||
p.pdu.origin_server_ts if v.enforce_key_validity else 0,
|
||||
)
|
||||
for p in pdus_to_check_event_id
|
||||
]
|
||||
)
|
||||
|
||||
def event_err(e, pdu_to_check):
|
||||
errmsg = (
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
import copy
|
||||
import itertools
|
||||
import logging
|
||||
import random
|
||||
|
||||
from six.moves import range
|
||||
|
||||
@@ -233,7 +232,8 @@ class FederationClient(FederationBase):
|
||||
moving to the next destination. None indicates no timeout.
|
||||
|
||||
Returns:
|
||||
Deferred: Results in the requested PDU.
|
||||
Deferred: Results in the requested PDU, or None if we were unable to find
|
||||
it.
|
||||
"""
|
||||
|
||||
# TODO: Rate limit the number of times we try and get the same event.
|
||||
@@ -258,7 +258,12 @@ class FederationClient(FederationBase):
|
||||
destination, event_id, timeout=timeout,
|
||||
)
|
||||
|
||||
logger.debug("transaction_data %r", transaction_data)
|
||||
logger.debug(
|
||||
"retrieved event id %s from %s: %r",
|
||||
event_id,
|
||||
destination,
|
||||
transaction_data,
|
||||
)
|
||||
|
||||
pdu_list = [
|
||||
event_from_pdu_json(p, format_ver, outlier=outlier)
|
||||
@@ -280,6 +285,7 @@ class FederationClient(FederationBase):
|
||||
"Failed to get PDU %s from %s because %s",
|
||||
event_id, destination, e,
|
||||
)
|
||||
continue
|
||||
except NotRetryingDestination as e:
|
||||
logger.info(str(e))
|
||||
continue
|
||||
@@ -326,12 +332,16 @@ class FederationClient(FederationBase):
|
||||
state_event_ids = result["pdu_ids"]
|
||||
auth_event_ids = result.get("auth_chain_ids", [])
|
||||
|
||||
fetched_events, failed_to_fetch = yield self.get_events(
|
||||
[destination], room_id, set(state_event_ids + auth_event_ids)
|
||||
fetched_events, failed_to_fetch = yield self.get_events_from_store_or_dest(
|
||||
destination, room_id, set(state_event_ids + auth_event_ids)
|
||||
)
|
||||
|
||||
if failed_to_fetch:
|
||||
logger.warn("Failed to get %r", failed_to_fetch)
|
||||
logger.warning(
|
||||
"Failed to fetch missing state/auth events for %s: %s",
|
||||
room_id,
|
||||
failed_to_fetch
|
||||
)
|
||||
|
||||
event_map = {
|
||||
ev.event_id: ev for ev in fetched_events
|
||||
@@ -397,27 +407,20 @@ class FederationClient(FederationBase):
|
||||
defer.returnValue((signed_pdus, signed_auth))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_events(self, destinations, room_id, event_ids, return_local=True):
|
||||
"""Fetch events from some remote destinations, checking if we already
|
||||
have them.
|
||||
def get_events_from_store_or_dest(self, destination, room_id, event_ids):
|
||||
"""Fetch events from a remote destination, checking if we already have them.
|
||||
|
||||
Args:
|
||||
destinations (list)
|
||||
destination (str)
|
||||
room_id (str)
|
||||
event_ids (list)
|
||||
return_local (bool): Whether to include events we already have in
|
||||
the DB in the returned list of events
|
||||
|
||||
Returns:
|
||||
Deferred: A deferred resolving to a 2-tuple where the first is a list of
|
||||
events and the second is a list of event ids that we failed to fetch.
|
||||
"""
|
||||
if return_local:
|
||||
seen_events = yield self.store.get_events(event_ids, allow_rejected=True)
|
||||
signed_events = list(seen_events.values())
|
||||
else:
|
||||
seen_events = yield self.store.have_seen_events(event_ids)
|
||||
signed_events = []
|
||||
seen_events = yield self.store.get_events(event_ids, allow_rejected=True)
|
||||
signed_events = list(seen_events.values())
|
||||
|
||||
failed_to_fetch = set()
|
||||
|
||||
@@ -428,10 +431,11 @@ class FederationClient(FederationBase):
|
||||
if not missing_events:
|
||||
defer.returnValue((signed_events, failed_to_fetch))
|
||||
|
||||
def random_server_list():
|
||||
srvs = list(destinations)
|
||||
random.shuffle(srvs)
|
||||
return srvs
|
||||
logger.debug(
|
||||
"Fetching unknown state/auth events %s for room %s",
|
||||
missing_events,
|
||||
event_ids,
|
||||
)
|
||||
|
||||
room_version = yield self.store.get_room_version(room_id)
|
||||
|
||||
@@ -443,7 +447,7 @@ class FederationClient(FederationBase):
|
||||
deferreds = [
|
||||
run_in_background(
|
||||
self.get_pdu,
|
||||
destinations=random_server_list(),
|
||||
destinations=[destination],
|
||||
event_id=e_id,
|
||||
room_version=room_version,
|
||||
)
|
||||
|
||||
@@ -23,7 +23,11 @@ from twisted.internet import defer
|
||||
import synapse
|
||||
from synapse.api.errors import Codes, FederationDeniedError, SynapseError
|
||||
from synapse.api.room_versions import RoomVersions
|
||||
from synapse.api.urls import FEDERATION_V1_PREFIX, FEDERATION_V2_PREFIX
|
||||
from synapse.api.urls import (
|
||||
FEDERATION_UNSTABLE_PREFIX,
|
||||
FEDERATION_V1_PREFIX,
|
||||
FEDERATION_V2_PREFIX,
|
||||
)
|
||||
from synapse.http.endpoint import parse_and_validate_server_name
|
||||
from synapse.http.server import JsonResource
|
||||
from synapse.http.servlet import (
|
||||
@@ -90,6 +94,7 @@ class NoAuthenticationError(AuthenticationError):
|
||||
|
||||
class Authenticator(object):
|
||||
def __init__(self, hs):
|
||||
self._clock = hs.get_clock()
|
||||
self.keyring = hs.get_keyring()
|
||||
self.server_name = hs.hostname
|
||||
self.store = hs.get_datastore()
|
||||
@@ -98,6 +103,7 @@ class Authenticator(object):
|
||||
# A method just so we can pass 'self' as the authenticator to the Servlets
|
||||
@defer.inlineCallbacks
|
||||
def authenticate_request(self, request, content):
|
||||
now = self._clock.time_msec()
|
||||
json_request = {
|
||||
"method": request.method.decode('ascii'),
|
||||
"uri": request.uri.decode('ascii'),
|
||||
@@ -134,7 +140,7 @@ class Authenticator(object):
|
||||
401, "Missing Authorization headers", Codes.UNAUTHORIZED,
|
||||
)
|
||||
|
||||
yield self.keyring.verify_json_for_server(origin, json_request)
|
||||
yield self.keyring.verify_json_for_server(origin, json_request, now)
|
||||
|
||||
logger.info("Request from %s", origin)
|
||||
request.authenticated_entity = origin
|
||||
@@ -1304,6 +1310,30 @@ class FederationGroupsSettingJoinPolicyServlet(BaseFederationServlet):
|
||||
defer.returnValue((200, new_content))
|
||||
|
||||
|
||||
class RoomComplexityServlet(BaseFederationServlet):
|
||||
"""
|
||||
Indicates to other servers how complex (and therefore likely
|
||||
resource-intensive) a public room this server knows about is.
|
||||
"""
|
||||
PATH = "/rooms/(?P<room_id>[^/]*)/complexity"
|
||||
PREFIX = FEDERATION_UNSTABLE_PREFIX
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, origin, content, query, room_id):
|
||||
|
||||
store = self.handler.hs.get_datastore()
|
||||
|
||||
is_public = yield store.is_room_world_readable_or_publicly_joinable(
|
||||
room_id
|
||||
)
|
||||
|
||||
if not is_public:
|
||||
raise SynapseError(404, "Room not found", errcode=Codes.INVALID_PARAM)
|
||||
|
||||
complexity = yield store.get_room_complexity(room_id)
|
||||
defer.returnValue((200, complexity))
|
||||
|
||||
|
||||
FEDERATION_SERVLET_CLASSES = (
|
||||
FederationSendServlet,
|
||||
FederationEventServlet,
|
||||
@@ -1327,6 +1357,7 @@ FEDERATION_SERVLET_CLASSES = (
|
||||
FederationThirdPartyInviteExchangeServlet,
|
||||
On3pidBindServlet,
|
||||
FederationVersionServlet,
|
||||
RoomComplexityServlet,
|
||||
)
|
||||
|
||||
OPENID_SERVLET_CLASSES = (
|
||||
|
||||
@@ -97,10 +97,11 @@ class GroupAttestationSigning(object):
|
||||
|
||||
# TODO: We also want to check that *new* attestations that people give
|
||||
# us to store are valid for at least a little while.
|
||||
if valid_until_ms < self.clock.time_msec():
|
||||
now = self.clock.time_msec()
|
||||
if valid_until_ms < now:
|
||||
raise SynapseError(400, "Attestation expired")
|
||||
|
||||
yield self.keyring.verify_json_for_server(server_name, attestation)
|
||||
yield self.keyring.verify_json_for_server(server_name, attestation, now)
|
||||
|
||||
def create_attestation(self, group_id, user_id):
|
||||
"""Create an attestation for the group_id and user_id with default
|
||||
|
||||
@@ -122,6 +122,9 @@ class EventStreamHandler(BaseHandler):
|
||||
|
||||
chunks = yield self._event_serializer.serialize_events(
|
||||
events, time_now, as_client_event=as_client_event,
|
||||
# We don't bundle "live" events, as otherwise clients
|
||||
# will end up double counting annotations.
|
||||
bundle_aggregations=False,
|
||||
)
|
||||
|
||||
chunk = {
|
||||
|
||||
+222
-127
@@ -35,6 +35,7 @@ from synapse.api.errors import (
|
||||
CodeMessageException,
|
||||
FederationDeniedError,
|
||||
FederationError,
|
||||
RequestSendFailed,
|
||||
StoreError,
|
||||
SynapseError,
|
||||
)
|
||||
@@ -2013,15 +2014,65 @@ class FederationHandler(BaseHandler):
|
||||
|
||||
Args:
|
||||
origin (str):
|
||||
event (synapse.events.FrozenEvent):
|
||||
event (synapse.events.EventBase):
|
||||
context (synapse.events.snapshot.EventContext):
|
||||
auth_events (dict[(str, str)->str]):
|
||||
auth_events (dict[(str, str)->synapse.events.EventBase]):
|
||||
Map from (event_type, state_key) to event
|
||||
|
||||
What we expect the event's auth_events to be, based on the event's
|
||||
position in the dag. I think? maybe??
|
||||
|
||||
Also NB that this function adds entries to it.
|
||||
Returns:
|
||||
defer.Deferred[None]
|
||||
"""
|
||||
room_version = yield self.store.get_room_version(event.room_id)
|
||||
|
||||
try:
|
||||
yield self._update_auth_events_and_context_for_auth(
|
||||
origin, event, context, auth_events
|
||||
)
|
||||
except Exception:
|
||||
# We don't really mind if the above fails, so lets not fail
|
||||
# processing if it does. However, it really shouldn't fail so
|
||||
# let's still log as an exception since we'll still want to fix
|
||||
# any bugs.
|
||||
logger.exception(
|
||||
"Failed to double check auth events for %s with remote. "
|
||||
"Ignoring failure and continuing processing of event.",
|
||||
event.event_id,
|
||||
)
|
||||
|
||||
try:
|
||||
self.auth.check(room_version, event, auth_events=auth_events)
|
||||
except AuthError as e:
|
||||
logger.warn("Failed auth resolution for %r because %s", event, e)
|
||||
raise e
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _update_auth_events_and_context_for_auth(
|
||||
self, origin, event, context, auth_events
|
||||
):
|
||||
"""Helper for do_auth. See there for docs.
|
||||
|
||||
Checks whether a given event has the expected auth events. If it
|
||||
doesn't then we talk to the remote server to compare state to see if
|
||||
we can come to a consensus (e.g. if one server missed some valid
|
||||
state).
|
||||
|
||||
This attempts to resovle any potential divergence of state between
|
||||
servers, but is not essential and so failures should not block further
|
||||
processing of the event.
|
||||
|
||||
Args:
|
||||
origin (str):
|
||||
event (synapse.events.EventBase):
|
||||
context (synapse.events.snapshot.EventContext):
|
||||
auth_events (dict[(str, str)->synapse.events.EventBase]):
|
||||
|
||||
Returns:
|
||||
defer.Deferred[None]
|
||||
"""
|
||||
# Check if we have all the auth events.
|
||||
current_state = set(e.event_id for e in auth_events.values())
|
||||
event_auth_events = set(event.auth_event_ids())
|
||||
|
||||
if event.is_state():
|
||||
@@ -2029,11 +2080,21 @@ class FederationHandler(BaseHandler):
|
||||
else:
|
||||
event_key = None
|
||||
|
||||
if event_auth_events - current_state:
|
||||
# if the event's auth_events refers to events which are not in our
|
||||
# calculated auth_events, we need to fetch those events from somewhere.
|
||||
#
|
||||
# we start by fetching them from the store, and then try calling /event_auth/.
|
||||
missing_auth = event_auth_events.difference(
|
||||
e.event_id for e in auth_events.values()
|
||||
)
|
||||
|
||||
if missing_auth:
|
||||
# TODO: can we use store.have_seen_events here instead?
|
||||
have_events = yield self.store.get_seen_events_with_rejections(
|
||||
event_auth_events - current_state
|
||||
missing_auth
|
||||
)
|
||||
logger.debug("Got events %s from store", have_events)
|
||||
missing_auth.difference_update(have_events.keys())
|
||||
else:
|
||||
have_events = {}
|
||||
|
||||
@@ -2042,17 +2103,22 @@ class FederationHandler(BaseHandler):
|
||||
for e in auth_events.values()
|
||||
})
|
||||
|
||||
seen_events = set(have_events.keys())
|
||||
|
||||
missing_auth = event_auth_events - seen_events - current_state
|
||||
|
||||
if missing_auth:
|
||||
logger.info("Missing auth: %s", missing_auth)
|
||||
# If we don't have all the auth events, we need to get them.
|
||||
logger.info(
|
||||
"auth_events contains unknown events: %s",
|
||||
missing_auth,
|
||||
)
|
||||
try:
|
||||
remote_auth_chain = yield self.federation_client.get_event_auth(
|
||||
origin, event.room_id, event.event_id
|
||||
)
|
||||
try:
|
||||
remote_auth_chain = yield self.federation_client.get_event_auth(
|
||||
origin, event.room_id, event.event_id
|
||||
)
|
||||
except RequestSendFailed as e:
|
||||
# The other side isn't around or doesn't implement the
|
||||
# endpoint, so lets just bail out.
|
||||
logger.info("Failed to get event auth from remote: %s", e)
|
||||
return
|
||||
|
||||
seen_remotes = yield self.store.have_seen_events(
|
||||
[e.event_id for e in remote_auth_chain]
|
||||
@@ -2089,145 +2155,174 @@ class FederationHandler(BaseHandler):
|
||||
have_events = yield self.store.get_seen_events_with_rejections(
|
||||
event.auth_event_ids()
|
||||
)
|
||||
seen_events = set(have_events.keys())
|
||||
except Exception:
|
||||
# FIXME:
|
||||
logger.exception("Failed to get auth chain")
|
||||
|
||||
if event.internal_metadata.is_outlier():
|
||||
logger.info("Skipping auth_event fetch for outlier")
|
||||
return
|
||||
|
||||
# FIXME: Assumes we have and stored all the state for all the
|
||||
# prev_events
|
||||
current_state = set(e.event_id for e in auth_events.values())
|
||||
different_auth = event_auth_events - current_state
|
||||
different_auth = event_auth_events.difference(
|
||||
e.event_id for e in auth_events.values()
|
||||
)
|
||||
|
||||
if not different_auth:
|
||||
return
|
||||
|
||||
logger.info(
|
||||
"auth_events refers to events which are not in our calculated auth "
|
||||
"chain: %s",
|
||||
different_auth,
|
||||
)
|
||||
|
||||
room_version = yield self.store.get_room_version(event.room_id)
|
||||
|
||||
if different_auth and not event.internal_metadata.is_outlier():
|
||||
# Do auth conflict res.
|
||||
logger.info("Different auth: %s", different_auth)
|
||||
|
||||
different_events = yield logcontext.make_deferred_yieldable(
|
||||
defer.gatherResults([
|
||||
logcontext.run_in_background(
|
||||
self.store.get_event,
|
||||
d,
|
||||
allow_none=True,
|
||||
allow_rejected=False,
|
||||
)
|
||||
for d in different_auth
|
||||
if d in have_events and not have_events[d]
|
||||
], consumeErrors=True)
|
||||
).addErrback(unwrapFirstError)
|
||||
|
||||
if different_events:
|
||||
local_view = dict(auth_events)
|
||||
remote_view = dict(auth_events)
|
||||
remote_view.update({
|
||||
(d.type, d.state_key): d for d in different_events if d
|
||||
})
|
||||
|
||||
new_state = yield self.state_handler.resolve_events(
|
||||
room_version,
|
||||
[list(local_view.values()), list(remote_view.values())],
|
||||
event
|
||||
different_events = yield logcontext.make_deferred_yieldable(
|
||||
defer.gatherResults([
|
||||
logcontext.run_in_background(
|
||||
self.store.get_event,
|
||||
d,
|
||||
allow_none=True,
|
||||
allow_rejected=False,
|
||||
)
|
||||
for d in different_auth
|
||||
if d in have_events and not have_events[d]
|
||||
], consumeErrors=True)
|
||||
).addErrback(unwrapFirstError)
|
||||
|
||||
auth_events.update(new_state)
|
||||
if different_events:
|
||||
local_view = dict(auth_events)
|
||||
remote_view = dict(auth_events)
|
||||
remote_view.update({
|
||||
(d.type, d.state_key): d for d in different_events if d
|
||||
})
|
||||
|
||||
current_state = set(e.event_id for e in auth_events.values())
|
||||
different_auth = event_auth_events - current_state
|
||||
new_state = yield self.state_handler.resolve_events(
|
||||
room_version,
|
||||
[list(local_view.values()), list(remote_view.values())],
|
||||
event
|
||||
)
|
||||
|
||||
yield self._update_context_for_auth_events(
|
||||
event, context, auth_events, event_key,
|
||||
)
|
||||
logger.info(
|
||||
"After state res: updating auth_events with new state %s",
|
||||
{
|
||||
(d.type, d.state_key): d.event_id for d in new_state.values()
|
||||
if auth_events.get((d.type, d.state_key)) != d
|
||||
},
|
||||
)
|
||||
|
||||
if different_auth and not event.internal_metadata.is_outlier():
|
||||
logger.info("Different auth after resolution: %s", different_auth)
|
||||
auth_events.update(new_state)
|
||||
|
||||
# Only do auth resolution if we have something new to say.
|
||||
# We can't rove an auth failure.
|
||||
do_resolution = False
|
||||
different_auth = event_auth_events.difference(
|
||||
e.event_id for e in auth_events.values()
|
||||
)
|
||||
|
||||
provable = [
|
||||
RejectedReason.NOT_ANCESTOR, RejectedReason.NOT_ANCESTOR,
|
||||
]
|
||||
yield self._update_context_for_auth_events(
|
||||
event, context, auth_events, event_key,
|
||||
)
|
||||
|
||||
for e_id in different_auth:
|
||||
if e_id in have_events:
|
||||
if have_events[e_id] in provable:
|
||||
do_resolution = True
|
||||
break
|
||||
if not different_auth:
|
||||
# we're done
|
||||
return
|
||||
|
||||
if do_resolution:
|
||||
prev_state_ids = yield context.get_prev_state_ids(self.store)
|
||||
# 1. Get what we think is the auth chain.
|
||||
auth_ids = yield self.auth.compute_auth_events(
|
||||
event, prev_state_ids
|
||||
)
|
||||
local_auth_chain = yield self.store.get_auth_chain(
|
||||
auth_ids, include_given=True
|
||||
)
|
||||
logger.info(
|
||||
"auth_events still refers to events which are not in the calculated auth "
|
||||
"chain after state resolution: %s",
|
||||
different_auth,
|
||||
)
|
||||
|
||||
try:
|
||||
# 2. Get remote difference.
|
||||
result = yield self.federation_client.query_auth(
|
||||
origin,
|
||||
event.room_id,
|
||||
event.event_id,
|
||||
local_auth_chain,
|
||||
)
|
||||
# Only do auth resolution if we have something new to say.
|
||||
# We can't prove an auth failure.
|
||||
do_resolution = False
|
||||
|
||||
seen_remotes = yield self.store.have_seen_events(
|
||||
[e.event_id for e in result["auth_chain"]]
|
||||
)
|
||||
for e_id in different_auth:
|
||||
if e_id in have_events:
|
||||
if have_events[e_id] == RejectedReason.NOT_ANCESTOR:
|
||||
do_resolution = True
|
||||
break
|
||||
|
||||
# 3. Process any remote auth chain events we haven't seen.
|
||||
for ev in result["auth_chain"]:
|
||||
if ev.event_id in seen_remotes:
|
||||
continue
|
||||
if not do_resolution:
|
||||
logger.info(
|
||||
"Skipping auth resolution due to lack of provable rejection reasons"
|
||||
)
|
||||
return
|
||||
|
||||
if ev.event_id == event.event_id:
|
||||
continue
|
||||
logger.info("Doing auth resolution")
|
||||
|
||||
try:
|
||||
auth_ids = ev.auth_event_ids()
|
||||
auth = {
|
||||
(e.type, e.state_key): e
|
||||
for e in result["auth_chain"]
|
||||
if e.event_id in auth_ids
|
||||
or event.type == EventTypes.Create
|
||||
}
|
||||
ev.internal_metadata.outlier = True
|
||||
prev_state_ids = yield context.get_prev_state_ids(self.store)
|
||||
|
||||
logger.debug(
|
||||
"do_auth %s different_auth: %s",
|
||||
event.event_id, e.event_id
|
||||
)
|
||||
|
||||
yield self._handle_new_event(
|
||||
origin, ev, auth_events=auth
|
||||
)
|
||||
|
||||
if ev.event_id in event_auth_events:
|
||||
auth_events[(ev.type, ev.state_key)] = ev
|
||||
except AuthError:
|
||||
pass
|
||||
|
||||
except Exception:
|
||||
# FIXME:
|
||||
logger.exception("Failed to query auth chain")
|
||||
|
||||
# 4. Look at rejects and their proofs.
|
||||
# TODO.
|
||||
|
||||
yield self._update_context_for_auth_events(
|
||||
event, context, auth_events, event_key,
|
||||
)
|
||||
# 1. Get what we think is the auth chain.
|
||||
auth_ids = yield self.auth.compute_auth_events(
|
||||
event, prev_state_ids
|
||||
)
|
||||
local_auth_chain = yield self.store.get_auth_chain(
|
||||
auth_ids, include_given=True
|
||||
)
|
||||
|
||||
try:
|
||||
self.auth.check(room_version, event, auth_events=auth_events)
|
||||
except AuthError as e:
|
||||
logger.warn("Failed auth resolution for %r because %s", event, e)
|
||||
raise e
|
||||
# 2. Get remote difference.
|
||||
try:
|
||||
result = yield self.federation_client.query_auth(
|
||||
origin,
|
||||
event.room_id,
|
||||
event.event_id,
|
||||
local_auth_chain,
|
||||
)
|
||||
except RequestSendFailed as e:
|
||||
# The other side isn't around or doesn't implement the
|
||||
# endpoint, so lets just bail out.
|
||||
logger.info("Failed to query auth from remote: %s", e)
|
||||
return
|
||||
|
||||
seen_remotes = yield self.store.have_seen_events(
|
||||
[e.event_id for e in result["auth_chain"]]
|
||||
)
|
||||
|
||||
# 3. Process any remote auth chain events we haven't seen.
|
||||
for ev in result["auth_chain"]:
|
||||
if ev.event_id in seen_remotes:
|
||||
continue
|
||||
|
||||
if ev.event_id == event.event_id:
|
||||
continue
|
||||
|
||||
try:
|
||||
auth_ids = ev.auth_event_ids()
|
||||
auth = {
|
||||
(e.type, e.state_key): e
|
||||
for e in result["auth_chain"]
|
||||
if e.event_id in auth_ids
|
||||
or event.type == EventTypes.Create
|
||||
}
|
||||
ev.internal_metadata.outlier = True
|
||||
|
||||
logger.debug(
|
||||
"do_auth %s different_auth: %s",
|
||||
event.event_id, e.event_id
|
||||
)
|
||||
|
||||
yield self._handle_new_event(
|
||||
origin, ev, auth_events=auth
|
||||
)
|
||||
|
||||
if ev.event_id in event_auth_events:
|
||||
auth_events[(ev.type, ev.state_key)] = ev
|
||||
except AuthError:
|
||||
pass
|
||||
|
||||
except Exception:
|
||||
# FIXME:
|
||||
logger.exception("Failed to query auth chain")
|
||||
|
||||
# 4. Look at rejects and their proofs.
|
||||
# TODO.
|
||||
|
||||
yield self._update_context_for_auth_events(
|
||||
event, context, auth_events, event_key,
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _update_context_for_auth_events(self, event, context, auth_events,
|
||||
|
||||
@@ -166,6 +166,9 @@ class MessageHandler(object):
|
||||
now = self.clock.time_msec()
|
||||
events = yield self._event_serializer.serialize_events(
|
||||
room_state.values(), now,
|
||||
# We don't bother bundling aggregations in when asked for state
|
||||
# events, as clients won't use them.
|
||||
bundle_aggregations=False,
|
||||
)
|
||||
defer.returnValue(events)
|
||||
|
||||
|
||||
@@ -182,17 +182,27 @@ class PresenceHandler(object):
|
||||
# Start a LoopingCall in 30s that fires every 5s.
|
||||
# The initial delay is to allow disconnected clients a chance to
|
||||
# reconnect before we treat them as offline.
|
||||
def run_timeout_handler():
|
||||
return run_as_background_process(
|
||||
"handle_presence_timeouts", self._handle_timeouts
|
||||
)
|
||||
|
||||
self.clock.call_later(
|
||||
30,
|
||||
self.clock.looping_call,
|
||||
self._handle_timeouts,
|
||||
run_timeout_handler,
|
||||
5000,
|
||||
)
|
||||
|
||||
def run_persister():
|
||||
return run_as_background_process(
|
||||
"persist_presence_changes", self._persist_unpersisted_changes
|
||||
)
|
||||
|
||||
self.clock.call_later(
|
||||
60,
|
||||
self.clock.looping_call,
|
||||
self._persist_unpersisted_changes,
|
||||
run_persister,
|
||||
60 * 1000,
|
||||
)
|
||||
|
||||
@@ -229,6 +239,7 @@ class PresenceHandler(object):
|
||||
)
|
||||
|
||||
if self.unpersisted_users_changes:
|
||||
|
||||
yield self.store.update_presence([
|
||||
self.user_to_current_state[user_id]
|
||||
for user_id in self.unpersisted_users_changes
|
||||
@@ -240,30 +251,18 @@ class PresenceHandler(object):
|
||||
"""We periodically persist the unpersisted changes, as otherwise they
|
||||
may stack up and slow down shutdown times.
|
||||
"""
|
||||
logger.info(
|
||||
"Performing _persist_unpersisted_changes. Persisting %d unpersisted changes",
|
||||
len(self.unpersisted_users_changes)
|
||||
)
|
||||
|
||||
unpersisted = self.unpersisted_users_changes
|
||||
self.unpersisted_users_changes = set()
|
||||
|
||||
if unpersisted:
|
||||
logger.info(
|
||||
"Persisting %d upersisted presence updates", len(unpersisted)
|
||||
)
|
||||
yield self.store.update_presence([
|
||||
self.user_to_current_state[user_id]
|
||||
for user_id in unpersisted
|
||||
])
|
||||
|
||||
logger.info("Finished _persist_unpersisted_changes")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _update_states_and_catch_exception(self, new_states):
|
||||
try:
|
||||
res = yield self._update_states(new_states)
|
||||
defer.returnValue(res)
|
||||
except Exception:
|
||||
logger.exception("Error updating presence")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _update_states(self, new_states):
|
||||
"""Updates presence of users. Sets the appropriate timeouts. Pokes
|
||||
@@ -338,45 +337,41 @@ class PresenceHandler(object):
|
||||
logger.info("Handling presence timeouts")
|
||||
now = self.clock.time_msec()
|
||||
|
||||
try:
|
||||
with Measure(self.clock, "presence_handle_timeouts"):
|
||||
# Fetch the list of users that *may* have timed out. Things may have
|
||||
# changed since the timeout was set, so we won't necessarily have to
|
||||
# take any action.
|
||||
users_to_check = set(self.wheel_timer.fetch(now))
|
||||
# Fetch the list of users that *may* have timed out. Things may have
|
||||
# changed since the timeout was set, so we won't necessarily have to
|
||||
# take any action.
|
||||
users_to_check = set(self.wheel_timer.fetch(now))
|
||||
|
||||
# Check whether the lists of syncing processes from an external
|
||||
# process have expired.
|
||||
expired_process_ids = [
|
||||
process_id for process_id, last_update
|
||||
in self.external_process_last_updated_ms.items()
|
||||
if now - last_update > EXTERNAL_PROCESS_EXPIRY
|
||||
]
|
||||
for process_id in expired_process_ids:
|
||||
users_to_check.update(
|
||||
self.external_process_last_updated_ms.pop(process_id, ())
|
||||
)
|
||||
self.external_process_last_update.pop(process_id)
|
||||
# Check whether the lists of syncing processes from an external
|
||||
# process have expired.
|
||||
expired_process_ids = [
|
||||
process_id for process_id, last_update
|
||||
in self.external_process_last_updated_ms.items()
|
||||
if now - last_update > EXTERNAL_PROCESS_EXPIRY
|
||||
]
|
||||
for process_id in expired_process_ids:
|
||||
users_to_check.update(
|
||||
self.external_process_last_updated_ms.pop(process_id, ())
|
||||
)
|
||||
self.external_process_last_update.pop(process_id)
|
||||
|
||||
states = [
|
||||
self.user_to_current_state.get(
|
||||
user_id, UserPresenceState.default(user_id)
|
||||
)
|
||||
for user_id in users_to_check
|
||||
]
|
||||
states = [
|
||||
self.user_to_current_state.get(
|
||||
user_id, UserPresenceState.default(user_id)
|
||||
)
|
||||
for user_id in users_to_check
|
||||
]
|
||||
|
||||
timers_fired_counter.inc(len(states))
|
||||
timers_fired_counter.inc(len(states))
|
||||
|
||||
changes = handle_timeouts(
|
||||
states,
|
||||
is_mine_fn=self.is_mine_id,
|
||||
syncing_user_ids=self.get_currently_syncing_users(),
|
||||
now=now,
|
||||
)
|
||||
changes = handle_timeouts(
|
||||
states,
|
||||
is_mine_fn=self.is_mine_id,
|
||||
syncing_user_ids=self.get_currently_syncing_users(),
|
||||
now=now,
|
||||
)
|
||||
|
||||
run_in_background(self._update_states_and_catch_exception, changes)
|
||||
except Exception:
|
||||
logger.exception("Exception in _handle_timeouts loop")
|
||||
return self._update_states(changes)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def bump_presence_active_time(self, user):
|
||||
@@ -833,14 +828,17 @@ class PresenceHandler(object):
|
||||
# joins.
|
||||
continue
|
||||
|
||||
event = yield self.store.get_event(event_id)
|
||||
if event.content.get("membership") != Membership.JOIN:
|
||||
event = yield self.store.get_event(event_id, allow_none=True)
|
||||
if not event or event.content.get("membership") != Membership.JOIN:
|
||||
# We only care about joins
|
||||
continue
|
||||
|
||||
if prev_event_id:
|
||||
prev_event = yield self.store.get_event(prev_event_id)
|
||||
if prev_event.content.get("membership") == Membership.JOIN:
|
||||
prev_event = yield self.store.get_event(prev_event_id, allow_none=True)
|
||||
if (
|
||||
prev_event
|
||||
and prev_event.content.get("membership") == Membership.JOIN
|
||||
):
|
||||
# Ignore changes to join events.
|
||||
continue
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ from ._base import BaseHandler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MAX_DISPLAYNAME_LEN = 100
|
||||
MAX_AVATAR_URL_LEN = 1000
|
||||
|
||||
|
||||
class BaseProfileHandler(BaseHandler):
|
||||
"""Handles fetching and updating user profile information.
|
||||
@@ -162,6 +165,11 @@ class BaseProfileHandler(BaseHandler):
|
||||
if not by_admin and target_user != requester.user:
|
||||
raise AuthError(400, "Cannot set another user's displayname")
|
||||
|
||||
if len(new_displayname) > MAX_DISPLAYNAME_LEN:
|
||||
raise SynapseError(
|
||||
400, "Displayname is too long (max %i)" % (MAX_DISPLAYNAME_LEN, ),
|
||||
)
|
||||
|
||||
if new_displayname == '':
|
||||
new_displayname = None
|
||||
|
||||
@@ -217,6 +225,11 @@ class BaseProfileHandler(BaseHandler):
|
||||
if not by_admin and target_user != requester.user:
|
||||
raise AuthError(400, "Cannot set another user's avatar_url")
|
||||
|
||||
if len(new_avatar_url) > MAX_AVATAR_URL_LEN:
|
||||
raise SynapseError(
|
||||
400, "Avatar URL is too long (max %i)" % (MAX_AVATAR_URL_LEN, ),
|
||||
)
|
||||
|
||||
yield self.store.set_profile_avatar_url(
|
||||
target_user.localpart, new_avatar_url
|
||||
)
|
||||
|
||||
@@ -531,6 +531,8 @@ class RegistrationHandler(BaseHandler):
|
||||
A tuple of (user_id, access_token).
|
||||
Raises:
|
||||
RegistrationError if there was a problem registering.
|
||||
|
||||
NB this is only used in tests. TODO: move it to the test package!
|
||||
"""
|
||||
if localpart is None:
|
||||
raise SynapseError(400, "Request must include user id")
|
||||
|
||||
@@ -27,7 +27,7 @@ from twisted.internet import defer
|
||||
|
||||
from synapse.api.constants import EventTypes, JoinRules, RoomCreationPreset
|
||||
from synapse.api.errors import AuthError, Codes, NotFoundError, StoreError, SynapseError
|
||||
from synapse.api.room_versions import DEFAULT_ROOM_VERSION, KNOWN_ROOM_VERSIONS
|
||||
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
|
||||
from synapse.storage.state import StateFilter
|
||||
from synapse.types import RoomAlias, RoomID, RoomStreamToken, StreamToken, UserID
|
||||
from synapse.util import stringutils
|
||||
@@ -70,6 +70,7 @@ class RoomCreationHandler(BaseHandler):
|
||||
self.spam_checker = hs.get_spam_checker()
|
||||
self.event_creation_handler = hs.get_event_creation_handler()
|
||||
self.room_member_handler = hs.get_room_member_handler()
|
||||
self.config = hs.config
|
||||
|
||||
# linearizer to stop two upgrades happening at once
|
||||
self._upgrade_linearizer = Linearizer("room_upgrade_linearizer")
|
||||
@@ -475,7 +476,11 @@ class RoomCreationHandler(BaseHandler):
|
||||
if ratelimit:
|
||||
yield self.ratelimit(requester)
|
||||
|
||||
room_version = config.get("room_version", DEFAULT_ROOM_VERSION.identifier)
|
||||
room_version = config.get(
|
||||
"room_version",
|
||||
self.config.default_room_version.identifier,
|
||||
)
|
||||
|
||||
if not isinstance(room_version, string_types):
|
||||
raise SynapseError(
|
||||
400,
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2018 New Vector Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.constants import EventTypes, JoinRules, Membership
|
||||
from synapse.handlers.state_deltas import StateDeltasHandler
|
||||
from synapse.metrics import event_processing_positions
|
||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||
from synapse.types import UserID
|
||||
from synapse.util.metrics import Measure
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StatsHandler(StateDeltasHandler):
|
||||
"""Handles keeping the *_stats tables updated with a simple time-series of
|
||||
information about the users, rooms and media on the server, such that admins
|
||||
have some idea of who is consuming their resources.
|
||||
|
||||
Heavily derived from UserDirectoryHandler
|
||||
"""
|
||||
|
||||
def __init__(self, hs):
|
||||
super(StatsHandler, self).__init__(hs)
|
||||
self.hs = hs
|
||||
self.store = hs.get_datastore()
|
||||
self.state = hs.get_state_handler()
|
||||
self.server_name = hs.hostname
|
||||
self.clock = hs.get_clock()
|
||||
self.notifier = hs.get_notifier()
|
||||
self.is_mine_id = hs.is_mine_id
|
||||
self.stats_bucket_size = hs.config.stats_bucket_size
|
||||
|
||||
# The current position in the current_state_delta stream
|
||||
self.pos = None
|
||||
|
||||
# Guard to ensure we only process deltas one at a time
|
||||
self._is_processing = False
|
||||
|
||||
if hs.config.stats_enabled:
|
||||
self.notifier.add_replication_callback(self.notify_new_event)
|
||||
|
||||
# We kick this off so that we don't have to wait for a change before
|
||||
# we start populating stats
|
||||
self.clock.call_later(0, self.notify_new_event)
|
||||
|
||||
def notify_new_event(self):
|
||||
"""Called when there may be more deltas to process
|
||||
"""
|
||||
if not self.hs.config.stats_enabled:
|
||||
return
|
||||
|
||||
if self._is_processing:
|
||||
return
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def process():
|
||||
try:
|
||||
yield self._unsafe_process()
|
||||
finally:
|
||||
self._is_processing = False
|
||||
|
||||
self._is_processing = True
|
||||
run_as_background_process("stats.notify_new_event", process)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _unsafe_process(self):
|
||||
# If self.pos is None then means we haven't fetched it from DB
|
||||
if self.pos is None:
|
||||
self.pos = yield self.store.get_stats_stream_pos()
|
||||
|
||||
# If still None then the initial background update hasn't happened yet
|
||||
if self.pos is None:
|
||||
defer.returnValue(None)
|
||||
|
||||
# Loop round handling deltas until we're up to date
|
||||
while True:
|
||||
with Measure(self.clock, "stats_delta"):
|
||||
deltas = yield self.store.get_current_state_deltas(self.pos)
|
||||
if not deltas:
|
||||
return
|
||||
|
||||
logger.info("Handling %d state deltas", len(deltas))
|
||||
yield self._handle_deltas(deltas)
|
||||
|
||||
self.pos = deltas[-1]["stream_id"]
|
||||
yield self.store.update_stats_stream_pos(self.pos)
|
||||
|
||||
event_processing_positions.labels("stats").set(self.pos)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _handle_deltas(self, deltas):
|
||||
"""
|
||||
Called with the state deltas to process
|
||||
"""
|
||||
for delta in deltas:
|
||||
typ = delta["type"]
|
||||
state_key = delta["state_key"]
|
||||
room_id = delta["room_id"]
|
||||
event_id = delta["event_id"]
|
||||
stream_id = delta["stream_id"]
|
||||
prev_event_id = delta["prev_event_id"]
|
||||
stream_pos = delta["stream_id"]
|
||||
|
||||
logger.debug("Handling: %r %r, %s", typ, state_key, event_id)
|
||||
|
||||
token = yield self.store.get_earliest_token_for_room_stats(room_id)
|
||||
|
||||
# If the earliest token to begin from is larger than our current
|
||||
# stream ID, skip processing this delta.
|
||||
if token is not None and token >= stream_id:
|
||||
logger.debug(
|
||||
"Ignoring: %s as earlier than this room's initial ingestion event",
|
||||
event_id,
|
||||
)
|
||||
continue
|
||||
|
||||
if event_id is None and prev_event_id is None:
|
||||
# Errr...
|
||||
continue
|
||||
|
||||
event_content = {}
|
||||
|
||||
if event_id is not None:
|
||||
event = yield self.store.get_event(event_id, allow_none=True)
|
||||
if event:
|
||||
event_content = event.content or {}
|
||||
|
||||
# We use stream_pos here rather than fetch by event_id as event_id
|
||||
# may be None
|
||||
now = yield self.store.get_received_ts_by_stream_pos(stream_pos)
|
||||
|
||||
# quantise time to the nearest bucket
|
||||
now = (now // 1000 // self.stats_bucket_size) * self.stats_bucket_size
|
||||
|
||||
if typ == EventTypes.Member:
|
||||
# we could use _get_key_change here but it's a bit inefficient
|
||||
# given we're not testing for a specific result; might as well
|
||||
# just grab the prev_membership and membership strings and
|
||||
# compare them.
|
||||
prev_event_content = {}
|
||||
if prev_event_id is not None:
|
||||
prev_event = yield self.store.get_event(
|
||||
prev_event_id, allow_none=True,
|
||||
)
|
||||
if prev_event:
|
||||
prev_event_content = prev_event.content
|
||||
|
||||
membership = event_content.get("membership", Membership.LEAVE)
|
||||
prev_membership = prev_event_content.get("membership", Membership.LEAVE)
|
||||
|
||||
if prev_membership == membership:
|
||||
continue
|
||||
|
||||
if prev_membership == Membership.JOIN:
|
||||
yield self.store.update_stats_delta(
|
||||
now, "room", room_id, "joined_members", -1
|
||||
)
|
||||
elif prev_membership == Membership.INVITE:
|
||||
yield self.store.update_stats_delta(
|
||||
now, "room", room_id, "invited_members", -1
|
||||
)
|
||||
elif prev_membership == Membership.LEAVE:
|
||||
yield self.store.update_stats_delta(
|
||||
now, "room", room_id, "left_members", -1
|
||||
)
|
||||
elif prev_membership == Membership.BAN:
|
||||
yield self.store.update_stats_delta(
|
||||
now, "room", room_id, "banned_members", -1
|
||||
)
|
||||
else:
|
||||
err = "%s is not a valid prev_membership" % (repr(prev_membership),)
|
||||
logger.error(err)
|
||||
raise ValueError(err)
|
||||
|
||||
if membership == Membership.JOIN:
|
||||
yield self.store.update_stats_delta(
|
||||
now, "room", room_id, "joined_members", +1
|
||||
)
|
||||
elif membership == Membership.INVITE:
|
||||
yield self.store.update_stats_delta(
|
||||
now, "room", room_id, "invited_members", +1
|
||||
)
|
||||
elif membership == Membership.LEAVE:
|
||||
yield self.store.update_stats_delta(
|
||||
now, "room", room_id, "left_members", +1
|
||||
)
|
||||
elif membership == Membership.BAN:
|
||||
yield self.store.update_stats_delta(
|
||||
now, "room", room_id, "banned_members", +1
|
||||
)
|
||||
else:
|
||||
err = "%s is not a valid membership" % (repr(membership),)
|
||||
logger.error(err)
|
||||
raise ValueError(err)
|
||||
|
||||
user_id = state_key
|
||||
if self.is_mine_id(user_id):
|
||||
# update user_stats as it's one of our users
|
||||
public = yield self._is_public_room(room_id)
|
||||
|
||||
if membership == Membership.LEAVE:
|
||||
yield self.store.update_stats_delta(
|
||||
now,
|
||||
"user",
|
||||
user_id,
|
||||
"public_rooms" if public else "private_rooms",
|
||||
-1,
|
||||
)
|
||||
elif membership == Membership.JOIN:
|
||||
yield self.store.update_stats_delta(
|
||||
now,
|
||||
"user",
|
||||
user_id,
|
||||
"public_rooms" if public else "private_rooms",
|
||||
+1,
|
||||
)
|
||||
|
||||
elif typ == EventTypes.Create:
|
||||
# Newly created room. Add it with all blank portions.
|
||||
yield self.store.update_room_state(
|
||||
room_id,
|
||||
{
|
||||
"join_rules": None,
|
||||
"history_visibility": None,
|
||||
"encryption": None,
|
||||
"name": None,
|
||||
"topic": None,
|
||||
"avatar": None,
|
||||
"canonical_alias": None,
|
||||
},
|
||||
)
|
||||
|
||||
elif typ == EventTypes.JoinRules:
|
||||
yield self.store.update_room_state(
|
||||
room_id, {"join_rules": event_content.get("join_rule")}
|
||||
)
|
||||
|
||||
is_public = yield self._get_key_change(
|
||||
prev_event_id, event_id, "join_rule", JoinRules.PUBLIC
|
||||
)
|
||||
if is_public is not None:
|
||||
yield self.update_public_room_stats(now, room_id, is_public)
|
||||
|
||||
elif typ == EventTypes.RoomHistoryVisibility:
|
||||
yield self.store.update_room_state(
|
||||
room_id,
|
||||
{"history_visibility": event_content.get("history_visibility")},
|
||||
)
|
||||
|
||||
is_public = yield self._get_key_change(
|
||||
prev_event_id, event_id, "history_visibility", "world_readable"
|
||||
)
|
||||
if is_public is not None:
|
||||
yield self.update_public_room_stats(now, room_id, is_public)
|
||||
|
||||
elif typ == EventTypes.Encryption:
|
||||
yield self.store.update_room_state(
|
||||
room_id, {"encryption": event_content.get("algorithm")}
|
||||
)
|
||||
elif typ == EventTypes.Name:
|
||||
yield self.store.update_room_state(
|
||||
room_id, {"name": event_content.get("name")}
|
||||
)
|
||||
elif typ == EventTypes.Topic:
|
||||
yield self.store.update_room_state(
|
||||
room_id, {"topic": event_content.get("topic")}
|
||||
)
|
||||
elif typ == EventTypes.RoomAvatar:
|
||||
yield self.store.update_room_state(
|
||||
room_id, {"avatar": event_content.get("url")}
|
||||
)
|
||||
elif typ == EventTypes.CanonicalAlias:
|
||||
yield self.store.update_room_state(
|
||||
room_id, {"canonical_alias": event_content.get("alias")}
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def update_public_room_stats(self, ts, room_id, is_public):
|
||||
"""
|
||||
Increment/decrement a user's number of public rooms when a room they are
|
||||
in changes to/from public visibility.
|
||||
|
||||
Args:
|
||||
ts (int): Timestamp in seconds
|
||||
room_id (str)
|
||||
is_public (bool)
|
||||
"""
|
||||
# For now, blindly iterate over all local users in the room so that
|
||||
# we can handle the whole problem of copying buckets over as needed
|
||||
user_ids = yield self.store.get_users_in_room(room_id)
|
||||
|
||||
for user_id in user_ids:
|
||||
if self.hs.is_mine(UserID.from_string(user_id)):
|
||||
yield self.store.update_stats_delta(
|
||||
ts, "user", user_id, "public_rooms", +1 if is_public else -1
|
||||
)
|
||||
yield self.store.update_stats_delta(
|
||||
ts, "user", user_id, "private_rooms", -1 if is_public else +1
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _is_public_room(self, room_id):
|
||||
join_rules = yield self.state.get_current_state(room_id, EventTypes.JoinRules)
|
||||
history_visibility = yield self.state.get_current_state(
|
||||
room_id, EventTypes.RoomHistoryVisibility
|
||||
)
|
||||
|
||||
if (join_rules and join_rules.content.get("join_rule") == JoinRules.PUBLIC) or (
|
||||
(
|
||||
history_visibility
|
||||
and history_visibility.content.get("history_visibility")
|
||||
== "world_readable"
|
||||
)
|
||||
):
|
||||
defer.returnValue(True)
|
||||
else:
|
||||
defer.returnValue(False)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user