Merge branch 'main' into backoff-fixes

This commit is contained in:
devonh 2022-10-13 14:34:47 +00:00 committed by GitHub
commit 85eaa9c725
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
102 changed files with 2042 additions and 593 deletions

View file

@ -7,6 +7,11 @@ about: Create a report to help us improve
<!-- <!--
All bug reports must provide the following background information All bug reports must provide the following background information
Text between <!-- and --> marks will be invisible in the report. Text between <!-- and --> marks will be invisible in the report.
IF YOUR ISSUE IS CONSIDERED A SECURITY VULNERABILITY THEN PLEASE STOP
AND DO NOT POST IT AS A GITHUB ISSUE! Please report the issue responsibly by
disclosing in private by email to security@matrix.org instead. For more details, please
see: https://www.matrix.org/security-disclosure-policy/
--> -->
### Background information ### Background information
@ -18,13 +23,12 @@ Text between <!-- and --> marks will be invisible in the report.
- **`go version`**: - **`go version`**:
- **Client used (if applicable)**: - **Client used (if applicable)**:
### Description ### Description
- **What** is the problem: - **What** is the problem:
- **Who** is affected: - **Who** is affected:
- **How** is this bug manifesting: - **How** is this bug manifesting:
- **When** did this first appear: - **When** did this first appear:
<!-- <!--
Examples of good descriptions: Examples of good descriptions:
@ -38,7 +42,6 @@ Examples of good descriptions:
- How: "Lots of logs about device change updates" - How: "Lots of logs about device change updates"
- When: "After my server joined Matrix HQ" - When: "After my server joined Matrix HQ"
Examples of bad descriptions: Examples of bad descriptions:
- What: "Can't send messages" - This is bad because it isn't specfic enough. Which endpoint isn't working and what is the response code? Does the message send but encryption fail? - What: "Can't send messages" - This is bad because it isn't specfic enough. Which endpoint isn't working and what is the response code? Does the message send but encryption fail?
- Who: "Me" - Who are you? Running the server or a user on a Dendrite server? - Who: "Me" - Who are you? Running the server or a user on a Dendrite server?

View file

@ -1,8 +1,8 @@
### Pull Request Checklist ### Pull Request Checklist
<!-- Please read docs/CONTRIBUTING.md before submitting your pull request --> <!-- Please read https://matrix-org.github.io/dendrite/development/contributing before submitting your pull request -->
* [ ] I have added tests for PR _or_ I have justified why this PR doesn't need tests. * [ ] I have added tests for PR _or_ I have justified why this PR doesn't need tests.
* [ ] Pull request includes a [sign off](https://github.com/matrix-org/dendrite/blob/main/docs/CONTRIBUTING.md#sign-off) * [ ] Pull request includes a [sign off below using a legally identifiable name](https://matrix-org.github.io/dendrite/development/contributing#sign-off) _or_ I have already signed off privately
Signed-off-by: `Your Name <your@email.example.org>` Signed-off-by: `Your Name <your@email.example.org>`

View file

@ -342,7 +342,7 @@ jobs:
# See https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-Readme.md specifically GOROOT_1_17_X64 # See https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-Readme.md specifically GOROOT_1_17_X64
run: | run: |
sudo apt-get update && sudo apt-get install -y libolm3 libolm-dev sudo apt-get update && sudo apt-get install -y libolm3 libolm-dev
go get -v github.com/haveyoudebuggedit/gotestfmt/v2/cmd/gotestfmt@latest go get -v github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest
- name: Run actions/checkout@v2 for dendrite - name: Run actions/checkout@v2 for dendrite
uses: actions/checkout@v2 uses: actions/checkout@v2

View file

@ -1,5 +1,27 @@
# Changelog # Changelog
## Dendrite 0.10.2 (2022-10-07)
### Features
* Dendrite will now fail to start if there is an obvious problem with the configured `max_open_conns` when using PostgreSQL database backends, since this can lead to instability and performance issues
* More information on this is available [in the documentation](https://matrix-org.github.io/dendrite/installation/start/optimisation#postgresql-connection-limit)
* Unnecessary/empty fields will no longer be sent in `/sync` responses
* It is now possible to configure `old_private_keys` from previous Matrix installations on the same domain if only public key is known, to make it easier to expire old keys correctly
* You can configure either just the `private_key` path, or you can supply both the `public_key` and `key_id`
### Fixes
* The sync transaction behaviour has been modified further so that errors in one stream should not propagate to other streams unnecessarily
* Rooms should now be classified as DM rooms correctly by passing through `is_direct` and unsigned hints
* A bug which caused marking device lists as stale to consume lots of CPU has been fixed
* Users accepting invites should no longer cause unnecessary federated joins if there are already other local users in the room
* The sync API state range queries have been optimised by adding missing indexes
* It should now be possible to configure non-English languages for full-text search in `search.language`
* The roomserver will no longer attempt to perform federated requests to the local server when trying to fetch missing events
* The `/keys/upload` endpoint will now always return the `one_time_keys_counts`, which may help with E2EE reliability
* The sync API will now retrieve the latest stream position before processing each stream rather than at the beginning of the request, to hopefully reduce the number of round-trips to `/sync`
## Dendrite 0.10.1 (2022-09-30) ## Dendrite 0.10.1 (2022-09-30)
### Features ### Features

View file

@ -79,7 +79,7 @@ $ ./bin/dendrite-monolith-server --tls-cert server.crt --tls-key server.key --co
# Create an user account (add -admin for an admin user). # Create an user account (add -admin for an admin user).
# Specify the localpart only, e.g. 'alice' for '@alice:domain.com' # Specify the localpart only, e.g. 'alice' for '@alice:domain.com'
$ ./bin/create-account --config dendrite.yaml --url http://localhost:8008 --username alice $ ./bin/create-account --config dendrite.yaml --username alice
``` ```
Then point your favourite Matrix client at `http://localhost:8008` or `https://localhost:8448`. Then point your favourite Matrix client at `http://localhost:8008` or `https://localhost:8448`.
@ -90,7 +90,7 @@ We use a script called Are We Synapse Yet which checks Sytest compliance rates.
test rig with around 900 tests. The script works out how many of these tests are passing on Dendrite and it test rig with around 900 tests. The script works out how many of these tests are passing on Dendrite and it
updates with CI. As of August 2022 we're at around 90% CS API coverage and 95% Federation coverage, though check updates with CI. As of August 2022 we're at around 90% CS API coverage and 95% Federation coverage, though check
CI for the latest numbers. In practice, this means you can communicate locally and via federation with Synapse CI for the latest numbers. In practice, this means you can communicate locally and via federation with Synapse
servers such as matrix.org reasonably well, although there are still some missing features (like Search). servers such as matrix.org reasonably well, although there are still some missing features (like SSO and Third-party ID APIs).
We are prioritising features that will benefit single-user homeservers first (e.g Receipts, E2E) rather We are prioritising features that will benefit single-user homeservers first (e.g Receipts, E2E) rather
than features that massive deployments may be interested in (OpenID, Guests, Admin APIs, AS API). than features that massive deployments may be interested in (OpenID, Guests, Admin APIs, AS API).
@ -112,6 +112,7 @@ This means Dendrite supports amongst others:
- Guests - Guests
- User Directory - User Directory
- Presence - Presence
- Fulltext search
## Contributing ## Contributing

View file

@ -101,6 +101,11 @@ func (s *OutputRoomEventConsumer) onMessage(
log.WithField("appservice", state.ID).Tracef("Appservice worker received %d message(s) from roomserver", len(msgs)) log.WithField("appservice", state.ID).Tracef("Appservice worker received %d message(s) from roomserver", len(msgs))
events := make([]*gomatrixserverlib.HeaderedEvent, 0, len(msgs)) events := make([]*gomatrixserverlib.HeaderedEvent, 0, len(msgs))
for _, msg := range msgs { for _, msg := range msgs {
// Only handle events we care about
receivedType := api.OutputType(msg.Header.Get(jetstream.RoomEventType))
if receivedType != api.OutputTypeNewRoomEvent && receivedType != api.OutputTypeNewInviteEvent {
continue
}
// Parse out the event JSON // Parse out the event JSON
var output api.OutputEvent var output api.OutputEvent
if err := json.Unmarshal(msg.Data, &output); err != nil { if err := json.Unmarshal(msg.Data, &output); err != nil {

View file

@ -643,7 +643,7 @@ fed Inbound federation redacts events from erased users
fme Outbound federation can request missing events fme Outbound federation can request missing events
fme Inbound federation can return missing events for world_readable visibility fme Inbound federation can return missing events for world_readable visibility
fme Inbound federation can return missing events for shared visibility fme Inbound federation can return missing events for shared visibility
fme Inbound federation can return missing events for invite visibility fme Inbound federation can return missing events for invited visibility
fme Inbound federation can return missing events for joined visibility fme Inbound federation can return missing events for joined visibility
fme outliers whose auth_events are in a different room are correctly rejected fme outliers whose auth_events are in a different room are correctly rejected
fbk Outbound federation can backfill events fbk Outbound federation can backfill events

View file

@ -68,6 +68,12 @@ func (t *LoginTypePassword) Login(ctx context.Context, req interface{}) (*Login,
JSON: jsonerror.BadJSON("A username must be supplied."), JSON: jsonerror.BadJSON("A username must be supplied."),
} }
} }
if len(r.Password) == 0 {
return nil, &util.JSONResponse{
Code: http.StatusUnauthorized,
JSON: jsonerror.BadJSON("A password must be supplied."),
}
}
localpart, err := userutil.ParseUsernameParam(username, &t.Config.Matrix.ServerName) localpart, err := userutil.ParseUsernameParam(username, &t.Config.Matrix.ServerName)
if err != nil { if err != nil {
return nil, &util.JSONResponse{ return nil, &util.JSONResponse{

View file

@ -154,33 +154,31 @@ func SaveReadMarker(
return *resErr return *resErr
} }
if r.FullyRead == "" { if r.FullyRead != "" {
return util.JSONResponse{ data, err := json.Marshal(fullyReadEvent{EventID: r.FullyRead})
Code: http.StatusBadRequest, if err != nil {
JSON: jsonerror.BadJSON("Missing m.fully_read mandatory field"), return jsonerror.InternalServerError()
}
dataReq := api.InputAccountDataRequest{
UserID: device.UserID,
DataType: "m.fully_read",
RoomID: roomID,
AccountData: data,
}
dataRes := api.InputAccountDataResponse{}
if err := userAPI.InputAccountData(req.Context(), &dataReq, &dataRes); err != nil {
util.GetLogger(req.Context()).WithError(err).Error("userAPI.InputAccountData failed")
return util.ErrorResponse(err)
} }
} }
data, err := json.Marshal(fullyReadEvent{EventID: r.FullyRead}) // Handle the read receipts that may be included in the read marker.
if err != nil {
return jsonerror.InternalServerError()
}
dataReq := api.InputAccountDataRequest{
UserID: device.UserID,
DataType: "m.fully_read",
RoomID: roomID,
AccountData: data,
}
dataRes := api.InputAccountDataResponse{}
if err := userAPI.InputAccountData(req.Context(), &dataReq, &dataRes); err != nil {
util.GetLogger(req.Context()).WithError(err).Error("userAPI.InputAccountData failed")
return util.ErrorResponse(err)
}
// Handle the read receipt that may be included in the read marker
if r.Read != "" { if r.Read != "" {
return SetReceipt(req, syncProducer, device, roomID, "m.read", r.Read) return SetReceipt(req, userAPI, syncProducer, device, roomID, "m.read", r.Read)
}
if r.ReadPrivate != "" {
return SetReceipt(req, userAPI, syncProducer, device, roomID, "m.read.private", r.ReadPrivate)
} }
return util.JSONResponse{ return util.JSONResponse{

View file

@ -1,138 +0,0 @@
// Copyright 2019 Alex Chen
//
// 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.
package routing
import (
"net/http"
"github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/dendrite/setup/config"
userapi "github.com/matrix-org/dendrite/userapi/api"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
)
type getEventRequest struct {
req *http.Request
device *userapi.Device
roomID string
eventID string
cfg *config.ClientAPI
requestedEvent *gomatrixserverlib.Event
}
// GetEvent implements GET /_matrix/client/r0/rooms/{roomId}/event/{eventId}
// https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-rooms-roomid-event-eventid
func GetEvent(
req *http.Request,
device *userapi.Device,
roomID string,
eventID string,
cfg *config.ClientAPI,
rsAPI api.ClientRoomserverAPI,
) util.JSONResponse {
eventsReq := api.QueryEventsByIDRequest{
EventIDs: []string{eventID},
}
var eventsResp api.QueryEventsByIDResponse
err := rsAPI.QueryEventsByID(req.Context(), &eventsReq, &eventsResp)
if err != nil {
util.GetLogger(req.Context()).WithError(err).Error("queryAPI.QueryEventsByID failed")
return jsonerror.InternalServerError()
}
if len(eventsResp.Events) == 0 {
// Event not found locally
return util.JSONResponse{
Code: http.StatusNotFound,
JSON: jsonerror.NotFound("The event was not found or you do not have permission to read this event"),
}
}
requestedEvent := eventsResp.Events[0].Event
r := getEventRequest{
req: req,
device: device,
roomID: roomID,
eventID: eventID,
cfg: cfg,
requestedEvent: requestedEvent,
}
stateReq := api.QueryStateAfterEventsRequest{
RoomID: r.requestedEvent.RoomID(),
PrevEventIDs: r.requestedEvent.PrevEventIDs(),
StateToFetch: []gomatrixserverlib.StateKeyTuple{{
EventType: gomatrixserverlib.MRoomMember,
StateKey: device.UserID,
}},
}
var stateResp api.QueryStateAfterEventsResponse
if err := rsAPI.QueryStateAfterEvents(req.Context(), &stateReq, &stateResp); err != nil {
util.GetLogger(req.Context()).WithError(err).Error("queryAPI.QueryStateAfterEvents failed")
return jsonerror.InternalServerError()
}
if !stateResp.RoomExists {
util.GetLogger(req.Context()).Errorf("Expected to find room for event %s but failed", r.requestedEvent.EventID())
return jsonerror.InternalServerError()
}
if !stateResp.PrevEventsExist {
// Missing some events locally; stateResp.StateEvents unavailable.
return util.JSONResponse{
Code: http.StatusNotFound,
JSON: jsonerror.NotFound("The event was not found or you do not have permission to read this event"),
}
}
var appService *config.ApplicationService
if device.AppserviceID != "" {
for _, as := range cfg.Derived.ApplicationServices {
if as.ID == device.AppserviceID {
appService = &as
break
}
}
}
for _, stateEvent := range stateResp.StateEvents {
if appService != nil {
if !appService.IsInterestedInUserID(*stateEvent.StateKey()) {
continue
}
} else if !stateEvent.StateKeyEquals(device.UserID) {
continue
}
membership, err := stateEvent.Membership()
if err != nil {
util.GetLogger(req.Context()).WithError(err).Error("stateEvent.Membership failed")
return jsonerror.InternalServerError()
}
if membership == gomatrixserverlib.Join {
return util.JSONResponse{
Code: http.StatusOK,
JSON: gomatrixserverlib.ToClientEvent(r.requestedEvent, gomatrixserverlib.FormatAll),
}
}
}
return util.JSONResponse{
Code: http.StatusNotFound,
JSON: jsonerror.NotFound("The event was not found or you do not have permission to read this event"),
}
}

View file

@ -19,11 +19,12 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/matrix-org/util"
"github.com/matrix-org/dendrite/clientapi/httputil" "github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/keyserver/api" "github.com/matrix-org/dendrite/keyserver/api"
userapi "github.com/matrix-org/dendrite/userapi/api" userapi "github.com/matrix-org/dendrite/userapi/api"
"github.com/matrix-org/util"
) )
type uploadKeysRequest struct { type uploadKeysRequest struct {
@ -77,7 +78,6 @@ func UploadKeys(req *http.Request, keyAPI api.ClientKeyAPI, device *userapi.Devi
} }
} }
keyCount := make(map[string]int) keyCount := make(map[string]int)
// we only return key counts when the client uploads OTKs
if len(uploadRes.OneTimeKeyCounts) > 0 { if len(uploadRes.OneTimeKeyCounts) > 0 {
keyCount = uploadRes.OneTimeKeyCounts[0].KeyCount keyCount = uploadRes.OneTimeKeyCounts[0].KeyCount
} }

View file

@ -15,19 +15,22 @@
package routing package routing
import ( import (
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"time" "time"
"github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/clientapi/producers" "github.com/matrix-org/dendrite/clientapi/producers"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/dendrite/userapi/api"
userapi "github.com/matrix-org/dendrite/userapi/api" userapi "github.com/matrix-org/dendrite/userapi/api"
"github.com/matrix-org/util" "github.com/matrix-org/util"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
func SetReceipt(req *http.Request, syncProducer *producers.SyncAPIProducer, device *userapi.Device, roomID, receiptType, eventID string) util.JSONResponse { func SetReceipt(req *http.Request, userAPI api.ClientUserAPI, syncProducer *producers.SyncAPIProducer, device *userapi.Device, roomID, receiptType, eventID string) util.JSONResponse {
timestamp := gomatrixserverlib.AsTimestamp(time.Now()) timestamp := gomatrixserverlib.AsTimestamp(time.Now())
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"roomID": roomID, "roomID": roomID,
@ -37,13 +40,32 @@ func SetReceipt(req *http.Request, syncProducer *producers.SyncAPIProducer, devi
"timestamp": timestamp, "timestamp": timestamp,
}).Debug("Setting receipt") }).Debug("Setting receipt")
// currently only m.read is accepted switch receiptType {
if receiptType != "m.read" { case "m.read", "m.read.private":
return util.MessageResponse(400, fmt.Sprintf("receipt type must be m.read not '%s'", receiptType)) if err := syncProducer.SendReceipt(req.Context(), device.UserID, roomID, eventID, receiptType, timestamp); err != nil {
} return util.ErrorResponse(err)
}
if err := syncProducer.SendReceipt(req.Context(), device.UserID, roomID, eventID, receiptType, timestamp); err != nil { case "m.fully_read":
return util.ErrorResponse(err) data, err := json.Marshal(fullyReadEvent{EventID: eventID})
if err != nil {
return jsonerror.InternalServerError()
}
dataReq := api.InputAccountDataRequest{
UserID: device.UserID,
DataType: "m.fully_read",
RoomID: roomID,
AccountData: data,
}
dataRes := api.InputAccountDataResponse{}
if err := userAPI.InputAccountData(req.Context(), &dataReq, &dataRes); err != nil {
util.GetLogger(req.Context()).WithError(err).Error("userAPI.InputAccountData failed")
return util.ErrorResponse(err)
}
default:
return util.MessageResponse(400, fmt.Sprintf("Receipt type '%s' not known", receiptType))
} }
return util.JSONResponse{ return util.JSONResponse{

View file

@ -367,15 +367,6 @@ func Setup(
nil, cfg, rsAPI, transactionsCache) nil, cfg, rsAPI, transactionsCache)
}), }),
).Methods(http.MethodPut, http.MethodOptions) ).Methods(http.MethodPut, http.MethodOptions)
v3mux.Handle("/rooms/{roomID}/event/{eventID}",
httputil.MakeAuthAPI("rooms_get_event", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
if err != nil {
return util.ErrorResponse(err)
}
return GetEvent(req, device, vars["roomID"], vars["eventID"], cfg, rsAPI)
}),
).Methods(http.MethodGet, http.MethodOptions)
v3mux.Handle("/rooms/{roomID}/state", httputil.MakeAuthAPI("room_state", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { v3mux.Handle("/rooms/{roomID}/state", httputil.MakeAuthAPI("room_state", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
@ -1352,7 +1343,7 @@ func Setup(
return util.ErrorResponse(err) return util.ErrorResponse(err)
} }
return SetReceipt(req, syncProducer, device, vars["roomId"], vars["receiptType"], vars["eventId"]) return SetReceipt(req, userAPI, syncProducer, device, vars["roomId"], vars["receiptType"], vars["eventId"])
}), }),
).Methods(http.MethodPost, http.MethodOptions) ).Methods(http.MethodPost, http.MethodOptions)
v3mux.Handle("/presence/{userId}/status", v3mux.Handle("/presence/{userId}/status",

View file

@ -18,12 +18,17 @@ global:
private_key: matrix_key.pem private_key: matrix_key.pem
# The paths and expiry timestamps (as a UNIX timestamp in millisecond precision) # The paths and expiry timestamps (as a UNIX timestamp in millisecond precision)
# to old signing private keys that were formerly in use on this domain. These # to old signing keys that were formerly in use on this domain name. These
# keys will not be used for federation request or event signing, but will be # keys will not be used for federation request or event signing, but will be
# provided to any other homeserver that asks when trying to verify old events. # provided to any other homeserver that asks when trying to verify old events.
old_private_keys: old_private_keys:
# If the old private key file is available:
# - private_key: old_matrix_key.pem # - private_key: old_matrix_key.pem
# expired_at: 1601024554498 # expired_at: 1601024554498
# If only the public key (in base64 format) and key ID are known:
# - public_key: mn59Kxfdq9VziYHSBzI7+EDPDcBS2Xl7jeUdiiQcOnM=
# key_id: ed25519:mykeyid
# expired_at: 1601024554498
# How long a remote server can cache our server signing key before requesting it # How long a remote server can cache our server signing key before requesting it
# again. Increasing this number will reduce the number of requests made by other # again. Increasing this number will reduce the number of requests made by other
@ -37,7 +42,7 @@ global:
# you must configure the "database" block for each component instead. # you must configure the "database" block for each component instead.
database: database:
connection_string: postgresql://username:password@hostname/dendrite?sslmode=disable connection_string: postgresql://username:password@hostname/dendrite?sslmode=disable
max_open_conns: 100 max_open_conns: 90
max_idle_conns: 5 max_idle_conns: 5
conn_max_lifetime: -1 conn_max_lifetime: -1

View file

@ -18,12 +18,17 @@ global:
private_key: matrix_key.pem private_key: matrix_key.pem
# The paths and expiry timestamps (as a UNIX timestamp in millisecond precision) # The paths and expiry timestamps (as a UNIX timestamp in millisecond precision)
# to old signing private keys that were formerly in use on this domain. These # to old signing keys that were formerly in use on this domain name. These
# keys will not be used for federation request or event signing, but will be # keys will not be used for federation request or event signing, but will be
# provided to any other homeserver that asks when trying to verify old events. # provided to any other homeserver that asks when trying to verify old events.
old_private_keys: old_private_keys:
# If the old private key file is available:
# - private_key: old_matrix_key.pem # - private_key: old_matrix_key.pem
# expired_at: 1601024554498 # expired_at: 1601024554498
# If only the public key (in base64 format) and key ID are known:
# - public_key: mn59Kxfdq9VziYHSBzI7+EDPDcBS2Xl7jeUdiiQcOnM=
# key_id: ed25519:mykeyid
# expired_at: 1601024554498
# How long a remote server can cache our server signing key before requesting it # How long a remote server can cache our server signing key before requesting it
# again. Increasing this number will reduce the number of requests made by other # again. Increasing this number will reduce the number of requests made by other

View file

@ -1,66 +1,85 @@
# Sample Caddyfile for using Caddy in front of Dendrite. # Sample Caddyfile for using Caddy in front of Dendrite
#
# Customize email address and domain names.
# Optional settings commented out.
#
# BE SURE YOUR DOMAINS ARE POINTED AT YOUR SERVER FIRST.
# Documentation: https://caddyserver.com/docs/
#
# Bonus tip: If your IP address changes, use Caddy's
# dynamic DNS plugin to update your DNS records to
# point to your new IP automatically:
# https://github.com/mholt/caddy-dynamicdns
# #
# Customize email address and domain names
# Optional settings commented out
#
# BE SURE YOUR DOMAINS ARE POINTED AT YOUR SERVER FIRST
# Documentation: <https://caddyserver.com/docs/>
#
# Bonus tip: If your IP address changes, use Caddy's
# dynamic DNS plugin to update your DNS records to
# point to your new IP automatically
# <https://github.com/mholt/caddy-dynamicdns>
#
# Global options block # Global options block
{ {
# In case there is a problem with your certificates. # In case there is a problem with your certificates.
# email example@example.com # email example@example.com
# Turn off the admin endpoint if you don't need graceful config # Turn off the admin endpoint if you don't need graceful config
# changes and/or are running untrusted code on your machine. # changes and/or are running untrusted code on your machine.
# admin off # admin off
# Enable this if your clients don't send ServerName in TLS handshakes. # Enable this if your clients don't send ServerName in TLS handshakes.
# default_sni example.com # default_sni example.com
# Enable debug mode for verbose logging. # Enable debug mode for verbose logging.
# debug # debug
# Use Let's Encrypt's staging endpoint for testing. # Use Let's Encrypt's staging endpoint for testing.
# acme_ca https://acme-staging-v02.api.letsencrypt.org/directory # acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
# If you're port-forwarding HTTP/HTTPS ports from 80/443 to something # If you're port-forwarding HTTP/HTTPS ports from 80/443 to something
# else, enable these and put the alternate port numbers here. # else, enable these and put the alternate port numbers here.
# http_port 8080 # http_port 8080
# https_port 8443 # https_port 8443
} }
# The server name of your matrix homeserver. This example shows # The server name of your matrix homeserver. This example shows
# "well-known delegation" from the registered domain to a subdomain,
# "well-known delegation" from the registered domain to a subdomain
# which is only needed if your server_name doesn't match your Matrix # which is only needed if your server_name doesn't match your Matrix
# homeserver URL (i.e. you can show users a vanity domain that looks # homeserver URL (i.e. you can show users a vanity domain that looks
# nice and is easy to remember but still have your Matrix server on # nice and is easy to remember but still have your Matrix server on
# its own subdomain or hosted service).
# its own subdomain or hosted service)
example.com { example.com {
header /.well-known/matrix/* Content-Type application/json header /.well-known/matrix/*Content-Type application/json
header /.well-known/matrix/* Access-Control-Allow-Origin * header /.well-known/matrix/* Access-Control-Allow-Origin *
respond /.well-known/matrix/server `{"m.server": "matrix.example.com:443"}` respond /.well-known/matrix/server `{"m.server": "matrix.example.com:443"}`
respond /.well-known/matrix/client `{"m.homeserver": {"base_url": "https://matrix.example.com"}}` respond /.well-known/matrix/client `{"m.homeserver": {"base_url": "https://matrix.example.com"}}`
} }
# The actual domain name whereby your Matrix server is accessed. # The actual domain name whereby your Matrix server is accessed
matrix.example.com { matrix.example.com {
# Change the end of each reverse_proxy line to the correct # Change the end of each reverse_proxy line to the correct
# address for your various services. # address for your various services.
@sync_api { @sync_api {
path_regexp /_matrix/client/.*?/(sync|user/.*?/filter/?.*|keys/changes|rooms/.*?/messages)$ path_regexp /_matrix/client/.*?/(sync|user/.*?/filter/?.*|keys/changes|rooms/.*?/(messages|context/.*?|relations/.*?|event/.*?))$
} }
reverse_proxy @sync_api sync_api:8073 reverse_proxy @sync_api sync_api:8073
reverse_proxy /_matrix/client* client_api:8071 reverse_proxy /_matrix/client* client_api:8071
reverse_proxy /_matrix/federation* federation_api:8071 reverse_proxy /_matrix/federation* federation_api:8071
reverse_proxy /_matrix/key* federation_api:8071 reverse_proxy /_matrix/key* federation_api:8071
reverse_proxy /_matrix/media* media_api:8071 reverse_proxy /_matrix/media* media_api:8071
} }

View file

@ -18,8 +18,13 @@ VirtualHost {
# /_matrix/client/.*/user/{userId}/filter/{filterID} # /_matrix/client/.*/user/{userId}/filter/{filterID}
# /_matrix/client/.*/keys/changes # /_matrix/client/.*/keys/changes
# /_matrix/client/.*/rooms/{roomId}/messages # /_matrix/client/.*/rooms/{roomId}/messages
# /_matrix/client/.*/rooms/{roomId}/context/{eventID}
# /_matrix/client/.*/rooms/{roomId}/event/{eventID}
# /_matrix/client/.*/rooms/{roomId}/relations/{eventID}
# /_matrix/client/.*/rooms/{roomId}/relations/{eventID}/{relType}
# /_matrix/client/.*/rooms/{roomId}/relations/{eventID}/{relType}/{eventType}
# to sync_api # to sync_api
ReverseProxy = /_matrix/client/.*?/(sync|user/.*?/filter/?.*|keys/changes|rooms/.*?/messages) http://localhost:8073 600 ReverseProxy = /_matrix/client/.*?/(sync|user/.*?/filter/?.*|keys/changes|rooms/.*?/(messages|context/.*?|relations/.*?|event/.*?))$ http://localhost:8073 600
ReverseProxy = /_matrix/client http://localhost:8071 600 ReverseProxy = /_matrix/client http://localhost:8071 600
ReverseProxy = /_matrix/federation http://localhost:8072 600 ReverseProxy = /_matrix/federation http://localhost:8072 600
ReverseProxy = /_matrix/key http://localhost:8072 600 ReverseProxy = /_matrix/key http://localhost:8072 600

View file

@ -11,6 +11,41 @@ permalink: /installation/start/optimisation
Now that you have Dendrite running, the following tweaks will improve the reliability Now that you have Dendrite running, the following tweaks will improve the reliability
and performance of your installation. and performance of your installation.
## PostgreSQL connection limit
A PostgreSQL database engine is configured to allow only a certain number of connections.
This is typically controlled by the `max_connections` and `superuser_reserved_connections`
configuration items in `postgresql.conf`. Once these limits are violated, **PostgreSQL will
immediately stop accepting new connections** until some of the existing connections are closed.
This is a common source of misconfiguration and requires particular care.
If your PostgreSQL `max_connections` is set to `100` and `superuser_reserved_connections` is
set to `3` then you have an effective connection limit of 97 database connections. It is
therefore important to ensure that Dendrite doesn't violate that limit, otherwise database
queries will unexpectedly fail and this will cause problems both within Dendrite and for users.
If you are also running other software that uses the same PostgreSQL database engine, then you
must also take into account that some connections will be already used by your other software
and therefore will not be available to Dendrite. Check the configuration of any other software
using the same database engine for their configured connection limits and adjust your calculations
accordingly.
Dendrite has a `max_open_conns` configuration item in each `database` block to control how many
connections it will open to the database.
**If you are using the `global` database pool** then you only need to configure the
`max_open_conns` setting once in the `global` section.
**If you are defining a `database` config per component** then you will need to ensure that
the **sum total** of all configured `max_open_conns` to a given database server do not exceed
the connection limit. If you configure a total that adds up to more connections than are available
then this will cause database queries to fail.
You may wish to raise the `max_connections` limit on your PostgreSQL server to accommodate
additional connections, in which case you should also update the `max_open_conns` in your
Dendrite configuration accordingly. However be aware that this is only advisable on particularly
powerful servers that can handle the concurrent load of additional queries running at one time.
## File descriptor limit ## File descriptor limit
Most platforms have a limit on how many file descriptors a single process can open. All Most platforms have a limit on how many file descriptors a single process can open. All

View file

@ -28,8 +28,13 @@ server {
# /_matrix/client/.*/user/{userId}/filter/{filterID} # /_matrix/client/.*/user/{userId}/filter/{filterID}
# /_matrix/client/.*/keys/changes # /_matrix/client/.*/keys/changes
# /_matrix/client/.*/rooms/{roomId}/messages # /_matrix/client/.*/rooms/{roomId}/messages
# /_matrix/client/.*/rooms/{roomId}/context/{eventID}
# /_matrix/client/.*/rooms/{roomId}/event/{eventID}
# /_matrix/client/.*/rooms/{roomId}/relations/{eventID}
# /_matrix/client/.*/rooms/{roomId}/relations/{eventID}/{relType}
# /_matrix/client/.*/rooms/{roomId}/relations/{eventID}/{relType}/{eventType}
# to sync_api # to sync_api
location ~ /_matrix/client/.*?/(sync|user/.*?/filter/?.*|keys/changes|rooms/.*?/messages)$ { location ~ /_matrix/client/.*?/(sync|user/.*?/filter/?.*|keys/changes|rooms/.*?/(messages|context/.*?|relations/.*?|event/.*?))$ {
proxy_pass http://sync_api:8073; proxy_pass http://sync_api:8073;
} }

View file

@ -159,6 +159,7 @@ type PerformJoinRequest struct {
// The sorted list of servers to try. Servers will be tried sequentially, after de-duplication. // The sorted list of servers to try. Servers will be tried sequentially, after de-duplication.
ServerNames types.ServerNames `json:"server_names"` ServerNames types.ServerNames `json:"server_names"`
Content map[string]interface{} `json:"content"` Content map[string]interface{} `json:"content"`
Unsigned map[string]interface{} `json:"unsigned"`
} }
type PerformJoinResponse struct { type PerformJoinResponse struct {

View file

@ -18,6 +18,11 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/getsentry/sentry-go"
"github.com/matrix-org/gomatrixserverlib"
"github.com/nats-io/nats.go"
"github.com/sirupsen/logrus"
"github.com/matrix-org/dendrite/federationapi/queue" "github.com/matrix-org/dendrite/federationapi/queue"
"github.com/matrix-org/dendrite/federationapi/storage" "github.com/matrix-org/dendrite/federationapi/storage"
"github.com/matrix-org/dendrite/federationapi/types" "github.com/matrix-org/dendrite/federationapi/types"
@ -26,9 +31,6 @@ import (
"github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/dendrite/setup/config"
"github.com/matrix-org/dendrite/setup/jetstream" "github.com/matrix-org/dendrite/setup/jetstream"
"github.com/matrix-org/dendrite/setup/process" "github.com/matrix-org/dendrite/setup/process"
"github.com/matrix-org/gomatrixserverlib"
"github.com/nats-io/nats.go"
"github.com/sirupsen/logrus"
) )
// KeyChangeConsumer consumes events that originate in key server. // KeyChangeConsumer consumes events that originate in key server.
@ -78,6 +80,7 @@ func (t *KeyChangeConsumer) onMessage(ctx context.Context, msgs []*nats.Msg) boo
msg := msgs[0] // Guaranteed to exist if onMessage is called msg := msgs[0] // Guaranteed to exist if onMessage is called
var m api.DeviceMessage var m api.DeviceMessage
if err := json.Unmarshal(msg.Data, &m); err != nil { if err := json.Unmarshal(msg.Data, &m); err != nil {
sentry.CaptureException(err)
logrus.WithError(err).Errorf("failed to read device message from key change topic") logrus.WithError(err).Errorf("failed to read device message from key change topic")
return true return true
} }
@ -105,6 +108,7 @@ func (t *KeyChangeConsumer) onDeviceKeyMessage(m api.DeviceMessage) bool {
// only send key change events which originated from us // only send key change events which originated from us
_, originServerName, err := gomatrixserverlib.SplitID('@', m.UserID) _, originServerName, err := gomatrixserverlib.SplitID('@', m.UserID)
if err != nil { if err != nil {
sentry.CaptureException(err)
logger.WithError(err).Error("Failed to extract domain from key change event") logger.WithError(err).Error("Failed to extract domain from key change event")
return true return true
} }
@ -118,6 +122,7 @@ func (t *KeyChangeConsumer) onDeviceKeyMessage(m api.DeviceMessage) bool {
WantMembership: "join", WantMembership: "join",
}, &queryRes) }, &queryRes)
if err != nil { if err != nil {
sentry.CaptureException(err)
logger.WithError(err).Error("failed to calculate joined rooms for user") logger.WithError(err).Error("failed to calculate joined rooms for user")
return true return true
} }
@ -125,6 +130,7 @@ func (t *KeyChangeConsumer) onDeviceKeyMessage(m api.DeviceMessage) bool {
// send this key change to all servers who share rooms with this user. // send this key change to all servers who share rooms with this user.
destinations, err := t.db.GetJoinedHostsForRooms(t.ctx, queryRes.RoomIDs, true) destinations, err := t.db.GetJoinedHostsForRooms(t.ctx, queryRes.RoomIDs, true)
if err != nil { if err != nil {
sentry.CaptureException(err)
logger.WithError(err).Error("failed to calculate joined hosts for rooms user is in") logger.WithError(err).Error("failed to calculate joined hosts for rooms user is in")
return true return true
} }
@ -147,6 +153,7 @@ func (t *KeyChangeConsumer) onDeviceKeyMessage(m api.DeviceMessage) bool {
Keys: m.KeyJSON, Keys: m.KeyJSON,
} }
if edu.Content, err = json.Marshal(event); err != nil { if edu.Content, err = json.Marshal(event); err != nil {
sentry.CaptureException(err)
logger.WithError(err).Error("failed to marshal EDU JSON") logger.WithError(err).Error("failed to marshal EDU JSON")
return true return true
} }
@ -160,6 +167,7 @@ func (t *KeyChangeConsumer) onCrossSigningMessage(m api.DeviceMessage) bool {
output := m.CrossSigningKeyUpdate output := m.CrossSigningKeyUpdate
_, host, err := gomatrixserverlib.SplitID('@', output.UserID) _, host, err := gomatrixserverlib.SplitID('@', output.UserID)
if err != nil { if err != nil {
sentry.CaptureException(err)
logrus.WithError(err).Errorf("fedsender key change consumer: user ID parse failure") logrus.WithError(err).Errorf("fedsender key change consumer: user ID parse failure")
return true return true
} }
@ -176,12 +184,14 @@ func (t *KeyChangeConsumer) onCrossSigningMessage(m api.DeviceMessage) bool {
WantMembership: "join", WantMembership: "join",
}, &queryRes) }, &queryRes)
if err != nil { if err != nil {
sentry.CaptureException(err)
logger.WithError(err).Error("fedsender key change consumer: failed to calculate joined rooms for user") logger.WithError(err).Error("fedsender key change consumer: failed to calculate joined rooms for user")
return true return true
} }
// send this key change to all servers who share rooms with this user. // send this key change to all servers who share rooms with this user.
destinations, err := t.db.GetJoinedHostsForRooms(t.ctx, queryRes.RoomIDs, true) destinations, err := t.db.GetJoinedHostsForRooms(t.ctx, queryRes.RoomIDs, true)
if err != nil { if err != nil {
sentry.CaptureException(err)
logger.WithError(err).Error("fedsender key change consumer: failed to calculate joined hosts for rooms user is in") logger.WithError(err).Error("fedsender key change consumer: failed to calculate joined hosts for rooms user is in")
return true return true
} }
@ -196,6 +206,7 @@ func (t *KeyChangeConsumer) onCrossSigningMessage(m api.DeviceMessage) bool {
Origin: string(t.serverName), Origin: string(t.serverName),
} }
if edu.Content, err = json.Marshal(output); err != nil { if edu.Content, err = json.Marshal(output); err != nil {
sentry.CaptureException(err)
logger.WithError(err).Error("fedsender key change consumer: failed to marshal output, dropping") logger.WithError(err).Error("fedsender key change consumer: failed to marshal output, dropping")
return true return true
} }

View file

@ -81,6 +81,14 @@ func (t *OutputReceiptConsumer) onMessage(ctx context.Context, msgs []*nats.Msg)
Type: msg.Header.Get("type"), Type: msg.Header.Get("type"),
} }
switch receipt.Type {
case "m.read":
// These are allowed to be sent over federation
case "m.read.private", "m.fully_read":
// These must not be sent over federation
return true
}
// only send receipt events which originated from us // only send receipt events which originated from us
_, receiptServerName, err := gomatrixserverlib.SplitID('@', receipt.UserID) _, receiptServerName, err := gomatrixserverlib.SplitID('@', receipt.UserID)
if err != nil { if err != nil {

View file

@ -79,6 +79,13 @@ func (s *OutputRoomEventConsumer) Start() error {
// realises that it cannot update the room state using the deltas. // realises that it cannot update the room state using the deltas.
func (s *OutputRoomEventConsumer) onMessage(ctx context.Context, msgs []*nats.Msg) bool { func (s *OutputRoomEventConsumer) onMessage(ctx context.Context, msgs []*nats.Msg) bool {
msg := msgs[0] // Guaranteed to exist if onMessage is called msg := msgs[0] // Guaranteed to exist if onMessage is called
receivedType := api.OutputType(msg.Header.Get(jetstream.RoomEventType))
// Only handle events we care about
if receivedType != api.OutputTypeNewRoomEvent && receivedType != api.OutputTypeNewInboundPeek {
return true
}
// Parse out the event JSON // Parse out the event JSON
var output api.OutputEvent var output api.OutputEvent
if err := json.Unmarshal(msg.Data, &output); err != nil { if err := json.Unmarshal(msg.Data, &output); err != nil {

View file

@ -18,16 +18,18 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/getsentry/sentry-go"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
"github.com/nats-io/nats.go"
log "github.com/sirupsen/logrus"
"github.com/matrix-org/dendrite/federationapi/queue" "github.com/matrix-org/dendrite/federationapi/queue"
"github.com/matrix-org/dendrite/federationapi/storage" "github.com/matrix-org/dendrite/federationapi/storage"
"github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/dendrite/setup/config"
"github.com/matrix-org/dendrite/setup/jetstream" "github.com/matrix-org/dendrite/setup/jetstream"
"github.com/matrix-org/dendrite/setup/process" "github.com/matrix-org/dendrite/setup/process"
syncTypes "github.com/matrix-org/dendrite/syncapi/types" syncTypes "github.com/matrix-org/dendrite/syncapi/types"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
"github.com/nats-io/nats.go"
log "github.com/sirupsen/logrus"
) )
// OutputSendToDeviceConsumer consumes events that originate in the clientapi. // OutputSendToDeviceConsumer consumes events that originate in the clientapi.
@ -76,6 +78,7 @@ func (t *OutputSendToDeviceConsumer) onMessage(ctx context.Context, msgs []*nats
sender := msg.Header.Get("sender") sender := msg.Header.Get("sender")
_, originServerName, err := gomatrixserverlib.SplitID('@', sender) _, originServerName, err := gomatrixserverlib.SplitID('@', sender)
if err != nil { if err != nil {
sentry.CaptureException(err)
log.WithError(err).WithField("user_id", sender).Error("Failed to extract domain from send-to-device sender") log.WithError(err).WithField("user_id", sender).Error("Failed to extract domain from send-to-device sender")
return true return true
} }
@ -85,12 +88,14 @@ func (t *OutputSendToDeviceConsumer) onMessage(ctx context.Context, msgs []*nats
// Extract the send-to-device event from msg. // Extract the send-to-device event from msg.
var ote syncTypes.OutputSendToDeviceEvent var ote syncTypes.OutputSendToDeviceEvent
if err = json.Unmarshal(msg.Data, &ote); err != nil { if err = json.Unmarshal(msg.Data, &ote); err != nil {
sentry.CaptureException(err)
log.WithError(err).Errorf("output log: message parse failed (expected send-to-device)") log.WithError(err).Errorf("output log: message parse failed (expected send-to-device)")
return true return true
} }
_, destServerName, err := gomatrixserverlib.SplitID('@', ote.UserID) _, destServerName, err := gomatrixserverlib.SplitID('@', ote.UserID)
if err != nil { if err != nil {
sentry.CaptureException(err)
log.WithError(err).WithField("user_id", ote.UserID).Error("Failed to extract domain from send-to-device destination") log.WithError(err).WithField("user_id", ote.UserID).Error("Failed to extract domain from send-to-device destination")
return true return true
} }
@ -116,6 +121,7 @@ func (t *OutputSendToDeviceConsumer) onMessage(ctx context.Context, msgs []*nats
}, },
} }
if edu.Content, err = json.Marshal(tdm); err != nil { if edu.Content, err = json.Marshal(tdm); err != nil {
sentry.CaptureException(err)
log.WithError(err).Error("failed to marshal EDU JSON") log.WithError(err).Error("failed to marshal EDU JSON")
return true return true
} }

View file

@ -7,14 +7,15 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/matrix-org/dendrite/federationapi/api"
"github.com/matrix-org/dendrite/federationapi/consumers"
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/dendrite/roomserver/version"
"github.com/matrix-org/gomatrix" "github.com/matrix-org/gomatrix"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util" "github.com/matrix-org/util"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/matrix-org/dendrite/federationapi/api"
"github.com/matrix-org/dendrite/federationapi/consumers"
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/dendrite/roomserver/version"
) )
// PerformLeaveRequest implements api.FederationInternalAPI // PerformLeaveRequest implements api.FederationInternalAPI
@ -95,6 +96,7 @@ func (r *FederationInternalAPI) PerformJoin(
request.Content, request.Content,
serverName, serverName,
supportedVersions, supportedVersions,
request.Unsigned,
); err != nil { ); err != nil {
logrus.WithError(err).WithFields(logrus.Fields{ logrus.WithError(err).WithFields(logrus.Fields{
"server_name": serverName, "server_name": serverName,
@ -139,6 +141,7 @@ func (r *FederationInternalAPI) performJoinUsingServer(
content map[string]interface{}, content map[string]interface{},
serverName gomatrixserverlib.ServerName, serverName gomatrixserverlib.ServerName,
supportedVersions []gomatrixserverlib.RoomVersion, supportedVersions []gomatrixserverlib.RoomVersion,
unsigned map[string]interface{},
) error { ) error {
// Try to perform a make_join using the information supplied in the // Try to perform a make_join using the information supplied in the
// request. // request.
@ -259,7 +262,7 @@ func (r *FederationInternalAPI) performJoinUsingServer(
if err != nil { if err != nil {
return fmt.Errorf("JoinedHostsFromEvents: failed to get joined hosts: %s", err) return fmt.Errorf("JoinedHostsFromEvents: failed to get joined hosts: %s", err)
} }
logrus.WithField("hosts", joinedHosts).WithField("room", roomID).Info("Joined federated room with hosts") logrus.WithField("room", roomID).Infof("Joined federated room with %d hosts", len(joinedHosts))
if _, err = r.db.UpdateRoom(context.Background(), roomID, joinedHosts, nil, true); err != nil { if _, err = r.db.UpdateRoom(context.Background(), roomID, joinedHosts, nil, true); err != nil {
return fmt.Errorf("UpdatedRoom: failed to update room with joined hosts: %s", err) return fmt.Errorf("UpdatedRoom: failed to update room with joined hosts: %s", err)
} }
@ -267,6 +270,14 @@ func (r *FederationInternalAPI) performJoinUsingServer(
// If we successfully performed a send_join above then the other // If we successfully performed a send_join above then the other
// server now thinks we're a part of the room. Send the newly // server now thinks we're a part of the room. Send the newly
// returned state to the roomserver to update our local view. // returned state to the roomserver to update our local view.
if unsigned != nil {
event, err = event.SetUnsigned(unsigned)
if err != nil {
// non-fatal, log and continue
logrus.WithError(err).Errorf("Failed to set unsigned content")
}
}
if err = roomserverAPI.SendEventWithState( if err = roomserverAPI.SendEventWithState(
context.Background(), context.Background(),
r.rsAPI, r.rsAPI,

View file

@ -21,6 +21,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/getsentry/sentry-go"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -307,11 +308,13 @@ func (oqs *OutgoingQueues) SendEDU(
ephemeralJSON, err := json.Marshal(e) ephemeralJSON, err := json.Marshal(e)
if err != nil { if err != nil {
sentry.CaptureException(err)
return fmt.Errorf("json.Marshal: %w", err) return fmt.Errorf("json.Marshal: %w", err)
} }
nid, err := oqs.db.StoreJSON(oqs.process.Context(), string(ephemeralJSON)) nid, err := oqs.db.StoreJSON(oqs.process.Context(), string(ephemeralJSON))
if err != nil { if err != nil {
sentry.CaptureException(err)
return fmt.Errorf("sendevent: oqs.db.StoreJSON: %w", err) return fmt.Errorf("sendevent: oqs.db.StoreJSON: %w", err)
} }

View file

@ -20,6 +20,7 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util" "github.com/matrix-org/util"
@ -95,7 +96,10 @@ func fetchEvent(ctx context.Context, rsAPI api.FederationRoomserverAPI, eventID
} }
if len(eventsResponse.Events) == 0 { if len(eventsResponse.Events) == 0 {
return nil, &util.JSONResponse{Code: http.StatusNotFound, JSON: nil} return nil, &util.JSONResponse{
Code: http.StatusNotFound,
JSON: jsonerror.NotFound("Event not found"),
}
} }
return eventsResponse.Events[0].Event, nil return eventsResponse.Events[0].Event, nil

View file

@ -160,7 +160,7 @@ func localKeys(cfg *config.FederationAPI, validUntil time.Time) (*gomatrixserver
for _, oldVerifyKey := range cfg.Matrix.OldVerifyKeys { for _, oldVerifyKey := range cfg.Matrix.OldVerifyKeys {
keys.OldVerifyKeys[oldVerifyKey.KeyID] = gomatrixserverlib.OldVerifyKey{ keys.OldVerifyKeys[oldVerifyKey.KeyID] = gomatrixserverlib.OldVerifyKey{
VerifyKey: gomatrixserverlib.VerifyKey{ VerifyKey: gomatrixserverlib.VerifyKey{
Key: gomatrixserverlib.Base64Bytes(oldVerifyKey.PrivateKey.Public().(ed25519.PublicKey)), Key: oldVerifyKey.PublicKey,
}, },
ExpiredTS: oldVerifyKey.ExpiredAt, ExpiredTS: oldVerifyKey.ExpiredAt,
} }

View file

@ -22,6 +22,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/getsentry/sentry-go"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util" "github.com/matrix-org/util"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
@ -350,6 +351,7 @@ func (t *txnReq) processEDUs(ctx context.Context) {
for deviceID, message := range byUser { for deviceID, message := range byUser {
// TODO: check that the user and the device actually exist here // TODO: check that the user and the device actually exist here
if err := t.producer.SendToDevice(ctx, directPayload.Sender, userID, deviceID, directPayload.Type, message); err != nil { if err := t.producer.SendToDevice(ctx, directPayload.Sender, userID, deviceID, directPayload.Type, message); err != nil {
sentry.CaptureException(err)
util.GetLogger(ctx).WithError(err).WithFields(logrus.Fields{ util.GetLogger(ctx).WithError(err).WithFields(logrus.Fields{
"sender": directPayload.Sender, "sender": directPayload.Sender,
"user_id": userID, "user_id": userID,
@ -360,6 +362,7 @@ func (t *txnReq) processEDUs(ctx context.Context) {
} }
case gomatrixserverlib.MDeviceListUpdate: case gomatrixserverlib.MDeviceListUpdate:
if err := t.producer.SendDeviceListUpdate(ctx, e.Content, t.Origin); err != nil { if err := t.producer.SendDeviceListUpdate(ctx, e.Content, t.Origin); err != nil {
sentry.CaptureException(err)
util.GetLogger(ctx).WithError(err).Error("failed to InputDeviceListUpdate") util.GetLogger(ctx).WithError(err).Error("failed to InputDeviceListUpdate")
} }
case gomatrixserverlib.MReceipt: case gomatrixserverlib.MReceipt:
@ -395,6 +398,7 @@ func (t *txnReq) processEDUs(ctx context.Context) {
} }
case types.MSigningKeyUpdate: case types.MSigningKeyUpdate:
if err := t.producer.SendSigningKeyUpdate(ctx, e.Content, t.Origin); err != nil { if err := t.producer.SendSigningKeyUpdate(ctx, e.Content, t.Origin); err != nil {
sentry.CaptureException(err)
logrus.WithError(err).Errorf("Failed to process signing key update") logrus.WithError(err).Errorf("Failed to process signing key update")
} }
case gomatrixserverlib.MPresence: case gomatrixserverlib.MPresence:

View file

@ -135,23 +135,24 @@ func getState(
return nil, nil, &resErr return nil, nil, &resErr
} }
if !response.StateKnown { switch {
case !response.RoomExists:
return nil, nil, &util.JSONResponse{
Code: http.StatusNotFound,
JSON: jsonerror.NotFound("Room not found"),
}
case !response.StateKnown:
return nil, nil, &util.JSONResponse{ return nil, nil, &util.JSONResponse{
Code: http.StatusNotFound, Code: http.StatusNotFound,
JSON: jsonerror.NotFound("State not known"), JSON: jsonerror.NotFound("State not known"),
} }
} case response.IsRejected:
if response.IsRejected {
return nil, nil, &util.JSONResponse{ return nil, nil, &util.JSONResponse{
Code: http.StatusNotFound, Code: http.StatusNotFound,
JSON: jsonerror.NotFound("Event not found"), JSON: jsonerror.NotFound("Event not found"),
} }
} }
if !response.RoomExists {
return nil, nil, &util.JSONResponse{Code: http.StatusNotFound, JSON: nil}
}
return response.StateEvents, response.AuthChainEvents, nil return response.StateEvents, response.AuthChainEvents, nil
} }

View file

@ -70,27 +70,27 @@ func (d *Database) UpdateRoom(
) (joinedHosts []types.JoinedHost, err error) { ) (joinedHosts []types.JoinedHost, err error) {
err = d.Writer.Do(d.DB, nil, func(txn *sql.Tx) error { err = d.Writer.Do(d.DB, nil, func(txn *sql.Tx) error {
if purgeRoomFirst { if purgeRoomFirst {
// If the event is a create event then we'll delete all of the existing
// data for the room. The only reason that a create event would be replayed
// to us in this way is if we're about to receive the entire room state.
if err = d.FederationJoinedHosts.DeleteJoinedHostsForRoom(ctx, txn, roomID); err != nil { if err = d.FederationJoinedHosts.DeleteJoinedHostsForRoom(ctx, txn, roomID); err != nil {
return fmt.Errorf("d.FederationJoinedHosts.DeleteJoinedHosts: %w", err) return fmt.Errorf("d.FederationJoinedHosts.DeleteJoinedHosts: %w", err)
} }
} for _, add := range addHosts {
if err = d.FederationJoinedHosts.InsertJoinedHosts(ctx, txn, roomID, add.MemberEventID, add.ServerName); err != nil {
joinedHosts, err = d.FederationJoinedHosts.SelectJoinedHostsWithTx(ctx, txn, roomID) return err
if err != nil { }
return err joinedHosts = append(joinedHosts, add)
} }
} else {
for _, add := range addHosts { if joinedHosts, err = d.FederationJoinedHosts.SelectJoinedHostsWithTx(ctx, txn, roomID); err != nil {
err = d.FederationJoinedHosts.InsertJoinedHosts(ctx, txn, roomID, add.MemberEventID, add.ServerName) return err
if err != nil { }
for _, add := range addHosts {
if err = d.FederationJoinedHosts.InsertJoinedHosts(ctx, txn, roomID, add.MemberEventID, add.ServerName); err != nil {
return err
}
}
if err = d.FederationJoinedHosts.DeleteJoinedHosts(ctx, txn, removeHosts); err != nil {
return err return err
} }
}
if err = d.FederationJoinedHosts.DeleteJoinedHosts(ctx, txn, removeHosts); err != nil {
return err
} }
return nil return nil
}) })

2
go.mod
View file

@ -22,7 +22,7 @@ require (
github.com/matrix-org/dugong v0.0.0-20210921133753-66e6b1c67e2e github.com/matrix-org/dugong v0.0.0-20210921133753-66e6b1c67e2e
github.com/matrix-org/go-sqlite3-js v0.0.0-20220419092513-28aa791a1c91 github.com/matrix-org/go-sqlite3-js v0.0.0-20220419092513-28aa791a1c91
github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16 github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16
github.com/matrix-org/gomatrixserverlib v0.0.0-20220929190355-91d455cd3621 github.com/matrix-org/gomatrixserverlib v0.0.0-20221011115330-49fa704b9a64
github.com/matrix-org/pinecone v0.0.0-20220929155234-2ce51dd4a42c github.com/matrix-org/pinecone v0.0.0-20220929155234-2ce51dd4a42c
github.com/matrix-org/util v0.0.0-20200807132607-55161520e1d4 github.com/matrix-org/util v0.0.0-20200807132607-55161520e1d4
github.com/mattn/go-sqlite3 v1.14.15 github.com/mattn/go-sqlite3 v1.14.15

4
go.sum
View file

@ -384,8 +384,8 @@ github.com/matrix-org/go-sqlite3-js v0.0.0-20220419092513-28aa791a1c91 h1:s7fexw
github.com/matrix-org/go-sqlite3-js v0.0.0-20220419092513-28aa791a1c91/go.mod h1:e+cg2q7C7yE5QnAXgzo512tgFh1RbQLC0+jozuegKgo= github.com/matrix-org/go-sqlite3-js v0.0.0-20220419092513-28aa791a1c91/go.mod h1:e+cg2q7C7yE5QnAXgzo512tgFh1RbQLC0+jozuegKgo=
github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16 h1:ZtO5uywdd5dLDCud4r0r55eP4j9FuUNpl60Gmntcop4= github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16 h1:ZtO5uywdd5dLDCud4r0r55eP4j9FuUNpl60Gmntcop4=
github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s= github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s=
github.com/matrix-org/gomatrixserverlib v0.0.0-20220929190355-91d455cd3621 h1:a8IaoSPDxevkgXnOUrtIW9AqVNvXBJAG0gtnX687S7g= github.com/matrix-org/gomatrixserverlib v0.0.0-20221011115330-49fa704b9a64 h1:QJmfAPC3P0ZHJzYD/QtbNc5EztKlK1ipRWP5SO/m4jw=
github.com/matrix-org/gomatrixserverlib v0.0.0-20220929190355-91d455cd3621/go.mod h1:Mtifyr8q8htcBeugvlDnkBcNUy5LO8OzUoplAf1+mb4= github.com/matrix-org/gomatrixserverlib v0.0.0-20221011115330-49fa704b9a64/go.mod h1:Mtifyr8q8htcBeugvlDnkBcNUy5LO8OzUoplAf1+mb4=
github.com/matrix-org/pinecone v0.0.0-20220929155234-2ce51dd4a42c h1:iCHLYwwlPsf4TYFrvhKdhQoAM2lXzcmDZYqwBNWcnVk= github.com/matrix-org/pinecone v0.0.0-20220929155234-2ce51dd4a42c h1:iCHLYwwlPsf4TYFrvhKdhQoAM2lXzcmDZYqwBNWcnVk=
github.com/matrix-org/pinecone v0.0.0-20220929155234-2ce51dd4a42c/go.mod h1:K0N1ixHQxXoCyqolDqVxPM3ArrDtcMs8yegOx2Lfv9k= github.com/matrix-org/pinecone v0.0.0-20220929155234-2ce51dd4a42c/go.mod h1:K0N1ixHQxXoCyqolDqVxPM3ArrDtcMs8yegOx2Lfv9k=
github.com/matrix-org/util v0.0.0-20200807132607-55161520e1d4 h1:eCEHXWDv9Rm335MSuB49mFUK44bwZPFSDde3ORE3syk= github.com/matrix-org/util v0.0.0-20200807132607-55161520e1d4 h1:eCEHXWDv9Rm335MSuB49mFUK44bwZPFSDde3ORE3syk=

View file

@ -35,8 +35,9 @@ type AccountData struct {
} }
type ReadMarkerJSON struct { type ReadMarkerJSON struct {
FullyRead string `json:"m.fully_read"` FullyRead string `json:"m.fully_read"`
Read string `json:"m.read"` Read string `json:"m.read"`
ReadPrivate string `json:"m.read.private"`
} }
// NotificationData contains statistics about notifications, sent from // NotificationData contains statistics about notifications, sent from

View file

@ -21,6 +21,29 @@ import (
"strings" "strings"
"github.com/blevesearch/bleve/v2" "github.com/blevesearch/bleve/v2"
// side effect imports to allow all possible languages
_ "github.com/blevesearch/bleve/v2/analysis/lang/ar"
_ "github.com/blevesearch/bleve/v2/analysis/lang/cjk"
_ "github.com/blevesearch/bleve/v2/analysis/lang/ckb"
_ "github.com/blevesearch/bleve/v2/analysis/lang/da"
_ "github.com/blevesearch/bleve/v2/analysis/lang/de"
_ "github.com/blevesearch/bleve/v2/analysis/lang/en"
_ "github.com/blevesearch/bleve/v2/analysis/lang/es"
_ "github.com/blevesearch/bleve/v2/analysis/lang/fa"
_ "github.com/blevesearch/bleve/v2/analysis/lang/fi"
_ "github.com/blevesearch/bleve/v2/analysis/lang/fr"
_ "github.com/blevesearch/bleve/v2/analysis/lang/hi"
_ "github.com/blevesearch/bleve/v2/analysis/lang/hr"
_ "github.com/blevesearch/bleve/v2/analysis/lang/hu"
_ "github.com/blevesearch/bleve/v2/analysis/lang/it"
_ "github.com/blevesearch/bleve/v2/analysis/lang/nl"
_ "github.com/blevesearch/bleve/v2/analysis/lang/no"
_ "github.com/blevesearch/bleve/v2/analysis/lang/pt"
_ "github.com/blevesearch/bleve/v2/analysis/lang/ro"
_ "github.com/blevesearch/bleve/v2/analysis/lang/ru"
_ "github.com/blevesearch/bleve/v2/analysis/lang/sv"
_ "github.com/blevesearch/bleve/v2/analysis/lang/tr"
"github.com/blevesearch/bleve/v2/mapping" "github.com/blevesearch/bleve/v2/mapping"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"

View file

@ -2,6 +2,7 @@ package sqlutil
import ( import (
"database/sql" "database/sql"
"flag"
"fmt" "fmt"
"regexp" "regexp"
@ -9,6 +10,8 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
var skipSanityChecks = flag.Bool("skip-db-sanity", false, "Ignore sanity checks on the database connections (NOT RECOMMENDED!)")
// Open opens a database specified by its database driver name and a driver-specific data source name, // Open opens a database specified by its database driver name and a driver-specific data source name,
// usually consisting of at least a database name and connection information. Includes tracing driver // usually consisting of at least a database name and connection information. Includes tracing driver
// if DENDRITE_TRACE_SQL=1 // if DENDRITE_TRACE_SQL=1
@ -37,15 +40,39 @@ func Open(dbProperties *config.DatabaseOptions, writer Writer) (*sql.DB, error)
return nil, err return nil, err
} }
if driverName != "sqlite3" { if driverName != "sqlite3" {
logrus.WithFields(logrus.Fields{ logger := logrus.WithFields(logrus.Fields{
"MaxOpenConns": dbProperties.MaxOpenConns(), "max_open_conns": dbProperties.MaxOpenConns(),
"MaxIdleConns": dbProperties.MaxIdleConns(), "max_idle_conns": dbProperties.MaxIdleConns(),
"ConnMaxLifetime": dbProperties.ConnMaxLifetime(), "conn_max_lifetime": dbProperties.ConnMaxLifetime(),
"dataSourceName": regexp.MustCompile(`://[^@]*@`).ReplaceAllLiteralString(dsn, "://"), "data_source_name": regexp.MustCompile(`://[^@]*@`).ReplaceAllLiteralString(dsn, "://"),
}).Debug("Setting DB connection limits") })
logger.Debug("Setting DB connection limits")
db.SetMaxOpenConns(dbProperties.MaxOpenConns()) db.SetMaxOpenConns(dbProperties.MaxOpenConns())
db.SetMaxIdleConns(dbProperties.MaxIdleConns()) db.SetMaxIdleConns(dbProperties.MaxIdleConns())
db.SetConnMaxLifetime(dbProperties.ConnMaxLifetime()) db.SetConnMaxLifetime(dbProperties.ConnMaxLifetime())
if !*skipSanityChecks {
if dbProperties.MaxOpenConns() == 0 {
logrus.Warnf("WARNING: Configuring 'max_open_conns' to be unlimited is not recommended. This can result in bad performance or deadlocks.")
}
switch driverName {
case "postgres":
// Perform a quick sanity check if possible that we aren't trying to use more database
// connections than PostgreSQL is willing to give us.
var max, reserved int
if err := db.QueryRow("SELECT setting::integer FROM pg_settings WHERE name='max_connections';").Scan(&max); err != nil {
return nil, fmt.Errorf("failed to find maximum connections: %w", err)
}
if err := db.QueryRow("SELECT setting::integer FROM pg_settings WHERE name='superuser_reserved_connections';").Scan(&reserved); err != nil {
return nil, fmt.Errorf("failed to find reserved connections: %w", err)
}
if configured, allowed := dbProperties.MaxOpenConns(), max-reserved; configured > allowed {
logrus.Errorf("ERROR: The configured 'max_open_conns' is greater than the %d non-superuser connections that PostgreSQL is configured to allow. This can result in bad performance or deadlocks. Please pay close attention to your configured database connection counts. If you REALLY know what you are doing and want to override this error, pass the --skip-db-sanity option to Dendrite.", allowed)
return nil, fmt.Errorf("database sanity checks failed")
}
}
}
} }
return db, nil return db, nil
} }

View file

@ -17,7 +17,7 @@ var build string
const ( const (
VersionMajor = 0 VersionMajor = 0
VersionMinor = 10 VersionMinor = 10
VersionPatch = 1 VersionPatch = 2
VersionTag = "" // example: "rc1" VersionTag = "" // example: "rc1"
) )

View file

@ -424,7 +424,7 @@ func (u *DeviceListUpdater) processServer(serverName gomatrixserverlib.ServerNam
"succeeded": successCount, "succeeded": successCount,
"failed": len(userIDs) - successCount, "failed": len(userIDs) - successCount,
"wait_time": waitTime, "wait_time": waitTime,
}).Warn("Failed to query device keys for some users") }).Debug("Failed to query device keys for some users")
} }
return waitTime, !allUsersSucceeded return waitTime, !allUsersSucceeded
} }

View file

@ -70,6 +70,11 @@ func (a *KeyInternalAPI) PerformUploadKeys(ctx context.Context, req *api.Perform
if len(req.OneTimeKeys) > 0 { if len(req.OneTimeKeys) > 0 {
a.uploadOneTimeKeys(ctx, req, res) a.uploadOneTimeKeys(ctx, req, res)
} }
otks, err := a.DB.OneTimeKeysCount(ctx, req.UserID, req.DeviceID)
if err != nil {
return err
}
res.OneTimeKeyCounts = []api.OneTimeKeysCount{*otks}
return nil return nil
} }
@ -207,15 +212,13 @@ func (a *KeyInternalAPI) QueryDeviceMessages(ctx context.Context, req *api.Query
return nil return nil
} }
maxStreamID := int64(0) maxStreamID := int64(0)
// remove deleted devices
var result []api.DeviceMessage
for _, m := range msgs { for _, m := range msgs {
if m.StreamID > maxStreamID { if m.StreamID > maxStreamID {
maxStreamID = m.StreamID maxStreamID = m.StreamID
} }
} if m.KeyJSON == nil || len(m.KeyJSON) == 0 {
// remove deleted devices
var result []api.DeviceMessage
for _, m := range msgs {
if m.KeyJSON == nil {
continue continue
} }
result = append(result, m) result = append(result, m)

View file

@ -0,0 +1,156 @@
package internal_test
import (
"context"
"reflect"
"testing"
"github.com/matrix-org/dendrite/keyserver/api"
"github.com/matrix-org/dendrite/keyserver/internal"
"github.com/matrix-org/dendrite/keyserver/storage"
"github.com/matrix-org/dendrite/setup/config"
"github.com/matrix-org/dendrite/test"
)
func mustCreateDatabase(t *testing.T, dbType test.DBType) (storage.Database, func()) {
t.Helper()
connStr, close := test.PrepareDBConnectionString(t, dbType)
db, err := storage.NewDatabase(nil, &config.DatabaseOptions{
ConnectionString: config.DataSource(connStr),
})
if err != nil {
t.Fatalf("failed to create new user db: %v", err)
}
return db, close
}
func Test_QueryDeviceMessages(t *testing.T) {
alice := test.NewUser(t)
type args struct {
req *api.QueryDeviceMessagesRequest
res *api.QueryDeviceMessagesResponse
}
tests := []struct {
name string
args args
wantErr bool
want *api.QueryDeviceMessagesResponse
}{
{
name: "no existing keys",
args: args{
req: &api.QueryDeviceMessagesRequest{
UserID: "@doesNotExist:localhost",
},
res: &api.QueryDeviceMessagesResponse{},
},
want: &api.QueryDeviceMessagesResponse{},
},
{
name: "existing user returns devices",
args: args{
req: &api.QueryDeviceMessagesRequest{
UserID: alice.ID,
},
res: &api.QueryDeviceMessagesResponse{},
},
want: &api.QueryDeviceMessagesResponse{
StreamID: 6,
Devices: []api.DeviceMessage{
{
Type: api.TypeDeviceKeyUpdate, StreamID: 5, DeviceKeys: &api.DeviceKeys{
DeviceID: "myDevice",
DisplayName: "first device",
UserID: alice.ID,
KeyJSON: []byte("ghi"),
},
},
{
Type: api.TypeDeviceKeyUpdate, StreamID: 6, DeviceKeys: &api.DeviceKeys{
DeviceID: "mySecondDevice",
DisplayName: "second device",
UserID: alice.ID,
KeyJSON: []byte("jkl"),
}, // streamID 6
},
},
},
},
}
deviceMessages := []api.DeviceMessage{
{ // not the user we're looking for
Type: api.TypeDeviceKeyUpdate, DeviceKeys: &api.DeviceKeys{
UserID: "@doesNotExist:localhost",
},
// streamID 1 for this user
},
{ // empty keyJSON will be ignored
Type: api.TypeDeviceKeyUpdate, DeviceKeys: &api.DeviceKeys{
DeviceID: "myDevice",
UserID: alice.ID,
}, // streamID 1
},
{
Type: api.TypeDeviceKeyUpdate, DeviceKeys: &api.DeviceKeys{
DeviceID: "myDevice",
UserID: alice.ID,
KeyJSON: []byte("abc"),
}, // streamID 2
},
{
Type: api.TypeDeviceKeyUpdate, DeviceKeys: &api.DeviceKeys{
DeviceID: "myDevice",
UserID: alice.ID,
KeyJSON: []byte("def"),
}, // streamID 3
},
{
Type: api.TypeDeviceKeyUpdate, DeviceKeys: &api.DeviceKeys{
DeviceID: "myDevice",
UserID: alice.ID,
KeyJSON: []byte(""),
}, // streamID 4
},
{
Type: api.TypeDeviceKeyUpdate, DeviceKeys: &api.DeviceKeys{
DeviceID: "myDevice",
DisplayName: "first device",
UserID: alice.ID,
KeyJSON: []byte("ghi"),
}, // streamID 5
},
{
Type: api.TypeDeviceKeyUpdate, DeviceKeys: &api.DeviceKeys{
DeviceID: "mySecondDevice",
UserID: alice.ID,
KeyJSON: []byte("jkl"),
DisplayName: "second device",
}, // streamID 6
},
}
ctx := context.Background()
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
db, closeDB := mustCreateDatabase(t, dbType)
defer closeDB()
if err := db.StoreLocalDeviceKeys(ctx, deviceMessages); err != nil {
t.Fatalf("failed to store local devicesKeys")
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &internal.KeyInternalAPI{
DB: db,
}
if err := a.QueryDeviceMessages(ctx, tt.args.req, tt.args.res); (err != nil) != tt.wantErr {
t.Errorf("QueryDeviceMessages() error = %v, wantErr %v", err, tt.wantErr)
}
got := tt.args.res
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("QueryDeviceMessages(): got:\n%+v, want:\n%+v", got, tt.want)
}
})
}
})
}

View file

@ -20,6 +20,7 @@ import (
"time" "time"
"github.com/lib/pq" "github.com/lib/pq"
"github.com/matrix-org/dendrite/internal" "github.com/matrix-org/dendrite/internal"
"github.com/matrix-org/dendrite/internal/sqlutil" "github.com/matrix-org/dendrite/internal/sqlutil"
"github.com/matrix-org/dendrite/keyserver/api" "github.com/matrix-org/dendrite/keyserver/api"
@ -204,20 +205,17 @@ func (s *deviceKeysStatements) SelectBatchDeviceKeys(ctx context.Context, userID
deviceIDMap[d] = true deviceIDMap[d] = true
} }
var result []api.DeviceMessage var result []api.DeviceMessage
var displayName sql.NullString
for rows.Next() { for rows.Next() {
dk := api.DeviceMessage{ dk := api.DeviceMessage{
Type: api.TypeDeviceKeyUpdate, Type: api.TypeDeviceKeyUpdate,
DeviceKeys: &api.DeviceKeys{}, DeviceKeys: &api.DeviceKeys{
UserID: userID,
},
} }
dk.UserID = userID if err := rows.Scan(&dk.DeviceID, &dk.KeyJSON, &dk.StreamID, &displayName); err != nil {
var keyJSON string
var streamID int64
var displayName sql.NullString
if err := rows.Scan(&dk.DeviceID, &keyJSON, &streamID, &displayName); err != nil {
return nil, err return nil, err
} }
dk.KeyJSON = []byte(keyJSON)
dk.StreamID = streamID
if displayName.Valid { if displayName.Valid {
dk.DisplayName = displayName.String dk.DisplayName = displayName.String
} }

View file

@ -137,21 +137,17 @@ func (s *deviceKeysStatements) SelectBatchDeviceKeys(ctx context.Context, userID
} }
defer internal.CloseAndLogIfError(ctx, rows, "selectBatchDeviceKeysStmt: rows.close() failed") defer internal.CloseAndLogIfError(ctx, rows, "selectBatchDeviceKeysStmt: rows.close() failed")
var result []api.DeviceMessage var result []api.DeviceMessage
var displayName sql.NullString
for rows.Next() { for rows.Next() {
dk := api.DeviceMessage{ dk := api.DeviceMessage{
Type: api.TypeDeviceKeyUpdate, Type: api.TypeDeviceKeyUpdate,
DeviceKeys: &api.DeviceKeys{}, DeviceKeys: &api.DeviceKeys{
UserID: userID,
},
} }
dk.Type = api.TypeDeviceKeyUpdate if err := rows.Scan(&dk.DeviceID, &dk.KeyJSON, &dk.StreamID, &displayName); err != nil {
dk.UserID = userID
var keyJSON string
var streamID int64
var displayName sql.NullString
if err := rows.Scan(&dk.DeviceID, &keyJSON, &streamID, &displayName); err != nil {
return nil, err return nil, err
} }
dk.KeyJSON = []byte(keyJSON)
dk.StreamID = streamID
if displayName.Valid { if displayName.Valid {
dk.DisplayName = displayName.String dk.DisplayName = displayName.String
} }

View file

@ -80,6 +80,7 @@ type PerformJoinRequest struct {
UserID string `json:"user_id"` UserID string `json:"user_id"`
Content map[string]interface{} `json:"content"` Content map[string]interface{} `json:"content"`
ServerNames []gomatrixserverlib.ServerName `json:"server_names"` ServerNames []gomatrixserverlib.ServerName `json:"server_names"`
Unsigned map[string]interface{} `json:"unsigned"`
} }
type PerformJoinResponse struct { type PerformJoinResponse struct {

View file

@ -278,6 +278,7 @@ type QuerySharedUsersRequest struct {
OtherUserIDs []string OtherUserIDs []string
ExcludeRoomIDs []string ExcludeRoomIDs []string
IncludeRoomIDs []string IncludeRoomIDs []string
LocalOnly bool
} }
type QuerySharedUsersResponse struct { type QuerySharedUsersResponse struct {

View file

@ -13,8 +13,6 @@
package auth package auth
import ( import (
"encoding/json"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
) )
@ -30,7 +28,7 @@ func IsServerAllowed(
historyVisibility := HistoryVisibilityForRoom(authEvents) historyVisibility := HistoryVisibilityForRoom(authEvents)
// 1. If the history_visibility was set to world_readable, allow. // 1. If the history_visibility was set to world_readable, allow.
if historyVisibility == "world_readable" { if historyVisibility == gomatrixserverlib.HistoryVisibilityWorldReadable {
return true return true
} }
// 2. If the user's membership was join, allow. // 2. If the user's membership was join, allow.
@ -39,12 +37,12 @@ func IsServerAllowed(
return true return true
} }
// 3. If history_visibility was set to shared, and the user joined the room at any point after the event was sent, allow. // 3. If history_visibility was set to shared, and the user joined the room at any point after the event was sent, allow.
if historyVisibility == "shared" && serverCurrentlyInRoom { if historyVisibility == gomatrixserverlib.HistoryVisibilityShared && serverCurrentlyInRoom {
return true return true
} }
// 4. If the user's membership was invite, and the history_visibility was set to invited, allow. // 4. If the user's membership was invite, and the history_visibility was set to invited, allow.
invitedUserExists := IsAnyUserOnServerWithMembership(serverName, authEvents, gomatrixserverlib.Invite) invitedUserExists := IsAnyUserOnServerWithMembership(serverName, authEvents, gomatrixserverlib.Invite)
if invitedUserExists && historyVisibility == "invited" { if invitedUserExists && historyVisibility == gomatrixserverlib.HistoryVisibilityInvited {
return true return true
} }
@ -52,27 +50,16 @@ func IsServerAllowed(
return false return false
} }
func HistoryVisibilityForRoom(authEvents []*gomatrixserverlib.Event) string { func HistoryVisibilityForRoom(authEvents []*gomatrixserverlib.Event) gomatrixserverlib.HistoryVisibility {
// https://matrix.org/docs/spec/client_server/r0.6.0#id87 // https://matrix.org/docs/spec/client_server/r0.6.0#id87
// By default if no history_visibility is set, or if the value is not understood, the visibility is assumed to be shared. // By default if no history_visibility is set, or if the value is not understood, the visibility is assumed to be shared.
visibility := "shared" visibility := gomatrixserverlib.HistoryVisibilityShared
knownStates := []string{"invited", "joined", "shared", "world_readable"}
for _, ev := range authEvents { for _, ev := range authEvents {
if ev.Type() != gomatrixserverlib.MRoomHistoryVisibility { if ev.Type() != gomatrixserverlib.MRoomHistoryVisibility {
continue continue
} }
// TODO: This should be HistoryVisibilityContent to match things like 'MemberContent'. Do this when moving to GMSL if vis, err := ev.HistoryVisibility(); err == nil {
content := struct { visibility = vis
HistoryVisibility string `json:"history_visibility"`
}{}
if err := json.Unmarshal(ev.Content(), &content); err != nil {
break // value is not understood
}
for _, s := range knownStates {
if s == content.HistoryVisibility {
visibility = s
break
}
} }
} }
return visibility return visibility
@ -80,6 +67,9 @@ func HistoryVisibilityForRoom(authEvents []*gomatrixserverlib.Event) string {
func IsAnyUserOnServerWithMembership(serverName gomatrixserverlib.ServerName, authEvents []*gomatrixserverlib.Event, wantMembership string) bool { func IsAnyUserOnServerWithMembership(serverName gomatrixserverlib.ServerName, authEvents []*gomatrixserverlib.Event, wantMembership string) bool {
for _, ev := range authEvents { for _, ev := range authEvents {
if ev.Type() != gomatrixserverlib.MRoomMember {
continue
}
membership, err := ev.Membership() membership, err := ev.Membership()
if err != nil || membership != wantMembership { if err != nil || membership != wantMembership {
continue continue

View file

@ -7,6 +7,9 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
"github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/dendrite/roomserver/auth" "github.com/matrix-org/dendrite/roomserver/auth"
"github.com/matrix-org/dendrite/roomserver/state" "github.com/matrix-org/dendrite/roomserver/state"
@ -14,8 +17,6 @@ import (
"github.com/matrix-org/dendrite/roomserver/storage/shared" "github.com/matrix-org/dendrite/roomserver/storage/shared"
"github.com/matrix-org/dendrite/roomserver/storage/tables" "github.com/matrix-org/dendrite/roomserver/storage/tables"
"github.com/matrix-org/dendrite/roomserver/types" "github.com/matrix-org/dendrite/roomserver/types"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
) )
// TODO: temporary package which has helper functions used by both internal/perform packages. // TODO: temporary package which has helper functions used by both internal/perform packages.
@ -97,35 +98,35 @@ func IsServerCurrentlyInRoom(ctx context.Context, db storage.Database, serverNam
func IsInvitePending( func IsInvitePending(
ctx context.Context, db storage.Database, ctx context.Context, db storage.Database,
roomID, userID string, roomID, userID string,
) (bool, string, string, error) { ) (bool, string, string, *gomatrixserverlib.Event, error) {
// Look up the room NID for the supplied room ID. // Look up the room NID for the supplied room ID.
info, err := db.RoomInfo(ctx, roomID) info, err := db.RoomInfo(ctx, roomID)
if err != nil { if err != nil {
return false, "", "", fmt.Errorf("r.DB.RoomInfo: %w", err) return false, "", "", nil, fmt.Errorf("r.DB.RoomInfo: %w", err)
} }
if info == nil { if info == nil {
return false, "", "", fmt.Errorf("cannot get RoomInfo: unknown room ID %s", roomID) return false, "", "", nil, fmt.Errorf("cannot get RoomInfo: unknown room ID %s", roomID)
} }
// Look up the state key NID for the supplied user ID. // Look up the state key NID for the supplied user ID.
targetUserNIDs, err := db.EventStateKeyNIDs(ctx, []string{userID}) targetUserNIDs, err := db.EventStateKeyNIDs(ctx, []string{userID})
if err != nil { if err != nil {
return false, "", "", fmt.Errorf("r.DB.EventStateKeyNIDs: %w", err) return false, "", "", nil, fmt.Errorf("r.DB.EventStateKeyNIDs: %w", err)
} }
targetUserNID, targetUserFound := targetUserNIDs[userID] targetUserNID, targetUserFound := targetUserNIDs[userID]
if !targetUserFound { if !targetUserFound {
return false, "", "", fmt.Errorf("missing NID for user %q (%+v)", userID, targetUserNIDs) return false, "", "", nil, fmt.Errorf("missing NID for user %q (%+v)", userID, targetUserNIDs)
} }
// Let's see if we have an event active for the user in the room. If // Let's see if we have an event active for the user in the room. If
// we do then it will contain a server name that we can direct the // we do then it will contain a server name that we can direct the
// send_leave to. // send_leave to.
senderUserNIDs, eventIDs, err := db.GetInvitesForUser(ctx, info.RoomNID, targetUserNID) senderUserNIDs, eventIDs, eventJSON, err := db.GetInvitesForUser(ctx, info.RoomNID, targetUserNID)
if err != nil { if err != nil {
return false, "", "", fmt.Errorf("r.DB.GetInvitesForUser: %w", err) return false, "", "", nil, fmt.Errorf("r.DB.GetInvitesForUser: %w", err)
} }
if len(senderUserNIDs) == 0 { if len(senderUserNIDs) == 0 {
return false, "", "", nil return false, "", "", nil, nil
} }
userNIDToEventID := make(map[types.EventStateKeyNID]string) userNIDToEventID := make(map[types.EventStateKeyNID]string)
for i, nid := range senderUserNIDs { for i, nid := range senderUserNIDs {
@ -135,18 +136,20 @@ func IsInvitePending(
// Look up the user ID from the NID. // Look up the user ID from the NID.
senderUsers, err := db.EventStateKeys(ctx, senderUserNIDs) senderUsers, err := db.EventStateKeys(ctx, senderUserNIDs)
if err != nil { if err != nil {
return false, "", "", fmt.Errorf("r.DB.EventStateKeys: %w", err) return false, "", "", nil, fmt.Errorf("r.DB.EventStateKeys: %w", err)
} }
if len(senderUsers) == 0 { if len(senderUsers) == 0 {
return false, "", "", fmt.Errorf("no senderUsers") return false, "", "", nil, fmt.Errorf("no senderUsers")
} }
senderUser, senderUserFound := senderUsers[senderUserNIDs[0]] senderUser, senderUserFound := senderUsers[senderUserNIDs[0]]
if !senderUserFound { if !senderUserFound {
return false, "", "", fmt.Errorf("missing user for NID %d (%+v)", senderUserNIDs[0], senderUsers) return false, "", "", nil, fmt.Errorf("missing user for NID %d (%+v)", senderUserNIDs[0], senderUsers)
} }
return true, senderUser, userNIDToEventID[senderUserNIDs[0]], nil event, err := gomatrixserverlib.NewEventFromTrustedJSON(eventJSON, false, info.RoomVersion)
return true, senderUser, userNIDToEventID[senderUserNIDs[0]], event, err
} }
// GetMembershipsAtState filters the state events to // GetMembershipsAtState filters the state events to
@ -321,7 +324,7 @@ func slowGetHistoryVisibilityState(
func ScanEventTree( func ScanEventTree(
ctx context.Context, db storage.Database, info *types.RoomInfo, front []string, visited map[string]bool, limit int, ctx context.Context, db storage.Database, info *types.RoomInfo, front []string, visited map[string]bool, limit int,
serverName gomatrixserverlib.ServerName, serverName gomatrixserverlib.ServerName,
) ([]types.EventNID, error) { ) ([]types.EventNID, map[string]struct{}, error) {
var resultNIDs []types.EventNID var resultNIDs []types.EventNID
var err error var err error
var allowed bool var allowed bool
@ -342,6 +345,7 @@ func ScanEventTree(
var checkedServerInRoom bool var checkedServerInRoom bool
var isServerInRoom bool var isServerInRoom bool
redactEventIDs := make(map[string]struct{})
// Loop through the event IDs to retrieve the requested events and go // Loop through the event IDs to retrieve the requested events and go
// through the whole tree (up to the provided limit) using the events' // through the whole tree (up to the provided limit) using the events'
@ -355,7 +359,7 @@ BFSLoop:
// Retrieve the events to process from the database. // Retrieve the events to process from the database.
events, err = db.EventsFromIDs(ctx, front) events, err = db.EventsFromIDs(ctx, front)
if err != nil { if err != nil {
return resultNIDs, err return resultNIDs, redactEventIDs, err
} }
if !checkedServerInRoom && len(events) > 0 { if !checkedServerInRoom && len(events) > 0 {
@ -392,16 +396,16 @@ BFSLoop:
) )
// drop the error, as we will often error at the DB level if we don't have the prev_event itself. Let's // drop the error, as we will often error at the DB level if we don't have the prev_event itself. Let's
// just return what we have. // just return what we have.
return resultNIDs, nil return resultNIDs, redactEventIDs, nil
} }
// If the event hasn't been seen before and the HS // If the event hasn't been seen before and the HS
// requesting to retrieve it is allowed to do so, add it to // requesting to retrieve it is allowed to do so, add it to
// the list of events to retrieve. // the list of events to retrieve.
if allowed { next = append(next, pre)
next = append(next, pre) if !allowed {
} else {
util.GetLogger(ctx).WithField("server", serverName).WithField("event_id", pre).Info("Not allowed to see event") util.GetLogger(ctx).WithField("server", serverName).WithField("event_id", pre).Info("Not allowed to see event")
redactEventIDs[pre] = struct{}{}
} }
} }
} }
@ -410,7 +414,7 @@ BFSLoop:
front = next front = next
} }
return resultNIDs, err return resultNIDs, redactEventIDs, err
} }
func QueryLatestEventsAndState( func QueryLatestEventsAndState(

View file

@ -173,12 +173,15 @@ func (r *Inputer) processRoomEvent(
for _, server := range serverRes.ServerNames { for _, server := range serverRes.ServerNames {
servers[server] = struct{}{} servers[server] = struct{}{}
} }
// Don't try to talk to ourselves.
delete(servers, r.Cfg.Matrix.ServerName)
// Now build up the list of servers.
serverRes.ServerNames = serverRes.ServerNames[:0] serverRes.ServerNames = serverRes.ServerNames[:0]
if input.Origin != "" { if input.Origin != "" && input.Origin != r.Cfg.Matrix.ServerName {
serverRes.ServerNames = append(serverRes.ServerNames, input.Origin) serverRes.ServerNames = append(serverRes.ServerNames, input.Origin)
delete(servers, input.Origin) delete(servers, input.Origin)
} }
if senderDomain != input.Origin { if senderDomain != input.Origin && senderDomain != r.Cfg.Matrix.ServerName {
serverRes.ServerNames = append(serverRes.ServerNames, senderDomain) serverRes.ServerNames = append(serverRes.ServerNames, senderDomain)
delete(servers, senderDomain) delete(servers, senderDomain)
} }

View file

@ -78,7 +78,7 @@ func (r *Backfiller) PerformBackfill(
} }
// Scan the event tree for events to send back. // Scan the event tree for events to send back.
resultNIDs, err := helpers.ScanEventTree(ctx, r.DB, info, front, visited, request.Limit, request.ServerName) resultNIDs, redactEventIDs, err := helpers.ScanEventTree(ctx, r.DB, info, front, visited, request.Limit, request.ServerName)
if err != nil { if err != nil {
return err return err
} }
@ -95,6 +95,9 @@ func (r *Backfiller) PerformBackfill(
} }
for _, event := range loadedEvents { for _, event := range loadedEvents {
if _, ok := redactEventIDs[event.EventID()]; ok {
event.Redact()
}
response.Events = append(response.Events, event.Headered(info.RoomVersion)) response.Events = append(response.Events, event.Headered(info.RoomVersion))
} }

View file

@ -22,6 +22,10 @@ import (
"time" "time"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
"github.com/matrix-org/gomatrixserverlib"
"github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
fsAPI "github.com/matrix-org/dendrite/federationapi/api" fsAPI "github.com/matrix-org/dendrite/federationapi/api"
"github.com/matrix-org/dendrite/internal/eventutil" "github.com/matrix-org/dendrite/internal/eventutil"
"github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/roomserver/api"
@ -32,8 +36,6 @@ import (
"github.com/matrix-org/dendrite/roomserver/storage" "github.com/matrix-org/dendrite/roomserver/storage"
"github.com/matrix-org/dendrite/roomserver/types" "github.com/matrix-org/dendrite/roomserver/types"
"github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/dendrite/setup/config"
"github.com/matrix-org/gomatrixserverlib"
"github.com/sirupsen/logrus"
) )
type Joiner struct { type Joiner struct {
@ -236,8 +238,8 @@ func (r *Joiner) performJoinRoomByID(
// Force a federated join if we're dealing with a pending invite // Force a federated join if we're dealing with a pending invite
// and we aren't in the room. // and we aren't in the room.
isInvitePending, inviteSender, _, err := helpers.IsInvitePending(ctx, r.DB, req.RoomIDOrAlias, req.UserID) isInvitePending, inviteSender, _, inviteEvent, err := helpers.IsInvitePending(ctx, r.DB, req.RoomIDOrAlias, req.UserID)
if err == nil && isInvitePending { if err == nil && !serverInRoom && isInvitePending {
_, inviterDomain, ierr := gomatrixserverlib.SplitID('@', inviteSender) _, inviterDomain, ierr := gomatrixserverlib.SplitID('@', inviteSender)
if ierr != nil { if ierr != nil {
return "", "", fmt.Errorf("gomatrixserverlib.SplitID: %w", err) return "", "", fmt.Errorf("gomatrixserverlib.SplitID: %w", err)
@ -248,6 +250,17 @@ func (r *Joiner) performJoinRoomByID(
if inviterDomain != r.Cfg.Matrix.ServerName { if inviterDomain != r.Cfg.Matrix.ServerName {
req.ServerNames = append(req.ServerNames, inviterDomain) req.ServerNames = append(req.ServerNames, inviterDomain)
forceFederatedJoin = true forceFederatedJoin = true
memberEvent := gjson.Parse(string(inviteEvent.JSON()))
// only set unsigned if we've got a content.membership, which we _should_
if memberEvent.Get("content.membership").Exists() {
req.Unsigned = map[string]interface{}{
"prev_sender": memberEvent.Get("sender").Str,
"prev_content": map[string]interface{}{
"is_direct": memberEvent.Get("content.is_direct").Bool(),
"membership": memberEvent.Get("content.membership").Str,
},
}
}
} }
} }
@ -348,6 +361,7 @@ func (r *Joiner) performFederatedJoinRoomByID(
UserID: req.UserID, // the user ID joining the room UserID: req.UserID, // the user ID joining the room
ServerNames: req.ServerNames, // the server to try joining with ServerNames: req.ServerNames, // the server to try joining with
Content: req.Content, // the membership event content Content: req.Content, // the membership event content
Unsigned: req.Unsigned, // the unsigned event content, if any
} }
fedRes := fsAPI.PerformJoinResponse{} fedRes := fsAPI.PerformJoinResponse{}
r.FSAPI.PerformJoin(ctx, &fedReq, &fedRes) r.FSAPI.PerformJoin(ctx, &fedReq, &fedRes)

View file

@ -79,7 +79,7 @@ func (r *Leaver) performLeaveRoomByID(
) ([]api.OutputEvent, error) { ) ([]api.OutputEvent, error) {
// If there's an invite outstanding for the room then respond to // If there's an invite outstanding for the room then respond to
// that. // that.
isInvitePending, senderUser, eventID, err := helpers.IsInvitePending(ctx, r.DB, req.RoomID, req.UserID) isInvitePending, senderUser, eventID, _, err := helpers.IsInvitePending(ctx, r.DB, req.RoomID, req.UserID)
if err == nil && isInvitePending { if err == nil && isInvitePending {
_, senderDomain, serr := gomatrixserverlib.SplitID('@', senderUser) _, senderDomain, serr := gomatrixserverlib.SplitID('@', senderUser)
if serr != nil { if serr != nil {

View file

@ -453,7 +453,7 @@ func (r *Queryer) QueryMissingEvents(
return fmt.Errorf("missing RoomInfo for room %s", events[0].RoomID()) return fmt.Errorf("missing RoomInfo for room %s", events[0].RoomID())
} }
resultNIDs, err := helpers.ScanEventTree(ctx, r.DB, info, front, visited, request.Limit, request.ServerName) resultNIDs, redactEventIDs, err := helpers.ScanEventTree(ctx, r.DB, info, front, visited, request.Limit, request.ServerName)
if err != nil { if err != nil {
return err return err
} }
@ -470,7 +470,9 @@ func (r *Queryer) QueryMissingEvents(
if verr != nil { if verr != nil {
return verr return verr
} }
if _, ok := redactEventIDs[event.EventID()]; ok {
event.Redact()
}
response.Events = append(response.Events, event.Headered(roomVersion)) response.Events = append(response.Events, event.Headered(roomVersion))
} }
} }
@ -799,7 +801,7 @@ func (r *Queryer) QuerySharedUsers(ctx context.Context, req *api.QuerySharedUser
} }
roomIDs = roomIDs[:j] roomIDs = roomIDs[:j]
users, err := r.DB.JoinedUsersSetInRooms(ctx, roomIDs, req.OtherUserIDs) users, err := r.DB.JoinedUsersSetInRooms(ctx, roomIDs, req.OtherUserIDs, req.LocalOnly)
if err != nil { if err != nil {
return err return err
} }
@ -872,7 +874,7 @@ func (r *Queryer) QueryRestrictedJoinAllowed(ctx context.Context, req *api.Query
// but we don't specify an authorised via user, since the event auth // but we don't specify an authorised via user, since the event auth
// will allow the join anyway. // will allow the join anyway.
var pending bool var pending bool
if pending, _, _, err = helpers.IsInvitePending(ctx, r.DB, req.RoomID, req.UserID); err != nil { if pending, _, _, _, err = helpers.IsInvitePending(ctx, r.DB, req.RoomID, req.UserID); err != nil {
return fmt.Errorf("helpers.IsInvitePending: %w", err) return fmt.Errorf("helpers.IsInvitePending: %w", err)
} else if pending { } else if pending {
res.Allowed = true res.Allowed = true

View file

@ -17,12 +17,13 @@ package producers
import ( import (
"encoding/json" "encoding/json"
"github.com/matrix-org/dendrite/roomserver/acls"
"github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/dendrite/setup/jetstream"
"github.com/nats-io/nats.go" "github.com/nats-io/nats.go"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"github.com/matrix-org/dendrite/roomserver/acls"
"github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/dendrite/setup/jetstream"
) )
var keyContentFields = map[string]string{ var keyContentFields = map[string]string{
@ -40,10 +41,8 @@ type RoomEventProducer struct {
func (r *RoomEventProducer) ProduceRoomEvents(roomID string, updates []api.OutputEvent) error { func (r *RoomEventProducer) ProduceRoomEvents(roomID string, updates []api.OutputEvent) error {
var err error var err error
for _, update := range updates { for _, update := range updates {
msg := &nats.Msg{ msg := nats.NewMsg(r.Topic)
Subject: r.Topic, msg.Header.Set(jetstream.RoomEventType, string(update.Type))
Header: nats.Header{},
}
msg.Header.Set(jetstream.RoomID, roomID) msg.Header.Set(jetstream.RoomID, roomID)
msg.Data, err = json.Marshal(update) msg.Data, err = json.Marshal(update)
if err != nil { if err != nil {

View file

@ -17,10 +17,11 @@ package storage
import ( import (
"context" "context"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/dendrite/roomserver/storage/shared" "github.com/matrix-org/dendrite/roomserver/storage/shared"
"github.com/matrix-org/dendrite/roomserver/storage/tables" "github.com/matrix-org/dendrite/roomserver/storage/tables"
"github.com/matrix-org/dendrite/roomserver/types" "github.com/matrix-org/dendrite/roomserver/types"
"github.com/matrix-org/gomatrixserverlib"
) )
type Database interface { type Database interface {
@ -104,7 +105,7 @@ type Database interface {
// Look up the active invites targeting a user in a room and return the // Look up the active invites targeting a user in a room and return the
// numeric state key IDs for the user IDs who sent them along with the event IDs for the invites. // numeric state key IDs for the user IDs who sent them along with the event IDs for the invites.
// Returns an error if there was a problem talking to the database. // Returns an error if there was a problem talking to the database.
GetInvitesForUser(ctx context.Context, roomNID types.RoomNID, targetUserNID types.EventStateKeyNID) (senderUserIDs []types.EventStateKeyNID, eventIDs []string, err error) GetInvitesForUser(ctx context.Context, roomNID types.RoomNID, targetUserNID types.EventStateKeyNID) (senderUserIDs []types.EventStateKeyNID, eventIDs []string, inviteEventJSON []byte, err error)
// Save a given room alias with the room ID it refers to. // Save a given room alias with the room ID it refers to.
// Returns an error if there was a problem talking to the database. // Returns an error if there was a problem talking to the database.
SetRoomAlias(ctx context.Context, alias string, roomID string, creatorUserID string) error SetRoomAlias(ctx context.Context, alias string, roomID string, creatorUserID string) error
@ -157,7 +158,7 @@ type Database interface {
// If a tuple has the StateKey of '*' and allowWildcards=true then all state events with the EventType should be returned. // If a tuple has the StateKey of '*' and allowWildcards=true then all state events with the EventType should be returned.
GetBulkStateContent(ctx context.Context, roomIDs []string, tuples []gomatrixserverlib.StateKeyTuple, allowWildcards bool) ([]tables.StrippedEvent, error) GetBulkStateContent(ctx context.Context, roomIDs []string, tuples []gomatrixserverlib.StateKeyTuple, allowWildcards bool) ([]tables.StrippedEvent, error)
// JoinedUsersSetInRooms returns how many times each of the given users appears across the given rooms. // JoinedUsersSetInRooms returns how many times each of the given users appears across the given rooms.
JoinedUsersSetInRooms(ctx context.Context, roomIDs, userIDs []string) (map[string]int, error) JoinedUsersSetInRooms(ctx context.Context, roomIDs, userIDs []string, localOnly bool) (map[string]int, error)
// GetLocalServerInRoom returns true if we think we're in a given room or false otherwise. // GetLocalServerInRoom returns true if we think we're in a given room or false otherwise.
GetLocalServerInRoom(ctx context.Context, roomNID types.RoomNID) (bool, error) GetLocalServerInRoom(ctx context.Context, roomNID types.RoomNID) (bool, error)
// GetServerInRoom returns true if we think a server is in a given room or false otherwise. // GetServerInRoom returns true if we think a server is in a given room or false otherwise.

View file

@ -61,7 +61,7 @@ const insertInviteEventSQL = "" +
" ON CONFLICT DO NOTHING" " ON CONFLICT DO NOTHING"
const selectInviteActiveForUserInRoomSQL = "" + const selectInviteActiveForUserInRoomSQL = "" +
"SELECT invite_event_id, sender_nid FROM roomserver_invites" + "SELECT invite_event_id, sender_nid, invite_event_json FROM roomserver_invites" +
" WHERE target_nid = $1 AND room_nid = $2" + " WHERE target_nid = $1 AND room_nid = $2" +
" AND NOT retired" " AND NOT retired"
@ -141,25 +141,26 @@ func (s *inviteStatements) UpdateInviteRetired(
func (s *inviteStatements) SelectInviteActiveForUserInRoom( func (s *inviteStatements) SelectInviteActiveForUserInRoom(
ctx context.Context, txn *sql.Tx, ctx context.Context, txn *sql.Tx,
targetUserNID types.EventStateKeyNID, roomNID types.RoomNID, targetUserNID types.EventStateKeyNID, roomNID types.RoomNID,
) ([]types.EventStateKeyNID, []string, error) { ) ([]types.EventStateKeyNID, []string, []byte, error) {
stmt := sqlutil.TxStmt(txn, s.selectInviteActiveForUserInRoomStmt) stmt := sqlutil.TxStmt(txn, s.selectInviteActiveForUserInRoomStmt)
rows, err := stmt.QueryContext( rows, err := stmt.QueryContext(
ctx, targetUserNID, roomNID, ctx, targetUserNID, roomNID,
) )
if err != nil { if err != nil {
return nil, nil, err return nil, nil, nil, err
} }
defer internal.CloseAndLogIfError(ctx, rows, "selectInviteActiveForUserInRoom: rows.close() failed") defer internal.CloseAndLogIfError(ctx, rows, "selectInviteActiveForUserInRoom: rows.close() failed")
var result []types.EventStateKeyNID var result []types.EventStateKeyNID
var eventIDs []string var eventIDs []string
var inviteEventID string var inviteEventID string
var senderUserNID int64 var senderUserNID int64
var eventJSON []byte
for rows.Next() { for rows.Next() {
if err := rows.Scan(&inviteEventID, &senderUserNID); err != nil { if err := rows.Scan(&inviteEventID, &senderUserNID, &eventJSON); err != nil {
return nil, nil, err return nil, nil, nil, err
} }
result = append(result, types.EventStateKeyNID(senderUserNID)) result = append(result, types.EventStateKeyNID(senderUserNID))
eventIDs = append(eventIDs, inviteEventID) eventIDs = append(eventIDs, inviteEventID)
} }
return result, eventIDs, rows.Err() return result, eventIDs, eventJSON, rows.Err()
} }

View file

@ -68,14 +68,18 @@ CREATE TABLE IF NOT EXISTS roomserver_membership (
var selectJoinedUsersSetForRoomsAndUserSQL = "" + var selectJoinedUsersSetForRoomsAndUserSQL = "" +
"SELECT target_nid, COUNT(room_nid) FROM roomserver_membership" + "SELECT target_nid, COUNT(room_nid) FROM roomserver_membership" +
" WHERE room_nid = ANY($1) AND target_nid = ANY($2) AND" + " WHERE (target_local OR $1 = false)" +
" membership_nid = " + fmt.Sprintf("%d", tables.MembershipStateJoin) + " and forgotten = false" + " AND room_nid = ANY($2) AND target_nid = ANY($3)" +
" AND membership_nid = " + fmt.Sprintf("%d", tables.MembershipStateJoin) +
" AND forgotten = false" +
" GROUP BY target_nid" " GROUP BY target_nid"
var selectJoinedUsersSetForRoomsSQL = "" + var selectJoinedUsersSetForRoomsSQL = "" +
"SELECT target_nid, COUNT(room_nid) FROM roomserver_membership" + "SELECT target_nid, COUNT(room_nid) FROM roomserver_membership" +
" WHERE room_nid = ANY($1) AND" + " WHERE (target_local OR $1 = false) " +
" membership_nid = " + fmt.Sprintf("%d", tables.MembershipStateJoin) + " and forgotten = false" + " AND room_nid = ANY($2)" +
" AND membership_nid = " + fmt.Sprintf("%d", tables.MembershipStateJoin) +
" AND forgotten = false" +
" GROUP BY target_nid" " GROUP BY target_nid"
// Insert a row in to membership table so that it can be locked by the // Insert a row in to membership table so that it can be locked by the
@ -334,6 +338,7 @@ func (s *membershipStatements) SelectJoinedUsersSetForRooms(
ctx context.Context, txn *sql.Tx, ctx context.Context, txn *sql.Tx,
roomNIDs []types.RoomNID, roomNIDs []types.RoomNID,
userNIDs []types.EventStateKeyNID, userNIDs []types.EventStateKeyNID,
localOnly bool,
) (map[types.EventStateKeyNID]int, error) { ) (map[types.EventStateKeyNID]int, error) {
var ( var (
rows *sql.Rows rows *sql.Rows
@ -342,9 +347,9 @@ func (s *membershipStatements) SelectJoinedUsersSetForRooms(
stmt := sqlutil.TxStmt(txn, s.selectJoinedUsersSetForRoomsStmt) stmt := sqlutil.TxStmt(txn, s.selectJoinedUsersSetForRoomsStmt)
if len(userNIDs) > 0 { if len(userNIDs) > 0 {
stmt = sqlutil.TxStmt(txn, s.selectJoinedUsersSetForRoomsAndUserStmt) stmt = sqlutil.TxStmt(txn, s.selectJoinedUsersSetForRoomsAndUserStmt)
rows, err = stmt.QueryContext(ctx, pq.Array(roomNIDs), pq.Array(userNIDs)) rows, err = stmt.QueryContext(ctx, localOnly, pq.Array(roomNIDs), pq.Array(userNIDs))
} else { } else {
rows, err = stmt.QueryContext(ctx, pq.Array(roomNIDs)) rows, err = stmt.QueryContext(ctx, localOnly, pq.Array(roomNIDs))
} }
if err != nil { if err != nil {

View file

@ -21,10 +21,11 @@ import (
"fmt" "fmt"
"github.com/lib/pq" "github.com/lib/pq"
"github.com/matrix-org/util"
"github.com/matrix-org/dendrite/internal/sqlutil" "github.com/matrix-org/dendrite/internal/sqlutil"
"github.com/matrix-org/dendrite/roomserver/storage/tables" "github.com/matrix-org/dendrite/roomserver/storage/tables"
"github.com/matrix-org/dendrite/roomserver/types" "github.com/matrix-org/dendrite/roomserver/types"
"github.com/matrix-org/util"
) )
const stateSnapshotSchema = ` const stateSnapshotSchema = `
@ -91,6 +92,7 @@ const bulkSelectStateForHistoryVisibilitySQL = `
WHERE state_snapshot_nid = $1 WHERE state_snapshot_nid = $1
) )
) )
ORDER BY depth ASC
) AS roomserver_events ) AS roomserver_events
INNER JOIN roomserver_event_state_keys INNER JOIN roomserver_event_state_keys
ON roomserver_events.event_state_key_nid = roomserver_event_state_keys.event_state_key_nid ON roomserver_events.event_state_key_nid = roomserver_event_state_keys.event_state_key_nid

View file

@ -7,13 +7,14 @@ import (
"fmt" "fmt"
"sort" "sort"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
"github.com/tidwall/gjson"
"github.com/matrix-org/dendrite/internal/caching" "github.com/matrix-org/dendrite/internal/caching"
"github.com/matrix-org/dendrite/internal/sqlutil" "github.com/matrix-org/dendrite/internal/sqlutil"
"github.com/matrix-org/dendrite/roomserver/storage/tables" "github.com/matrix-org/dendrite/roomserver/storage/tables"
"github.com/matrix-org/dendrite/roomserver/types" "github.com/matrix-org/dendrite/roomserver/types"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
"github.com/tidwall/gjson"
) )
// Ideally, when we have both events we should redact the event JSON and forget about the redaction, but we currently // Ideally, when we have both events we should redact the event JSON and forget about the redaction, but we currently
@ -445,7 +446,7 @@ func (d *Database) GetInvitesForUser(
ctx context.Context, ctx context.Context,
roomNID types.RoomNID, roomNID types.RoomNID,
targetUserNID types.EventStateKeyNID, targetUserNID types.EventStateKeyNID,
) (senderUserIDs []types.EventStateKeyNID, eventIDs []string, err error) { ) (senderUserIDs []types.EventStateKeyNID, eventIDs []string, inviteEventJSON []byte, err error) {
return d.InvitesTable.SelectInviteActiveForUserInRoom(ctx, nil, targetUserNID, roomNID) return d.InvitesTable.SelectInviteActiveForUserInRoom(ctx, nil, targetUserNID, roomNID)
} }
@ -1280,7 +1281,7 @@ func (d *Database) GetBulkStateContent(ctx context.Context, roomIDs []string, tu
} }
// JoinedUsersSetInRooms returns a map of how many times the given users appear in the specified rooms. // JoinedUsersSetInRooms returns a map of how many times the given users appear in the specified rooms.
func (d *Database) JoinedUsersSetInRooms(ctx context.Context, roomIDs, userIDs []string) (map[string]int, error) { func (d *Database) JoinedUsersSetInRooms(ctx context.Context, roomIDs, userIDs []string, localOnly bool) (map[string]int, error) {
roomNIDs, err := d.RoomsTable.BulkSelectRoomNIDs(ctx, nil, roomIDs) roomNIDs, err := d.RoomsTable.BulkSelectRoomNIDs(ctx, nil, roomIDs)
if err != nil { if err != nil {
return nil, err return nil, err
@ -1295,7 +1296,7 @@ func (d *Database) JoinedUsersSetInRooms(ctx context.Context, roomIDs, userIDs [
userNIDs = append(userNIDs, nid) userNIDs = append(userNIDs, nid)
nidToUserID[nid] = id nidToUserID[nid] = id
} }
userNIDToCount, err := d.MembershipTable.SelectJoinedUsersSetForRooms(ctx, nil, roomNIDs, userNIDs) userNIDToCount, err := d.MembershipTable.SelectJoinedUsersSetForRooms(ctx, nil, roomNIDs, userNIDs, localOnly)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -44,7 +44,7 @@ const insertInviteEventSQL = "" +
" ON CONFLICT DO NOTHING" " ON CONFLICT DO NOTHING"
const selectInviteActiveForUserInRoomSQL = "" + const selectInviteActiveForUserInRoomSQL = "" +
"SELECT invite_event_id, sender_nid FROM roomserver_invites" + "SELECT invite_event_id, sender_nid, invite_event_json FROM roomserver_invites" +
" WHERE target_nid = $1 AND room_nid = $2" + " WHERE target_nid = $1 AND room_nid = $2" +
" AND NOT retired" " AND NOT retired"
@ -136,25 +136,26 @@ func (s *inviteStatements) UpdateInviteRetired(
func (s *inviteStatements) SelectInviteActiveForUserInRoom( func (s *inviteStatements) SelectInviteActiveForUserInRoom(
ctx context.Context, txn *sql.Tx, ctx context.Context, txn *sql.Tx,
targetUserNID types.EventStateKeyNID, roomNID types.RoomNID, targetUserNID types.EventStateKeyNID, roomNID types.RoomNID,
) ([]types.EventStateKeyNID, []string, error) { ) ([]types.EventStateKeyNID, []string, []byte, error) {
stmt := sqlutil.TxStmt(txn, s.selectInviteActiveForUserInRoomStmt) stmt := sqlutil.TxStmt(txn, s.selectInviteActiveForUserInRoomStmt)
rows, err := stmt.QueryContext( rows, err := stmt.QueryContext(
ctx, targetUserNID, roomNID, ctx, targetUserNID, roomNID,
) )
if err != nil { if err != nil {
return nil, nil, err return nil, nil, nil, err
} }
defer internal.CloseAndLogIfError(ctx, rows, "selectInviteActiveForUserInRoom: rows.close() failed") defer internal.CloseAndLogIfError(ctx, rows, "selectInviteActiveForUserInRoom: rows.close() failed")
var result []types.EventStateKeyNID var result []types.EventStateKeyNID
var eventIDs []string var eventIDs []string
var eventID string var eventID string
var senderUserNID int64 var senderUserNID int64
var eventJSON []byte
for rows.Next() { for rows.Next() {
if err := rows.Scan(&eventID, &senderUserNID); err != nil { if err := rows.Scan(&eventID, &senderUserNID, &eventJSON); err != nil {
return nil, nil, err return nil, nil, nil, err
} }
result = append(result, types.EventStateKeyNID(senderUserNID)) result = append(result, types.EventStateKeyNID(senderUserNID))
eventIDs = append(eventIDs, eventID) eventIDs = append(eventIDs, eventID)
} }
return result, eventIDs, nil return result, eventIDs, eventJSON, nil
} }

View file

@ -44,14 +44,18 @@ const membershipSchema = `
var selectJoinedUsersSetForRoomsAndUserSQL = "" + var selectJoinedUsersSetForRoomsAndUserSQL = "" +
"SELECT target_nid, COUNT(room_nid) FROM roomserver_membership" + "SELECT target_nid, COUNT(room_nid) FROM roomserver_membership" +
" WHERE room_nid IN ($1) AND target_nid IN ($2) AND" + " WHERE (target_local OR $1 = false)" +
" membership_nid = " + fmt.Sprintf("%d", tables.MembershipStateJoin) + " and forgotten = false" + " AND room_nid IN ($2) AND target_nid IN ($3)" +
" AND membership_nid = " + fmt.Sprintf("%d", tables.MembershipStateJoin) +
" AND forgotten = false" +
" GROUP BY target_nid" " GROUP BY target_nid"
var selectJoinedUsersSetForRoomsSQL = "" + var selectJoinedUsersSetForRoomsSQL = "" +
"SELECT target_nid, COUNT(room_nid) FROM roomserver_membership" + "SELECT target_nid, COUNT(room_nid) FROM roomserver_membership" +
" WHERE room_nid IN ($1) AND " + " WHERE (target_local OR $1 = false)" +
" membership_nid = " + fmt.Sprintf("%d", tables.MembershipStateJoin) + " and forgotten = false" + " AND room_nid IN ($2)" +
" AND membership_nid = " + fmt.Sprintf("%d", tables.MembershipStateJoin) +
" AND forgotten = false" +
" GROUP BY target_nid" " GROUP BY target_nid"
// Insert a row in to membership table so that it can be locked by the // Insert a row in to membership table so that it can be locked by the
@ -305,8 +309,9 @@ func (s *membershipStatements) SelectRoomsWithMembership(
return roomNIDs, nil return roomNIDs, nil
} }
func (s *membershipStatements) SelectJoinedUsersSetForRooms(ctx context.Context, txn *sql.Tx, roomNIDs []types.RoomNID, userNIDs []types.EventStateKeyNID) (map[types.EventStateKeyNID]int, error) { func (s *membershipStatements) SelectJoinedUsersSetForRooms(ctx context.Context, txn *sql.Tx, roomNIDs []types.RoomNID, userNIDs []types.EventStateKeyNID, localOnly bool) (map[types.EventStateKeyNID]int, error) {
params := make([]interface{}, 0, len(roomNIDs)+len(userNIDs)) params := make([]interface{}, 0, 1+len(roomNIDs)+len(userNIDs))
params = append(params, localOnly)
for _, v := range roomNIDs { for _, v := range roomNIDs {
params = append(params, v) params = append(params, v)
} }
@ -314,10 +319,10 @@ func (s *membershipStatements) SelectJoinedUsersSetForRooms(ctx context.Context,
params = append(params, v) params = append(params, v)
} }
query := strings.Replace(selectJoinedUsersSetForRoomsSQL, "($1)", sqlutil.QueryVariadic(len(roomNIDs)), 1) query := strings.Replace(selectJoinedUsersSetForRoomsSQL, "($2)", sqlutil.QueryVariadicOffset(len(roomNIDs), 1), 1)
if len(userNIDs) > 0 { if len(userNIDs) > 0 {
query = strings.Replace(selectJoinedUsersSetForRoomsAndUserSQL, "($1)", sqlutil.QueryVariadic(len(roomNIDs)), 1) query = strings.Replace(selectJoinedUsersSetForRoomsAndUserSQL, "($2)", sqlutil.QueryVariadicOffset(len(roomNIDs), 1), 1)
query = strings.Replace(query, "($2)", sqlutil.QueryVariadicOffset(len(userNIDs), len(roomNIDs)), 1) query = strings.Replace(query, "($3)", sqlutil.QueryVariadicOffset(len(userNIDs), len(roomNIDs)+1), 1)
} }
var rows *sql.Rows var rows *sql.Rows
var err error var err error

View file

@ -116,7 +116,7 @@ type Invites interface {
InsertInviteEvent(ctx context.Context, txn *sql.Tx, inviteEventID string, roomNID types.RoomNID, targetUserNID, senderUserNID types.EventStateKeyNID, inviteEventJSON []byte) (bool, error) InsertInviteEvent(ctx context.Context, txn *sql.Tx, inviteEventID string, roomNID types.RoomNID, targetUserNID, senderUserNID types.EventStateKeyNID, inviteEventJSON []byte) (bool, error)
UpdateInviteRetired(ctx context.Context, txn *sql.Tx, roomNID types.RoomNID, targetUserNID types.EventStateKeyNID) ([]string, error) UpdateInviteRetired(ctx context.Context, txn *sql.Tx, roomNID types.RoomNID, targetUserNID types.EventStateKeyNID) ([]string, error)
// SelectInviteActiveForUserInRoom returns a list of sender state key NIDs and invite event IDs matching those nids. // SelectInviteActiveForUserInRoom returns a list of sender state key NIDs and invite event IDs matching those nids.
SelectInviteActiveForUserInRoom(ctx context.Context, txn *sql.Tx, targetUserNID types.EventStateKeyNID, roomNID types.RoomNID) ([]types.EventStateKeyNID, []string, error) SelectInviteActiveForUserInRoom(ctx context.Context, txn *sql.Tx, targetUserNID types.EventStateKeyNID, roomNID types.RoomNID) ([]types.EventStateKeyNID, []string, []byte, error)
} }
type MembershipState int64 type MembershipState int64
@ -137,7 +137,7 @@ type Membership interface {
UpdateMembership(ctx context.Context, txn *sql.Tx, roomNID types.RoomNID, targetUserNID types.EventStateKeyNID, senderUserNID types.EventStateKeyNID, membership MembershipState, eventNID types.EventNID, forgotten bool) (bool, error) UpdateMembership(ctx context.Context, txn *sql.Tx, roomNID types.RoomNID, targetUserNID types.EventStateKeyNID, senderUserNID types.EventStateKeyNID, membership MembershipState, eventNID types.EventNID, forgotten bool) (bool, error)
SelectRoomsWithMembership(ctx context.Context, txn *sql.Tx, userID types.EventStateKeyNID, membershipState MembershipState) ([]types.RoomNID, error) SelectRoomsWithMembership(ctx context.Context, txn *sql.Tx, userID types.EventStateKeyNID, membershipState MembershipState) ([]types.RoomNID, error)
// SelectJoinedUsersSetForRooms returns how many times each of the given users appears across the given rooms. // SelectJoinedUsersSetForRooms returns how many times each of the given users appears across the given rooms.
SelectJoinedUsersSetForRooms(ctx context.Context, txn *sql.Tx, roomNIDs []types.RoomNID, userNIDs []types.EventStateKeyNID) (map[types.EventStateKeyNID]int, error) SelectJoinedUsersSetForRooms(ctx context.Context, txn *sql.Tx, roomNIDs []types.RoomNID, userNIDs []types.EventStateKeyNID, localOnly bool) (map[types.EventStateKeyNID]int, error)
SelectKnownUsers(ctx context.Context, txn *sql.Tx, userID types.EventStateKeyNID, searchString string, limit int) ([]string, error) SelectKnownUsers(ctx context.Context, txn *sql.Tx, userID types.EventStateKeyNID, searchString string, limit int) ([]string, error)
UpdateForgetMembership(ctx context.Context, txn *sql.Tx, roomNID types.RoomNID, targetUserNID types.EventStateKeyNID, forget bool) error UpdateForgetMembership(ctx context.Context, txn *sql.Tx, roomNID types.RoomNID, targetUserNID types.EventStateKeyNID, forget bool) error
SelectLocalServerInRoom(ctx context.Context, txn *sql.Tx, roomNID types.RoomNID) (bool, error) SelectLocalServerInRoom(ctx context.Context, txn *sql.Tx, roomNID types.RoomNID) (bool, error)

View file

@ -4,6 +4,9 @@ import (
"context" "context"
"testing" "testing"
"github.com/matrix-org/util"
"github.com/stretchr/testify/assert"
"github.com/matrix-org/dendrite/internal/sqlutil" "github.com/matrix-org/dendrite/internal/sqlutil"
"github.com/matrix-org/dendrite/roomserver/storage/postgres" "github.com/matrix-org/dendrite/roomserver/storage/postgres"
"github.com/matrix-org/dendrite/roomserver/storage/sqlite3" "github.com/matrix-org/dendrite/roomserver/storage/sqlite3"
@ -11,8 +14,6 @@ import (
"github.com/matrix-org/dendrite/roomserver/types" "github.com/matrix-org/dendrite/roomserver/types"
"github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/dendrite/setup/config"
"github.com/matrix-org/dendrite/test" "github.com/matrix-org/dendrite/test"
"github.com/matrix-org/util"
"github.com/stretchr/testify/assert"
) )
func mustCreateInviteTable(t *testing.T, dbType test.DBType) (tables.Invites, func()) { func mustCreateInviteTable(t *testing.T, dbType test.DBType) (tables.Invites, func()) {
@ -67,7 +68,7 @@ func TestInviteTable(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, newInvite) assert.True(t, newInvite)
stateKeyNIDs, eventIDs, err := tab.SelectInviteActiveForUserInRoom(ctx, nil, targetUserNID, roomNID) stateKeyNIDs, eventIDs, _, err := tab.SelectInviteActiveForUserInRoom(ctx, nil, targetUserNID, roomNID)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []string{eventID1, eventID2}, eventIDs) assert.Equal(t, []string{eventID1, eventID2}, eventIDs)
assert.Equal(t, []types.EventStateKeyNID{2, 2}, stateKeyNIDs) assert.Equal(t, []types.EventStateKeyNID{2, 2}, stateKeyNIDs)
@ -78,13 +79,13 @@ func TestInviteTable(t *testing.T) {
assert.Equal(t, []string{eventID1, eventID2}, retiredEventIDs) assert.Equal(t, []string{eventID1, eventID2}, retiredEventIDs)
// This should now be empty // This should now be empty
stateKeyNIDs, eventIDs, err = tab.SelectInviteActiveForUserInRoom(ctx, nil, targetUserNID, roomNID) stateKeyNIDs, eventIDs, _, err = tab.SelectInviteActiveForUserInRoom(ctx, nil, targetUserNID, roomNID)
assert.NoError(t, err) assert.NoError(t, err)
assert.Empty(t, eventIDs) assert.Empty(t, eventIDs)
assert.Empty(t, stateKeyNIDs) assert.Empty(t, stateKeyNIDs)
// Non-existent targetUserNID // Non-existent targetUserNID
stateKeyNIDs, eventIDs, err = tab.SelectInviteActiveForUserInRoom(ctx, nil, types.EventStateKeyNID(10), roomNID) stateKeyNIDs, eventIDs, _, err = tab.SelectInviteActiveForUserInRoom(ctx, nil, types.EventStateKeyNID(10), roomNID)
assert.NoError(t, err) assert.NoError(t, err)
assert.Empty(t, stateKeyNIDs) assert.Empty(t, stateKeyNIDs)
assert.Empty(t, eventIDs) assert.Empty(t, eventIDs)

View file

@ -79,7 +79,7 @@ func TestMembershipTable(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, inRoom) assert.True(t, inRoom)
userJoinedToRooms, err := tab.SelectJoinedUsersSetForRooms(ctx, nil, []types.RoomNID{1}, userNIDs) userJoinedToRooms, err := tab.SelectJoinedUsersSetForRooms(ctx, nil, []types.RoomNID{1}, userNIDs, false)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 1, len(userJoinedToRooms)) assert.Equal(t, 1, len(userJoinedToRooms))

View file

@ -18,6 +18,7 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"database/sql" "database/sql"
"encoding/json"
"fmt" "fmt"
"io" "io"
"net" "net"
@ -467,8 +468,13 @@ func (b *BaseDendrite) SetupAndServeHTTP(
w.WriteHeader(200) w.WriteHeader(200)
}) })
b.DendriteAdminMux.HandleFunc("/monitor/health", func(w http.ResponseWriter, r *http.Request) { b.DendriteAdminMux.HandleFunc("/monitor/health", func(w http.ResponseWriter, r *http.Request) {
if b.ProcessContext.IsDegraded() { if isDegraded, reasons := b.ProcessContext.IsDegraded(); isDegraded {
w.WriteHeader(503) w.WriteHeader(503)
_ = json.NewEncoder(w).Encode(struct {
Warnings []string `json:"warnings"`
}{
Warnings: reasons,
})
return return
} }
w.WriteHeader(200) w.WriteHeader(200)

View file

@ -231,24 +231,40 @@ func loadConfig(
return nil, err return nil, err
} }
for i, oldPrivateKey := range c.Global.OldVerifyKeys { for _, key := range c.Global.OldVerifyKeys {
var oldPrivateKeyData []byte switch {
case key.PrivateKeyPath != "":
var oldPrivateKeyData []byte
oldPrivateKeyPath := absPath(basePath, key.PrivateKeyPath)
oldPrivateKeyData, err = readFile(oldPrivateKeyPath)
if err != nil {
return nil, fmt.Errorf("failed to read %q: %w", oldPrivateKeyPath, err)
}
oldPrivateKeyPath := absPath(basePath, oldPrivateKey.PrivateKeyPath) // NOTSPEC: Ordinarily we should enforce key ID formatting, but since there are
oldPrivateKeyData, err = readFile(oldPrivateKeyPath) // a number of private keys out there with non-compatible symbols in them due
if err != nil { // to lack of validation in Synapse, we won't enforce that for old verify keys.
return nil, err keyID, privateKey, perr := readKeyPEM(oldPrivateKeyPath, oldPrivateKeyData, false)
if perr != nil {
return nil, fmt.Errorf("failed to parse %q: %w", oldPrivateKeyPath, perr)
}
key.KeyID = keyID
key.PrivateKey = privateKey
key.PublicKey = gomatrixserverlib.Base64Bytes(privateKey.Public().(ed25519.PublicKey))
case key.KeyID == "":
return nil, fmt.Errorf("'key_id' must be specified if 'public_key' is specified")
case len(key.PublicKey) == ed25519.PublicKeySize:
continue
case len(key.PublicKey) > 0:
return nil, fmt.Errorf("the supplied 'public_key' is the wrong length")
default:
return nil, fmt.Errorf("either specify a 'private_key' path or supply both 'public_key' and 'key_id'")
} }
// NOTSPEC: Ordinarily we should enforce key ID formatting, but since there are
// a number of private keys out there with non-compatible symbols in them due
// to lack of validation in Synapse, we won't enforce that for old verify keys.
keyID, privateKey, perr := readKeyPEM(oldPrivateKeyPath, oldPrivateKeyData, false)
if perr != nil {
return nil, perr
}
c.Global.OldVerifyKeys[i].KeyID, c.Global.OldVerifyKeys[i].PrivateKey = keyID, privateKey
} }
c.MediaAPI.AbsBasePath = Path(absPath(basePath, c.MediaAPI.BasePath)) c.MediaAPI.AbsBasePath = Path(absPath(basePath, c.MediaAPI.BasePath))

View file

@ -27,7 +27,7 @@ type Global struct {
// Information about old private keys that used to be used to sign requests and // Information about old private keys that used to be used to sign requests and
// events on this domain. They will not be used but will be advertised to other // events on this domain. They will not be used but will be advertised to other
// servers that ask for them to help verify old events. // servers that ask for them to help verify old events.
OldVerifyKeys []OldVerifyKeys `yaml:"old_private_keys"` OldVerifyKeys []*OldVerifyKeys `yaml:"old_private_keys"`
// How long a remote server can cache our server key for before requesting it again. // How long a remote server can cache our server key for before requesting it again.
// Increasing this number will reduce the number of requests made by remote servers // Increasing this number will reduce the number of requests made by remote servers
@ -127,8 +127,11 @@ type OldVerifyKeys struct {
// The private key itself. // The private key itself.
PrivateKey ed25519.PrivateKey `yaml:"-"` PrivateKey ed25519.PrivateKey `yaml:"-"`
// The public key, in case only that part is known.
PublicKey gomatrixserverlib.Base64Bytes `yaml:"public_key"`
// The key ID of the private key. // The key ID of the private key.
KeyID gomatrixserverlib.KeyID `yaml:"-"` KeyID gomatrixserverlib.KeyID `yaml:"key_id"`
// When the private key was designed as "expired", as a UNIX timestamp // When the private key was designed as "expired", as a UNIX timestamp
// in millisecond precision. // in millisecond precision.

View file

@ -169,9 +169,9 @@ func setupNATS(process *process.ProcessContext, cfg *config.JetStream, nc *natsc
// We've managed to add the stream in memory. What's on the // We've managed to add the stream in memory. What's on the
// disk will be left alone, but our ability to recover from a // disk will be left alone, but our ability to recover from a
// future crash will be limited. Yell about it. // future crash will be limited. Yell about it.
sentry.CaptureException(fmt.Errorf("Stream %q is running in-memory; this may be due to data corruption in the JetStream storage directory, investigate as soon as possible", namespaced.Name)) err := fmt.Errorf("Stream %q is running in-memory; this may be due to data corruption in the JetStream storage directory", namespaced.Name)
logrus.Warn("Stream is running in-memory; this may be due to data corruption in the JetStream storage directory, investigate as soon as possible") sentry.CaptureException(err)
process.Degraded() process.Degraded(err)
} }
} }
} }

View file

@ -9,9 +9,10 @@ import (
) )
const ( const (
UserID = "user_id" UserID = "user_id"
RoomID = "room_id" RoomID = "room_id"
EventID = "event_id" EventID = "event_id"
RoomEventType = "output_room_event_type"
) )
var ( var (

View file

@ -2,19 +2,18 @@ package process
import ( import (
"context" "context"
"fmt"
"sync" "sync"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"go.uber.org/atomic"
) )
type ProcessContext struct { type ProcessContext struct {
wg *sync.WaitGroup // used to wait for components to shutdown mu sync.RWMutex
ctx context.Context // cancelled when Stop is called wg *sync.WaitGroup // used to wait for components to shutdown
shutdown context.CancelFunc // shut down Dendrite ctx context.Context // cancelled when Stop is called
degraded atomic.Bool shutdown context.CancelFunc // shut down Dendrite
degraded map[string]struct{} // reasons why the process is degraded
} }
func NewProcessContext() *ProcessContext { func NewProcessContext() *ProcessContext {
@ -50,13 +49,25 @@ func (b *ProcessContext) WaitForComponentsToFinish() {
b.wg.Wait() b.wg.Wait()
} }
func (b *ProcessContext) Degraded() { func (b *ProcessContext) Degraded(err error) {
if b.degraded.CompareAndSwap(false, true) { b.mu.Lock()
logrus.Warn("Dendrite is running in a degraded state") defer b.mu.Unlock()
sentry.CaptureException(fmt.Errorf("Process is running in a degraded state")) if _, ok := b.degraded[err.Error()]; !ok {
logrus.WithError(err).Warn("Dendrite has entered a degraded state")
sentry.CaptureException(err)
b.degraded[err.Error()] = struct{}{}
} }
} }
func (b *ProcessContext) IsDegraded() bool { func (b *ProcessContext) IsDegraded() (bool, []string) {
return b.degraded.Load() b.mu.RLock()
defer b.mu.RUnlock()
if len(b.degraded) == 0 {
return false, nil
}
reasons := make([]string, 0, len(b.degraded))
for reason := range b.degraded {
reasons = append(reasons, reason)
}
return true, reasons
} }

View file

@ -111,7 +111,8 @@ func (s *OutputKeyChangeEventConsumer) onDeviceKeyMessage(m api.DeviceMessage, d
// work out who we need to notify about the new key // work out who we need to notify about the new key
var queryRes roomserverAPI.QuerySharedUsersResponse var queryRes roomserverAPI.QuerySharedUsersResponse
err := s.rsAPI.QuerySharedUsers(s.ctx, &roomserverAPI.QuerySharedUsersRequest{ err := s.rsAPI.QuerySharedUsers(s.ctx, &roomserverAPI.QuerySharedUsersRequest{
UserID: output.UserID, UserID: output.UserID,
LocalOnly: true,
}, &queryRes) }, &queryRes)
if err != nil { if err != nil {
logrus.WithError(err).Error("syncapi: failed to QuerySharedUsers for key change event from key server") logrus.WithError(err).Error("syncapi: failed to QuerySharedUsers for key change event from key server")
@ -135,7 +136,8 @@ func (s *OutputKeyChangeEventConsumer) onCrossSigningMessage(m api.DeviceMessage
// work out who we need to notify about the new key // work out who we need to notify about the new key
var queryRes roomserverAPI.QuerySharedUsersResponse var queryRes roomserverAPI.QuerySharedUsersResponse
err := s.rsAPI.QuerySharedUsers(s.ctx, &roomserverAPI.QuerySharedUsersRequest{ err := s.rsAPI.QuerySharedUsers(s.ctx, &roomserverAPI.QuerySharedUsersRequest{
UserID: output.UserID, UserID: output.UserID,
LocalOnly: true,
}, &queryRes) }, &queryRes)
if err != nil { if err != nil {
logrus.WithError(err).Error("syncapi: failed to QuerySharedUsers for key change event from key server") logrus.WithError(err).Error("syncapi: failed to QuerySharedUsers for key change event from key server")

View file

@ -148,6 +148,16 @@ func (s *OutputRoomEventConsumer) onRedactEvent(
log.WithError(err).Error("RedactEvent error'd") log.WithError(err).Error("RedactEvent error'd")
return err return err
} }
if err = s.db.RedactRelations(ctx, msg.RedactedBecause.RoomID(), msg.RedactedEventID); err != nil {
log.WithFields(log.Fields{
"room_id": msg.RedactedBecause.RoomID(),
"event_id": msg.RedactedBecause.EventID(),
"redacted_event_id": msg.RedactedEventID,
}).WithError(err).Warn("Failed to redact relations")
return err
}
// fake a room event so we notify clients about the redaction, as if it were // fake a room event so we notify clients about the redaction, as if it were
// a normal event. // a normal event.
return s.onNewRoomEvent(ctx, api.OutputNewRoomEvent{ return s.onNewRoomEvent(ctx, api.OutputNewRoomEvent{
@ -271,6 +281,14 @@ func (s *OutputRoomEventConsumer) onNewRoomEvent(
return err return err
} }
if err = s.db.UpdateRelations(ctx, ev); err != nil {
log.WithFields(log.Fields{
"event_id": ev.EventID(),
"type": ev.Type(),
}).WithError(err).Warn("Failed to update relations")
return err
}
s.pduStream.Advance(pduPos) s.pduStream.Advance(pduPos)
s.notifier.OnNewEvent(ev, ev.RoomID(), nil, types.StreamingToken{PDUPosition: pduPos}) s.notifier.OnNewEvent(ev, ev.RoomID(), nil, types.StreamingToken{PDUPosition: pduPos})
@ -315,6 +333,15 @@ func (s *OutputRoomEventConsumer) onOldRoomEvent(
}).WithError(err).Warn("failed to index fulltext element") }).WithError(err).Warn("failed to index fulltext element")
} }
if err = s.db.UpdateRelations(ctx, ev); err != nil {
log.WithFields(log.Fields{
"room_id": ev.RoomID(),
"event_id": ev.EventID(),
"type": ev.Type(),
}).WithError(err).Warn("Failed to update relations")
return err
}
if pduPos, err = s.notifyJoinedPeeks(ctx, ev, pduPos); err != nil { if pduPos, err = s.notifyJoinedPeeks(ctx, ev, pduPos); err != nil {
log.WithError(err).Errorf("Failed to notifyJoinedPeeks for PDU pos %d", pduPos) log.WithError(err).Errorf("Failed to notifyJoinedPeeks for PDU pos %d", pduPos)
return err return err

View file

@ -19,11 +19,13 @@ import (
"math" "math"
"time" "time"
"github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/dendrite/syncapi/storage"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/dendrite/syncapi/storage"
) )
func init() { func init() {
@ -189,7 +191,7 @@ func visibilityForEvents(
UserID: userID, UserID: userID,
}, membershipResp) }, membershipResp)
if err != nil { if err != nil {
return result, err logrus.WithError(err).Error("visibilityForEvents: failed to fetch membership at event, defaulting to 'leave'")
} }
// Create a map from eventID -> eventVisibility // Create a map from eventID -> eventVisibility

View file

@ -170,9 +170,12 @@ func joinResponseWithRooms(syncResponse *types.Response, userID string, roomIDs
Content: []byte(`{"membership":"join"}`), Content: []byte(`{"membership":"join"}`),
}, },
} }
jr, ok := syncResponse.Rooms.Join[roomID]
jr := syncResponse.Rooms.Join[roomID] if !ok {
jr.State.Events = roomEvents jr = types.NewJoinResponse()
}
jr.Timeline = &types.Timeline{}
jr.State = &types.ClientEvents{Events: roomEvents}
syncResponse.Rooms.Join[roomID] = jr syncResponse.Rooms.Join[roomID] = jr
} }
return syncResponse return syncResponse
@ -191,8 +194,11 @@ func leaveResponseWithRooms(syncResponse *types.Response, userID string, roomIDs
}, },
} }
lr := syncResponse.Rooms.Leave[roomID] lr, ok := syncResponse.Rooms.Leave[roomID]
lr.Timeline.Events = roomEvents if !ok {
lr = types.NewLeaveResponse()
}
lr.Timeline = &types.Timeline{Events: roomEvents}
syncResponse.Rooms.Leave[roomID] = lr syncResponse.Rooms.Leave[roomID] = lr
} }
return syncResponse return syncResponse
@ -328,9 +334,13 @@ func TestKeyChangeCatchupNoNewJoinsButMessages(t *testing.T) {
}, },
} }
jr := syncResponse.Rooms.Join[roomID] jr, ok := syncResponse.Rooms.Join[roomID]
jr.State.Events = roomStateEvents if !ok {
jr.Timeline.Events = roomTimelineEvents jr = types.NewJoinResponse()
}
jr.State = &types.ClientEvents{Events: roomStateEvents}
jr.Timeline = &types.Timeline{Events: roomTimelineEvents}
syncResponse.Rooms.Join[roomID] = jr syncResponse.Rooms.Join[roomID] = jr
rsAPI := &mockRoomserverAPI{ rsAPI := &mockRoomserverAPI{
@ -442,8 +452,11 @@ func TestKeyChangeCatchupChangeAndLeftSameRoom(t *testing.T) {
}, },
} }
lr := syncResponse.Rooms.Leave[roomID] lr, ok := syncResponse.Rooms.Leave[roomID]
lr.Timeline.Events = roomEvents if !ok {
lr = types.NewLeaveResponse()
}
lr.Timeline = &types.Timeline{Events: roomEvents}
syncResponse.Rooms.Leave[roomID] = lr syncResponse.Rooms.Leave[roomID] = lr
rsAPI := &mockRoomserverAPI{ rsAPI := &mockRoomserverAPI{

View file

@ -48,6 +48,7 @@ type Notifier struct {
lastCleanUpTime time.Time lastCleanUpTime time.Time
// This map is reused to prevent allocations and GC pressure in SharedUsers. // This map is reused to prevent allocations and GC pressure in SharedUsers.
_sharedUserMap map[string]struct{} _sharedUserMap map[string]struct{}
_wakeupUserMap map[string]struct{}
} }
// NewNotifier creates a new notifier set to the given sync position. // NewNotifier creates a new notifier set to the given sync position.
@ -61,6 +62,7 @@ func NewNotifier() *Notifier {
lock: &sync.RWMutex{}, lock: &sync.RWMutex{},
lastCleanUpTime: time.Now(), lastCleanUpTime: time.Now(),
_sharedUserMap: map[string]struct{}{}, _sharedUserMap: map[string]struct{}{},
_wakeupUserMap: map[string]struct{}{},
} }
} }
@ -408,12 +410,16 @@ func (n *Notifier) setPeekingDevices(roomIDToPeekingDevices map[string][]types.P
// specified user IDs, and also the specified peekingDevices // specified user IDs, and also the specified peekingDevices
func (n *Notifier) _wakeupUsers(userIDs []string, peekingDevices []types.PeekingDevice, newPos types.StreamingToken) { func (n *Notifier) _wakeupUsers(userIDs []string, peekingDevices []types.PeekingDevice, newPos types.StreamingToken) {
for _, userID := range userIDs { for _, userID := range userIDs {
n._wakeupUserMap[userID] = struct{}{}
}
for userID := range n._wakeupUserMap {
for _, stream := range n._fetchUserStreams(userID) { for _, stream := range n._fetchUserStreams(userID) {
if stream == nil { if stream == nil {
continue continue
} }
stream.Broadcast(newPos) // wake up all goroutines Wait()ing on this stream stream.Broadcast(newPos) // wake up all goroutines Wait()ing on this stream
} }
delete(n._wakeupUserMap, userID)
} }
for _, peekingDevice := range peekingDevices { for _, peekingDevice := range peekingDevices {

102
syncapi/routing/getevent.go Normal file
View file

@ -0,0 +1,102 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package routing
import (
"net/http"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
"github.com/sirupsen/logrus"
"github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/dendrite/setup/config"
"github.com/matrix-org/dendrite/syncapi/internal"
"github.com/matrix-org/dendrite/syncapi/storage"
userapi "github.com/matrix-org/dendrite/userapi/api"
)
// GetEvent implements
//
// GET /_matrix/client/r0/rooms/{roomId}/event/{eventId}
//
// https://spec.matrix.org/v1.4/client-server-api/#get_matrixclientv3roomsroomideventeventid
func GetEvent(
req *http.Request,
device *userapi.Device,
roomID string,
eventID string,
cfg *config.SyncAPI,
syncDB storage.Database,
rsAPI api.SyncRoomserverAPI,
) util.JSONResponse {
ctx := req.Context()
db, err := syncDB.NewDatabaseTransaction(ctx)
logger := util.GetLogger(ctx).WithFields(logrus.Fields{
"event_id": eventID,
"room_id": roomID,
})
if err != nil {
logger.WithError(err).Error("GetEvent: syncDB.NewDatabaseTransaction failed")
return jsonerror.InternalServerError()
}
events, err := db.Events(ctx, []string{eventID})
if err != nil {
logger.WithError(err).Error("GetEvent: syncDB.Events failed")
return jsonerror.InternalServerError()
}
// The requested event does not exist in our database
if len(events) == 0 {
logger.Debugf("GetEvent: requested event doesn't exist locally")
return util.JSONResponse{
Code: http.StatusNotFound,
JSON: jsonerror.NotFound("The event was not found or you do not have permission to read this event"),
}
}
// If the request is coming from an appservice, get the user from the request
userID := device.UserID
if asUserID := req.FormValue("user_id"); device.AppserviceID != "" && asUserID != "" {
userID = asUserID
}
// Apply history visibility to determine if the user is allowed to view the event
events, err = internal.ApplyHistoryVisibilityFilter(ctx, db, rsAPI, events, nil, userID, "event")
if err != nil {
logger.WithError(err).Error("GetEvent: internal.ApplyHistoryVisibilityFilter failed")
return util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: jsonerror.InternalServerError(),
}
}
// We only ever expect there to be one event
if len(events) != 1 {
// 0 events -> not allowed to view event; > 1 events -> something that shouldn't happen
logger.WithField("event_count", len(events)).Debug("GetEvent: can't return the requested event")
return util.JSONResponse{
Code: http.StatusNotFound,
JSON: jsonerror.NotFound("The event was not found or you do not have permission to read this event"),
}
}
return util.JSONResponse{
Code: http.StatusOK,
JSON: gomatrixserverlib.HeaderedToClientEvent(events[0], gomatrixserverlib.FormatAll),
}
}

View file

@ -0,0 +1,124 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package routing
import (
"net/http"
"strconv"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
"github.com/sirupsen/logrus"
"github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/internal/sqlutil"
"github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/dendrite/syncapi/internal"
"github.com/matrix-org/dendrite/syncapi/storage"
"github.com/matrix-org/dendrite/syncapi/types"
userapi "github.com/matrix-org/dendrite/userapi/api"
)
type RelationsResponse struct {
Chunk []gomatrixserverlib.ClientEvent `json:"chunk"`
NextBatch string `json:"next_batch,omitempty"`
PrevBatch string `json:"prev_batch,omitempty"`
}
// nolint:gocyclo
func Relations(
req *http.Request, device *userapi.Device,
syncDB storage.Database,
rsAPI api.SyncRoomserverAPI,
roomID, eventID, relType, eventType string,
) util.JSONResponse {
var err error
var from, to types.StreamPosition
var limit int
dir := req.URL.Query().Get("dir")
if f := req.URL.Query().Get("from"); f != "" {
if from, err = types.NewStreamPositionFromString(f); err != nil {
return util.ErrorResponse(err)
}
}
if t := req.URL.Query().Get("to"); t != "" {
if to, err = types.NewStreamPositionFromString(t); err != nil {
return util.ErrorResponse(err)
}
}
if l := req.URL.Query().Get("limit"); l != "" {
if limit, err = strconv.Atoi(l); err != nil {
return util.ErrorResponse(err)
}
}
if limit == 0 || limit > 50 {
limit = 50
}
if dir == "" {
dir = "b"
}
if dir != "b" && dir != "f" {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: jsonerror.MissingArgument("Bad or missing dir query parameter (should be either 'b' or 'f')"),
}
}
snapshot, err := syncDB.NewDatabaseSnapshot(req.Context())
if err != nil {
logrus.WithError(err).Error("Failed to get snapshot for relations")
return jsonerror.InternalServerError()
}
var succeeded bool
defer sqlutil.EndTransactionWithCheck(snapshot, &succeeded, &err)
res := &RelationsResponse{
Chunk: []gomatrixserverlib.ClientEvent{},
}
var events []types.StreamEvent
events, res.PrevBatch, res.NextBatch, err = snapshot.RelationsFor(
req.Context(), roomID, eventID, relType, eventType, from, to, dir == "b", limit,
)
if err != nil {
return util.ErrorResponse(err)
}
headeredEvents := make([]*gomatrixserverlib.HeaderedEvent, 0, len(events))
for _, event := range events {
headeredEvents = append(headeredEvents, event.HeaderedEvent)
}
// Apply history visibility to the result events.
filteredEvents, err := internal.ApplyHistoryVisibilityFilter(req.Context(), snapshot, rsAPI, headeredEvents, nil, device.UserID, "relations")
if err != nil {
return util.ErrorResponse(err)
}
// Convert the events into client events, and optionally filter based on the event
// type if it was specified.
res.Chunk = make([]gomatrixserverlib.ClientEvent, 0, len(filteredEvents))
for _, event := range filteredEvents {
res.Chunk = append(
res.Chunk,
gomatrixserverlib.ToClientEvent(event.Event, gomatrixserverlib.FormatAll),
)
}
succeeded = true
return util.JSONResponse{
Code: http.StatusOK,
JSON: res,
}
}

View file

@ -45,6 +45,7 @@ func Setup(
lazyLoadCache caching.LazyLoadCache, lazyLoadCache caching.LazyLoadCache,
fts *fulltext.Search, fts *fulltext.Search,
) { ) {
v1unstablemux := csMux.PathPrefix("/{apiversion:(?:v1|unstable)}/").Subrouter()
v3mux := csMux.PathPrefix("/{apiversion:(?:r0|v3)}/").Subrouter() v3mux := csMux.PathPrefix("/{apiversion:(?:r0|v3)}/").Subrouter()
// TODO: Add AS support for all handlers below. // TODO: Add AS support for all handlers below.
@ -60,6 +61,16 @@ func Setup(
return OnIncomingMessagesRequest(req, syncDB, vars["roomID"], device, rsAPI, cfg, srp, lazyLoadCache) return OnIncomingMessagesRequest(req, syncDB, vars["roomID"], device, rsAPI, cfg, srp, lazyLoadCache)
})).Methods(http.MethodGet, http.MethodOptions) })).Methods(http.MethodGet, http.MethodOptions)
v3mux.Handle("/rooms/{roomID}/event/{eventID}",
httputil.MakeAuthAPI("rooms_get_event", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
if err != nil {
return util.ErrorResponse(err)
}
return GetEvent(req, device, vars["roomID"], vars["eventID"], cfg, syncDB, rsAPI)
}),
).Methods(http.MethodGet, http.MethodOptions)
v3mux.Handle("/user/{userId}/filter", v3mux.Handle("/user/{userId}/filter",
httputil.MakeAuthAPI("put_filter", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { httputil.MakeAuthAPI("put_filter", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
@ -100,6 +111,48 @@ func Setup(
}), }),
).Methods(http.MethodGet, http.MethodOptions) ).Methods(http.MethodGet, http.MethodOptions)
v1unstablemux.Handle("/rooms/{roomId}/relations/{eventId}",
httputil.MakeAuthAPI(gomatrixserverlib.Join, userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
if err != nil {
return util.ErrorResponse(err)
}
return Relations(
req, device, syncDB, rsAPI,
vars["roomId"], vars["eventId"], "", "",
)
}),
).Methods(http.MethodGet, http.MethodOptions)
v1unstablemux.Handle("/rooms/{roomId}/relations/{eventId}/{relType}",
httputil.MakeAuthAPI(gomatrixserverlib.Join, userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
if err != nil {
return util.ErrorResponse(err)
}
return Relations(
req, device, syncDB, rsAPI,
vars["roomId"], vars["eventId"], vars["relType"], "",
)
}),
).Methods(http.MethodGet, http.MethodOptions)
v1unstablemux.Handle("/rooms/{roomId}/relations/{eventId}/{relType}/{eventType}",
httputil.MakeAuthAPI(gomatrixserverlib.Join, userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
if err != nil {
return util.ErrorResponse(err)
}
return Relations(
req, device, syncDB, rsAPI,
vars["roomId"], vars["eventId"], vars["relType"], vars["eventType"],
)
}),
).Methods(http.MethodGet, http.MethodOptions)
v3mux.Handle("/search", v3mux.Handle("/search",
httputil.MakeAuthAPI("search", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { httputil.MakeAuthAPI("search", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
if !cfg.Fulltext.Enabled { if !cfg.Fulltext.Enabled {

View file

@ -38,6 +38,7 @@ type DatabaseTransaction interface {
MaxStreamPositionForSendToDeviceMessages(ctx context.Context) (types.StreamPosition, error) MaxStreamPositionForSendToDeviceMessages(ctx context.Context) (types.StreamPosition, error)
MaxStreamPositionForNotificationData(ctx context.Context) (types.StreamPosition, error) MaxStreamPositionForNotificationData(ctx context.Context) (types.StreamPosition, error)
MaxStreamPositionForPresence(ctx context.Context) (types.StreamPosition, error) MaxStreamPositionForPresence(ctx context.Context) (types.StreamPosition, error)
MaxStreamPositionForRelations(ctx context.Context) (types.StreamPosition, error)
CurrentState(ctx context.Context, roomID string, stateFilterPart *gomatrixserverlib.StateFilter, excludeEventIDs []string) ([]*gomatrixserverlib.HeaderedEvent, error) CurrentState(ctx context.Context, roomID string, stateFilterPart *gomatrixserverlib.StateFilter, excludeEventIDs []string) ([]*gomatrixserverlib.HeaderedEvent, error)
GetStateDeltasForFullStateSync(ctx context.Context, device *userapi.Device, r types.Range, userID string, stateFilter *gomatrixserverlib.StateFilter) ([]types.StateDelta, []string, error) GetStateDeltasForFullStateSync(ctx context.Context, device *userapi.Device, r types.Range, userID string, stateFilter *gomatrixserverlib.StateFilter) ([]types.StateDelta, []string, error)
@ -107,6 +108,7 @@ type DatabaseTransaction interface {
GetUserUnreadNotificationCountsForRooms(ctx context.Context, userID string, roomIDs map[string]string) (map[string]*eventutil.NotificationData, error) GetUserUnreadNotificationCountsForRooms(ctx context.Context, userID string, roomIDs map[string]string) (map[string]*eventutil.NotificationData, error)
GetPresence(ctx context.Context, userID string) (*types.PresenceInternal, error) GetPresence(ctx context.Context, userID string) (*types.PresenceInternal, error)
PresenceAfter(ctx context.Context, after types.StreamPosition, filter gomatrixserverlib.EventFilter) (map[string]*types.PresenceInternal, error) PresenceAfter(ctx context.Context, after types.StreamPosition, filter gomatrixserverlib.EventFilter) (map[string]*types.PresenceInternal, error)
RelationsFor(ctx context.Context, roomID, eventID, relType, eventType string, from, to types.StreamPosition, backwards bool, limit int) (events []types.StreamEvent, prevBatch, nextBatch string, err error)
} }
type Database interface { type Database interface {
@ -174,6 +176,8 @@ type Database interface {
StoreReceipt(ctx context.Context, roomId, receiptType, userId, eventId string, timestamp gomatrixserverlib.Timestamp) (pos types.StreamPosition, err error) StoreReceipt(ctx context.Context, roomId, receiptType, userId, eventId string, timestamp gomatrixserverlib.Timestamp) (pos types.StreamPosition, err error)
UpdateIgnoresForUser(ctx context.Context, userID string, ignores *types.IgnoredUsers) error UpdateIgnoresForUser(ctx context.Context, userID string, ignores *types.IgnoredUsers) error
ReIndex(ctx context.Context, limit, afterID int64) (map[int64]gomatrixserverlib.HeaderedEvent, error) ReIndex(ctx context.Context, limit, afterID int64) (map[int64]gomatrixserverlib.HeaderedEvent, error)
UpdateRelations(ctx context.Context, event *gomatrixserverlib.HeaderedEvent) error
RedactRelations(ctx context.Context, roomID, redactedEventID string) error
} }
type Presence interface { type Presence interface {

View file

@ -76,6 +76,8 @@ CREATE INDEX IF NOT EXISTS syncapi_output_room_events_type_idx ON syncapi_output
CREATE INDEX IF NOT EXISTS syncapi_output_room_events_sender_idx ON syncapi_output_room_events (sender); CREATE INDEX IF NOT EXISTS syncapi_output_room_events_sender_idx ON syncapi_output_room_events (sender);
CREATE INDEX IF NOT EXISTS syncapi_output_room_events_room_id_idx ON syncapi_output_room_events (room_id); CREATE INDEX IF NOT EXISTS syncapi_output_room_events_room_id_idx ON syncapi_output_room_events (room_id);
CREATE INDEX IF NOT EXISTS syncapi_output_room_events_exclude_from_sync_idx ON syncapi_output_room_events (exclude_from_sync); CREATE INDEX IF NOT EXISTS syncapi_output_room_events_exclude_from_sync_idx ON syncapi_output_room_events (exclude_from_sync);
CREATE INDEX IF NOT EXISTS syncapi_output_room_events_add_state_ids_idx ON syncapi_output_room_events ((add_state_ids IS NOT NULL));
CREATE INDEX IF NOT EXISTS syncapi_output_room_events_remove_state_ids_idx ON syncapi_output_room_events ((remove_state_ids IS NOT NULL));
` `
const insertEventSQL = "" + const insertEventSQL = "" +

View file

@ -0,0 +1,158 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package postgres
import (
"context"
"database/sql"
"github.com/matrix-org/dendrite/internal"
"github.com/matrix-org/dendrite/internal/sqlutil"
"github.com/matrix-org/dendrite/syncapi/storage/tables"
"github.com/matrix-org/dendrite/syncapi/types"
)
const relationsSchema = `
CREATE SEQUENCE IF NOT EXISTS syncapi_relation_id;
CREATE TABLE IF NOT EXISTS syncapi_relations (
id BIGINT PRIMARY KEY DEFAULT nextval('syncapi_relation_id'),
room_id TEXT NOT NULL,
event_id TEXT NOT NULL,
child_event_id TEXT NOT NULL,
child_event_type TEXT NOT NULL,
rel_type TEXT NOT NULL,
CONSTRAINT syncapi_relations_unique UNIQUE (room_id, event_id, child_event_id, rel_type)
);
`
const insertRelationSQL = "" +
"INSERT INTO syncapi_relations (" +
" room_id, event_id, child_event_id, child_event_type, rel_type" +
") VALUES ($1, $2, $3, $4, $5) " +
" ON CONFLICT DO NOTHING"
const deleteRelationSQL = "" +
"DELETE FROM syncapi_relations WHERE room_id = $1 AND child_event_id = $2"
const selectRelationsInRangeAscSQL = "" +
"SELECT id, child_event_id, rel_type FROM syncapi_relations" +
" WHERE room_id = $1 AND event_id = $2" +
" AND ( $3 = '' OR rel_type = $3 )" +
" AND ( $4 = '' OR child_event_type = $4 )" +
" AND id > $5 AND id <= $6" +
" ORDER BY id ASC LIMIT $7"
const selectRelationsInRangeDescSQL = "" +
"SELECT id, child_event_id, rel_type FROM syncapi_relations" +
" WHERE room_id = $1 AND event_id = $2" +
" AND ( $3 = '' OR rel_type = $3 )" +
" AND ( $4 = '' OR child_event_type = $4 )" +
" AND id >= $5 AND id < $6" +
" ORDER BY id DESC LIMIT $7"
const selectMaxRelationIDSQL = "" +
"SELECT COALESCE(MAX(id), 0) FROM syncapi_relations"
type relationsStatements struct {
insertRelationStmt *sql.Stmt
selectRelationsInRangeAscStmt *sql.Stmt
selectRelationsInRangeDescStmt *sql.Stmt
deleteRelationStmt *sql.Stmt
selectMaxRelationIDStmt *sql.Stmt
}
func NewPostgresRelationsTable(db *sql.DB) (tables.Relations, error) {
s := &relationsStatements{}
_, err := db.Exec(relationsSchema)
if err != nil {
return nil, err
}
return s, sqlutil.StatementList{
{&s.insertRelationStmt, insertRelationSQL},
{&s.selectRelationsInRangeAscStmt, selectRelationsInRangeAscSQL},
{&s.selectRelationsInRangeDescStmt, selectRelationsInRangeDescSQL},
{&s.deleteRelationStmt, deleteRelationSQL},
{&s.selectMaxRelationIDStmt, selectMaxRelationIDSQL},
}.Prepare(db)
}
func (s *relationsStatements) InsertRelation(
ctx context.Context, txn *sql.Tx, roomID, eventID, childEventID, childEventType, relType string,
) (err error) {
_, err = sqlutil.TxStmt(txn, s.insertRelationStmt).ExecContext(
ctx, roomID, eventID, childEventID, childEventType, relType,
)
return
}
func (s *relationsStatements) DeleteRelation(
ctx context.Context, txn *sql.Tx, roomID, childEventID string,
) error {
stmt := sqlutil.TxStmt(txn, s.deleteRelationStmt)
_, err := stmt.ExecContext(
ctx, roomID, childEventID,
)
return err
}
// SelectRelationsInRange returns a map rel_type -> []child_event_id
func (s *relationsStatements) SelectRelationsInRange(
ctx context.Context, txn *sql.Tx, roomID, eventID, relType, eventType string,
r types.Range, limit int,
) (map[string][]types.RelationEntry, types.StreamPosition, error) {
var lastPos types.StreamPosition
var stmt *sql.Stmt
if r.Backwards {
stmt = sqlutil.TxStmt(txn, s.selectRelationsInRangeDescStmt)
} else {
stmt = sqlutil.TxStmt(txn, s.selectRelationsInRangeAscStmt)
}
rows, err := stmt.QueryContext(ctx, roomID, eventID, relType, eventType, r.Low(), r.High(), limit)
if err != nil {
return nil, lastPos, err
}
defer internal.CloseAndLogIfError(ctx, rows, "selectRelationsInRange: rows.close() failed")
result := map[string][]types.RelationEntry{}
var (
id types.StreamPosition
childEventID string
relationType string
)
for rows.Next() {
if err = rows.Scan(&id, &childEventID, &relationType); err != nil {
return nil, lastPos, err
}
if id > lastPos {
lastPos = id
}
result[relationType] = append(result[relationType], types.RelationEntry{
Position: id,
EventID: childEventID,
})
}
if lastPos == 0 {
lastPos = r.To
}
return result, lastPos, rows.Err()
}
func (s *relationsStatements) SelectMaxRelationID(
ctx context.Context, txn *sql.Tx,
) (id int64, err error) {
stmt := sqlutil.TxStmt(txn, s.selectMaxRelationIDStmt)
err = stmt.QueryRowContext(ctx).Scan(&id)
return
}

View file

@ -98,6 +98,10 @@ func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions)
if err != nil { if err != nil {
return nil, err return nil, err
} }
relations, err := NewPostgresRelationsTable(d.db)
if err != nil {
return nil, err
}
// apply migrations which need multiple tables // apply migrations which need multiple tables
m := sqlutil.NewMigrator(d.db) m := sqlutil.NewMigrator(d.db)
@ -129,6 +133,7 @@ func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions)
NotificationData: notificationData, NotificationData: notificationData,
Ignores: ignores, Ignores: ignores,
Presence: presence, Presence: presence,
Relations: relations,
} }
return &d, nil return &d, nil
} }

View file

@ -53,6 +53,7 @@ type Database struct {
NotificationData tables.NotificationData NotificationData tables.NotificationData
Ignores tables.Ignores Ignores tables.Ignores
Presence tables.Presence Presence tables.Presence
Relations tables.Relations
} }
func (d *Database) NewDatabaseSnapshot(ctx context.Context) (*DatabaseTransaction, error) { func (d *Database) NewDatabaseSnapshot(ctx context.Context) (*DatabaseTransaction, error) {
@ -579,10 +580,40 @@ func (d *Database) SelectMembershipForUser(ctx context.Context, roomID, userID s
return d.Memberships.SelectMembershipForUser(ctx, nil, roomID, userID, pos) return d.Memberships.SelectMembershipForUser(ctx, nil, roomID, userID, pos)
} }
func (s *Database) ReIndex(ctx context.Context, limit, afterID int64) (map[int64]gomatrixserverlib.HeaderedEvent, error) { func (d *Database) ReIndex(ctx context.Context, limit, afterID int64) (map[int64]gomatrixserverlib.HeaderedEvent, error) {
return s.OutputEvents.ReIndex(ctx, nil, limit, afterID, []string{ return d.OutputEvents.ReIndex(ctx, nil, limit, afterID, []string{
gomatrixserverlib.MRoomName, gomatrixserverlib.MRoomName,
gomatrixserverlib.MRoomTopic, gomatrixserverlib.MRoomTopic,
"m.room.message", "m.room.message",
}) })
} }
func (d *Database) UpdateRelations(ctx context.Context, event *gomatrixserverlib.HeaderedEvent) error {
var content gomatrixserverlib.RelationContent
if err := json.Unmarshal(event.Content(), &content); err != nil {
return fmt.Errorf("json.Unmarshal: %w", err)
}
switch {
case content.Relations == nil:
return nil
case content.Relations.EventID == "":
return nil
case content.Relations.RelationType == "":
return nil
case event.Type() == gomatrixserverlib.MRoomRedaction:
return nil
default:
return d.Writer.Do(d.DB, nil, func(txn *sql.Tx) error {
return d.Relations.InsertRelation(
ctx, txn, event.RoomID(), content.Relations.EventID,
event.EventID(), event.Type(), content.Relations.RelationType,
)
})
}
}
func (d *Database) RedactRelations(ctx context.Context, roomID, redactedEventID string) error {
return d.Writer.Do(d.DB, nil, func(txn *sql.Tx) error {
return d.Relations.DeleteRelation(ctx, txn, roomID, redactedEventID)
})
}

View file

@ -360,34 +360,50 @@ func (d *DatabaseTransaction) GetStateDeltas(
newlyJoinedRooms := make(map[string]bool, len(state)) newlyJoinedRooms := make(map[string]bool, len(state))
for roomID, stateStreamEvents := range state { for roomID, stateStreamEvents := range state {
for _, ev := range stateStreamEvents { for _, ev := range stateStreamEvents {
if membership, prevMembership := getMembershipFromEvent(ev.Event, userID); membership != "" { // Look for our membership in the state events and skip over any
if membership == gomatrixserverlib.Join && prevMembership != membership { // membership events that are not related to us.
// send full room state down instead of a delta membership, prevMembership := getMembershipFromEvent(ev.Event, userID)
if membership == "" {
continue
}
if membership == gomatrixserverlib.Join {
// If our membership is now join but the previous membership wasn't
// then this is a "join transition", so we'll insert this room.
if prevMembership != membership {
// Get the full room state, as we'll send that down for a newly
// joined room instead of a delta.
var s []types.StreamEvent var s []types.StreamEvent
s, err = d.currentStateStreamEventsForRoom(ctx, roomID, stateFilter) if s, err = d.currentStateStreamEventsForRoom(ctx, roomID, stateFilter); err != nil {
if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
continue continue
} }
return nil, nil, err return nil, nil, err
} }
// Add the information for this room into the state so that
// it will get added with all of the rest of the joined rooms.
state[roomID] = s state[roomID] = s
newlyJoinedRooms[roomID] = true newlyJoinedRooms[roomID] = true
continue // we'll add this room in when we do joined rooms
} }
deltas = append(deltas, types.StateDelta{ // We won't add joined rooms into the delta at this point as they
Membership: membership, // are added later on.
MembershipPos: ev.StreamPosition, continue
StateEvents: d.StreamEventsToEvents(device, stateStreamEvents),
RoomID: roomID,
})
break
} }
deltas = append(deltas, types.StateDelta{
Membership: membership,
MembershipPos: ev.StreamPosition,
StateEvents: d.StreamEventsToEvents(device, stateStreamEvents),
RoomID: roomID,
})
break
} }
} }
// Add in currently joined rooms // Finally, add in currently joined rooms, including those from the
// join transitions above.
for _, joinedRoomID := range joinedRoomIDs { for _, joinedRoomID := range joinedRoomIDs {
deltas = append(deltas, types.StateDelta{ deltas = append(deltas, types.StateDelta{
Membership: gomatrixserverlib.Join, Membership: gomatrixserverlib.Join,
@ -573,3 +589,84 @@ func (d *DatabaseTransaction) PresenceAfter(ctx context.Context, after types.Str
func (d *DatabaseTransaction) MaxStreamPositionForPresence(ctx context.Context) (types.StreamPosition, error) { func (d *DatabaseTransaction) MaxStreamPositionForPresence(ctx context.Context) (types.StreamPosition, error) {
return d.Presence.GetMaxPresenceID(ctx, d.txn) return d.Presence.GetMaxPresenceID(ctx, d.txn)
} }
func (d *DatabaseTransaction) MaxStreamPositionForRelations(ctx context.Context) (types.StreamPosition, error) {
id, err := d.Relations.SelectMaxRelationID(ctx, d.txn)
return types.StreamPosition(id), err
}
func (d *DatabaseTransaction) RelationsFor(ctx context.Context, roomID, eventID, relType, eventType string, from, to types.StreamPosition, backwards bool, limit int) (
events []types.StreamEvent, prevBatch, nextBatch string, err error,
) {
r := types.Range{
From: from,
To: to,
Backwards: backwards,
}
if r.Backwards && r.From == 0 {
// If we're working backwards (dir=b) and there's no ?from= specified then
// we will automatically want to work backwards from the current position,
// so find out what that is.
if r.From, err = d.MaxStreamPositionForRelations(ctx); err != nil {
return nil, "", "", fmt.Errorf("d.MaxStreamPositionForRelations: %w", err)
}
// The result normally isn't inclusive of the event *at* the ?from=
// position, so add 1 here so that we include the most recent relation.
r.From++
} else if !r.Backwards && r.To == 0 {
// If we're working forwards (dir=f) and there's no ?to= specified then
// we will automatically want to work forwards towards the current position,
// so find out what that is.
if r.To, err = d.MaxStreamPositionForRelations(ctx); err != nil {
return nil, "", "", fmt.Errorf("d.MaxStreamPositionForRelations: %w", err)
}
}
// First look up any relations from the database. We add one to the limit here
// so that we can tell if we're overflowing, as we will only set the "next_batch"
// in the response if we are.
relations, _, err := d.Relations.SelectRelationsInRange(ctx, d.txn, roomID, eventID, relType, eventType, r, limit+1)
if err != nil {
return nil, "", "", fmt.Errorf("d.Relations.SelectRelationsInRange: %w", err)
}
// If we specified a relation type then just get those results, otherwise collate
// them from all of the returned relation types.
entries := []types.RelationEntry{}
if relType != "" {
entries = relations[relType]
} else {
for _, e := range relations {
entries = append(entries, e...)
}
}
// If there were no entries returned, there were no relations, so stop at this point.
if len(entries) == 0 {
return nil, "", "", nil
}
// Otherwise, let's try and work out what sensible prev_batch and next_batch values
// could be. We've requested an extra event by adding one to the limit already so
// that we can determine whether or not to provide a "next_batch", so trim off that
// event off the end if needs be.
if len(entries) > limit {
entries = entries[:len(entries)-1]
nextBatch = fmt.Sprintf("%d", entries[len(entries)-1].Position)
}
// TODO: set prevBatch? doesn't seem to affect the tests...
// Extract all of the event IDs from the relation entries so that we can pull the
// events out of the database. Then go and fetch the events.
eventIDs := make([]string, 0, len(entries))
for _, entry := range entries {
eventIDs = append(eventIDs, entry.EventID)
}
events, err = d.OutputEvents.SelectEvents(ctx, d.txn, eventIDs, nil, true)
if err != nil {
return nil, "", "", fmt.Errorf("d.OutputEvents.SelectEvents: %w", err)
}
return events, prevBatch, nextBatch, nil
}

View file

@ -55,6 +55,8 @@ CREATE INDEX IF NOT EXISTS syncapi_output_room_events_type_idx ON syncapi_output
CREATE INDEX IF NOT EXISTS syncapi_output_room_events_sender_idx ON syncapi_output_room_events (sender); CREATE INDEX IF NOT EXISTS syncapi_output_room_events_sender_idx ON syncapi_output_room_events (sender);
CREATE INDEX IF NOT EXISTS syncapi_output_room_events_room_id_idx ON syncapi_output_room_events (room_id); CREATE INDEX IF NOT EXISTS syncapi_output_room_events_room_id_idx ON syncapi_output_room_events (room_id);
CREATE INDEX IF NOT EXISTS syncapi_output_room_events_exclude_from_sync_idx ON syncapi_output_room_events (exclude_from_sync); CREATE INDEX IF NOT EXISTS syncapi_output_room_events_exclude_from_sync_idx ON syncapi_output_room_events (exclude_from_sync);
CREATE INDEX IF NOT EXISTS syncapi_output_room_events_add_state_ids_idx ON syncapi_output_room_events ((add_state_ids IS NOT NULL));
CREATE INDEX IF NOT EXISTS syncapi_output_room_events_remove_state_ids_idx ON syncapi_output_room_events ((remove_state_ids IS NOT NULL));
` `
const insertEventSQL = "" + const insertEventSQL = "" +

View file

@ -0,0 +1,163 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package sqlite3
import (
"context"
"database/sql"
"github.com/matrix-org/dendrite/internal"
"github.com/matrix-org/dendrite/internal/sqlutil"
"github.com/matrix-org/dendrite/syncapi/storage/tables"
"github.com/matrix-org/dendrite/syncapi/types"
)
const relationsSchema = `
CREATE TABLE IF NOT EXISTS syncapi_relations (
id BIGINT PRIMARY KEY,
room_id TEXT NOT NULL,
event_id TEXT NOT NULL,
child_event_id TEXT NOT NULL,
child_event_type TEXT NOT NULL,
rel_type TEXT NOT NULL,
UNIQUE (room_id, event_id, child_event_id, rel_type)
);
`
const insertRelationSQL = "" +
"INSERT INTO syncapi_relations (" +
" id, room_id, event_id, child_event_id, child_event_type, rel_type" +
") VALUES ($1, $2, $3, $4, $5, $6) " +
" ON CONFLICT DO NOTHING"
const deleteRelationSQL = "" +
"DELETE FROM syncapi_relations WHERE room_id = $1 AND child_event_id = $2"
const selectRelationsInRangeAscSQL = "" +
"SELECT id, child_event_id, rel_type FROM syncapi_relations" +
" WHERE room_id = $1 AND event_id = $2" +
" AND ( $3 = '' OR rel_type = $3 )" +
" AND ( $4 = '' OR child_event_type = $4 )" +
" AND id > $5 AND id <= $6" +
" ORDER BY id ASC LIMIT $7"
const selectRelationsInRangeDescSQL = "" +
"SELECT id, child_event_id, rel_type FROM syncapi_relations" +
" WHERE room_id = $1 AND event_id = $2" +
" AND ( $3 = '' OR rel_type = $3 )" +
" AND ( $4 = '' OR child_event_type = $4 )" +
" AND id >= $5 AND id < $6" +
" ORDER BY id DESC LIMIT $7"
const selectMaxRelationIDSQL = "" +
"SELECT COALESCE(MAX(id), 0) FROM syncapi_relations"
type relationsStatements struct {
streamIDStatements *StreamIDStatements
insertRelationStmt *sql.Stmt
selectRelationsInRangeAscStmt *sql.Stmt
selectRelationsInRangeDescStmt *sql.Stmt
deleteRelationStmt *sql.Stmt
selectMaxRelationIDStmt *sql.Stmt
}
func NewSqliteRelationsTable(db *sql.DB, streamID *StreamIDStatements) (tables.Relations, error) {
s := &relationsStatements{
streamIDStatements: streamID,
}
_, err := db.Exec(relationsSchema)
if err != nil {
return nil, err
}
return s, sqlutil.StatementList{
{&s.insertRelationStmt, insertRelationSQL},
{&s.selectRelationsInRangeAscStmt, selectRelationsInRangeAscSQL},
{&s.selectRelationsInRangeDescStmt, selectRelationsInRangeDescSQL},
{&s.deleteRelationStmt, deleteRelationSQL},
{&s.selectMaxRelationIDStmt, selectMaxRelationIDSQL},
}.Prepare(db)
}
func (s *relationsStatements) InsertRelation(
ctx context.Context, txn *sql.Tx, roomID, eventID, childEventID, childEventType, relType string,
) (err error) {
var streamPos types.StreamPosition
if streamPos, err = s.streamIDStatements.nextRelationID(ctx, txn); err != nil {
return
}
_, err = sqlutil.TxStmt(txn, s.insertRelationStmt).ExecContext(
ctx, streamPos, roomID, eventID, childEventID, childEventType, relType,
)
return
}
func (s *relationsStatements) DeleteRelation(
ctx context.Context, txn *sql.Tx, roomID, childEventID string,
) error {
stmt := sqlutil.TxStmt(txn, s.deleteRelationStmt)
_, err := stmt.ExecContext(
ctx, roomID, childEventID,
)
return err
}
// SelectRelationsInRange returns a map rel_type -> []child_event_id
func (s *relationsStatements) SelectRelationsInRange(
ctx context.Context, txn *sql.Tx, roomID, eventID, relType, eventType string,
r types.Range, limit int,
) (map[string][]types.RelationEntry, types.StreamPosition, error) {
var lastPos types.StreamPosition
var stmt *sql.Stmt
if r.Backwards {
stmt = sqlutil.TxStmt(txn, s.selectRelationsInRangeDescStmt)
} else {
stmt = sqlutil.TxStmt(txn, s.selectRelationsInRangeAscStmt)
}
rows, err := stmt.QueryContext(ctx, roomID, eventID, relType, eventType, r.Low(), r.High(), limit)
if err != nil {
return nil, lastPos, err
}
defer internal.CloseAndLogIfError(ctx, rows, "selectRelationsInRange: rows.close() failed")
result := map[string][]types.RelationEntry{}
var (
id types.StreamPosition
childEventID string
relationType string
)
for rows.Next() {
if err = rows.Scan(&id, &childEventID, &relationType); err != nil {
return nil, lastPos, err
}
if id > lastPos {
lastPos = id
}
result[relationType] = append(result[relationType], types.RelationEntry{
Position: id,
EventID: childEventID,
})
}
if lastPos == 0 {
lastPos = r.To
}
return result, lastPos, rows.Err()
}
func (s *relationsStatements) SelectMaxRelationID(
ctx context.Context, txn *sql.Tx,
) (id int64, err error) {
stmt := sqlutil.TxStmt(txn, s.selectMaxRelationIDStmt)
err = stmt.QueryRowContext(ctx).Scan(&id)
return
}

View file

@ -28,6 +28,8 @@ INSERT INTO syncapi_stream_id (stream_name, stream_id) VALUES ("presence", 0)
ON CONFLICT DO NOTHING; ON CONFLICT DO NOTHING;
INSERT INTO syncapi_stream_id (stream_name, stream_id) VALUES ("notification", 0) INSERT INTO syncapi_stream_id (stream_name, stream_id) VALUES ("notification", 0)
ON CONFLICT DO NOTHING; ON CONFLICT DO NOTHING;
INSERT INTO syncapi_stream_id (stream_name, stream_id) VALUES ("relation", 0)
ON CONFLICT DO NOTHING;
` `
const increaseStreamIDStmt = "" + const increaseStreamIDStmt = "" +
@ -86,3 +88,9 @@ func (s *StreamIDStatements) nextNotificationID(ctx context.Context, txn *sql.Tx
err = increaseStmt.QueryRowContext(ctx, "notification").Scan(&pos) err = increaseStmt.QueryRowContext(ctx, "notification").Scan(&pos)
return return
} }
func (s *StreamIDStatements) nextRelationID(ctx context.Context, txn *sql.Tx) (pos types.StreamPosition, err error) {
increaseStmt := sqlutil.TxStmt(txn, s.increaseStreamIDStmt)
err = increaseStmt.QueryRowContext(ctx, "relation").Scan(&pos)
return
}

View file

@ -123,6 +123,10 @@ func (d *SyncServerDatasource) prepare(ctx context.Context) (err error) {
if err != nil { if err != nil {
return err return err
} }
relations, err := NewSqliteRelationsTable(d.db, &d.streamID)
if err != nil {
return err
}
// apply migrations which need multiple tables // apply migrations which need multiple tables
m := sqlutil.NewMigrator(d.db) m := sqlutil.NewMigrator(d.db)
@ -153,6 +157,7 @@ func (d *SyncServerDatasource) prepare(ctx context.Context) (err error) {
NotificationData: notificationData, NotificationData: notificationData,
Ignores: ignores, Ignores: ignores,
Presence: presence, Presence: presence,
Relations: relations,
} }
return nil return nil
} }

View file

@ -206,3 +206,22 @@ type Presence interface {
GetMaxPresenceID(ctx context.Context, txn *sql.Tx) (pos types.StreamPosition, err error) GetMaxPresenceID(ctx context.Context, txn *sql.Tx) (pos types.StreamPosition, err error)
GetPresenceAfter(ctx context.Context, txn *sql.Tx, after types.StreamPosition, filter gomatrixserverlib.EventFilter) (presences map[string]*types.PresenceInternal, err error) GetPresenceAfter(ctx context.Context, txn *sql.Tx, after types.StreamPosition, filter gomatrixserverlib.EventFilter) (presences map[string]*types.PresenceInternal, err error)
} }
type Relations interface {
// Inserts a relation which refers from the child event ID to the event ID in the given room.
// If the relation already exists then this function will do nothing and return no error.
InsertRelation(ctx context.Context, txn *sql.Tx, roomID, eventID, childEventID, childEventType, relType string) (err error)
// Deletes a relation which already exists as the result of an event redaction. If the relation
// does not exist then this function will do nothing and return no error.
DeleteRelation(ctx context.Context, txn *sql.Tx, roomID, childEventID string) error
// SelectRelationsInRange will return relations grouped by relation type within the given range.
// The map is relType -> []entry. If a relType parameter is specified then the results will only
// contain relations of that type, otherwise if "" is specified then all relations in the range
// will be returned, inclusive of the "to" position but excluding the "from" position. The stream
// position returned is the maximum position of the returned results.
SelectRelationsInRange(ctx context.Context, txn *sql.Tx, roomID, eventID, relType, eventType string, r types.Range, limit int) (map[string][]types.RelationEntry, types.StreamPosition, error)
// SelectMaxRelationID returns the maximum ID of all relations, used to determine what the boundaries
// should be if there are no boundaries supplied (i.e. we want to work backwards but don't have a
// "from" or want to work forwards and don't have a "to").
SelectMaxRelationID(ctx context.Context, txn *sql.Tx) (id int64, err error)
}

View file

@ -0,0 +1,186 @@
package tables_test
import (
"context"
"database/sql"
"testing"
"github.com/matrix-org/dendrite/internal/sqlutil"
"github.com/matrix-org/dendrite/setup/config"
"github.com/matrix-org/dendrite/syncapi/storage/postgres"
"github.com/matrix-org/dendrite/syncapi/storage/sqlite3"
"github.com/matrix-org/dendrite/syncapi/storage/tables"
"github.com/matrix-org/dendrite/syncapi/types"
"github.com/matrix-org/dendrite/test"
)
func newRelationsTable(t *testing.T, dbType test.DBType) (tables.Relations, *sql.DB, func()) {
t.Helper()
connStr, close := test.PrepareDBConnectionString(t, dbType)
db, err := sqlutil.Open(&config.DatabaseOptions{
ConnectionString: config.DataSource(connStr),
}, sqlutil.NewExclusiveWriter())
if err != nil {
t.Fatalf("failed to open db: %s", err)
}
var tab tables.Relations
switch dbType {
case test.DBTypePostgres:
tab, err = postgres.NewPostgresRelationsTable(db)
case test.DBTypeSQLite:
var stream sqlite3.StreamIDStatements
if err = stream.Prepare(db); err != nil {
t.Fatalf("failed to prepare stream stmts: %s", err)
}
tab, err = sqlite3.NewSqliteRelationsTable(db, &stream)
}
if err != nil {
t.Fatalf("failed to make new table: %s", err)
}
return tab, db, close
}
func compareRelationsToExpected(t *testing.T, tab tables.Relations, r types.Range, expected []types.RelationEntry) {
ctx := context.Background()
relations, _, err := tab.SelectRelationsInRange(ctx, nil, roomID, "a", "", "", r, 50)
if err != nil {
t.Fatal(err)
}
if len(relations[relType]) != len(expected) {
t.Fatalf("incorrect number of values returned for range %v (got %d, want %d)", r, len(relations[relType]), len(expected))
}
for i := 0; i < len(relations[relType]); i++ {
got := relations[relType][i]
want := expected[i]
if got != want {
t.Fatalf("range %v position %d should have been %q but got %q", r, i, got, want)
}
}
}
const roomID = "!roomid:server"
const childType = "m.room.something"
const relType = "m.reaction"
func TestRelationsTable(t *testing.T) {
ctx := context.Background()
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
tab, _, close := newRelationsTable(t, dbType)
defer close()
// Insert some relations
for _, child := range []string{"b", "c", "d"} {
if err := tab.InsertRelation(ctx, nil, roomID, "a", child, childType, relType); err != nil {
t.Fatal(err)
}
}
// Check the max position, we've inserted three things so it
// should be 3
if max, err := tab.SelectMaxRelationID(ctx, nil); err != nil {
t.Fatal(err)
} else if max != 3 {
t.Fatalf("max position should have been 3 but got %d", max)
}
// Query some ranges for "a"
for r, expected := range map[types.Range][]types.RelationEntry{
{From: 0, To: 10, Backwards: false}: {
{Position: 1, EventID: "b"},
{Position: 2, EventID: "c"},
{Position: 3, EventID: "d"},
},
{From: 1, To: 2, Backwards: false}: {
{Position: 2, EventID: "c"},
},
{From: 1, To: 3, Backwards: false}: {
{Position: 2, EventID: "c"},
{Position: 3, EventID: "d"},
},
{From: 10, To: 0, Backwards: true}: {
{Position: 3, EventID: "d"},
{Position: 2, EventID: "c"},
{Position: 1, EventID: "b"},
},
{From: 3, To: 1, Backwards: true}: {
{Position: 2, EventID: "c"},
{Position: 1, EventID: "b"},
},
} {
compareRelationsToExpected(t, tab, r, expected)
}
// Now delete one of the relations
if err := tab.DeleteRelation(ctx, nil, roomID, "c"); err != nil {
t.Fatal(err)
}
// Query some more ranges for "a"
for r, expected := range map[types.Range][]types.RelationEntry{
{From: 0, To: 10, Backwards: false}: {
{Position: 1, EventID: "b"},
{Position: 3, EventID: "d"},
},
{From: 1, To: 2, Backwards: false}: {},
{From: 1, To: 3, Backwards: false}: {
{Position: 3, EventID: "d"},
},
{From: 10, To: 0, Backwards: true}: {
{Position: 3, EventID: "d"},
{Position: 1, EventID: "b"},
},
{From: 3, To: 1, Backwards: true}: {
{Position: 1, EventID: "b"},
},
} {
compareRelationsToExpected(t, tab, r, expected)
}
// Insert some new relations
for _, child := range []string{"e", "f", "g", "h"} {
if err := tab.InsertRelation(ctx, nil, roomID, "a", child, childType, relType); err != nil {
t.Fatal(err)
}
}
// Check the max position, we've inserted four things so it
// should now be 7
if max, err := tab.SelectMaxRelationID(ctx, nil); err != nil {
t.Fatal(err)
} else if max != 7 {
t.Fatalf("max position should have been 3 but got %d", max)
}
// Query last set of ranges for "a"
for r, expected := range map[types.Range][]types.RelationEntry{
{From: 0, To: 10, Backwards: false}: {
{Position: 1, EventID: "b"},
{Position: 3, EventID: "d"},
{Position: 4, EventID: "e"},
{Position: 5, EventID: "f"},
{Position: 6, EventID: "g"},
{Position: 7, EventID: "h"},
},
{From: 1, To: 2, Backwards: false}: {},
{From: 1, To: 3, Backwards: false}: {
{Position: 3, EventID: "d"},
},
{From: 10, To: 0, Backwards: true}: {
{Position: 7, EventID: "h"},
{Position: 6, EventID: "g"},
{Position: 5, EventID: "f"},
{Position: 4, EventID: "e"},
{Position: 3, EventID: "d"},
{Position: 1, EventID: "b"},
},
{From: 6, To: 3, Backwards: true}: {
{Position: 5, EventID: "f"},
{Position: 4, EventID: "e"},
{Position: 3, EventID: "d"},
},
} {
compareRelationsToExpected(t, tab, r, expected)
}
})
}

View file

@ -90,9 +90,9 @@ func (p *AccountDataStreamProvider) IncrementalSync(
} }
} else { } else {
if roomData, ok := dataRes.RoomAccountData[roomID][dataType]; ok { if roomData, ok := dataRes.RoomAccountData[roomID][dataType]; ok {
joinData := *types.NewJoinResponse() joinData, ok := req.Response.Rooms.Join[roomID]
if existing, ok := req.Response.Rooms.Join[roomID]; ok { if !ok {
joinData = existing joinData = types.NewJoinResponse()
} }
joinData.AccountData.Events = append( joinData.AccountData.Events = append(
joinData.AccountData.Events, joinData.AccountData.Events,

View file

@ -65,7 +65,7 @@ func (p *InviteStreamProvider) IncrementalSync(
continue continue
} }
ir := types.NewInviteResponse(inviteEvent) ir := types.NewInviteResponse(inviteEvent)
req.Response.Rooms.Invite[roomID] = *ir req.Response.Rooms.Invite[roomID] = ir
} }
// When doing an initial sync, we don't want to add retired invites, as this // When doing an initial sync, we don't want to add retired invites, as this
@ -87,7 +87,7 @@ func (p *InviteStreamProvider) IncrementalSync(
Type: "m.room.member", Type: "m.room.member",
Content: gomatrixserverlib.RawJSON(`{"membership":"leave"}`), Content: gomatrixserverlib.RawJSON(`{"membership":"leave"}`),
}) })
req.Response.Rooms.Leave[roomID] = *lr req.Response.Rooms.Leave[roomID] = lr
} }
} }

View file

@ -106,7 +106,7 @@ func (p *PDUStreamProvider) CompleteSync(
} }
continue continue
} }
req.Response.Rooms.Join[roomID] = *jr req.Response.Rooms.Join[roomID] = jr
req.Rooms[roomID] = gomatrixserverlib.Join req.Rooms[roomID] = gomatrixserverlib.Join
} }
@ -129,7 +129,7 @@ func (p *PDUStreamProvider) CompleteSync(
} }
continue continue
} }
req.Response.Rooms.Peek[peek.RoomID] = *jr req.Response.Rooms.Peek[peek.RoomID] = jr
} }
} }
@ -320,7 +320,7 @@ func (p *PDUStreamProvider) addRoomDeltaToResponse(
// didn't "remove" events, return that the response is limited. // didn't "remove" events, return that the response is limited.
jr.Timeline.Limited = limited && len(events) == len(recentEvents) jr.Timeline.Limited = limited && len(events) == len(recentEvents)
jr.State.Events = gomatrixserverlib.HeaderedToClientEvents(delta.StateEvents, gomatrixserverlib.FormatSync) jr.State.Events = gomatrixserverlib.HeaderedToClientEvents(delta.StateEvents, gomatrixserverlib.FormatSync)
res.Rooms.Join[delta.RoomID] = *jr res.Rooms.Join[delta.RoomID] = jr
case gomatrixserverlib.Peek: case gomatrixserverlib.Peek:
jr := types.NewJoinResponse() jr := types.NewJoinResponse()
@ -329,7 +329,7 @@ func (p *PDUStreamProvider) addRoomDeltaToResponse(
jr.Timeline.Events = gomatrixserverlib.HeaderedToClientEvents(recentEvents, gomatrixserverlib.FormatSync) jr.Timeline.Events = gomatrixserverlib.HeaderedToClientEvents(recentEvents, gomatrixserverlib.FormatSync)
jr.Timeline.Limited = limited jr.Timeline.Limited = limited
jr.State.Events = gomatrixserverlib.HeaderedToClientEvents(delta.StateEvents, gomatrixserverlib.FormatSync) jr.State.Events = gomatrixserverlib.HeaderedToClientEvents(delta.StateEvents, gomatrixserverlib.FormatSync)
res.Rooms.Peek[delta.RoomID] = *jr res.Rooms.Peek[delta.RoomID] = jr
case gomatrixserverlib.Leave: case gomatrixserverlib.Leave:
fallthrough // transitions to leave are the same as ban fallthrough // transitions to leave are the same as ban
@ -342,7 +342,7 @@ func (p *PDUStreamProvider) addRoomDeltaToResponse(
// didn't "remove" events, return that the response is limited. // didn't "remove" events, return that the response is limited.
lr.Timeline.Limited = limited && len(events) == len(recentEvents) lr.Timeline.Limited = limited && len(events) == len(recentEvents)
lr.State.Events = gomatrixserverlib.HeaderedToClientEvents(delta.StateEvents, gomatrixserverlib.FormatSync) lr.State.Events = gomatrixserverlib.HeaderedToClientEvents(delta.StateEvents, gomatrixserverlib.FormatSync)
res.Rooms.Leave[delta.RoomID] = *lr res.Rooms.Leave[delta.RoomID] = lr
} }
return latestPosition, nil return latestPosition, nil

View file

@ -4,9 +4,10 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/dendrite/syncapi/storage" "github.com/matrix-org/dendrite/syncapi/storage"
"github.com/matrix-org/dendrite/syncapi/types" "github.com/matrix-org/dendrite/syncapi/types"
"github.com/matrix-org/gomatrixserverlib"
) )
type ReceiptStreamProvider struct { type ReceiptStreamProvider struct {
@ -66,6 +67,10 @@ func (p *ReceiptStreamProvider) IncrementalSync(
if _, ok := req.IgnoredUsers.List[receipt.UserID]; ok { if _, ok := req.IgnoredUsers.List[receipt.UserID]; ok {
continue continue
} }
// Don't send private read receipts to other users
if receipt.Type == "m.read.private" && req.Device.UserID != receipt.UserID {
continue
}
receiptsByRoom[receipt.RoomID] = append(receiptsByRoom[receipt.RoomID], receipt) receiptsByRoom[receipt.RoomID] = append(receiptsByRoom[receipt.RoomID], receipt)
} }
@ -76,9 +81,9 @@ func (p *ReceiptStreamProvider) IncrementalSync(
continue continue
} }
jr := *types.NewJoinResponse() jr, ok := req.Response.Rooms.Join[roomID]
if existing, ok := req.Response.Rooms.Join[roomID]; ok { if !ok {
jr = existing jr = types.NewJoinResponse()
} }
ev := gomatrixserverlib.ClientEvent{ ev := gomatrixserverlib.ClientEvent{

View file

@ -4,10 +4,11 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/dendrite/internal/caching" "github.com/matrix-org/dendrite/internal/caching"
"github.com/matrix-org/dendrite/syncapi/storage" "github.com/matrix-org/dendrite/syncapi/storage"
"github.com/matrix-org/dendrite/syncapi/types" "github.com/matrix-org/dendrite/syncapi/types"
"github.com/matrix-org/gomatrixserverlib"
) )
type TypingStreamProvider struct { type TypingStreamProvider struct {
@ -35,9 +36,9 @@ func (p *TypingStreamProvider) IncrementalSync(
continue continue
} }
jr := *types.NewJoinResponse() jr, ok := req.Response.Rooms.Join[roomID]
if existing, ok := req.Response.Rooms.Join[roomID]; ok { if !ok {
jr = existing jr = types.NewJoinResponse()
} }
if users, updated := p.EDUCache.GetTypingUsersIfUpdatedAfter( if users, updated := p.EDUCache.GetTypingUsersIfUpdatedAfter(

View file

@ -407,7 +407,7 @@ func (rp *RequestPool) OnIncomingSyncRequest(req *http.Request, device *userapi.
func(txn storage.DatabaseTransaction) types.StreamPosition { func(txn storage.DatabaseTransaction) types.StreamPosition {
return rp.streams.PDUStreamProvider.IncrementalSync( return rp.streams.PDUStreamProvider.IncrementalSync(
syncReq.Context, txn, syncReq, syncReq.Context, txn, syncReq,
syncReq.Since.PDUPosition, currentPos.PDUPosition, syncReq.Since.PDUPosition, rp.Notifier.CurrentPosition().PDUPosition,
) )
}, },
), ),
@ -416,7 +416,7 @@ func (rp *RequestPool) OnIncomingSyncRequest(req *http.Request, device *userapi.
func(txn storage.DatabaseTransaction) types.StreamPosition { func(txn storage.DatabaseTransaction) types.StreamPosition {
return rp.streams.TypingStreamProvider.IncrementalSync( return rp.streams.TypingStreamProvider.IncrementalSync(
syncReq.Context, txn, syncReq, syncReq.Context, txn, syncReq,
syncReq.Since.TypingPosition, currentPos.TypingPosition, syncReq.Since.TypingPosition, rp.Notifier.CurrentPosition().TypingPosition,
) )
}, },
), ),
@ -425,7 +425,7 @@ func (rp *RequestPool) OnIncomingSyncRequest(req *http.Request, device *userapi.
func(txn storage.DatabaseTransaction) types.StreamPosition { func(txn storage.DatabaseTransaction) types.StreamPosition {
return rp.streams.ReceiptStreamProvider.IncrementalSync( return rp.streams.ReceiptStreamProvider.IncrementalSync(
syncReq.Context, txn, syncReq, syncReq.Context, txn, syncReq,
syncReq.Since.ReceiptPosition, currentPos.ReceiptPosition, syncReq.Since.ReceiptPosition, rp.Notifier.CurrentPosition().ReceiptPosition,
) )
}, },
), ),
@ -434,7 +434,7 @@ func (rp *RequestPool) OnIncomingSyncRequest(req *http.Request, device *userapi.
func(txn storage.DatabaseTransaction) types.StreamPosition { func(txn storage.DatabaseTransaction) types.StreamPosition {
return rp.streams.InviteStreamProvider.IncrementalSync( return rp.streams.InviteStreamProvider.IncrementalSync(
syncReq.Context, txn, syncReq, syncReq.Context, txn, syncReq,
syncReq.Since.InvitePosition, currentPos.InvitePosition, syncReq.Since.InvitePosition, rp.Notifier.CurrentPosition().InvitePosition,
) )
}, },
), ),
@ -443,7 +443,7 @@ func (rp *RequestPool) OnIncomingSyncRequest(req *http.Request, device *userapi.
func(txn storage.DatabaseTransaction) types.StreamPosition { func(txn storage.DatabaseTransaction) types.StreamPosition {
return rp.streams.SendToDeviceStreamProvider.IncrementalSync( return rp.streams.SendToDeviceStreamProvider.IncrementalSync(
syncReq.Context, txn, syncReq, syncReq.Context, txn, syncReq,
syncReq.Since.SendToDevicePosition, currentPos.SendToDevicePosition, syncReq.Since.SendToDevicePosition, rp.Notifier.CurrentPosition().SendToDevicePosition,
) )
}, },
), ),
@ -452,7 +452,7 @@ func (rp *RequestPool) OnIncomingSyncRequest(req *http.Request, device *userapi.
func(txn storage.DatabaseTransaction) types.StreamPosition { func(txn storage.DatabaseTransaction) types.StreamPosition {
return rp.streams.AccountDataStreamProvider.IncrementalSync( return rp.streams.AccountDataStreamProvider.IncrementalSync(
syncReq.Context, txn, syncReq, syncReq.Context, txn, syncReq,
syncReq.Since.AccountDataPosition, currentPos.AccountDataPosition, syncReq.Since.AccountDataPosition, rp.Notifier.CurrentPosition().AccountDataPosition,
) )
}, },
), ),
@ -461,7 +461,7 @@ func (rp *RequestPool) OnIncomingSyncRequest(req *http.Request, device *userapi.
func(txn storage.DatabaseTransaction) types.StreamPosition { func(txn storage.DatabaseTransaction) types.StreamPosition {
return rp.streams.NotificationDataStreamProvider.IncrementalSync( return rp.streams.NotificationDataStreamProvider.IncrementalSync(
syncReq.Context, txn, syncReq, syncReq.Context, txn, syncReq,
syncReq.Since.NotificationDataPosition, currentPos.NotificationDataPosition, syncReq.Since.NotificationDataPosition, rp.Notifier.CurrentPosition().NotificationDataPosition,
) )
}, },
), ),
@ -470,7 +470,7 @@ func (rp *RequestPool) OnIncomingSyncRequest(req *http.Request, device *userapi.
func(txn storage.DatabaseTransaction) types.StreamPosition { func(txn storage.DatabaseTransaction) types.StreamPosition {
return rp.streams.DeviceListStreamProvider.IncrementalSync( return rp.streams.DeviceListStreamProvider.IncrementalSync(
syncReq.Context, txn, syncReq, syncReq.Context, txn, syncReq,
syncReq.Since.DeviceListPosition, currentPos.DeviceListPosition, syncReq.Since.DeviceListPosition, rp.Notifier.CurrentPosition().DeviceListPosition,
) )
}, },
), ),
@ -479,7 +479,7 @@ func (rp *RequestPool) OnIncomingSyncRequest(req *http.Request, device *userapi.
func(txn storage.DatabaseTransaction) types.StreamPosition { func(txn storage.DatabaseTransaction) types.StreamPosition {
return rp.streams.PresenceStreamProvider.IncrementalSync( return rp.streams.PresenceStreamProvider.IncrementalSync(
syncReq.Context, txn, syncReq, syncReq.Context, txn, syncReq,
syncReq.Since.PresencePosition, currentPos.PresencePosition, syncReq.Since.PresencePosition, rp.Notifier.CurrentPosition().PresencePosition,
) )
}, },
), ),

View file

@ -47,6 +47,14 @@ type StateDelta struct {
// StreamPosition represents the offset in the sync stream a client is at. // StreamPosition represents the offset in the sync stream a client is at.
type StreamPosition int64 type StreamPosition int64
func NewStreamPositionFromString(s string) (StreamPosition, error) {
n, err := strconv.Atoi(s)
if err != nil {
return 0, err
}
return StreamPosition(n), nil
}
// StreamEvent is the same as gomatrixserverlib.Event but also has the PDU stream position for this event. // StreamEvent is the same as gomatrixserverlib.Event but also has the PDU stream position for this event.
type StreamEvent struct { type StreamEvent struct {
*gomatrixserverlib.HeaderedEvent *gomatrixserverlib.HeaderedEvent
@ -327,29 +335,57 @@ type PrevEventRef struct {
PrevSender string `json:"prev_sender"` PrevSender string `json:"prev_sender"`
} }
type DeviceLists struct {
Changed []string `json:"changed,omitempty"`
Left []string `json:"left,omitempty"`
}
type RoomsResponse struct {
Join map[string]*JoinResponse `json:"join,omitempty"`
Peek map[string]*JoinResponse `json:"peek,omitempty"`
Invite map[string]*InviteResponse `json:"invite,omitempty"`
Leave map[string]*LeaveResponse `json:"leave,omitempty"`
}
type ToDeviceResponse struct {
Events []gomatrixserverlib.SendToDeviceEvent `json:"events,omitempty"`
}
// Response represents a /sync API response. See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-sync // Response represents a /sync API response. See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-sync
type Response struct { type Response struct {
NextBatch StreamingToken `json:"next_batch"` NextBatch StreamingToken `json:"next_batch"`
AccountData struct { AccountData *ClientEvents `json:"account_data,omitempty"`
Events []gomatrixserverlib.ClientEvent `json:"events,omitempty"` Presence *ClientEvents `json:"presence,omitempty"`
} `json:"account_data,omitempty"` Rooms *RoomsResponse `json:"rooms,omitempty"`
Presence struct { ToDevice *ToDeviceResponse `json:"to_device,omitempty"`
Events []gomatrixserverlib.ClientEvent `json:"events,omitempty"` DeviceLists *DeviceLists `json:"device_lists,omitempty"`
} `json:"presence,omitempty"` DeviceListsOTKCount map[string]int `json:"device_one_time_keys_count,omitempty"`
Rooms struct { }
Join map[string]JoinResponse `json:"join,omitempty"`
Peek map[string]JoinResponse `json:"peek,omitempty"` func (r Response) MarshalJSON() ([]byte, error) {
Invite map[string]InviteResponse `json:"invite,omitempty"` type alias Response
Leave map[string]LeaveResponse `json:"leave,omitempty"` a := alias(r)
} `json:"rooms,omitempty"` if r.AccountData != nil && len(r.AccountData.Events) == 0 {
ToDevice struct { a.AccountData = nil
Events []gomatrixserverlib.SendToDeviceEvent `json:"events,omitempty"` }
} `json:"to_device,omitempty"` if r.Presence != nil && len(r.Presence.Events) == 0 {
DeviceLists struct { a.Presence = nil
Changed []string `json:"changed,omitempty"` }
Left []string `json:"left,omitempty"` if r.DeviceLists != nil {
} `json:"device_lists,omitempty"` if len(r.DeviceLists.Left) == 0 && len(r.DeviceLists.Changed) == 0 {
DeviceListsOTKCount map[string]int `json:"device_one_time_keys_count,omitempty"` a.DeviceLists = nil
}
}
if r.Rooms != nil {
if len(r.Rooms.Join) == 0 && len(r.Rooms.Peek) == 0 &&
len(r.Rooms.Invite) == 0 && len(r.Rooms.Leave) == 0 {
a.Rooms = nil
}
}
if r.ToDevice != nil && len(r.ToDevice.Events) == 0 {
a.ToDevice = nil
}
return json.Marshal(a)
} }
func (r *Response) HasUpdates() bool { func (r *Response) HasUpdates() bool {
@ -370,18 +406,21 @@ func NewResponse() *Response {
res := Response{} res := Response{}
// Pre-initialise the maps. Synapse will return {} even if there are no rooms under a specific section, // Pre-initialise the maps. Synapse will return {} even if there are no rooms under a specific section,
// so let's do the same thing. Bonus: this means we can't get dreaded 'assignment to entry in nil map' errors. // so let's do the same thing. Bonus: this means we can't get dreaded 'assignment to entry in nil map' errors.
res.Rooms.Join = map[string]JoinResponse{} res.Rooms = &RoomsResponse{
res.Rooms.Peek = map[string]JoinResponse{} Join: map[string]*JoinResponse{},
res.Rooms.Invite = map[string]InviteResponse{} Peek: map[string]*JoinResponse{},
res.Rooms.Leave = map[string]LeaveResponse{} Invite: map[string]*InviteResponse{},
Leave: map[string]*LeaveResponse{},
}
// Also pre-intialise empty slices or else we'll insert 'null' instead of '[]' for the value. // Also pre-intialise empty slices or else we'll insert 'null' instead of '[]' for the value.
// TODO: We really shouldn't have to do all this to coerce encoding/json to Do The Right Thing. We should // TODO: We really shouldn't have to do all this to coerce encoding/json to Do The Right Thing. We should
// really be using our own Marshal/Unmarshal implementations otherwise this may prove to be a CPU bottleneck. // really be using our own Marshal/Unmarshal implementations otherwise this may prove to be a CPU bottleneck.
// This also applies to NewJoinResponse, NewInviteResponse and NewLeaveResponse. // This also applies to NewJoinResponse, NewInviteResponse and NewLeaveResponse.
res.AccountData.Events = []gomatrixserverlib.ClientEvent{} res.AccountData = &ClientEvents{}
res.Presence.Events = []gomatrixserverlib.ClientEvent{} res.Presence = &ClientEvents{}
res.ToDevice.Events = []gomatrixserverlib.SendToDeviceEvent{} res.DeviceLists = &DeviceLists{}
res.ToDevice = &ToDeviceResponse{}
res.DeviceListsOTKCount = map[string]int{} res.DeviceListsOTKCount = map[string]int{}
return &res return &res
@ -403,38 +442,73 @@ type UnreadNotifications struct {
NotificationCount int `json:"notification_count"` NotificationCount int `json:"notification_count"`
} }
type ClientEvents struct {
Events []gomatrixserverlib.ClientEvent `json:"events,omitempty"`
}
type Timeline struct {
Events []gomatrixserverlib.ClientEvent `json:"events"`
Limited bool `json:"limited"`
PrevBatch *TopologyToken `json:"prev_batch,omitempty"`
}
type Summary struct {
Heroes []string `json:"m.heroes,omitempty"`
JoinedMemberCount *int `json:"m.joined_member_count,omitempty"`
InvitedMemberCount *int `json:"m.invited_member_count,omitempty"`
}
// JoinResponse represents a /sync response for a room which is under the 'join' or 'peek' key. // JoinResponse represents a /sync response for a room which is under the 'join' or 'peek' key.
type JoinResponse struct { type JoinResponse struct {
Summary struct { Summary *Summary `json:"summary,omitempty"`
Heroes []string `json:"m.heroes,omitempty"` State *ClientEvents `json:"state,omitempty"`
JoinedMemberCount *int `json:"m.joined_member_count,omitempty"` Timeline *Timeline `json:"timeline,omitempty"`
InvitedMemberCount *int `json:"m.invited_member_count,omitempty"` Ephemeral *ClientEvents `json:"ephemeral,omitempty"`
} `json:"summary"` AccountData *ClientEvents `json:"account_data,omitempty"`
State struct {
Events []gomatrixserverlib.ClientEvent `json:"events"`
} `json:"state"`
Timeline struct {
Events []gomatrixserverlib.ClientEvent `json:"events"`
Limited bool `json:"limited"`
PrevBatch *TopologyToken `json:"prev_batch,omitempty"`
} `json:"timeline"`
Ephemeral struct {
Events []gomatrixserverlib.ClientEvent `json:"events"`
} `json:"ephemeral"`
AccountData struct {
Events []gomatrixserverlib.ClientEvent `json:"events"`
} `json:"account_data"`
*UnreadNotifications `json:"unread_notifications,omitempty"` *UnreadNotifications `json:"unread_notifications,omitempty"`
} }
func (jr JoinResponse) MarshalJSON() ([]byte, error) {
type alias JoinResponse
a := alias(jr)
if jr.State != nil && len(jr.State.Events) == 0 {
a.State = nil
}
if jr.Ephemeral != nil && len(jr.Ephemeral.Events) == 0 {
a.Ephemeral = nil
}
if jr.AccountData != nil && len(jr.AccountData.Events) == 0 {
a.AccountData = nil
}
if jr.Timeline != nil && len(jr.Timeline.Events) == 0 {
a.Timeline = nil
}
if jr.Summary != nil {
var nilPtr int
joinedEmpty := jr.Summary.JoinedMemberCount == nil || jr.Summary.JoinedMemberCount == &nilPtr
invitedEmpty := jr.Summary.InvitedMemberCount == nil || jr.Summary.InvitedMemberCount == &nilPtr
if joinedEmpty && invitedEmpty && len(jr.Summary.Heroes) == 0 {
a.Summary = nil
}
}
if jr.UnreadNotifications != nil &&
jr.UnreadNotifications.NotificationCount == 0 && jr.UnreadNotifications.HighlightCount == 0 {
a.UnreadNotifications = nil
}
return json.Marshal(a)
}
// NewJoinResponse creates an empty response with initialised arrays. // NewJoinResponse creates an empty response with initialised arrays.
func NewJoinResponse() *JoinResponse { func NewJoinResponse() *JoinResponse {
res := JoinResponse{} return &JoinResponse{
res.State.Events = []gomatrixserverlib.ClientEvent{} Summary: &Summary{},
res.Timeline.Events = []gomatrixserverlib.ClientEvent{} State: &ClientEvents{},
res.Ephemeral.Events = []gomatrixserverlib.ClientEvent{} Timeline: &Timeline{},
res.AccountData.Events = []gomatrixserverlib.ClientEvent{} Ephemeral: &ClientEvents{},
return &res AccountData: &ClientEvents{},
UnreadNotifications: &UnreadNotifications{},
}
} }
// InviteResponse represents a /sync response for a room which is under the 'invite' key. // InviteResponse represents a /sync response for a room which is under the 'invite' key.
@ -469,21 +543,28 @@ func NewInviteResponse(event *gomatrixserverlib.HeaderedEvent) *InviteResponse {
// LeaveResponse represents a /sync response for a room which is under the 'leave' key. // LeaveResponse represents a /sync response for a room which is under the 'leave' key.
type LeaveResponse struct { type LeaveResponse struct {
State struct { State *ClientEvents `json:"state,omitempty"`
Events []gomatrixserverlib.ClientEvent `json:"events"` Timeline *Timeline `json:"timeline,omitempty"`
} `json:"state"` }
Timeline struct {
Events []gomatrixserverlib.ClientEvent `json:"events"` func (lr LeaveResponse) MarshalJSON() ([]byte, error) {
Limited bool `json:"limited"` type alias LeaveResponse
PrevBatch *TopologyToken `json:"prev_batch,omitempty"` a := alias(lr)
} `json:"timeline"` if lr.State != nil && len(lr.State.Events) == 0 {
a.State = nil
}
if lr.Timeline != nil && len(lr.Timeline.Events) == 0 {
a.Timeline = nil
}
return json.Marshal(a)
} }
// NewLeaveResponse creates an empty response with initialised arrays. // NewLeaveResponse creates an empty response with initialised arrays.
func NewLeaveResponse() *LeaveResponse { func NewLeaveResponse() *LeaveResponse {
res := LeaveResponse{} res := LeaveResponse{
res.State.Events = []gomatrixserverlib.ClientEvent{} State: &ClientEvents{},
res.Timeline.Events = []gomatrixserverlib.ClientEvent{} Timeline: &Timeline{},
}
return &res return &res
} }
@ -526,3 +607,8 @@ type OutputSendToDeviceEvent struct {
type IgnoredUsers struct { type IgnoredUsers struct {
List map[string]interface{} `json:"ignored_users"` List map[string]interface{} `json:"ignored_users"`
} }
type RelationEntry struct {
Position StreamPosition
EventID string
}

View file

@ -47,7 +47,6 @@ Notifications can be viewed with GET /notifications
# More flakey # More flakey
If remote user leaves room we no longer receive device updates
Guest users can join guest_access rooms Guest users can join guest_access rooms
# This will fail in HTTP API mode, so blacklisted for now # This will fail in HTTP API mode, so blacklisted for now

View file

@ -306,7 +306,7 @@ Alternative server names do not cause a routing loop
Events whose auth_events are in the wrong room do not mess up the room state Events whose auth_events are in the wrong room do not mess up the room state
Inbound federation can return events Inbound federation can return events
Inbound federation can return missing events for world_readable visibility Inbound federation can return missing events for world_readable visibility
Inbound federation can return missing events for invite visibility Inbound federation can return missing events for invited visibility
Inbound federation can get public room list Inbound federation can get public room list
PUT /rooms/:room_id/redact/:event_id/:txn_id as power user redacts message PUT /rooms/:room_id/redact/:event_id/:txn_id as power user redacts message
PUT /rooms/:room_id/redact/:event_id/:txn_id as original message sender redacts message PUT /rooms/:room_id/redact/:event_id/:txn_id as original message sender redacts message
@ -742,3 +742,5 @@ User in private room doesn't appear in user directory
User joining then leaving public room appears and dissappears from directory User joining then leaving public room appears and dissappears from directory
User in remote room doesn't appear in user directory after server left room User in remote room doesn't appear in user directory after server left room
User in shared private room does appear in user directory until leave User in shared private room does appear in user directory until leave
Existing members see new member's presence
Inbound federation can return missing events for joined visibility

View file

@ -4,10 +4,11 @@ import (
"encoding/json" "encoding/json"
"testing" "testing"
"github.com/nats-io/nats.go"
"github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/base"
"github.com/matrix-org/dendrite/setup/jetstream" "github.com/matrix-org/dendrite/setup/jetstream"
"github.com/nats-io/nats.go"
) )
func MustPublishMsgs(t *testing.T, jsctx nats.JetStreamContext, msgs ...*nats.Msg) { func MustPublishMsgs(t *testing.T, jsctx nats.JetStreamContext, msgs ...*nats.Msg) {
@ -21,10 +22,8 @@ func MustPublishMsgs(t *testing.T, jsctx nats.JetStreamContext, msgs ...*nats.Ms
func NewOutputEventMsg(t *testing.T, base *base.BaseDendrite, roomID string, update api.OutputEvent) *nats.Msg { func NewOutputEventMsg(t *testing.T, base *base.BaseDendrite, roomID string, update api.OutputEvent) *nats.Msg {
t.Helper() t.Helper()
msg := &nats.Msg{ msg := nats.NewMsg(base.Cfg.Global.JetStream.Prefixed(jetstream.OutputRoomEvent))
Subject: base.Cfg.Global.JetStream.Prefixed(jetstream.OutputRoomEvent), msg.Header.Set(jetstream.RoomEventType, string(update.Type))
Header: nats.Header{},
}
msg.Header.Set(jetstream.RoomID, roomID) msg.Header.Set(jetstream.RoomID, roomID)
var err error var err error
msg.Data, err = json.Marshal(update) msg.Data, err = json.Marshal(update)

View file

@ -72,15 +72,16 @@ func (s *OutputRoomEventConsumer) Start() error {
func (s *OutputRoomEventConsumer) onMessage(ctx context.Context, msgs []*nats.Msg) bool { func (s *OutputRoomEventConsumer) onMessage(ctx context.Context, msgs []*nats.Msg) bool {
msg := msgs[0] // Guaranteed to exist if onMessage is called msg := msgs[0] // Guaranteed to exist if onMessage is called
// Only handle events we care about
if rsapi.OutputType(msg.Header.Get(jetstream.RoomEventType)) != rsapi.OutputTypeNewRoomEvent {
return true
}
var output rsapi.OutputEvent var output rsapi.OutputEvent
if err := json.Unmarshal(msg.Data, &output); err != nil { if err := json.Unmarshal(msg.Data, &output); err != nil {
// If the message was invalid, log it and move on to the next message in the stream // If the message was invalid, log it and move on to the next message in the stream
log.WithError(err).Errorf("roomserver output log: message parse failure") log.WithError(err).Errorf("roomserver output log: message parse failure")
return true return true
} }
if output.Type != rsapi.OutputTypeNewRoomEvent {
return true
}
event := output.NewRoomEvent.Event event := output.NewRoomEvent.Event
if event == nil { if event == nil {
log.Errorf("userapi consumer: expected event") log.Errorf("userapi consumer: expected event")

View file

@ -838,6 +838,8 @@ func (a *UserInternalAPI) QueryAccountByPassword(ctx context.Context, req *api.Q
return nil return nil
case bcrypt.ErrMismatchedHashAndPassword: // user exists, but password doesn't match case bcrypt.ErrMismatchedHashAndPassword: // user exists, but password doesn't match
return nil return nil
case bcrypt.ErrHashTooShort: // user exists, but probably a passwordless account
return nil
default: default:
res.Exists = true res.Exists = true
res.Account = acc res.Account = acc

Some files were not shown because too many files have changed in this diff Show more