Skip to content
This repository has been archived by the owner on Nov 25, 2024. It is now read-only.

Commit

Permalink
Implement server notices (#2180)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
S7evinK and kegsay authored Feb 18, 2022
1 parent dbded87 commit 002429c
Show file tree
Hide file tree
Showing 18 changed files with 689 additions and 82 deletions.
9 changes: 9 additions & 0 deletions clientapi/jsonerror/jsonerror.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
79 changes: 40 additions & 39 deletions clientapi/routing/createroom.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package routing

import (
"context"
"encoding/json"
"fmt"
"net/http"
Expand Down Expand Up @@ -140,40 +141,40 @@ 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{
Code: http.StatusBadRequest,
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

Expand All @@ -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"),
Expand All @@ -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"),
Expand Down Expand Up @@ -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 != "" {
Expand Down Expand Up @@ -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 {
Expand All @@ -435,20 +436,20 @@ 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()
}

// Add the event to the list of auth events
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()
}
}
Expand All @@ -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()
}

Expand All @@ -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()
}

Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand All @@ -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(),
Expand All @@ -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")
}
}

Expand Down
6 changes: 6 additions & 0 deletions clientapi/routing/leaveroom.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
35 changes: 25 additions & 10 deletions clientapi/routing/membership.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,46 +226,61 @@ 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,
nil,
)
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
}
}

Expand Down
45 changes: 45 additions & 0 deletions clientapi/routing/routing.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package routing

import (
"context"
"encoding/json"
"net/http"
"strings"
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 002429c

Please sign in to comment.