Move create room logic to roomserver

This commit is contained in:
Devon Hudson 2023-05-30 20:01:17 -06:00
parent b7856766e3
commit cf64e3e620
No known key found for this signature in database
GPG key ID: CD06B18E77F6A628
8 changed files with 605 additions and 485 deletions

View file

@ -22,17 +22,13 @@ import (
"strings"
"time"
"github.com/getsentry/sentry-go"
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/dendrite/roomserver/types"
roomserverVersion "github.com/matrix-org/dendrite/roomserver/version"
"github.com/matrix-org/dendrite/userapi/api"
"github.com/matrix-org/gomatrixserverlib/fclient"
"github.com/matrix-org/gomatrixserverlib/spec"
"github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/internal/eventutil"
"github.com/matrix-org/dendrite/setup/config"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
@ -41,32 +37,19 @@ import (
// https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
type createRoomRequest struct {
Invite []string `json:"invite"`
Name string `json:"name"`
Visibility string `json:"visibility"`
Topic string `json:"topic"`
Preset string `json:"preset"`
CreationContent json.RawMessage `json:"creation_content"`
InitialState []fledglingEvent `json:"initial_state"`
RoomAliasName string `json:"room_alias_name"`
RoomVersion gomatrixserverlib.RoomVersion `json:"room_version"`
PowerLevelContentOverride json.RawMessage `json:"power_level_content_override"`
IsDirect bool `json:"is_direct"`
Invite []string `json:"invite"`
Name string `json:"name"`
Visibility string `json:"visibility"`
Topic string `json:"topic"`
Preset string `json:"preset"`
CreationContent json.RawMessage `json:"creation_content"`
InitialState []roomserverAPI.FledglingEvent `json:"initial_state"`
RoomAliasName string `json:"room_alias_name"`
RoomVersion gomatrixserverlib.RoomVersion `json:"room_version"`
PowerLevelContentOverride json.RawMessage `json:"power_level_content_override"`
IsDirect bool `json:"is_direct"`
}
const (
presetPrivateChat = "private_chat"
presetTrustedPrivateChat = "trusted_private_chat"
presetPublicChat = "public_chat"
)
const (
historyVisibilityShared = "shared"
// TODO: These should be implemented once history visibility is implemented
// historyVisibilityWorldReadable = "world_readable"
// historyVisibilityInvited = "invited"
)
func (r createRoomRequest) Validate() *util.JSONResponse {
whitespace := "\t\n\x0b\x0c\r " // https://docs.python.org/2/library/string.html#string.whitespace
// https://github.com/matrix-org/synapse/blob/v0.19.2/synapse/handlers/room.py#L81
@ -86,7 +69,7 @@ func (r createRoomRequest) Validate() *util.JSONResponse {
}
}
switch r.Preset {
case presetPrivateChat, presetTrustedPrivateChat, presetPublicChat, "":
case roomserverAPI.PresetPrivateChat, roomserverAPI.PresetTrustedPrivateChat, roomserverAPI.PresetPublicChat, "":
default:
return &util.JSONResponse{
Code: http.StatusBadRequest,
@ -124,13 +107,6 @@ type createRoomResponse struct {
RoomAlias string `json:"room_alias,omitempty"` // in synapse not spec
}
// fledglingEvent is a helper representation of an event used when creating many events in succession.
type fledglingEvent struct {
Type string `json:"type"`
StateKey string `json:"state_key"`
Content interface{} `json:"content"`
}
// CreateRoom implements /createRoom
func CreateRoom(
req *http.Request, device *api.Device,
@ -184,8 +160,8 @@ func createRoom(
logger := util.GetLogger(ctx)
// TODO (#267): Check room ID doesn't clash with an existing one, and we
// probably shouldn't be using pseudo-random strings, maybe GUIDs?
// TODO: Check room ID doesn't clash with an existing one, and we
// probably shouldn't be using pseudo-random strings, maybe GUIDs?
roomID, err := spec.NewRoomID(fmt.Sprintf("!%s:%s", util.RandomString(16), userID.Domain()))
if err != nil {
util.GetLogger(ctx).WithError(err).Error("invalid roomID")
@ -231,430 +207,28 @@ func createRoom(
keyID := cfg.Matrix.KeyID
privateKey := cfg.Matrix.PrivateKey
// TODO: Move this whole function to roomserver to start
// HACK: asdklfjsdklfj
req := roomserverAPI.PerformCreateRoomRequest{
InvitedUsers: createRequest.Invite,
RoomName: createRequest.Name,
Visibility: createRequest.Visibility,
Topic: createRequest.Topic,
StatePreset: createRequest.Preset,
CreationContent: createRequest.CreationContent,
InitialState: createRequest.InitialState,
RoomAliasName: createRequest.RoomAliasName,
RoomVersion: roomVersion,
PowerLevelContentOverride: createRequest.PowerLevelContentOverride,
IsDirect: createRequest.IsDirect,
// TODO: visibility/presets/raw initial state
// TODO: Create room alias association
// Make sure this doesn't fall into an application service's namespace though!
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"),
}
}
UserDisplayName: userDisplayName,
UserAvatarURL: userAvatarURL,
KeyID: keyID,
PrivateKey: privateKey,
EventTime: evTime,
}
createContent["creator"] = userID.String()
createContent["room_version"] = 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.Preset {
case presetPrivateChat:
joinRuleContent.JoinRule = spec.Invite
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
guestsCanJoin = true
case presetTrustedPrivateChat:
joinRuleContent.JoinRule = spec.Invite
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
for _, invitee := range createRequest.Invite {
powerLevelContent.Users[invitee] = 100
}
guestsCanJoin = true
case presetPublicChat:
joinRuleContent.JoinRule = spec.Public
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
}
createEvent := fledglingEvent{
Type: spec.MRoomCreate,
Content: createContent,
}
powerLevelEvent := fledglingEvent{
Type: spec.MRoomPowerLevels,
Content: powerLevelContent,
}
joinRuleEvent := fledglingEvent{
Type: spec.MRoomJoinRules,
Content: joinRuleContent,
}
historyVisibilityEvent := fledglingEvent{
Type: spec.MRoomHistoryVisibility,
Content: historyVisibilityContent,
}
membershipEvent := fledglingEvent{
Type: spec.MRoomMember,
StateKey: userID.String(),
Content: gomatrixserverlib.MemberContent{
Membership: spec.Join,
DisplayName: userDisplayName,
AvatarURL: userAvatarURL,
},
}
var nameEvent *fledglingEvent
var topicEvent *fledglingEvent
var guestAccessEvent *fledglingEvent
var aliasEvent *fledglingEvent
if createRequest.Name != "" {
nameEvent = &fledglingEvent{
Type: spec.MRoomName,
Content: eventutil.NameContent{
Name: createRequest.Name,
},
}
}
if createRequest.Topic != "" {
topicEvent = &fledglingEvent{
Type: spec.MRoomTopic,
Content: eventutil.TopicContent{
Topic: createRequest.Topic,
},
}
}
if guestsCanJoin {
guestAccessEvent = &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 := roomserverAPI.GetRoomIDForAliasRequest{
Alias: roomAlias,
IncludeAppservices: false,
}
var aliasResp roomserverAPI.GetRoomIDForAliasResponse
err = 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 = &fledglingEvent{
Type: spec.MRoomCanonicalAlias,
Content: eventutil.CanonicalAlias{
Alias: roomAlias,
},
}
}
var initialStateEvents []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 := []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
verImpl, err := gomatrixserverlib.GetRoomVersion(roomVersion)
if err != nil {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.BadJSON("unknown room version"),
}
}
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(evTime, userID.Domain(), keyID, 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([]roomserverAPI.InputRoomEvent, 0, len(builtEvents))
for _, event := range builtEvents {
inputs = append(inputs, roomserverAPI.InputRoomEvent{
Kind: roomserverAPI.KindNew,
Event: event,
Origin: userID.Domain(),
SendAsServer: roomserverAPI.DoNotSendToOtherServers,
})
}
if err = roomserverAPI.SendInputRoomEvents(ctx, 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 := roomserverAPI.SetRoomAliasRequest{
Alias: roomAlias,
RoomID: roomID.String(),
UserID: userID.String(),
}
var aliasResp roomserverAPI.SetRoomAliasResponse
err = 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.Invite) > 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.Invite {
// Build the invite event.
inviteEvent, err = buildMembershipEventDirect(
ctx, invitee, "", userDisplayName, userAvatarURL, userID.String(), userID.Domain(), spec.Invite,
roomID.String(), createRequest.IsDirect, keyID, privateKey, evTime, rsAPI,
)
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 = rsAPI.PerformInvite(ctx, &roomserverAPI.PerformInviteRequest{
Event: event,
InviteRoomState: inviteStrippedState,
RoomVersion: event.Version(),
SendAsServer: string(userID.Domain()),
})
switch e := err.(type) {
case roomserverAPI.ErrInvalidID:
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.Unknown(e.Error()),
}
case roomserverAPI.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 = rsAPI.PerformPublish(ctx, &roomserverAPI.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{},
}
}
roomAlias, createRes := rsAPI.PerformCreateRoom(ctx, *userID, *roomID, &req)
if createRes != nil {
return *createRes
}
response := createRoomResponse{

View file

@ -14,6 +14,7 @@ import (
"github.com/matrix-org/dendrite/appservice"
"github.com/matrix-org/dendrite/roomserver"
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/dendrite/test"
"github.com/matrix-org/dendrite/test/testrig"
"github.com/matrix-org/dendrite/userapi"
@ -63,7 +64,7 @@ func TestJoinRoomByIDOrAlias(t *testing.T) {
IsDirect: true,
Topic: "testing",
Visibility: "public",
Preset: presetPublicChat,
Preset: roomserverAPI.PresetPublicChat,
RoomAliasName: "alias",
Invite: []string{bob.ID},
}, aliceDev, &cfg.ClientAPI, userAPI, rsAPI, asAPI, time.Now())
@ -78,7 +79,7 @@ func TestJoinRoomByIDOrAlias(t *testing.T) {
IsDirect: true,
Topic: "testing",
Visibility: "public",
Preset: presetPublicChat,
Preset: roomserverAPI.PresetPublicChat,
Invite: []string{charlie.ID},
}, aliceDev, &cfg.ClientAPI, userAPI, rsAPI, asAPI, time.Now())
crRespWithGuestAccess, ok := resp.JSON.(createRoomResponse)

View file

@ -155,7 +155,7 @@ func SendServerNotice(
Invite: []string{r.UserID},
Name: cfgNotices.RoomName,
Visibility: "private",
Preset: presetPrivateChat,
Preset: api.PresetPrivateChat,
CreationContent: cc,
RoomVersion: roomVersion,
PowerLevelContentOverride: pl,

View file

@ -5,6 +5,7 @@ import (
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/gomatrixserverlib/spec"
"github.com/matrix-org/util"
asAPI "github.com/matrix-org/dendrite/appservice/api"
fsAPI "github.com/matrix-org/dendrite/federationapi/api"
@ -12,6 +13,13 @@ import (
userapi "github.com/matrix-org/dendrite/userapi/api"
)
// TODO: Move to GMSL?
const (
PresetPrivateChat = "private_chat"
PresetTrustedPrivateChat = "trusted_private_chat"
PresetPublicChat = "public_chat"
)
// ErrInvalidID is an error returned if the userID is invalid
type ErrInvalidID struct {
Err error
@ -169,6 +177,7 @@ type ClientRoomserverAPI interface {
GetRoomIDForAlias(ctx context.Context, req *GetRoomIDForAliasRequest, res *GetRoomIDForAliasResponse) error
GetAliasesForRoomID(ctx context.Context, req *GetAliasesForRoomIDRequest, res *GetAliasesForRoomIDResponse) error
PerformCreateRoom(ctx context.Context, userID spec.UserID, roomID spec.RoomID, createRequest *PerformCreateRoomRequest) (string, *util.JSONResponse)
// PerformRoomUpgrade upgrades a room to a newer version
PerformRoomUpgrade(ctx context.Context, roomID, userID string, roomVersion gomatrixserverlib.RoomVersion) (newRoomID string, err error)
PerformAdminEvacuateRoom(ctx context.Context, roomID string) (affected []string, err error)

View file

@ -1,6 +1,10 @@
package api
import (
"crypto/ed25519"
"encoding/json"
"time"
"github.com/matrix-org/dendrite/roomserver/types"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/gomatrixserverlib/fclient"
@ -8,6 +12,34 @@ import (
"github.com/matrix-org/util"
)
// TODO: Move to GMSL
// FledglingEvent is a helper representation of an event used when creating many events in succession.
type FledglingEvent struct {
Type string `json:"type"`
StateKey string `json:"state_key"`
Content interface{} `json:"content"`
}
type PerformCreateRoomRequest struct {
InvitedUsers []string
RoomName string
Visibility string
Topic string
StatePreset string
CreationContent json.RawMessage
InitialState []FledglingEvent
RoomAliasName string
RoomVersion gomatrixserverlib.RoomVersion
PowerLevelContentOverride json.RawMessage
IsDirect bool
UserDisplayName string
UserAvatarURL string
KeyID gomatrixserverlib.KeyID
PrivateKey ed25519.PrivateKey
EventTime time.Time
}
type PerformJoinRequest struct {
RoomIDOrAlias string `json:"room_id_or_alias"`
UserID string `json:"user_id"`

View file

@ -6,6 +6,7 @@ import (
"github.com/getsentry/sentry-go"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/gomatrixserverlib/spec"
"github.com/matrix-org/util"
"github.com/nats-io/nats.go"
"github.com/sirupsen/logrus"
@ -40,6 +41,7 @@ type RoomserverInternalAPI struct {
*perform.Forgetter
*perform.Upgrader
*perform.Admin
*perform.Creator
ProcessContext *process.ProcessContext
DB storage.Database
Cfg *config.Dendrite
@ -191,6 +193,11 @@ func (r *RoomserverInternalAPI) SetFederationAPI(fsAPI fsAPI.RoomserverFederatio
Queryer: r.Queryer,
Leaver: r.Leaver,
}
r.Creator = &perform.Creator{
DB: r.DB,
Cfg: &r.Cfg.RoomServer,
RSAPI: r,
}
if err := r.Inputer.Start(); err != nil {
logrus.WithError(err).Panic("failed to start roomserver input API")
@ -206,6 +213,12 @@ func (r *RoomserverInternalAPI) SetAppserviceAPI(asAPI asAPI.AppServiceInternalA
r.asAPI = asAPI
}
func (r *RoomserverInternalAPI) PerformCreateRoom(
ctx context.Context, userID spec.UserID, roomID spec.RoomID, createRequest *api.PerformCreateRoomRequest,
) (string, *util.JSONResponse) {
return r.Creator.PerformCreateRoom(ctx, userID, roomID, createRequest)
}
func (r *RoomserverInternalAPI) PerformInvite(
ctx context.Context,
req *api.PerformInviteRequest,

View 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"
// TODO: These should be implemented once history visibility is implemented
// historyVisibilityWorldReadable = "world_readable"
// historyVisibilityInvited = "invited"
)
type Creator struct {
DB storage.Database
Cfg *config.RoomServer
RSAPI api.RoomserverInternalAPI
}
func (c *Creator) PerformCreateRoom(ctx context.Context, userID spec.UserID, roomID spec.RoomID, createRequest *api.PerformCreateRoomRequest) (string, *util.JSONResponse) {
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 api.PresetPrivateChat:
joinRuleContent.JoinRule = spec.Invite
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
guestsCanJoin = true
case api.PresetTrustedPrivateChat:
joinRuleContent.JoinRule = spec.Invite
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
for _, invitee := range createRequest.InvitedUsers {
powerLevelContent.Users[invitee] = 100
}
guestsCanJoin = true
case api.PresetPublicChat:
joinRuleContent.JoinRule = spec.Public
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
}
createEvent := api.FledglingEvent{
Type: spec.MRoomCreate,
Content: createContent,
}
powerLevelEvent := api.FledglingEvent{
Type: spec.MRoomPowerLevels,
Content: powerLevelContent,
}
joinRuleEvent := api.FledglingEvent{
Type: spec.MRoomJoinRules,
Content: joinRuleContent,
}
historyVisibilityEvent := api.FledglingEvent{
Type: spec.MRoomHistoryVisibility,
Content: historyVisibilityContent,
}
membershipEvent := api.FledglingEvent{
Type: spec.MRoomMember,
StateKey: userID.String(),
Content: gomatrixserverlib.MemberContent{
Membership: spec.Join,
DisplayName: createRequest.UserDisplayName,
AvatarURL: createRequest.UserAvatarURL,
},
}
var nameEvent *api.FledglingEvent
var topicEvent *api.FledglingEvent
var guestAccessEvent *api.FledglingEvent
var aliasEvent *api.FledglingEvent
if createRequest.RoomName != "" {
nameEvent = &api.FledglingEvent{
Type: spec.MRoomName,
Content: eventutil.NameContent{
Name: createRequest.RoomName,
},
}
}
if createRequest.Topic != "" {
topicEvent = &api.FledglingEvent{
Type: spec.MRoomTopic,
Content: eventutil.TopicContent{
Topic: createRequest.Topic,
},
}
}
if guestsCanJoin {
guestAccessEvent = &api.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 = &api.FledglingEvent{
Type: spec.MRoomCanonicalAlias,
Content: eventutil.CanonicalAlias{
Alias: roomAlias,
},
}
}
var initialStateEvents []api.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 := []api.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
verImpl, err := gomatrixserverlib.GetRoomVersion(createRequest.RoomVersion)
if err != nil {
return "", &util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.BadJSON("unknown room version"),
}
}
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
}

View file

@ -35,13 +35,6 @@ type Upgrader struct {
URSAPI api.RoomserverInternalAPI
}
// fledglingEvent is a helper representation of an event used when creating many events in succession.
type fledglingEvent struct {
Type string `json:"type"`
StateKey string `json:"state_key"`
Content interface{} `json:"content"`
}
// PerformRoomUpgrade upgrades a room from one version to another
func (r *Upgrader) PerformRoomUpgrade(
ctx context.Context,
@ -154,7 +147,7 @@ func (r *Upgrader) restrictOldRoomPowerLevels(ctx context.Context, evTime time.T
restrictedPowerLevelContent.EventsDefault = restrictedDefaultPowerLevel
restrictedPowerLevelContent.Invite = restrictedDefaultPowerLevel
restrictedPowerLevelsHeadered, resErr := r.makeHeaderedEvent(ctx, evTime, userID, roomID, fledglingEvent{
restrictedPowerLevelsHeadered, resErr := r.makeHeaderedEvent(ctx, evTime, userID, roomID, api.FledglingEvent{
Type: spec.MRoomPowerLevels,
StateKey: "",
Content: restrictedPowerLevelContent,
@ -216,7 +209,7 @@ func (r *Upgrader) clearOldCanonicalAliasEvent(ctx context.Context, oldRoom *api
}
}
emptyCanonicalAliasEvent, resErr := r.makeHeaderedEvent(ctx, evTime, userID, roomID, fledglingEvent{
emptyCanonicalAliasEvent, resErr := r.makeHeaderedEvent(ctx, evTime, userID, roomID, api.FledglingEvent{
Type: spec.MRoomCanonicalAlias,
Content: map[string]interface{}{},
})
@ -298,7 +291,7 @@ func (r *Upgrader) userIsAuthorized(ctx context.Context, userID, roomID string,
}
// nolint:gocyclo
func (r *Upgrader) generateInitialEvents(ctx context.Context, oldRoom *api.QueryLatestEventsAndStateResponse, userID, roomID string, newVersion gomatrixserverlib.RoomVersion, tombstoneEvent *types.HeaderedEvent) ([]fledglingEvent, error) {
func (r *Upgrader) generateInitialEvents(ctx context.Context, oldRoom *api.QueryLatestEventsAndStateResponse, userID, roomID string, newVersion gomatrixserverlib.RoomVersion, tombstoneEvent *types.HeaderedEvent) ([]api.FledglingEvent, error) {
state := make(map[gomatrixserverlib.StateKeyTuple]*types.HeaderedEvent, len(oldRoom.StateEvents))
for _, event := range oldRoom.StateEvents {
if event.StateKey() == nil {
@ -361,7 +354,7 @@ func (r *Upgrader) generateInitialEvents(ctx context.Context, oldRoom *api.Query
EventID: tombstoneEvent.EventID(),
RoomID: roomID,
}
newCreateEvent := fledglingEvent{
newCreateEvent := api.FledglingEvent{
Type: spec.MRoomCreate,
StateKey: "",
Content: newCreateContent,
@ -374,7 +367,7 @@ func (r *Upgrader) generateInitialEvents(ctx context.Context, oldRoom *api.Query
newMembershipContent := map[string]interface{}{}
_ = json.Unmarshal(oldMembershipEvent.Content(), &newMembershipContent)
newMembershipContent["membership"] = spec.Join
newMembershipEvent := fledglingEvent{
newMembershipEvent := api.FledglingEvent{
Type: spec.MRoomMember,
StateKey: userID,
Content: newMembershipContent,
@ -400,13 +393,13 @@ func (r *Upgrader) generateInitialEvents(ctx context.Context, oldRoom *api.Query
"join_rule": spec.Invite, // sane default
}
_ = json.Unmarshal(oldJoinRulesEvent.Content(), &newJoinRulesContent)
newJoinRulesEvent := fledglingEvent{
newJoinRulesEvent := api.FledglingEvent{
Type: spec.MRoomJoinRules,
StateKey: "",
Content: newJoinRulesContent,
}
eventsToMake := make([]fledglingEvent, 0, len(state))
eventsToMake := make([]api.FledglingEvent, 0, len(state))
eventsToMake = append(
eventsToMake, newCreateEvent, newMembershipEvent,
tempPowerLevelsEvent, newJoinRulesEvent,
@ -415,7 +408,7 @@ func (r *Upgrader) generateInitialEvents(ctx context.Context, oldRoom *api.Query
// For some reason Sytest expects there to be a guest access event.
// Create one if it doesn't exist.
if _, ok := state[gomatrixserverlib.StateKeyTuple{EventType: spec.MRoomGuestAccess, StateKey: ""}]; !ok {
eventsToMake = append(eventsToMake, fledglingEvent{
eventsToMake = append(eventsToMake, api.FledglingEvent{
Type: spec.MRoomGuestAccess,
Content: map[string]string{
"guest_access": "forbidden",
@ -430,7 +423,7 @@ func (r *Upgrader) generateInitialEvents(ctx context.Context, oldRoom *api.Query
// are already in `eventsToMake`.
continue
}
newEvent := fledglingEvent{
newEvent := api.FledglingEvent{
Type: tuple.EventType,
StateKey: tuple.StateKey,
}
@ -444,7 +437,7 @@ func (r *Upgrader) generateInitialEvents(ctx context.Context, oldRoom *api.Query
// If we sent a temporary power level event into the room before,
// override that now by restoring the original power levels.
if powerLevelsOverridden {
eventsToMake = append(eventsToMake, fledglingEvent{
eventsToMake = append(eventsToMake, api.FledglingEvent{
Type: spec.MRoomPowerLevels,
Content: powerLevelContent,
})
@ -452,7 +445,7 @@ func (r *Upgrader) generateInitialEvents(ctx context.Context, oldRoom *api.Query
return eventsToMake, nil
}
func (r *Upgrader) sendInitialEvents(ctx context.Context, evTime time.Time, userID string, userDomain spec.ServerName, newRoomID string, newVersion gomatrixserverlib.RoomVersion, eventsToMake []fledglingEvent) error {
func (r *Upgrader) sendInitialEvents(ctx context.Context, evTime time.Time, userID string, userDomain spec.ServerName, newRoomID string, newVersion gomatrixserverlib.RoomVersion, eventsToMake []api.FledglingEvent) error {
var err error
var builtEvents []*types.HeaderedEvent
authEvents := gomatrixserverlib.NewAuthEvents(nil)
@ -527,14 +520,14 @@ func (r *Upgrader) makeTombstoneEvent(
"body": "This room has been replaced",
"replacement_room": newRoomID,
}
event := fledglingEvent{
event := api.FledglingEvent{
Type: "m.room.tombstone",
Content: content,
}
return r.makeHeaderedEvent(ctx, evTime, userID, roomID, event)
}
func (r *Upgrader) makeHeaderedEvent(ctx context.Context, evTime time.Time, userID, roomID string, event fledglingEvent) (*types.HeaderedEvent, error) {
func (r *Upgrader) makeHeaderedEvent(ctx context.Context, evTime time.Time, userID, roomID string, event api.FledglingEvent) (*types.HeaderedEvent, error) {
proto := gomatrixserverlib.ProtoEvent{
Sender: userID,
RoomID: roomID,
@ -581,7 +574,7 @@ func (r *Upgrader) makeHeaderedEvent(ctx context.Context, evTime time.Time, user
return headeredEvent, nil
}
func createTemporaryPowerLevels(powerLevelContent *gomatrixserverlib.PowerLevelContent, userID string) (fledglingEvent, bool) {
func createTemporaryPowerLevels(powerLevelContent *gomatrixserverlib.PowerLevelContent, userID string) (api.FledglingEvent, bool) {
// Work out what power level we need in order to be able to send events
// of all types into the room.
neededPowerLevel := powerLevelContent.StateDefault
@ -612,7 +605,7 @@ func createTemporaryPowerLevels(powerLevelContent *gomatrixserverlib.PowerLevelC
}
// Then return the temporary power levels event.
return fledglingEvent{
return api.FledglingEvent{
Type: spec.MRoomPowerLevels,
Content: tempPowerLevelContent,
}, powerLevelsOverridden