mirror of
https://github.com/hoernschen/dendrite.git
synced 2025-07-29 12:42:46 +00:00
Synchronous invites (#1273)
* Refactor invites to be synchronous * Fix synchronous invites * Fix client API return type for send invite error * Linter * Restore PerformError on rsAPI.PerformInvite * Update sytest-whitelist * Don't override PerformError with normal errors * Fix error passing * Un-whitelist a couple of tests * Update sytest-whitelist * Try to handle multiple invite rejections better * nolint * Update gomatrixserverlib * Fix /v1/invite test * Remove replace from go.mod
This commit is contained in:
parent
6820b3e024
commit
6cb1a65809
23 changed files with 334 additions and 443 deletions
|
@ -22,7 +22,7 @@ type RoomserverInternalAPI interface {
|
|||
ctx context.Context,
|
||||
req *PerformInviteRequest,
|
||||
res *PerformInviteResponse,
|
||||
)
|
||||
) error
|
||||
|
||||
PerformJoin(
|
||||
ctx context.Context,
|
||||
|
|
|
@ -33,9 +33,9 @@ func (t *RoomserverInternalAPITrace) PerformInvite(
|
|||
ctx context.Context,
|
||||
req *PerformInviteRequest,
|
||||
res *PerformInviteResponse,
|
||||
) {
|
||||
t.Impl.PerformInvite(ctx, req, res)
|
||||
) error {
|
||||
util.GetLogger(ctx).Infof("PerformInvite req=%+v res=%+v", js(req), js(res))
|
||||
return t.Impl.PerformInvite(ctx, req, res)
|
||||
}
|
||||
|
||||
func (t *RoomserverInternalAPITrace) PerformJoin(
|
||||
|
|
|
@ -105,7 +105,6 @@ type PerformInviteRequest struct {
|
|||
}
|
||||
|
||||
type PerformInviteResponse struct {
|
||||
// If non-nil, the invite request failed. Contains more information why it failed.
|
||||
Error *PerformError
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ package api
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/matrix-org/gomatrixserverlib"
|
||||
"github.com/matrix-org/util"
|
||||
|
@ -99,21 +100,41 @@ func SendInvite(
|
|||
rsAPI RoomserverInternalAPI, inviteEvent gomatrixserverlib.HeaderedEvent,
|
||||
inviteRoomState []gomatrixserverlib.InviteV2StrippedState,
|
||||
sendAsServer gomatrixserverlib.ServerName, txnID *TransactionID,
|
||||
) *PerformError {
|
||||
request := PerformInviteRequest{
|
||||
) error {
|
||||
// Start by sending the invite request into the roomserver. This will
|
||||
// trigger the federation request amongst other things if needed.
|
||||
request := &PerformInviteRequest{
|
||||
Event: inviteEvent,
|
||||
InviteRoomState: inviteRoomState,
|
||||
RoomVersion: inviteEvent.RoomVersion,
|
||||
SendAsServer: string(sendAsServer),
|
||||
TransactionID: txnID,
|
||||
}
|
||||
var response PerformInviteResponse
|
||||
rsAPI.PerformInvite(ctx, &request, &response)
|
||||
// we need to do this because many places people will use `var err error` as the return
|
||||
// arg and a nil interface != nil pointer to a concrete interface (in this case PerformError)
|
||||
if response.Error != nil && response.Error.Msg != "" {
|
||||
response := &PerformInviteResponse{}
|
||||
if err := rsAPI.PerformInvite(ctx, request, response); err != nil {
|
||||
return fmt.Errorf("rsAPI.PerformInvite: %w", err)
|
||||
}
|
||||
if response.Error != nil {
|
||||
return response.Error
|
||||
}
|
||||
|
||||
// Now send the invite event into the roomserver. If the room is known
|
||||
// locally then this will succeed, notifying existing users in the room
|
||||
// about the new invite. If the room isn't known locally then this will
|
||||
// fail - and that's also OK.
|
||||
inputReq := &InputRoomEventsRequest{
|
||||
InputRoomEvents: []InputRoomEvent{
|
||||
{
|
||||
Kind: KindNew,
|
||||
Event: inviteEvent,
|
||||
AuthEventIDs: inviteEvent.AuthEventIDs(),
|
||||
SendAsServer: string(sendAsServer),
|
||||
},
|
||||
},
|
||||
}
|
||||
inputRes := &InputRoomEventsResponse{}
|
||||
_ = rsAPI.InputRoomEvents(ctx, inputReq, inputRes)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,9 @@ package internal
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
federationSenderAPI "github.com/matrix-org/dendrite/federationsender/api"
|
||||
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||
"github.com/matrix-org/dendrite/roomserver/api"
|
||||
"github.com/matrix-org/dendrite/roomserver/state"
|
||||
|
@ -14,73 +14,50 @@ import (
|
|||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// PerformInvite handles inviting to matrix rooms, including over federation by talking to the federationsender.
|
||||
// nolint:gocyclo
|
||||
func (r *RoomserverInternalAPI) PerformInvite(
|
||||
ctx context.Context,
|
||||
req *api.PerformInviteRequest,
|
||||
res *api.PerformInviteResponse,
|
||||
) {
|
||||
err := r.performInvite(ctx, req)
|
||||
if err != nil {
|
||||
perr, ok := err.(*api.PerformError)
|
||||
if ok {
|
||||
res.Error = perr
|
||||
} else {
|
||||
res.Error = &api.PerformError{
|
||||
Msg: err.Error(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RoomserverInternalAPI) performInvite(ctx context.Context,
|
||||
req *api.PerformInviteRequest,
|
||||
) error {
|
||||
loopback, err := r.processInviteEvent(ctx, r, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// The processInviteEvent function can optionally return a
|
||||
// loopback room event containing the invite, for local invites.
|
||||
// If it does, we should process it with the room events below.
|
||||
if loopback != nil {
|
||||
var loopbackRes api.InputRoomEventsResponse
|
||||
err := r.InputRoomEvents(ctx, &api.InputRoomEventsRequest{
|
||||
InputRoomEvents: []api.InputRoomEvent{*loopback},
|
||||
}, &loopbackRes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// nolint:gocyclo
|
||||
func (r *RoomserverInternalAPI) processInviteEvent(
|
||||
ctx context.Context,
|
||||
ow *RoomserverInternalAPI,
|
||||
input *api.PerformInviteRequest,
|
||||
) (*api.InputRoomEvent, error) {
|
||||
if input.Event.StateKey() == nil {
|
||||
return nil, fmt.Errorf("invite must be a state event")
|
||||
event := req.Event
|
||||
if event.StateKey() == nil {
|
||||
return fmt.Errorf("invite must be a state event")
|
||||
}
|
||||
|
||||
roomID := input.Event.RoomID()
|
||||
targetUserID := *input.Event.StateKey()
|
||||
roomID := event.RoomID()
|
||||
targetUserID := *event.StateKey()
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"event_id": input.Event.EventID(),
|
||||
"event_id": event.EventID(),
|
||||
"room_id": roomID,
|
||||
"room_version": input.RoomVersion,
|
||||
"room_version": req.RoomVersion,
|
||||
"target_user_id": targetUserID,
|
||||
}).Info("processing invite event")
|
||||
|
||||
_, domain, _ := gomatrixserverlib.SplitID('@', targetUserID)
|
||||
isTargetLocalUser := domain == r.Cfg.Matrix.ServerName
|
||||
isTargetLocal := domain == r.Cfg.Matrix.ServerName
|
||||
isOriginLocal := event.Origin() == r.Cfg.Matrix.ServerName
|
||||
|
||||
updater, err := r.DB.MembershipUpdater(ctx, roomID, targetUserID, isTargetLocalUser, input.RoomVersion)
|
||||
inviteState := req.InviteRoomState
|
||||
if len(inviteState) == 0 {
|
||||
if is, err := buildInviteStrippedState(ctx, r.DB, req); err == nil {
|
||||
inviteState = is
|
||||
}
|
||||
}
|
||||
if len(inviteState) == 0 {
|
||||
if err := event.SetUnsignedField("invite_room_state", struct{}{}); err != nil {
|
||||
return fmt.Errorf("event.SetUnsignedField: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := event.SetUnsignedField("invite_room_state", inviteState); err != nil {
|
||||
return fmt.Errorf("event.SetUnsignedField: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
updater, err := r.DB.MembershipUpdater(ctx, roomID, targetUserID, isTargetLocal, req.RoomVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return fmt.Errorf("r.DB.MembershipUpdater: %w", err)
|
||||
}
|
||||
succeeded := false
|
||||
defer func() {
|
||||
|
@ -118,109 +95,66 @@ func (r *RoomserverInternalAPI) processInviteEvent(
|
|||
// For now we will implement option 2. Since in the abesence of a retry
|
||||
// mechanism it will be equivalent to option 1, and we don't have a
|
||||
// signalling mechanism to implement option 3.
|
||||
return nil, &api.PerformError{
|
||||
Code: api.PerformErrorNoOperation,
|
||||
Msg: "user is already joined to room",
|
||||
res.Error = &api.PerformError{
|
||||
Code: api.PerformErrorNotAllowed,
|
||||
Msg: "User is already joined to room",
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Normally, with a federated invite, the federation sender would do
|
||||
// the /v2/invite request (in which the remote server signs the invite)
|
||||
// and then the signed event gets sent back to the roomserver as an input
|
||||
// event. When the invite is local, we don't interact with the federation
|
||||
// sender therefore we need to generate the loopback invite event for
|
||||
// the room ourselves.
|
||||
loopback, err := localInviteLoopback(ow, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
event := input.Event.Unwrap()
|
||||
|
||||
// check that the user is allowed to do this. We can only do this check if it is
|
||||
// a local invite as we have the auth events, else we have to take it on trust.
|
||||
if loopback != nil {
|
||||
_, err = checkAuthEvents(ctx, r.DB, input.Event, input.Event.AuthEventIDs())
|
||||
if isOriginLocal {
|
||||
// check that the user is allowed to do this. We can only do this check if it is
|
||||
// a local invite as we have the auth events, else we have to take it on trust.
|
||||
_, err = checkAuthEvents(ctx, r.DB, req.Event, req.Event.AuthEventIDs())
|
||||
if err != nil {
|
||||
log.WithError(err).WithField("event_id", event.EventID()).WithField("auth_event_ids", event.AuthEventIDs()).Error(
|
||||
"processInviteEvent.checkAuthEvents failed for event",
|
||||
)
|
||||
if _, ok := err.(*gomatrixserverlib.NotAllowed); ok {
|
||||
return nil, &api.PerformError{
|
||||
res.Error = &api.PerformError{
|
||||
Msg: err.Error(),
|
||||
Code: api.PerformErrorNotAllowed,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil, err
|
||||
return fmt.Errorf("checkAuthEvents: %w", err)
|
||||
}
|
||||
|
||||
// If the invite originated from us and the target isn't local then we
|
||||
// should try and send the invite over federation first. It might be
|
||||
// that the remote user doesn't exist, in which case we can give up
|
||||
// processing here.
|
||||
if req.SendAsServer != api.DoNotSendToOtherServers && !isTargetLocal {
|
||||
fsReq := &federationSenderAPI.PerformInviteRequest{
|
||||
RoomVersion: req.RoomVersion,
|
||||
Event: req.Event,
|
||||
InviteRoomState: inviteState,
|
||||
}
|
||||
fsRes := &federationSenderAPI.PerformInviteResponse{}
|
||||
if err = r.fsAPI.PerformInvite(ctx, fsReq, fsRes); err != nil {
|
||||
res.Error = &api.PerformError{
|
||||
Msg: err.Error(),
|
||||
Code: api.PerformErrorNoOperation,
|
||||
}
|
||||
log.WithError(err).WithField("event_id", event.EventID()).Error("r.fsAPI.PerformInvite failed")
|
||||
return nil
|
||||
}
|
||||
event = fsRes.Event
|
||||
}
|
||||
}
|
||||
|
||||
if len(input.InviteRoomState) > 0 {
|
||||
// If we were supplied with some invite room state already (which is
|
||||
// most likely to be if the event came in over federation) then use
|
||||
// that.
|
||||
if err = event.SetUnsignedField("invite_room_state", input.InviteRoomState); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// There's no invite room state, so let's have a go at building it
|
||||
// up from local data (which is most likely to be if the event came
|
||||
// from the CS API). If we know about the room then we can insert
|
||||
// the invite room state, if we don't then we just fail quietly.
|
||||
if irs, ierr := buildInviteStrippedState(ctx, r.DB, input); ierr == nil {
|
||||
if err = event.SetUnsignedField("invite_room_state", irs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
log.WithError(ierr).Error("failed to build invite stripped state")
|
||||
// still set the field else synapse deployments don't process the invite
|
||||
if err = event.SetUnsignedField("invite_room_state", struct{}{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
outputUpdates, err := updateToInviteMembership(updater, &event, nil, input.Event.RoomVersion)
|
||||
unwrapped := event.Unwrap()
|
||||
outputUpdates, err := updateToInviteMembership(updater, &unwrapped, nil, req.Event.RoomVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return fmt.Errorf("updateToInviteMembership: %w", err)
|
||||
}
|
||||
|
||||
if err = ow.WriteOutputEvents(roomID, outputUpdates); err != nil {
|
||||
return nil, err
|
||||
if err = r.WriteOutputEvents(roomID, outputUpdates); err != nil {
|
||||
return fmt.Errorf("r.WriteOutputEvents: %w", err)
|
||||
}
|
||||
|
||||
succeeded = true
|
||||
return loopback, nil
|
||||
}
|
||||
|
||||
func localInviteLoopback(
|
||||
ow *RoomserverInternalAPI,
|
||||
input *api.PerformInviteRequest,
|
||||
) (ire *api.InputRoomEvent, err error) {
|
||||
if input.Event.StateKey() == nil {
|
||||
return nil, errors.New("no state key on invite event")
|
||||
}
|
||||
ourServerName := string(ow.Cfg.Matrix.ServerName)
|
||||
_, theirServerName, err := gomatrixserverlib.SplitID('@', *input.Event.StateKey())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Check if the invite originated locally and is destined locally.
|
||||
if input.Event.Origin() == ow.Cfg.Matrix.ServerName && string(theirServerName) == ourServerName {
|
||||
rsEvent := input.Event.Sign(
|
||||
ourServerName,
|
||||
ow.Cfg.Matrix.KeyID,
|
||||
ow.Cfg.Matrix.PrivateKey,
|
||||
).Headered(input.RoomVersion)
|
||||
ire = &api.InputRoomEvent{
|
||||
Kind: api.KindNew,
|
||||
Event: rsEvent,
|
||||
AuthEventIDs: rsEvent.AuthEventIDs(),
|
||||
SendAsServer: ourServerName,
|
||||
TransactionID: nil,
|
||||
}
|
||||
}
|
||||
return ire, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildInviteStrippedState(
|
||||
|
@ -238,7 +172,7 @@ func buildInviteStrippedState(
|
|||
for _, t := range []string{
|
||||
gomatrixserverlib.MRoomName, gomatrixserverlib.MRoomCanonicalAlias,
|
||||
gomatrixserverlib.MRoomAliases, gomatrixserverlib.MRoomJoinRules,
|
||||
"m.room.avatar",
|
||||
"m.room.avatar", "m.room.encryption",
|
||||
} {
|
||||
stateWanted = append(stateWanted, gomatrixserverlib.StateKeyTuple{
|
||||
EventType: t,
|
||||
|
|
|
@ -154,17 +154,12 @@ func (h *httpRoomserverInternalAPI) PerformInvite(
|
|||
ctx context.Context,
|
||||
request *api.PerformInviteRequest,
|
||||
response *api.PerformInviteResponse,
|
||||
) {
|
||||
) error {
|
||||
span, ctx := opentracing.StartSpanFromContext(ctx, "PerformInvite")
|
||||
defer span.Finish()
|
||||
|
||||
apiURL := h.roomserverURL + RoomserverPerformInvitePath
|
||||
err := httputil.PostJSON(ctx, span, h.httpClient, apiURL, request, response)
|
||||
if err != nil {
|
||||
response.Error = &api.PerformError{
|
||||
Msg: fmt.Sprintf("failed to communicate with roomserver: %s", err),
|
||||
}
|
||||
}
|
||||
return httputil.PostJSON(ctx, span, h.httpClient, apiURL, request, response)
|
||||
}
|
||||
|
||||
func (h *httpRoomserverInternalAPI) PerformJoin(
|
||||
|
|
|
@ -33,7 +33,9 @@ func AddRoutes(r api.RoomserverInternalAPI, internalAPIMux *mux.Router) {
|
|||
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
|
||||
return util.MessageResponse(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
r.PerformInvite(req.Context(), &request, &response)
|
||||
if err := r.PerformInvite(req.Context(), &request, &response); err != nil {
|
||||
return util.ErrorResponse(err)
|
||||
}
|
||||
return util.JSONResponse{Code: http.StatusOK, JSON: &response}
|
||||
}),
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue