dendrite/clientapi/routing/server_notices.go
Neil Alexander 529df30b56
Virtual hosting schema and logic changes (#2876)
Note that virtual users cannot federate correctly yet.
2022-11-11 16:41:37 +00:00

369 lines
12 KiB
Go

// 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"
"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"
"github.com/matrix-org/dendrite/roomserver/version"
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.ClientUserAPI,
rsAPI api.ClientRoomserverAPI,
asAPI appserviceAPI.AppServiceInternalAPI,
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, req.URL); 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{}
// Get rooms the user is either joined, invited or has left.
for _, membership := range []string{"join", "invite", "leave"} {
if err := rsAPI.QueryRoomsForUser(ctx, &api.QueryRoomsForUserRequest{
UserID: r.UserID,
WantMembership: membership,
}, &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 = version.DefaultRoomVersion()
)
// 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, userAPI, 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]
membershipRes := api.QueryMembershipForUserResponse{}
err := rsAPI.QueryMembershipForUser(ctx, &api.QueryMembershipForUserRequest{UserID: r.UserID, RoomID: roomID}, &membershipRes)
if err != nil {
util.GetLogger(ctx).WithError(err).Error("unable to query membership for user")
return jsonerror.InternalServerError()
}
if !membershipRes.IsInRoom {
// re-invite the user
res, err := sendInvite(ctx, userAPI, 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, req.URL, &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,
rsAPI api.ClientRoomserverAPI,
userAPI userapi.ClientUserAPI,
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,
ServerName: cfg.Matrix.ServerName,
OnConflict: userapi.ConflictUpdate,
}, &accRes)
if err != nil {
return nil, err
}
// Set the avatarurl for the user
avatarRes := &userapi.PerformSetAvatarURLResponse{}
if err = userAPI.SetAvatarURL(ctx, &userapi.PerformSetAvatarURLRequest{
Localpart: cfg.Matrix.ServerNotices.LocalPart,
ServerName: cfg.Matrix.ServerName,
AvatarURL: cfg.Matrix.ServerNotices.AvatarURL,
}, avatarRes); err != nil {
util.GetLogger(ctx).WithError(err).Error("userAPI.SetAvatarURL failed")
return nil, err
}
profile := avatarRes.Profile
// Set the displayname for the user
displayNameRes := &userapi.PerformUpdateDisplayNameResponse{}
if err = userAPI.SetDisplayName(ctx, &userapi.PerformUpdateDisplayNameRequest{
Localpart: cfg.Matrix.ServerNotices.LocalPart,
ServerName: cfg.Matrix.ServerName,
DisplayName: cfg.Matrix.ServerNotices.DisplayName,
}, displayNameRes); err != nil {
util.GetLogger(ctx).WithError(err).Error("userAPI.SetDisplayName failed")
return nil, err
}
if displayNameRes.Changed {
profile.DisplayName = cfg.Matrix.ServerNotices.DisplayName
}
// 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
}
// We've got an existing account, return the first device of it
if len(deviceRes.Devices) > 0 {
// If there were changes to the profile, create a new membership event
if displayNameRes.Changed || avatarRes.Changed {
_, err = updateProfile(ctx, rsAPI, &deviceRes.Devices[0], profile, accRes.Account.UserID, cfg, time.Now())
if err != nil {
return nil, err
}
}
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,
ServerName: cfg.Matrix.ServerName,
DeviceDisplayName: &cfg.Matrix.ServerNotices.LocalPart,
AccessToken: token,
NoDeviceListUpdate: true,
}, &devRes)
if err != nil {
return nil, err
}
return devRes.Device, nil
}