diff options
author | S7evinK <2353100+S7evinK@users.noreply.github.com> | 2022-02-18 16:05:03 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-02-18 16:05:03 +0100 |
commit | 002429c9e24cc746e0929b41eccbe429f89a6e1f (patch) | |
tree | 217eaea280343f46c61868a694d75fccd8fe66af /clientapi | |
parent | dbded875257703eb63c8eb8af8d47d74c811642f (diff) |
Implement server notices (#2180)
* Add server_notices config
* Disallow rejecting "server notice" invites
* Update config
* Slightly refactor sendEvent and CreateRoom so it can be reused
* Implement unspecced server notices
* Validate the request
* Set the user api when starting
* Rename function/variables
* Update comments
* Update config
* Set the avatar on account creation
* Update test
* Only create the account when starting
Only add routes if sever notices are enabled
* Use reserver username
Check that we actually got roomData
* Add check for admin account
Enable server notices for CI
Return same values as Synapse
* Add custom error for rejecting server notice invite
* Move building an invite to it's own function, for reusability
* Don't create new rooms, use the existing one (follow Synapse behavior)
Co-authored-by: kegsay <kegan@matrix.org>
Diffstat (limited to 'clientapi')
-rw-r--r-- | clientapi/jsonerror/jsonerror.go | 9 | ||||
-rw-r--r-- | clientapi/routing/createroom.go | 79 | ||||
-rw-r--r-- | clientapi/routing/leaveroom.go | 6 | ||||
-rw-r--r-- | clientapi/routing/membership.go | 35 | ||||
-rw-r--r-- | clientapi/routing/routing.go | 45 | ||||
-rw-r--r-- | clientapi/routing/sendevent.go | 52 | ||||
-rw-r--r-- | clientapi/routing/server_notices.go | 343 | ||||
-rw-r--r-- | clientapi/routing/server_notices_test.go | 83 |
8 files changed, 580 insertions, 72 deletions
diff --git a/clientapi/jsonerror/jsonerror.go b/clientapi/jsonerror/jsonerror.go index caa216e6..97c59703 100644 --- a/clientapi/jsonerror/jsonerror.go +++ b/clientapi/jsonerror/jsonerror.go @@ -149,6 +149,15 @@ func MissingParam(msg string) *MatrixError { return &MatrixError{"M_MISSING_PARAM", msg} } +// LeaveServerNoticeError is an error returned when trying to reject an invite +// for a server notice room. +func LeaveServerNoticeError() *MatrixError { + return &MatrixError{ + ErrCode: "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM", + Err: "You cannot reject this invite", + } +} + type IncompatibleRoomVersionError struct { RoomVersion string `json:"room_version"` Error string `json:"error"` diff --git a/clientapi/routing/createroom.go b/clientapi/routing/createroom.go index 80ac2293..fcacc76c 100644 --- a/clientapi/routing/createroom.go +++ b/clientapi/routing/createroom.go @@ -15,6 +15,7 @@ package routing import ( + "context" "encoding/json" "fmt" "net/http" @@ -140,33 +141,14 @@ func CreateRoom( accountDB userdb.Database, rsAPI roomserverAPI.RoomserverInternalAPI, asAPI appserviceAPI.AppServiceQueryAPI, ) util.JSONResponse { - // TODO (#267): Check room ID doesn't clash with an existing one, and we - // probably shouldn't be using pseudo-random strings, maybe GUIDs? - roomID := fmt.Sprintf("!%s:%s", util.RandomString(16), cfg.Matrix.ServerName) - return createRoom(req, device, cfg, roomID, accountDB, rsAPI, asAPI) -} - -// createRoom implements /createRoom -// nolint: gocyclo -func createRoom( - req *http.Request, device *api.Device, - cfg *config.ClientAPI, roomID string, - accountDB userdb.Database, rsAPI roomserverAPI.RoomserverInternalAPI, - asAPI appserviceAPI.AppServiceQueryAPI, -) util.JSONResponse { - logger := util.GetLogger(req.Context()) - userID := device.UserID var r createRoomRequest resErr := httputil.UnmarshalJSONRequest(req, &r) if resErr != nil { return *resErr } - // TODO: apply rate-limit - if resErr = r.Validate(); resErr != nil { return *resErr } - evTime, err := httputil.ParseTSParam(req) if err != nil { return util.JSONResponse{ @@ -174,6 +156,25 @@ func createRoom( JSON: jsonerror.InvalidArgumentValue(err.Error()), } } + return createRoom(req.Context(), r, device, cfg, accountDB, rsAPI, asAPI, evTime) +} + +// createRoom implements /createRoom +// nolint: gocyclo +func createRoom( + ctx context.Context, + r createRoomRequest, device *api.Device, + cfg *config.ClientAPI, + accountDB userdb.Database, rsAPI roomserverAPI.RoomserverInternalAPI, + asAPI appserviceAPI.AppServiceQueryAPI, + evTime time.Time, +) util.JSONResponse { + // TODO (#267): Check room ID doesn't clash with an existing one, and we + // probably shouldn't be using pseudo-random strings, maybe GUIDs? + roomID := fmt.Sprintf("!%s:%s", util.RandomString(16), cfg.Matrix.ServerName) + + logger := util.GetLogger(ctx) + userID := device.UserID // Clobber keys: creator, room_version @@ -200,16 +201,16 @@ func createRoom( "roomVersion": roomVersion, }).Info("Creating new room") - profile, err := appserviceAPI.RetrieveUserProfile(req.Context(), userID, asAPI, accountDB) + profile, err := appserviceAPI.RetrieveUserProfile(ctx, userID, asAPI, accountDB) if err != nil { - util.GetLogger(req.Context()).WithError(err).Error("appserviceAPI.RetrieveUserProfile failed") + util.GetLogger(ctx).WithError(err).Error("appserviceAPI.RetrieveUserProfile failed") return jsonerror.InternalServerError() } createContent := map[string]interface{}{} if len(r.CreationContent) > 0 { if err = json.Unmarshal(r.CreationContent, &createContent); err != nil { - util.GetLogger(req.Context()).WithError(err).Error("json.Unmarshal for creation_content failed") + util.GetLogger(ctx).WithError(err).Error("json.Unmarshal for creation_content failed") return util.JSONResponse{ Code: http.StatusBadRequest, JSON: jsonerror.BadJSON("invalid create content"), @@ -230,7 +231,7 @@ func createRoom( // Merge powerLevelContentOverride fields by unmarshalling it atop the defaults err = json.Unmarshal(r.PowerLevelContentOverride, &powerLevelContent) if err != nil { - util.GetLogger(req.Context()).WithError(err).Error("json.Unmarshal for power_level_content_override failed") + util.GetLogger(ctx).WithError(err).Error("json.Unmarshal for power_level_content_override failed") return util.JSONResponse{ Code: http.StatusBadRequest, JSON: jsonerror.BadJSON("malformed power_level_content_override"), @@ -319,9 +320,9 @@ func createRoom( } var aliasResp roomserverAPI.GetRoomIDForAliasResponse - err = rsAPI.GetRoomIDForAlias(req.Context(), &hasAliasReq, &aliasResp) + err = rsAPI.GetRoomIDForAlias(ctx, &hasAliasReq, &aliasResp) if err != nil { - util.GetLogger(req.Context()).WithError(err).Error("aliasAPI.GetRoomIDForAlias failed") + util.GetLogger(ctx).WithError(err).Error("aliasAPI.GetRoomIDForAlias failed") return jsonerror.InternalServerError() } if aliasResp.RoomID != "" { @@ -426,7 +427,7 @@ func createRoom( } err = builder.SetContent(e.Content) if err != nil { - util.GetLogger(req.Context()).WithError(err).Error("builder.SetContent failed") + util.GetLogger(ctx).WithError(err).Error("builder.SetContent failed") return jsonerror.InternalServerError() } if i > 0 { @@ -435,12 +436,12 @@ func createRoom( var ev *gomatrixserverlib.Event ev, err = buildEvent(&builder, &authEvents, cfg, evTime, roomVersion) if err != nil { - util.GetLogger(req.Context()).WithError(err).Error("buildEvent failed") + util.GetLogger(ctx).WithError(err).Error("buildEvent failed") return jsonerror.InternalServerError() } if err = gomatrixserverlib.Allowed(ev, &authEvents); err != nil { - util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.Allowed failed") + util.GetLogger(ctx).WithError(err).Error("gomatrixserverlib.Allowed failed") return jsonerror.InternalServerError() } @@ -448,7 +449,7 @@ func createRoom( builtEvents = append(builtEvents, ev.Headered(roomVersion)) err = authEvents.AddEvent(ev) if err != nil { - util.GetLogger(req.Context()).WithError(err).Error("authEvents.AddEvent failed") + util.GetLogger(ctx).WithError(err).Error("authEvents.AddEvent failed") return jsonerror.InternalServerError() } } @@ -462,8 +463,8 @@ func createRoom( SendAsServer: roomserverAPI.DoNotSendToOtherServers, }) } - if err = roomserverAPI.SendInputRoomEvents(req.Context(), rsAPI, inputs, false); err != nil { - util.GetLogger(req.Context()).WithError(err).Error("roomserverAPI.SendInputRoomEvents failed") + if err = roomserverAPI.SendInputRoomEvents(ctx, rsAPI, inputs, false); err != nil { + util.GetLogger(ctx).WithError(err).Error("roomserverAPI.SendInputRoomEvents failed") return jsonerror.InternalServerError() } @@ -478,9 +479,9 @@ func createRoom( } var aliasResp roomserverAPI.SetRoomAliasResponse - err = rsAPI.SetRoomAlias(req.Context(), &aliasReq, &aliasResp) + err = rsAPI.SetRoomAlias(ctx, &aliasReq, &aliasResp) if err != nil { - util.GetLogger(req.Context()).WithError(err).Error("aliasAPI.SetRoomAlias failed") + util.GetLogger(ctx).WithError(err).Error("aliasAPI.SetRoomAlias failed") return jsonerror.InternalServerError() } @@ -519,11 +520,11 @@ func createRoom( for _, invitee := range r.Invite { // Build the invite event. inviteEvent, err := buildMembershipEvent( - req.Context(), invitee, "", accountDB, device, gomatrixserverlib.Invite, + ctx, invitee, "", accountDB, device, gomatrixserverlib.Invite, roomID, true, cfg, evTime, rsAPI, asAPI, ) if err != nil { - util.GetLogger(req.Context()).WithError(err).Error("buildMembershipEvent failed") + util.GetLogger(ctx).WithError(err).Error("buildMembershipEvent failed") continue } inviteStrippedState := append( @@ -532,7 +533,7 @@ func createRoom( ) // Send the invite event to the roomserver. err = roomserverAPI.SendInvite( - req.Context(), + ctx, rsAPI, inviteEvent.Headered(roomVersion), inviteStrippedState, // invite room state @@ -544,7 +545,7 @@ func createRoom( return e.JSONResponse() case nil: default: - util.GetLogger(req.Context()).WithError(err).Error("roomserverAPI.SendInvite failed") + util.GetLogger(ctx).WithError(err).Error("roomserverAPI.SendInvite failed") return util.JSONResponse{ Code: http.StatusInternalServerError, JSON: jsonerror.InternalServerError(), @@ -556,13 +557,13 @@ func createRoom( if r.Visibility == "public" { // expose this room in the published room list var pubRes roomserverAPI.PerformPublishResponse - rsAPI.PerformPublish(req.Context(), &roomserverAPI.PerformPublishRequest{ + rsAPI.PerformPublish(ctx, &roomserverAPI.PerformPublishRequest{ RoomID: roomID, Visibility: "public", }, &pubRes) if pubRes.Error != nil { // treat as non-fatal since the room is already made by this point - util.GetLogger(req.Context()).WithError(pubRes.Error).Error("failed to visibility:public") + util.GetLogger(ctx).WithError(pubRes.Error).Error("failed to visibility:public") } } diff --git a/clientapi/routing/leaveroom.go b/clientapi/routing/leaveroom.go index 38cef118..a34dd02d 100644 --- a/clientapi/routing/leaveroom.go +++ b/clientapi/routing/leaveroom.go @@ -38,6 +38,12 @@ func LeaveRoomByID( // Ask the roomserver to perform the leave. if err := rsAPI.PerformLeave(req.Context(), &leaveReq, &leaveRes); err != nil { + if leaveRes.Code != 0 { + return util.JSONResponse{ + Code: leaveRes.Code, + JSON: jsonerror.LeaveServerNoticeError(), + } + } return util.JSONResponse{ Code: http.StatusBadRequest, JSON: jsonerror.Unknown(err.Error()), diff --git a/clientapi/routing/membership.go b/clientapi/routing/membership.go index 11223924..ffe8da13 100644 --- a/clientapi/routing/membership.go +++ b/clientapi/routing/membership.go @@ -226,27 +226,42 @@ func SendInvite( } } + // We already received the return value, so no need to check for an error here. + response, _ := sendInvite(req.Context(), accountDB, device, roomID, body.UserID, body.Reason, cfg, rsAPI, asAPI, evTime) + return response +} + +// sendInvite sends an invitation to a user. Returns a JSONResponse and an error +func sendInvite( + ctx context.Context, + accountDB userdb.Database, + device *userapi.Device, + roomID, userID, reason string, + cfg *config.ClientAPI, + rsAPI roomserverAPI.RoomserverInternalAPI, + asAPI appserviceAPI.AppServiceQueryAPI, evTime time.Time, +) (util.JSONResponse, error) { event, err := buildMembershipEvent( - req.Context(), body.UserID, body.Reason, accountDB, device, "invite", + ctx, userID, reason, accountDB, device, "invite", roomID, false, cfg, evTime, rsAPI, asAPI, ) if err == errMissingUserID { return util.JSONResponse{ Code: http.StatusBadRequest, JSON: jsonerror.BadJSON(err.Error()), - } + }, err } else if err == eventutil.ErrRoomNoExists { return util.JSONResponse{ Code: http.StatusNotFound, JSON: jsonerror.NotFound(err.Error()), - } + }, err } else if err != nil { - util.GetLogger(req.Context()).WithError(err).Error("buildMembershipEvent failed") - return jsonerror.InternalServerError() + util.GetLogger(ctx).WithError(err).Error("buildMembershipEvent failed") + return jsonerror.InternalServerError(), err } err = roomserverAPI.SendInvite( - req.Context(), rsAPI, + ctx, rsAPI, event, nil, // ask the roomserver to draw up invite room state for us cfg.Matrix.ServerName, @@ -254,18 +269,18 @@ func SendInvite( ) switch e := err.(type) { case *roomserverAPI.PerformError: - return e.JSONResponse() + return e.JSONResponse(), err case nil: return util.JSONResponse{ Code: http.StatusOK, JSON: struct{}{}, - } + }, nil default: - util.GetLogger(req.Context()).WithError(err).Error("roomserverAPI.SendInvite failed") + util.GetLogger(ctx).WithError(err).Error("roomserverAPI.SendInvite failed") return util.JSONResponse{ Code: http.StatusInternalServerError, JSON: jsonerror.InternalServerError(), - } + }, err } } diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go index 63dcaa41..d75f58b8 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -15,6 +15,7 @@ package routing import ( + "context" "encoding/json" "net/http" "strings" @@ -117,6 +118,50 @@ func Setup( ).Methods(http.MethodGet, http.MethodPost, http.MethodOptions) } + // server notifications + if cfg.Matrix.ServerNotices.Enabled { + logrus.Info("Enabling server notices at /_synapse/admin/v1/send_server_notice") + serverNotificationSender, err := getSenderDevice(context.Background(), userAPI, accountDB, cfg) + if err != nil { + logrus.WithError(err).Fatal("unable to get account for sending sending server notices") + } + + synapseAdminRouter.Handle("/admin/v1/send_server_notice/{txnID}", + httputil.MakeAuthAPI("send_server_notice", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + // not specced, but ensure we're rate limiting requests to this endpoint + if r := rateLimits.Limit(req); r != nil { + return *r + } + vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) + if err != nil { + return util.ErrorResponse(err) + } + txnID := vars["txnID"] + return SendServerNotice( + req, &cfg.Matrix.ServerNotices, + cfg, userAPI, rsAPI, accountDB, asAPI, + device, serverNotificationSender, + &txnID, transactionsCache, + ) + }), + ).Methods(http.MethodPut, http.MethodOptions) + + synapseAdminRouter.Handle("/admin/v1/send_server_notice", + httputil.MakeAuthAPI("send_server_notice", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + // not specced, but ensure we're rate limiting requests to this endpoint + if r := rateLimits.Limit(req); r != nil { + return *r + } + return SendServerNotice( + req, &cfg.Matrix.ServerNotices, + cfg, userAPI, rsAPI, accountDB, asAPI, + device, serverNotificationSender, + nil, transactionsCache, + ) + }), + ).Methods(http.MethodPost, http.MethodOptions) + } + // You can't just do PathPrefix("/(r0|v3)") because regexps only apply when inside named path variables. // So make a named path variable called 'apiversion' (which we will never read in handlers) and then do // (r0|v3) - BUT this is a captured group, which makes no sense because you cannot extract this group diff --git a/clientapi/routing/sendevent.go b/clientapi/routing/sendevent.go index 606107b9..23935b5d 100644 --- a/clientapi/routing/sendevent.go +++ b/clientapi/routing/sendevent.go @@ -15,10 +15,16 @@ package routing import ( + "context" "net/http" "sync" "time" + "github.com/matrix-org/gomatrixserverlib" + "github.com/matrix-org/util" + "github.com/prometheus/client_golang/prometheus" + "github.com/sirupsen/logrus" + "github.com/matrix-org/dendrite/clientapi/httputil" "github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/internal/eventutil" @@ -26,10 +32,6 @@ import ( "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" - "github.com/prometheus/client_golang/prometheus" - "github.com/sirupsen/logrus" ) // http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid @@ -97,7 +99,22 @@ func SendEvent( defer mutex.(*sync.Mutex).Unlock() startedGeneratingEvent := time.Now() - e, resErr := generateSendEvent(req, device, roomID, eventType, stateKey, cfg, rsAPI) + + var r map[string]interface{} // must be a JSON object + resErr := httputil.UnmarshalJSONRequest(req, &r) + if resErr != nil { + return *resErr + } + + evTime, err := httputil.ParseTSParam(req) + if err != nil { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.InvalidArgumentValue(err.Error()), + } + } + + e, resErr := generateSendEvent(req.Context(), r, device, roomID, eventType, stateKey, cfg, rsAPI, evTime) if resErr != nil { return *resErr } @@ -153,27 +170,16 @@ func SendEvent( } func generateSendEvent( - req *http.Request, + ctx context.Context, + r map[string]interface{}, device *userapi.Device, roomID, eventType string, stateKey *string, cfg *config.ClientAPI, rsAPI api.RoomserverInternalAPI, + evTime time.Time, ) (*gomatrixserverlib.Event, *util.JSONResponse) { // parse the incoming http request userID := device.UserID - var r map[string]interface{} // must be a JSON object - resErr := httputil.UnmarshalJSONRequest(req, &r) - if resErr != nil { - return nil, resErr - } - - evTime, err := httputil.ParseTSParam(req) - if err != nil { - return nil, &util.JSONResponse{ - Code: http.StatusBadRequest, - JSON: jsonerror.InvalidArgumentValue(err.Error()), - } - } // create the new event and set all the fields we can builder := gomatrixserverlib.EventBuilder{ @@ -182,15 +188,15 @@ func generateSendEvent( Type: eventType, StateKey: stateKey, } - err = builder.SetContent(r) + err := builder.SetContent(r) if err != nil { - util.GetLogger(req.Context()).WithError(err).Error("builder.SetContent failed") + util.GetLogger(ctx).WithError(err).Error("builder.SetContent failed") resErr := jsonerror.InternalServerError() return nil, &resErr } var queryRes api.QueryLatestEventsAndStateResponse - e, err := eventutil.QueryAndBuildEvent(req.Context(), &builder, cfg.Matrix, evTime, rsAPI, &queryRes) + e, err := eventutil.QueryAndBuildEvent(ctx, &builder, cfg.Matrix, evTime, rsAPI, &queryRes) if err == eventutil.ErrRoomNoExists { return nil, &util.JSONResponse{ Code: http.StatusNotFound, @@ -213,7 +219,7 @@ func generateSendEvent( JSON: jsonerror.BadJSON(e.Error()), } } else if err != nil { - util.GetLogger(req.Context()).WithError(err).Error("eventutil.BuildEvent failed") + util.GetLogger(ctx).WithError(err).Error("eventutil.BuildEvent failed") resErr := jsonerror.InternalServerError() return nil, &resErr } diff --git a/clientapi/routing/server_notices.go b/clientapi/routing/server_notices.go new file mode 100644 index 00000000..42a303a6 --- /dev/null +++ b/clientapi/routing/server_notices.go @@ -0,0 +1,343 @@ +// 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 ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + userdb "github.com/matrix-org/dendrite/userapi/storage" + "github.com/matrix-org/gomatrix" + "github.com/matrix-org/gomatrixserverlib" + "github.com/matrix-org/gomatrixserverlib/tokens" + "github.com/matrix-org/util" + "github.com/prometheus/client_golang/prometheus" + "github.com/sirupsen/logrus" + + appserviceAPI "github.com/matrix-org/dendrite/appservice/api" + "github.com/matrix-org/dendrite/clientapi/httputil" + "github.com/matrix-org/dendrite/clientapi/jsonerror" + "github.com/matrix-org/dendrite/internal/eventutil" + "github.com/matrix-org/dendrite/internal/transactions" + "github.com/matrix-org/dendrite/roomserver/api" + "github.com/matrix-org/dendrite/setup/config" + userapi "github.com/matrix-org/dendrite/userapi/api" +) + +// Unspecced server notice request +// https://github.com/matrix-org/synapse/blob/develop/docs/admin_api/server_notices.md +type sendServerNoticeRequest struct { + UserID string `json:"user_id,omitempty"` + Content struct { + MsgType string `json:"msgtype,omitempty"` + Body string `json:"body,omitempty"` + } `json:"content,omitempty"` + Type string `json:"type,omitempty"` + StateKey string `json:"state_key,omitempty"` +} + +// SendServerNotice sends a message to a specific user. It can only be invoked by an admin. +func SendServerNotice( + req *http.Request, + cfgNotices *config.ServerNotices, + cfgClient *config.ClientAPI, + userAPI userapi.UserInternalAPI, + rsAPI api.RoomserverInternalAPI, + accountsDB userdb.Database, + asAPI appserviceAPI.AppServiceQueryAPI, + device *userapi.Device, + senderDevice *userapi.Device, + txnID *string, + txnCache *transactions.Cache, +) util.JSONResponse { + if device.AccountType != userapi.AccountTypeAdmin { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("This API can only be used by admin users."), + } + } + + if txnID != nil { + // Try to fetch response from transactionsCache + if res, ok := txnCache.FetchTransaction(device.AccessToken, *txnID); ok { + return *res + } + } + + ctx := req.Context() + var r sendServerNoticeRequest + resErr := httputil.UnmarshalJSONRequest(req, &r) + if resErr != nil { + return *resErr + } + + // check that all required fields are set + if !r.valid() { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.BadJSON("Invalid request"), + } + } + + // get rooms for specified user + allUserRooms := []string{} + userRooms := api.QueryRoomsForUserResponse{} + if err := rsAPI.QueryRoomsForUser(ctx, &api.QueryRoomsForUserRequest{ + UserID: r.UserID, + WantMembership: "join", + }, &userRooms); err != nil { + return util.ErrorResponse(err) + } + allUserRooms = append(allUserRooms, userRooms.RoomIDs...) + // get invites for specified user + if err := rsAPI.QueryRoomsForUser(ctx, &api.QueryRoomsForUserRequest{ + UserID: r.UserID, + WantMembership: "invite", + }, &userRooms); err != nil { + return util.ErrorResponse(err) + } + allUserRooms = append(allUserRooms, userRooms.RoomIDs...) + // get left rooms for specified user + if err := rsAPI.QueryRoomsForUser(ctx, &api.QueryRoomsForUserRequest{ + UserID: r.UserID, + WantMembership: "leave", + }, &userRooms); err != nil { + return util.ErrorResponse(err) + } + allUserRooms = append(allUserRooms, userRooms.RoomIDs...) + + // get rooms of the sender + senderUserID := fmt.Sprintf("@%s:%s", cfgNotices.LocalPart, cfgClient.Matrix.ServerName) + senderRooms := api.QueryRoomsForUserResponse{} + if err := rsAPI.QueryRoomsForUser(ctx, &api.QueryRoomsForUserRequest{ + UserID: senderUserID, + WantMembership: "join", + }, &senderRooms); err != nil { + return util.ErrorResponse(err) + } + + // check if we have rooms in common + commonRooms := []string{} + for _, userRoomID := range allUserRooms { + for _, senderRoomID := range senderRooms.RoomIDs { + if userRoomID == senderRoomID { + commonRooms = append(commonRooms, senderRoomID) + } + } + } + + if len(commonRooms) > 1 { + return util.ErrorResponse(fmt.Errorf("expected to find one room, but got %d", len(commonRooms))) + } + + var ( + roomID string + roomVersion = gomatrixserverlib.RoomVersionV6 + ) + + // create a new room for the user + if len(commonRooms) == 0 { + powerLevelContent := eventutil.InitialPowerLevelsContent(senderUserID) + powerLevelContent.Users[r.UserID] = -10 // taken from Synapse + pl, err := json.Marshal(powerLevelContent) + if err != nil { + return util.ErrorResponse(err) + } + createContent := map[string]interface{}{} + createContent["m.federate"] = false + cc, err := json.Marshal(createContent) + if err != nil { + return util.ErrorResponse(err) + } + crReq := createRoomRequest{ + Invite: []string{r.UserID}, + Name: cfgNotices.RoomName, + Visibility: "private", + Preset: presetPrivateChat, + CreationContent: cc, + GuestCanJoin: false, + RoomVersion: roomVersion, + PowerLevelContentOverride: pl, + } + + roomRes := createRoom(ctx, crReq, senderDevice, cfgClient, accountsDB, rsAPI, asAPI, time.Now()) + + switch data := roomRes.JSON.(type) { + case createRoomResponse: + roomID = data.RoomID + + // tag the room, so we can later check if the user tries to reject an invite + serverAlertTag := gomatrix.TagContent{Tags: map[string]gomatrix.TagProperties{ + "m.server_notice": { + Order: 1.0, + }, + }} + if err = saveTagData(req, r.UserID, roomID, userAPI, serverAlertTag); err != nil { + util.GetLogger(ctx).WithError(err).Error("saveTagData failed") + return jsonerror.InternalServerError() + } + + default: + // if we didn't get a createRoomResponse, we probably received an error, so return that. + return roomRes + } + + } else { + // we've found a room in common, check the membership + roomID = commonRooms[0] + // re-invite the user + res, err := sendInvite(ctx, accountsDB, senderDevice, roomID, r.UserID, "Server notice room", cfgClient, rsAPI, asAPI, time.Now()) + if err != nil { + return res + } + } + + startedGeneratingEvent := time.Now() + + request := map[string]interface{}{ + "body": r.Content.Body, + "msgtype": r.Content.MsgType, + } + e, resErr := generateSendEvent(ctx, request, senderDevice, roomID, "m.room.message", nil, cfgClient, rsAPI, time.Now()) + if resErr != nil { + logrus.Errorf("failed to send message: %+v", resErr) + return *resErr + } + timeToGenerateEvent := time.Since(startedGeneratingEvent) + + var txnAndSessionID *api.TransactionID + if txnID != nil { + txnAndSessionID = &api.TransactionID{ + TransactionID: *txnID, + SessionID: device.SessionID, + } + } + + // pass the new event to the roomserver and receive the correct event ID + // event ID in case of duplicate transaction is discarded + startedSubmittingEvent := time.Now() + if err := api.SendEvents( + ctx, rsAPI, + api.KindNew, + []*gomatrixserverlib.HeaderedEvent{ + e.Headered(roomVersion), + }, + cfgClient.Matrix.ServerName, + cfgClient.Matrix.ServerName, + txnAndSessionID, + false, + ); err != nil { + util.GetLogger(ctx).WithError(err).Error("SendEvents failed") + return jsonerror.InternalServerError() + } + util.GetLogger(ctx).WithFields(logrus.Fields{ + "event_id": e.EventID(), + "room_id": roomID, + "room_version": roomVersion, + }).Info("Sent event to roomserver") + timeToSubmitEvent := time.Since(startedSubmittingEvent) + + res := util.JSONResponse{ + Code: http.StatusOK, + JSON: sendEventResponse{e.EventID()}, + } + // Add response to transactionsCache + if txnID != nil { + txnCache.AddTransaction(device.AccessToken, *txnID, &res) + } + + // Take a note of how long it took to generate the event vs submit + // it to the roomserver. + sendEventDuration.With(prometheus.Labels{"action": "build"}).Observe(float64(timeToGenerateEvent.Milliseconds())) + sendEventDuration.With(prometheus.Labels{"action": "submit"}).Observe(float64(timeToSubmitEvent.Milliseconds())) + + return res +} + +func (r sendServerNoticeRequest) valid() (ok bool) { + if r.UserID == "" { + return false + } + if r.Content.MsgType == "" || r.Content.Body == "" { + return false + } + return true +} + +// getSenderDevice creates a user account to be used when sending server notices. +// It returns an userapi.Device, which is used for building the event +func getSenderDevice( + ctx context.Context, + userAPI userapi.UserInternalAPI, + accountDB userdb.Database, + cfg *config.ClientAPI, +) (*userapi.Device, error) { + var accRes userapi.PerformAccountCreationResponse + // create account if it doesn't exist + err := userAPI.PerformAccountCreation(ctx, &userapi.PerformAccountCreationRequest{ + AccountType: userapi.AccountTypeUser, + Localpart: cfg.Matrix.ServerNotices.LocalPart, + OnConflict: userapi.ConflictUpdate, + }, &accRes) + if err != nil { + return nil, err + } + + // set the avatarurl for the user + if err = accountDB.SetAvatarURL(ctx, cfg.Matrix.ServerNotices.LocalPart, cfg.Matrix.ServerNotices.AvatarURL); err != nil { + util.GetLogger(ctx).WithError(err).Error("accountDB.SetAvatarURL failed") + return nil, err + } + + // Check if we got existing devices + deviceRes := &userapi.QueryDevicesResponse{} + err = userAPI.QueryDevices(ctx, &userapi.QueryDevicesRequest{ + UserID: accRes.Account.UserID, + }, deviceRes) + if err != nil { + return nil, err + } + + if len(deviceRes.Devices) > 0 { + return &deviceRes.Devices[0], nil + } + + // create an AccessToken + token, err := tokens.GenerateLoginToken(tokens.TokenOptions{ + ServerPrivateKey: cfg.Matrix.PrivateKey.Seed(), + ServerName: string(cfg.Matrix.ServerName), + UserID: accRes.Account.UserID, + }) + if err != nil { + return nil, err + } + + // create a new device, if we didn't find any + var devRes userapi.PerformDeviceCreationResponse + err = userAPI.PerformDeviceCreation(ctx, &userapi.PerformDeviceCreationRequest{ + Localpart: cfg.Matrix.ServerNotices.LocalPart, + DeviceDisplayName: &cfg.Matrix.ServerNotices.LocalPart, + AccessToken: token, + NoDeviceListUpdate: true, + }, &devRes) + + if err != nil { + return nil, err + } + return devRes.Device, nil +} diff --git a/clientapi/routing/server_notices_test.go b/clientapi/routing/server_notices_test.go new file mode 100644 index 00000000..2fac072c --- /dev/null +++ b/clientapi/routing/server_notices_test.go @@ -0,0 +1,83 @@ +package routing + +import ( + "testing" +) + +func Test_sendServerNoticeRequest_validate(t *testing.T) { + type fields struct { + UserID string `json:"user_id,omitempty"` + Content struct { + MsgType string `json:"msgtype,omitempty"` + Body string `json:"body,omitempty"` + } `json:"content,omitempty"` + Type string `json:"type,omitempty"` + StateKey string `json:"state_key,omitempty"` + } + + content := struct { + MsgType string `json:"msgtype,omitempty"` + Body string `json:"body,omitempty"` + }{ + MsgType: "m.text", + Body: "Hello world!", + } + + tests := []struct { + name string + fields fields + wantOk bool + }{ + { + name: "empty request", + fields: fields{}, + }, + { + name: "msgtype empty", + fields: fields{ + UserID: "@alice:localhost", + Content: struct { + MsgType string `json:"msgtype,omitempty"` + Body string `json:"body,omitempty"` + }{ + Body: "Hello world!", + }, + }, + }, + { + name: "msg body empty", + fields: fields{ + UserID: "@alice:localhost", + }, + }, + { + name: "statekey empty", + fields: fields{ + UserID: "@alice:localhost", + Content: content, + }, + wantOk: true, + }, + { + name: "type empty", + fields: fields{ + UserID: "@alice:localhost", + Content: content, + }, + wantOk: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := sendServerNoticeRequest{ + UserID: tt.fields.UserID, + Content: tt.fields.Content, + Type: tt.fields.Type, + StateKey: tt.fields.StateKey, + } + if gotOk := r.valid(); gotOk != tt.wantOk { + t.Errorf("valid() = %v, want %v", gotOk, tt.wantOk) + } + }) + } +} |