mirror of
https://github.com/element-hq/dendrite.git
synced 2025-09-13 12:52:24 +03:00
Move CreateRoom logic to Roomserver (#3093)
Move create room logic over to roomserver.
This commit is contained in:
parent
61341aca50
commit
cbdc601f1b
24 changed files with 684 additions and 550 deletions
498
roomserver/internal/perform/perform_create_room.go
Normal file
498
roomserver/internal/perform/perform_create_room.go
Normal file
|
@ -0,0 +1,498 @@
|
|||
// Copyright 2023 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 perform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/matrix-org/dendrite/internal/eventutil"
|
||||
"github.com/matrix-org/dendrite/roomserver/api"
|
||||
"github.com/matrix-org/dendrite/roomserver/storage"
|
||||
"github.com/matrix-org/dendrite/roomserver/types"
|
||||
"github.com/matrix-org/dendrite/setup/config"
|
||||
"github.com/matrix-org/gomatrixserverlib"
|
||||
"github.com/matrix-org/gomatrixserverlib/fclient"
|
||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||
"github.com/matrix-org/util"
|
||||
)
|
||||
|
||||
const (
|
||||
historyVisibilityShared = "shared"
|
||||
)
|
||||
|
||||
type Creator struct {
|
||||
DB storage.Database
|
||||
Cfg *config.RoomServer
|
||||
RSAPI api.RoomserverInternalAPI
|
||||
}
|
||||
|
||||
// PerformCreateRoom handles all the steps necessary to create a new room.
|
||||
// nolint: gocyclo
|
||||
func (c *Creator) PerformCreateRoom(ctx context.Context, userID spec.UserID, roomID spec.RoomID, createRequest *api.PerformCreateRoomRequest) (string, *util.JSONResponse) {
|
||||
verImpl, err := gomatrixserverlib.GetRoomVersion(createRequest.RoomVersion)
|
||||
if err != nil {
|
||||
return "", &util.JSONResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
JSON: spec.BadJSON("unknown room version"),
|
||||
}
|
||||
}
|
||||
|
||||
createContent := map[string]interface{}{}
|
||||
if len(createRequest.CreationContent) > 0 {
|
||||
if err = json.Unmarshal(createRequest.CreationContent, &createContent); err != nil {
|
||||
util.GetLogger(ctx).WithError(err).Error("json.Unmarshal for creation_content failed")
|
||||
return "", &util.JSONResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
JSON: spec.BadJSON("invalid create content"),
|
||||
}
|
||||
}
|
||||
}
|
||||
createContent["creator"] = userID.String()
|
||||
createContent["room_version"] = createRequest.RoomVersion
|
||||
powerLevelContent := eventutil.InitialPowerLevelsContent(userID.String())
|
||||
joinRuleContent := gomatrixserverlib.JoinRuleContent{
|
||||
JoinRule: spec.Invite,
|
||||
}
|
||||
historyVisibilityContent := gomatrixserverlib.HistoryVisibilityContent{
|
||||
HistoryVisibility: historyVisibilityShared,
|
||||
}
|
||||
|
||||
if createRequest.PowerLevelContentOverride != nil {
|
||||
// Merge powerLevelContentOverride fields by unmarshalling it atop the defaults
|
||||
err = json.Unmarshal(createRequest.PowerLevelContentOverride, &powerLevelContent)
|
||||
if err != nil {
|
||||
util.GetLogger(ctx).WithError(err).Error("json.Unmarshal for power_level_content_override failed")
|
||||
return "", &util.JSONResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
JSON: spec.BadJSON("malformed power_level_content_override"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var guestsCanJoin bool
|
||||
switch createRequest.StatePreset {
|
||||
case spec.PresetPrivateChat:
|
||||
joinRuleContent.JoinRule = spec.Invite
|
||||
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
|
||||
guestsCanJoin = true
|
||||
case spec.PresetTrustedPrivateChat:
|
||||
joinRuleContent.JoinRule = spec.Invite
|
||||
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
|
||||
for _, invitee := range createRequest.InvitedUsers {
|
||||
powerLevelContent.Users[invitee] = 100
|
||||
}
|
||||
guestsCanJoin = true
|
||||
case spec.PresetPublicChat:
|
||||
joinRuleContent.JoinRule = spec.Public
|
||||
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
|
||||
}
|
||||
|
||||
createEvent := gomatrixserverlib.FledglingEvent{
|
||||
Type: spec.MRoomCreate,
|
||||
Content: createContent,
|
||||
}
|
||||
powerLevelEvent := gomatrixserverlib.FledglingEvent{
|
||||
Type: spec.MRoomPowerLevels,
|
||||
Content: powerLevelContent,
|
||||
}
|
||||
joinRuleEvent := gomatrixserverlib.FledglingEvent{
|
||||
Type: spec.MRoomJoinRules,
|
||||
Content: joinRuleContent,
|
||||
}
|
||||
historyVisibilityEvent := gomatrixserverlib.FledglingEvent{
|
||||
Type: spec.MRoomHistoryVisibility,
|
||||
Content: historyVisibilityContent,
|
||||
}
|
||||
membershipEvent := gomatrixserverlib.FledglingEvent{
|
||||
Type: spec.MRoomMember,
|
||||
StateKey: userID.String(),
|
||||
Content: gomatrixserverlib.MemberContent{
|
||||
Membership: spec.Join,
|
||||
DisplayName: createRequest.UserDisplayName,
|
||||
AvatarURL: createRequest.UserAvatarURL,
|
||||
},
|
||||
}
|
||||
|
||||
var nameEvent *gomatrixserverlib.FledglingEvent
|
||||
var topicEvent *gomatrixserverlib.FledglingEvent
|
||||
var guestAccessEvent *gomatrixserverlib.FledglingEvent
|
||||
var aliasEvent *gomatrixserverlib.FledglingEvent
|
||||
|
||||
if createRequest.RoomName != "" {
|
||||
nameEvent = &gomatrixserverlib.FledglingEvent{
|
||||
Type: spec.MRoomName,
|
||||
Content: eventutil.NameContent{
|
||||
Name: createRequest.RoomName,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if createRequest.Topic != "" {
|
||||
topicEvent = &gomatrixserverlib.FledglingEvent{
|
||||
Type: spec.MRoomTopic,
|
||||
Content: eventutil.TopicContent{
|
||||
Topic: createRequest.Topic,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if guestsCanJoin {
|
||||
guestAccessEvent = &gomatrixserverlib.FledglingEvent{
|
||||
Type: spec.MRoomGuestAccess,
|
||||
Content: eventutil.GuestAccessContent{
|
||||
GuestAccess: "can_join",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var roomAlias string
|
||||
if createRequest.RoomAliasName != "" {
|
||||
roomAlias = fmt.Sprintf("#%s:%s", createRequest.RoomAliasName, userID.Domain())
|
||||
// check it's free
|
||||
// TODO: This races but is better than nothing
|
||||
hasAliasReq := api.GetRoomIDForAliasRequest{
|
||||
Alias: roomAlias,
|
||||
IncludeAppservices: false,
|
||||
}
|
||||
|
||||
var aliasResp api.GetRoomIDForAliasResponse
|
||||
err = c.RSAPI.GetRoomIDForAlias(ctx, &hasAliasReq, &aliasResp)
|
||||
if err != nil {
|
||||
util.GetLogger(ctx).WithError(err).Error("aliasAPI.GetRoomIDForAlias failed")
|
||||
return "", &util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.InternalServerError{},
|
||||
}
|
||||
}
|
||||
if aliasResp.RoomID != "" {
|
||||
return "", &util.JSONResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
JSON: spec.RoomInUse("Room ID already exists."),
|
||||
}
|
||||
}
|
||||
|
||||
aliasEvent = &gomatrixserverlib.FledglingEvent{
|
||||
Type: spec.MRoomCanonicalAlias,
|
||||
Content: eventutil.CanonicalAlias{
|
||||
Alias: roomAlias,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var initialStateEvents []gomatrixserverlib.FledglingEvent
|
||||
for i := range createRequest.InitialState {
|
||||
if createRequest.InitialState[i].StateKey != "" {
|
||||
initialStateEvents = append(initialStateEvents, createRequest.InitialState[i])
|
||||
continue
|
||||
}
|
||||
|
||||
switch createRequest.InitialState[i].Type {
|
||||
case spec.MRoomCreate:
|
||||
continue
|
||||
|
||||
case spec.MRoomPowerLevels:
|
||||
powerLevelEvent = createRequest.InitialState[i]
|
||||
|
||||
case spec.MRoomJoinRules:
|
||||
joinRuleEvent = createRequest.InitialState[i]
|
||||
|
||||
case spec.MRoomHistoryVisibility:
|
||||
historyVisibilityEvent = createRequest.InitialState[i]
|
||||
|
||||
case spec.MRoomGuestAccess:
|
||||
guestAccessEvent = &createRequest.InitialState[i]
|
||||
|
||||
case spec.MRoomName:
|
||||
nameEvent = &createRequest.InitialState[i]
|
||||
|
||||
case spec.MRoomTopic:
|
||||
topicEvent = &createRequest.InitialState[i]
|
||||
|
||||
default:
|
||||
initialStateEvents = append(initialStateEvents, createRequest.InitialState[i])
|
||||
}
|
||||
}
|
||||
|
||||
// send events into the room in order of:
|
||||
// 1- m.room.create
|
||||
// 2- room creator join member
|
||||
// 3- m.room.power_levels
|
||||
// 4- m.room.join_rules
|
||||
// 5- m.room.history_visibility
|
||||
// 6- m.room.canonical_alias (opt)
|
||||
// 7- m.room.guest_access (opt)
|
||||
// 8- other initial state items
|
||||
// 9- m.room.name (opt)
|
||||
// 10- m.room.topic (opt)
|
||||
// 11- invite events (opt) - with is_direct flag if applicable TODO
|
||||
// 12- 3pid invite events (opt) TODO
|
||||
// This differs from Synapse slightly. Synapse would vary the ordering of 3-7
|
||||
// depending on if those events were in "initial_state" or not. This made it
|
||||
// harder to reason about, hence sticking to a strict static ordering.
|
||||
// TODO: Synapse has txn/token ID on each event. Do we need to do this here?
|
||||
eventsToMake := []gomatrixserverlib.FledglingEvent{
|
||||
createEvent, membershipEvent, powerLevelEvent, joinRuleEvent, historyVisibilityEvent,
|
||||
}
|
||||
if guestAccessEvent != nil {
|
||||
eventsToMake = append(eventsToMake, *guestAccessEvent)
|
||||
}
|
||||
eventsToMake = append(eventsToMake, initialStateEvents...)
|
||||
if nameEvent != nil {
|
||||
eventsToMake = append(eventsToMake, *nameEvent)
|
||||
}
|
||||
if topicEvent != nil {
|
||||
eventsToMake = append(eventsToMake, *topicEvent)
|
||||
}
|
||||
if aliasEvent != nil {
|
||||
// TODO: bit of a chicken and egg problem here as the alias doesn't exist and cannot until we have made the room.
|
||||
// This means we might fail creating the alias but say the canonical alias is something that doesn't exist.
|
||||
eventsToMake = append(eventsToMake, *aliasEvent)
|
||||
}
|
||||
|
||||
// TODO: invite events
|
||||
// TODO: 3pid invite events
|
||||
|
||||
var builtEvents []*types.HeaderedEvent
|
||||
authEvents := gomatrixserverlib.NewAuthEvents(nil)
|
||||
for i, e := range eventsToMake {
|
||||
depth := i + 1 // depth starts at 1
|
||||
|
||||
builder := verImpl.NewEventBuilderFromProtoEvent(&gomatrixserverlib.ProtoEvent{
|
||||
Sender: userID.String(),
|
||||
RoomID: roomID.String(),
|
||||
Type: e.Type,
|
||||
StateKey: &e.StateKey,
|
||||
Depth: int64(depth),
|
||||
})
|
||||
err = builder.SetContent(e.Content)
|
||||
if err != nil {
|
||||
util.GetLogger(ctx).WithError(err).Error("builder.SetContent failed")
|
||||
return "", &util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.InternalServerError{},
|
||||
}
|
||||
}
|
||||
if i > 0 {
|
||||
builder.PrevEvents = []string{builtEvents[i-1].EventID()}
|
||||
}
|
||||
var ev gomatrixserverlib.PDU
|
||||
if err = builder.AddAuthEvents(&authEvents); err != nil {
|
||||
util.GetLogger(ctx).WithError(err).Error("AddAuthEvents failed")
|
||||
return "", &util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.InternalServerError{},
|
||||
}
|
||||
}
|
||||
ev, err = builder.Build(createRequest.EventTime, userID.Domain(), createRequest.KeyID, createRequest.PrivateKey)
|
||||
if err != nil {
|
||||
util.GetLogger(ctx).WithError(err).Error("buildEvent failed")
|
||||
return "", &util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.InternalServerError{},
|
||||
}
|
||||
}
|
||||
|
||||
if err = gomatrixserverlib.Allowed(ev, &authEvents); err != nil {
|
||||
util.GetLogger(ctx).WithError(err).Error("gomatrixserverlib.Allowed failed")
|
||||
return "", &util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.InternalServerError{},
|
||||
}
|
||||
}
|
||||
|
||||
// Add the event to the list of auth events
|
||||
builtEvents = append(builtEvents, &types.HeaderedEvent{PDU: ev})
|
||||
err = authEvents.AddEvent(ev)
|
||||
if err != nil {
|
||||
util.GetLogger(ctx).WithError(err).Error("authEvents.AddEvent failed")
|
||||
return "", &util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.InternalServerError{},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inputs := make([]api.InputRoomEvent, 0, len(builtEvents))
|
||||
for _, event := range builtEvents {
|
||||
inputs = append(inputs, api.InputRoomEvent{
|
||||
Kind: api.KindNew,
|
||||
Event: event,
|
||||
Origin: userID.Domain(),
|
||||
SendAsServer: api.DoNotSendToOtherServers,
|
||||
})
|
||||
}
|
||||
if err = api.SendInputRoomEvents(ctx, c.RSAPI, userID.Domain(), inputs, false); err != nil {
|
||||
util.GetLogger(ctx).WithError(err).Error("roomserverAPI.SendInputRoomEvents failed")
|
||||
return "", &util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.InternalServerError{},
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(#269): Reserve room alias while we create the room. This stops us
|
||||
// from creating the room but still failing due to the alias having already
|
||||
// been taken.
|
||||
if roomAlias != "" {
|
||||
aliasReq := api.SetRoomAliasRequest{
|
||||
Alias: roomAlias,
|
||||
RoomID: roomID.String(),
|
||||
UserID: userID.String(),
|
||||
}
|
||||
|
||||
var aliasResp api.SetRoomAliasResponse
|
||||
err = c.RSAPI.SetRoomAlias(ctx, &aliasReq, &aliasResp)
|
||||
if err != nil {
|
||||
util.GetLogger(ctx).WithError(err).Error("aliasAPI.SetRoomAlias failed")
|
||||
return "", &util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.InternalServerError{},
|
||||
}
|
||||
}
|
||||
|
||||
if aliasResp.AliasExists {
|
||||
return "", &util.JSONResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
JSON: spec.RoomInUse("Room alias already exists."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a direct message then we should invite the participants.
|
||||
if len(createRequest.InvitedUsers) > 0 {
|
||||
// Build some stripped state for the invite.
|
||||
var globalStrippedState []fclient.InviteV2StrippedState
|
||||
for _, event := range builtEvents {
|
||||
// Chosen events from the spec:
|
||||
// https://spec.matrix.org/v1.3/client-server-api/#stripped-state
|
||||
switch event.Type() {
|
||||
case spec.MRoomCreate:
|
||||
fallthrough
|
||||
case spec.MRoomName:
|
||||
fallthrough
|
||||
case spec.MRoomAvatar:
|
||||
fallthrough
|
||||
case spec.MRoomTopic:
|
||||
fallthrough
|
||||
case spec.MRoomCanonicalAlias:
|
||||
fallthrough
|
||||
case spec.MRoomEncryption:
|
||||
fallthrough
|
||||
case spec.MRoomMember:
|
||||
fallthrough
|
||||
case spec.MRoomJoinRules:
|
||||
ev := event.PDU
|
||||
globalStrippedState = append(
|
||||
globalStrippedState,
|
||||
fclient.NewInviteV2StrippedState(ev),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Process the invites.
|
||||
var inviteEvent *types.HeaderedEvent
|
||||
for _, invitee := range createRequest.InvitedUsers {
|
||||
proto := gomatrixserverlib.ProtoEvent{
|
||||
Sender: userID.String(),
|
||||
RoomID: roomID.String(),
|
||||
Type: "m.room.member",
|
||||
StateKey: &invitee,
|
||||
}
|
||||
|
||||
content := gomatrixserverlib.MemberContent{
|
||||
Membership: spec.Invite,
|
||||
DisplayName: createRequest.UserDisplayName,
|
||||
AvatarURL: createRequest.UserAvatarURL,
|
||||
Reason: "",
|
||||
IsDirect: createRequest.IsDirect,
|
||||
}
|
||||
|
||||
if err = proto.SetContent(content); err != nil {
|
||||
return "", &util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.InternalServerError{},
|
||||
}
|
||||
}
|
||||
|
||||
// Build the invite event.
|
||||
identity := &fclient.SigningIdentity{
|
||||
ServerName: userID.Domain(),
|
||||
KeyID: createRequest.KeyID,
|
||||
PrivateKey: createRequest.PrivateKey,
|
||||
}
|
||||
inviteEvent, err = eventutil.QueryAndBuildEvent(ctx, &proto, identity, createRequest.EventTime, c.RSAPI, nil)
|
||||
|
||||
if err != nil {
|
||||
util.GetLogger(ctx).WithError(err).Error("buildMembershipEvent failed")
|
||||
continue
|
||||
}
|
||||
inviteStrippedState := append(
|
||||
globalStrippedState,
|
||||
fclient.NewInviteV2StrippedState(inviteEvent.PDU),
|
||||
)
|
||||
// Send the invite event to the roomserver.
|
||||
event := inviteEvent
|
||||
err = c.RSAPI.PerformInvite(ctx, &api.PerformInviteRequest{
|
||||
Event: event,
|
||||
InviteRoomState: inviteStrippedState,
|
||||
RoomVersion: event.Version(),
|
||||
SendAsServer: string(userID.Domain()),
|
||||
})
|
||||
switch e := err.(type) {
|
||||
case api.ErrInvalidID:
|
||||
return "", &util.JSONResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
JSON: spec.Unknown(e.Error()),
|
||||
}
|
||||
case api.ErrNotAllowed:
|
||||
return "", &util.JSONResponse{
|
||||
Code: http.StatusForbidden,
|
||||
JSON: spec.Forbidden(e.Error()),
|
||||
}
|
||||
case nil:
|
||||
default:
|
||||
util.GetLogger(ctx).WithError(err).Error("PerformInvite failed")
|
||||
sentry.CaptureException(err)
|
||||
return "", &util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.InternalServerError{},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if createRequest.Visibility == spec.Public {
|
||||
// expose this room in the published room list
|
||||
if err = c.RSAPI.PerformPublish(ctx, &api.PerformPublishRequest{
|
||||
RoomID: roomID.String(),
|
||||
Visibility: spec.Public,
|
||||
}); err != nil {
|
||||
util.GetLogger(ctx).WithError(err).Error("failed to publish room")
|
||||
return "", &util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.InternalServerError{},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: visibility/presets/raw initial state
|
||||
// TODO: Create room alias association
|
||||
// Make sure this doesn't fall into an application service's namespace though!
|
||||
|
||||
return roomAlias, nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue