mirror of
https://github.com/1f349/dendrite.git
synced 2024-11-09 22:42:58 +00:00
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>
This commit is contained in:
parent
dbded87525
commit
002429c9e2
@ -149,6 +149,15 @@ func MissingParam(msg string) *MatrixError {
|
|||||||
return &MatrixError{"M_MISSING_PARAM", msg}
|
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 {
|
type IncompatibleRoomVersionError struct {
|
||||||
RoomVersion string `json:"room_version"`
|
RoomVersion string `json:"room_version"`
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -140,33 +141,14 @@ func CreateRoom(
|
|||||||
accountDB userdb.Database, rsAPI roomserverAPI.RoomserverInternalAPI,
|
accountDB userdb.Database, rsAPI roomserverAPI.RoomserverInternalAPI,
|
||||||
asAPI appserviceAPI.AppServiceQueryAPI,
|
asAPI appserviceAPI.AppServiceQueryAPI,
|
||||||
) util.JSONResponse {
|
) 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
|
var r createRoomRequest
|
||||||
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
// TODO: apply rate-limit
|
|
||||||
|
|
||||||
if resErr = r.Validate(); resErr != nil {
|
if resErr = r.Validate(); resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
evTime, err := httputil.ParseTSParam(req)
|
evTime, err := httputil.ParseTSParam(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
@ -174,6 +156,25 @@ func createRoom(
|
|||||||
JSON: jsonerror.InvalidArgumentValue(err.Error()),
|
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
|
// Clobber keys: creator, room_version
|
||||||
|
|
||||||
@ -200,16 +201,16 @@ func createRoom(
|
|||||||
"roomVersion": roomVersion,
|
"roomVersion": roomVersion,
|
||||||
}).Info("Creating new room")
|
}).Info("Creating new room")
|
||||||
|
|
||||||
profile, err := appserviceAPI.RetrieveUserProfile(req.Context(), userID, asAPI, accountDB)
|
profile, err := appserviceAPI.RetrieveUserProfile(ctx, userID, asAPI, accountDB)
|
||||||
if err != nil {
|
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()
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
createContent := map[string]interface{}{}
|
createContent := map[string]interface{}{}
|
||||||
if len(r.CreationContent) > 0 {
|
if len(r.CreationContent) > 0 {
|
||||||
if err = json.Unmarshal(r.CreationContent, &createContent); err != nil {
|
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{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("invalid create content"),
|
JSON: jsonerror.BadJSON("invalid create content"),
|
||||||
@ -230,7 +231,7 @@ func createRoom(
|
|||||||
// Merge powerLevelContentOverride fields by unmarshalling it atop the defaults
|
// Merge powerLevelContentOverride fields by unmarshalling it atop the defaults
|
||||||
err = json.Unmarshal(r.PowerLevelContentOverride, &powerLevelContent)
|
err = json.Unmarshal(r.PowerLevelContentOverride, &powerLevelContent)
|
||||||
if err != nil {
|
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{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("malformed power_level_content_override"),
|
JSON: jsonerror.BadJSON("malformed power_level_content_override"),
|
||||||
@ -319,9 +320,9 @@ func createRoom(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var aliasResp roomserverAPI.GetRoomIDForAliasResponse
|
var aliasResp roomserverAPI.GetRoomIDForAliasResponse
|
||||||
err = rsAPI.GetRoomIDForAlias(req.Context(), &hasAliasReq, &aliasResp)
|
err = rsAPI.GetRoomIDForAlias(ctx, &hasAliasReq, &aliasResp)
|
||||||
if err != nil {
|
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()
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
if aliasResp.RoomID != "" {
|
if aliasResp.RoomID != "" {
|
||||||
@ -426,7 +427,7 @@ func createRoom(
|
|||||||
}
|
}
|
||||||
err = builder.SetContent(e.Content)
|
err = builder.SetContent(e.Content)
|
||||||
if err != nil {
|
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()
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
@ -435,12 +436,12 @@ func createRoom(
|
|||||||
var ev *gomatrixserverlib.Event
|
var ev *gomatrixserverlib.Event
|
||||||
ev, err = buildEvent(&builder, &authEvents, cfg, evTime, roomVersion)
|
ev, err = buildEvent(&builder, &authEvents, cfg, evTime, roomVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("buildEvent failed")
|
util.GetLogger(ctx).WithError(err).Error("buildEvent failed")
|
||||||
return jsonerror.InternalServerError()
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = gomatrixserverlib.Allowed(ev, &authEvents); err != nil {
|
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()
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -448,7 +449,7 @@ func createRoom(
|
|||||||
builtEvents = append(builtEvents, ev.Headered(roomVersion))
|
builtEvents = append(builtEvents, ev.Headered(roomVersion))
|
||||||
err = authEvents.AddEvent(ev)
|
err = authEvents.AddEvent(ev)
|
||||||
if err != nil {
|
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()
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -462,8 +463,8 @@ func createRoom(
|
|||||||
SendAsServer: roomserverAPI.DoNotSendToOtherServers,
|
SendAsServer: roomserverAPI.DoNotSendToOtherServers,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if err = roomserverAPI.SendInputRoomEvents(req.Context(), rsAPI, inputs, false); err != nil {
|
if err = roomserverAPI.SendInputRoomEvents(ctx, rsAPI, inputs, false); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("roomserverAPI.SendInputRoomEvents failed")
|
util.GetLogger(ctx).WithError(err).Error("roomserverAPI.SendInputRoomEvents failed")
|
||||||
return jsonerror.InternalServerError()
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -478,9 +479,9 @@ func createRoom(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var aliasResp roomserverAPI.SetRoomAliasResponse
|
var aliasResp roomserverAPI.SetRoomAliasResponse
|
||||||
err = rsAPI.SetRoomAlias(req.Context(), &aliasReq, &aliasResp)
|
err = rsAPI.SetRoomAlias(ctx, &aliasReq, &aliasResp)
|
||||||
if err != nil {
|
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()
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -519,11 +520,11 @@ func createRoom(
|
|||||||
for _, invitee := range r.Invite {
|
for _, invitee := range r.Invite {
|
||||||
// Build the invite event.
|
// Build the invite event.
|
||||||
inviteEvent, err := buildMembershipEvent(
|
inviteEvent, err := buildMembershipEvent(
|
||||||
req.Context(), invitee, "", accountDB, device, gomatrixserverlib.Invite,
|
ctx, invitee, "", accountDB, device, gomatrixserverlib.Invite,
|
||||||
roomID, true, cfg, evTime, rsAPI, asAPI,
|
roomID, true, cfg, evTime, rsAPI, asAPI,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("buildMembershipEvent failed")
|
util.GetLogger(ctx).WithError(err).Error("buildMembershipEvent failed")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
inviteStrippedState := append(
|
inviteStrippedState := append(
|
||||||
@ -532,7 +533,7 @@ func createRoom(
|
|||||||
)
|
)
|
||||||
// Send the invite event to the roomserver.
|
// Send the invite event to the roomserver.
|
||||||
err = roomserverAPI.SendInvite(
|
err = roomserverAPI.SendInvite(
|
||||||
req.Context(),
|
ctx,
|
||||||
rsAPI,
|
rsAPI,
|
||||||
inviteEvent.Headered(roomVersion),
|
inviteEvent.Headered(roomVersion),
|
||||||
inviteStrippedState, // invite room state
|
inviteStrippedState, // invite room state
|
||||||
@ -544,7 +545,7 @@ func createRoom(
|
|||||||
return e.JSONResponse()
|
return e.JSONResponse()
|
||||||
case nil:
|
case nil:
|
||||||
default:
|
default:
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("roomserverAPI.SendInvite failed")
|
util.GetLogger(ctx).WithError(err).Error("roomserverAPI.SendInvite failed")
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.InternalServerError(),
|
JSON: jsonerror.InternalServerError(),
|
||||||
@ -556,13 +557,13 @@ func createRoom(
|
|||||||
if r.Visibility == "public" {
|
if r.Visibility == "public" {
|
||||||
// expose this room in the published room list
|
// expose this room in the published room list
|
||||||
var pubRes roomserverAPI.PerformPublishResponse
|
var pubRes roomserverAPI.PerformPublishResponse
|
||||||
rsAPI.PerformPublish(req.Context(), &roomserverAPI.PerformPublishRequest{
|
rsAPI.PerformPublish(ctx, &roomserverAPI.PerformPublishRequest{
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
Visibility: "public",
|
Visibility: "public",
|
||||||
}, &pubRes)
|
}, &pubRes)
|
||||||
if pubRes.Error != nil {
|
if pubRes.Error != nil {
|
||||||
// treat as non-fatal since the room is already made by this point
|
// 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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,6 +38,12 @@ func LeaveRoomByID(
|
|||||||
|
|
||||||
// Ask the roomserver to perform the leave.
|
// Ask the roomserver to perform the leave.
|
||||||
if err := rsAPI.PerformLeave(req.Context(), &leaveReq, &leaveRes); err != nil {
|
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{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.Unknown(err.Error()),
|
JSON: jsonerror.Unknown(err.Error()),
|
||||||
|
@ -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(
|
event, err := buildMembershipEvent(
|
||||||
req.Context(), body.UserID, body.Reason, accountDB, device, "invite",
|
ctx, userID, reason, accountDB, device, "invite",
|
||||||
roomID, false, cfg, evTime, rsAPI, asAPI,
|
roomID, false, cfg, evTime, rsAPI, asAPI,
|
||||||
)
|
)
|
||||||
if err == errMissingUserID {
|
if err == errMissingUserID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON(err.Error()),
|
JSON: jsonerror.BadJSON(err.Error()),
|
||||||
}
|
}, err
|
||||||
} else if err == eventutil.ErrRoomNoExists {
|
} else if err == eventutil.ErrRoomNoExists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: jsonerror.NotFound(err.Error()),
|
JSON: jsonerror.NotFound(err.Error()),
|
||||||
}
|
}, err
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("buildMembershipEvent failed")
|
util.GetLogger(ctx).WithError(err).Error("buildMembershipEvent failed")
|
||||||
return jsonerror.InternalServerError()
|
return jsonerror.InternalServerError(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = roomserverAPI.SendInvite(
|
err = roomserverAPI.SendInvite(
|
||||||
req.Context(), rsAPI,
|
ctx, rsAPI,
|
||||||
event,
|
event,
|
||||||
nil, // ask the roomserver to draw up invite room state for us
|
nil, // ask the roomserver to draw up invite room state for us
|
||||||
cfg.Matrix.ServerName,
|
cfg.Matrix.ServerName,
|
||||||
@ -254,18 +269,18 @@ func SendInvite(
|
|||||||
)
|
)
|
||||||
switch e := err.(type) {
|
switch e := err.(type) {
|
||||||
case *roomserverAPI.PerformError:
|
case *roomserverAPI.PerformError:
|
||||||
return e.JSONResponse()
|
return e.JSONResponse(), err
|
||||||
case nil:
|
case nil:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
JSON: struct{}{},
|
JSON: struct{}{},
|
||||||
}
|
}, nil
|
||||||
default:
|
default:
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("roomserverAPI.SendInvite failed")
|
util.GetLogger(ctx).WithError(err).Error("roomserverAPI.SendInvite failed")
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.InternalServerError(),
|
JSON: jsonerror.InternalServerError(),
|
||||||
}
|
}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@ -117,6 +118,50 @@ func Setup(
|
|||||||
).Methods(http.MethodGet, http.MethodPost, http.MethodOptions)
|
).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.
|
// 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
|
// 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
|
// (r0|v3) - BUT this is a captured group, which makes no sense because you cannot extract this group
|
||||||
|
@ -15,10 +15,16 @@
|
|||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"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/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/internal/eventutil"
|
"github.com/matrix-org/dendrite/internal/eventutil"
|
||||||
@ -26,10 +32,6 @@ import (
|
|||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
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
|
// 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()
|
defer mutex.(*sync.Mutex).Unlock()
|
||||||
|
|
||||||
startedGeneratingEvent := time.Now()
|
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 {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
@ -153,27 +170,16 @@ func SendEvent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func generateSendEvent(
|
func generateSendEvent(
|
||||||
req *http.Request,
|
ctx context.Context,
|
||||||
|
r map[string]interface{},
|
||||||
device *userapi.Device,
|
device *userapi.Device,
|
||||||
roomID, eventType string, stateKey *string,
|
roomID, eventType string, stateKey *string,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
rsAPI api.RoomserverInternalAPI,
|
rsAPI api.RoomserverInternalAPI,
|
||||||
|
evTime time.Time,
|
||||||
) (*gomatrixserverlib.Event, *util.JSONResponse) {
|
) (*gomatrixserverlib.Event, *util.JSONResponse) {
|
||||||
// parse the incoming http request
|
// parse the incoming http request
|
||||||
userID := device.UserID
|
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
|
// create the new event and set all the fields we can
|
||||||
builder := gomatrixserverlib.EventBuilder{
|
builder := gomatrixserverlib.EventBuilder{
|
||||||
@ -182,15 +188,15 @@ func generateSendEvent(
|
|||||||
Type: eventType,
|
Type: eventType,
|
||||||
StateKey: stateKey,
|
StateKey: stateKey,
|
||||||
}
|
}
|
||||||
err = builder.SetContent(r)
|
err := builder.SetContent(r)
|
||||||
if err != nil {
|
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()
|
resErr := jsonerror.InternalServerError()
|
||||||
return nil, &resErr
|
return nil, &resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
var queryRes api.QueryLatestEventsAndStateResponse
|
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 {
|
if err == eventutil.ErrRoomNoExists {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
@ -213,7 +219,7 @@ func generateSendEvent(
|
|||||||
JSON: jsonerror.BadJSON(e.Error()),
|
JSON: jsonerror.BadJSON(e.Error()),
|
||||||
}
|
}
|
||||||
} else if err != nil {
|
} 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()
|
resErr := jsonerror.InternalServerError()
|
||||||
return nil, &resErr
|
return nil, &resErr
|
||||||
}
|
}
|
||||||
|
343
clientapi/routing/server_notices.go
Normal file
343
clientapi/routing/server_notices.go
Normal file
@ -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
|
||||||
|
}
|
83
clientapi/routing/server_notices_test.go
Normal file
83
clientapi/routing/server_notices_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -132,6 +132,7 @@ func main() {
|
|||||||
// dependency. Other components also need updating after their dependencies are up.
|
// dependency. Other components also need updating after their dependencies are up.
|
||||||
rsImpl.SetFederationAPI(fsAPI, keyRing)
|
rsImpl.SetFederationAPI(fsAPI, keyRing)
|
||||||
rsImpl.SetAppserviceAPI(asAPI)
|
rsImpl.SetAppserviceAPI(asAPI)
|
||||||
|
rsImpl.SetUserAPI(userAPI)
|
||||||
keyImpl.SetUserAPI(userAPI)
|
keyImpl.SetUserAPI(userAPI)
|
||||||
|
|
||||||
eduInputAPI := eduserver.NewInternalAPI(
|
eduInputAPI := eduserver.NewInternalAPI(
|
||||||
|
@ -68,6 +68,18 @@ global:
|
|||||||
# to other servers and the federation API will not be exposed.
|
# to other servers and the federation API will not be exposed.
|
||||||
disable_federation: false
|
disable_federation: false
|
||||||
|
|
||||||
|
# Server notices allows server admins to send messages to all users.
|
||||||
|
server_notices:
|
||||||
|
enabled: false
|
||||||
|
# The server localpart to be used when sending notices, ensure this is not yet taken
|
||||||
|
local_part: "_server"
|
||||||
|
# The displayname to be used when sending notices
|
||||||
|
display_name: "Server alerts"
|
||||||
|
# The mxid of the avatar to use
|
||||||
|
avatar_url: ""
|
||||||
|
# The roomname to be used when creating messages
|
||||||
|
room_name: "Server Alerts"
|
||||||
|
|
||||||
# Configuration for NATS JetStream
|
# Configuration for NATS JetStream
|
||||||
jetstream:
|
jetstream:
|
||||||
# A list of NATS Server addresses to connect to. If none are specified, an
|
# A list of NATS Server addresses to connect to. If none are specified, an
|
||||||
|
@ -3,9 +3,11 @@ package api
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
|
||||||
asAPI "github.com/matrix-org/dendrite/appservice/api"
|
asAPI "github.com/matrix-org/dendrite/appservice/api"
|
||||||
fsAPI "github.com/matrix-org/dendrite/federationapi/api"
|
fsAPI "github.com/matrix-org/dendrite/federationapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RoomserverInputAPI is used to write events to the room server.
|
// RoomserverInputAPI is used to write events to the room server.
|
||||||
@ -14,6 +16,7 @@ type RoomserverInternalAPI interface {
|
|||||||
// interdependencies between the roomserver and other input APIs
|
// interdependencies between the roomserver and other input APIs
|
||||||
SetFederationAPI(fsAPI fsAPI.FederationInternalAPI, keyRing *gomatrixserverlib.KeyRing)
|
SetFederationAPI(fsAPI fsAPI.FederationInternalAPI, keyRing *gomatrixserverlib.KeyRing)
|
||||||
SetAppserviceAPI(asAPI asAPI.AppServiceQueryAPI)
|
SetAppserviceAPI(asAPI asAPI.AppServiceQueryAPI)
|
||||||
|
SetUserAPI(userAPI userapi.UserInternalAPI)
|
||||||
|
|
||||||
InputRoomEvents(
|
InputRoomEvents(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
@ -5,10 +5,12 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
asAPI "github.com/matrix-org/dendrite/appservice/api"
|
|
||||||
fsAPI "github.com/matrix-org/dendrite/federationapi/api"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
|
|
||||||
|
asAPI "github.com/matrix-org/dendrite/appservice/api"
|
||||||
|
fsAPI "github.com/matrix-org/dendrite/federationapi/api"
|
||||||
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RoomserverInternalAPITrace wraps a RoomserverInternalAPI and logs the
|
// RoomserverInternalAPITrace wraps a RoomserverInternalAPI and logs the
|
||||||
@ -25,6 +27,10 @@ func (t *RoomserverInternalAPITrace) SetAppserviceAPI(asAPI asAPI.AppServiceQuer
|
|||||||
t.Impl.SetAppserviceAPI(asAPI)
|
t.Impl.SetAppserviceAPI(asAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *RoomserverInternalAPITrace) SetUserAPI(userAPI userapi.UserInternalAPI) {
|
||||||
|
t.Impl.SetUserAPI(userAPI)
|
||||||
|
}
|
||||||
|
|
||||||
func (t *RoomserverInternalAPITrace) InputRoomEvents(
|
func (t *RoomserverInternalAPITrace) InputRoomEvents(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
req *InputRoomEventsRequest,
|
req *InputRoomEventsRequest,
|
||||||
|
@ -95,6 +95,8 @@ type PerformLeaveRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PerformLeaveResponse struct {
|
type PerformLeaveResponse struct {
|
||||||
|
Code int `json:"code,omitempty"`
|
||||||
|
Message interface{} `json:"message,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PerformInviteRequest struct {
|
type PerformInviteRequest struct {
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/matrix-org/dendrite/roomserver/storage"
|
"github.com/matrix-org/dendrite/roomserver/storage"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/setup/process"
|
"github.com/matrix-org/dendrite/setup/process"
|
||||||
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/nats-io/nats.go"
|
"github.com/nats-io/nats.go"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -159,6 +160,10 @@ func (r *RoomserverInternalAPI) SetFederationAPI(fsAPI fsAPI.FederationInternalA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RoomserverInternalAPI) SetUserAPI(userAPI userapi.UserInternalAPI) {
|
||||||
|
r.Leaver.UserAPI = userAPI
|
||||||
|
}
|
||||||
|
|
||||||
func (r *RoomserverInternalAPI) SetAppserviceAPI(asAPI asAPI.AppServiceQueryAPI) {
|
func (r *RoomserverInternalAPI) SetAppserviceAPI(asAPI asAPI.AppServiceQueryAPI) {
|
||||||
r.asAPI = asAPI
|
r.asAPI = asAPI
|
||||||
}
|
}
|
||||||
|
@ -16,25 +16,29 @@ package perform
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/matrix-org/gomatrix"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
fsAPI "github.com/matrix-org/dendrite/federationapi/api"
|
fsAPI "github.com/matrix-org/dendrite/federationapi/api"
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/roomserver/internal/helpers"
|
"github.com/matrix-org/dendrite/roomserver/internal/helpers"
|
||||||
"github.com/matrix-org/dendrite/roomserver/internal/input"
|
"github.com/matrix-org/dendrite/roomserver/internal/input"
|
||||||
"github.com/matrix-org/dendrite/roomserver/storage"
|
"github.com/matrix-org/dendrite/roomserver/storage"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/util"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Leaver struct {
|
type Leaver struct {
|
||||||
Cfg *config.RoomServer
|
Cfg *config.RoomServer
|
||||||
DB storage.Database
|
DB storage.Database
|
||||||
FSAPI fsAPI.FederationInternalAPI
|
FSAPI fsAPI.FederationInternalAPI
|
||||||
|
UserAPI userapi.UserInternalAPI
|
||||||
Inputer *input.Inputer
|
Inputer *input.Inputer
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,6 +89,31 @@ func (r *Leaver) performLeaveRoomByID(
|
|||||||
if host != r.Cfg.Matrix.ServerName {
|
if host != r.Cfg.Matrix.ServerName {
|
||||||
return r.performFederatedRejectInvite(ctx, req, res, senderUser, eventID)
|
return r.performFederatedRejectInvite(ctx, req, res, senderUser, eventID)
|
||||||
}
|
}
|
||||||
|
// check that this is not a "server notice room"
|
||||||
|
accData := &userapi.QueryAccountDataResponse{}
|
||||||
|
if err := r.UserAPI.QueryAccountData(ctx, &userapi.QueryAccountDataRequest{
|
||||||
|
UserID: req.UserID,
|
||||||
|
RoomID: req.RoomID,
|
||||||
|
DataType: "m.tag",
|
||||||
|
}, accData); err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to query account data")
|
||||||
|
}
|
||||||
|
|
||||||
|
if roomData, ok := accData.RoomAccountData[req.RoomID]; ok {
|
||||||
|
tagData, ok := roomData["m.tag"]
|
||||||
|
if ok {
|
||||||
|
tags := gomatrix.TagContent{}
|
||||||
|
if err = json.Unmarshal(tagData, &tags); err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to unmarshal tag content")
|
||||||
|
}
|
||||||
|
if _, ok = tags.Tags["m.server_notice"]; ok {
|
||||||
|
// mimic the returned values from Synapse
|
||||||
|
res.Message = "You cannot reject this invite"
|
||||||
|
res.Code = 403
|
||||||
|
return nil, fmt.Errorf("You cannot reject this invite")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// There's no invite pending, so first of all we want to find out
|
// There's no invite pending, so first of all we want to find out
|
||||||
|
@ -11,6 +11,8 @@ import (
|
|||||||
"github.com/matrix-org/dendrite/internal/caching"
|
"github.com/matrix-org/dendrite/internal/caching"
|
||||||
"github.com/matrix-org/dendrite/internal/httputil"
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/opentracing/opentracing-go"
|
"github.com/opentracing/opentracing-go"
|
||||||
)
|
)
|
||||||
@ -90,6 +92,10 @@ func (h *httpRoomserverInternalAPI) SetFederationAPI(fsAPI fsInputAPI.Federation
|
|||||||
func (h *httpRoomserverInternalAPI) SetAppserviceAPI(asAPI asAPI.AppServiceQueryAPI) {
|
func (h *httpRoomserverInternalAPI) SetAppserviceAPI(asAPI asAPI.AppServiceQueryAPI) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetUserAPI no-ops in HTTP client mode as there is no chicken/egg scenario
|
||||||
|
func (h *httpRoomserverInternalAPI) SetUserAPI(userAPI userapi.UserInternalAPI) {
|
||||||
|
}
|
||||||
|
|
||||||
// SetRoomAlias implements RoomserverAliasAPI
|
// SetRoomAlias implements RoomserverAliasAPI
|
||||||
func (h *httpRoomserverInternalAPI) SetRoomAlias(
|
func (h *httpRoomserverInternalAPI) SetRoomAlias(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
@ -57,6 +57,9 @@ type Global struct {
|
|||||||
|
|
||||||
// DNS caching options for all outbound HTTP requests
|
// DNS caching options for all outbound HTTP requests
|
||||||
DNSCache DNSCacheOptions `yaml:"dns_cache"`
|
DNSCache DNSCacheOptions `yaml:"dns_cache"`
|
||||||
|
|
||||||
|
// ServerNotices configuration used for sending server notices
|
||||||
|
ServerNotices ServerNotices `yaml:"server_notices"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Global) Defaults(generate bool) {
|
func (c *Global) Defaults(generate bool) {
|
||||||
@ -72,6 +75,7 @@ func (c *Global) Defaults(generate bool) {
|
|||||||
c.Metrics.Defaults(generate)
|
c.Metrics.Defaults(generate)
|
||||||
c.DNSCache.Defaults()
|
c.DNSCache.Defaults()
|
||||||
c.Sentry.Defaults()
|
c.Sentry.Defaults()
|
||||||
|
c.ServerNotices.Defaults(generate)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Global) Verify(configErrs *ConfigErrors, isMonolith bool) {
|
func (c *Global) Verify(configErrs *ConfigErrors, isMonolith bool) {
|
||||||
@ -82,6 +86,7 @@ func (c *Global) Verify(configErrs *ConfigErrors, isMonolith bool) {
|
|||||||
c.Metrics.Verify(configErrs, isMonolith)
|
c.Metrics.Verify(configErrs, isMonolith)
|
||||||
c.Sentry.Verify(configErrs, isMonolith)
|
c.Sentry.Verify(configErrs, isMonolith)
|
||||||
c.DNSCache.Verify(configErrs, isMonolith)
|
c.DNSCache.Verify(configErrs, isMonolith)
|
||||||
|
c.ServerNotices.Verify(configErrs, isMonolith)
|
||||||
}
|
}
|
||||||
|
|
||||||
type OldVerifyKeys struct {
|
type OldVerifyKeys struct {
|
||||||
@ -123,6 +128,31 @@ func (c *Metrics) Defaults(generate bool) {
|
|||||||
func (c *Metrics) Verify(configErrs *ConfigErrors, isMonolith bool) {
|
func (c *Metrics) Verify(configErrs *ConfigErrors, isMonolith bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServerNotices defines the configuration used for sending server notices
|
||||||
|
type ServerNotices struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
// The localpart to be used when sending notices
|
||||||
|
LocalPart string `yaml:"local_part"`
|
||||||
|
// The displayname to be used when sending notices
|
||||||
|
DisplayName string `yaml:"display_name"`
|
||||||
|
// The avatar of this user
|
||||||
|
AvatarURL string `yaml:"avatar"`
|
||||||
|
// The roomname to be used when creating messages
|
||||||
|
RoomName string `yaml:"room_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ServerNotices) Defaults(generate bool) {
|
||||||
|
if generate {
|
||||||
|
c.Enabled = true
|
||||||
|
c.LocalPart = "_server"
|
||||||
|
c.DisplayName = "Server Alert"
|
||||||
|
c.RoomName = "Server Alert"
|
||||||
|
c.AvatarURL = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ServerNotices) Verify(errors *ConfigErrors, isMonolith bool) {}
|
||||||
|
|
||||||
// The configuration to use for Sentry error reporting
|
// The configuration to use for Sentry error reporting
|
||||||
type Sentry struct {
|
type Sentry struct {
|
||||||
Enabled bool `yaml:"enabled"`
|
Enabled bool `yaml:"enabled"`
|
||||||
|
@ -58,6 +58,11 @@ global:
|
|||||||
basic_auth:
|
basic_auth:
|
||||||
username: metrics
|
username: metrics
|
||||||
password: metrics
|
password: metrics
|
||||||
|
server_notices:
|
||||||
|
local_part: "_server"
|
||||||
|
display_name: "Server alerts"
|
||||||
|
avatar: ""
|
||||||
|
room_name: "Server Alerts"
|
||||||
app_service_api:
|
app_service_api:
|
||||||
internal_api:
|
internal_api:
|
||||||
listen: http://localhost:7777
|
listen: http://localhost:7777
|
||||||
|
Loading…
Reference in New Issue
Block a user