From 6622fda08cd01b1676d5888e2f84e0354e222cbe Mon Sep 17 00:00:00 2001 From: Till Faelligen Date: Mon, 21 Feb 2022 14:27:59 +0100 Subject: [PATCH] Add sending server notices on startup --- clientapi/routing/consent_tracking.go | 86 ++++++++++++++++++++ clientapi/routing/room_tagging.go | 9 ++- clientapi/routing/routing.go | 12 ++- clientapi/routing/server_notices.go | 111 ++++++++++++++++---------- 4 files changed, 170 insertions(+), 48 deletions(-) diff --git a/clientapi/routing/consent_tracking.go b/clientapi/routing/consent_tracking.go index 2ab739067..ca33da08c 100644 --- a/clientapi/routing/consent_tracking.go +++ b/clientapi/routing/consent_tracking.go @@ -1,14 +1,20 @@ package routing import ( + "bytes" + "context" "crypto/hmac" "crypto/sha256" "encoding/hex" + "fmt" "net/http" + appserviceAPI "github.com/matrix-org/dendrite/appservice/api" "github.com/matrix-org/dendrite/clientapi/jsonerror" + "github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/setup/config" userapi "github.com/matrix-org/dendrite/userapi/api" + userdb "github.com/matrix-org/dendrite/userapi/storage" "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/util" "github.com/sirupsen/logrus" @@ -104,6 +110,86 @@ func consent(writer http.ResponseWriter, req *http.Request, userAPI userapi.User return &util.JSONResponse{Code: http.StatusOK} } +func sendServerNoticeForConsent(userAPI userapi.UserInternalAPI, rsAPI api.RoomserverInternalAPI, + cfgNotices *config.ServerNotices, + cfgClient *config.ClientAPI, + senderDevice *userapi.Device, + accountsDB userdb.Database, + asAPI appserviceAPI.AppServiceQueryAPI, +) { + logrus.Infof("Sending server notice to users who have not yet accepted the policy") + res := &userapi.QueryOutdatedPolicyUsersResponse{} + if err := userAPI.GetOutdatedPolicy(context.Background(), &userapi.QueryOutdatedPolicyUsersRequest{ + PolicyVersion: cfgClient.Matrix.UserConsentOptions.Version, + }, res); err != nil { + logrus.WithError(err).Error("unable to fetch users with outdated consent policy") + return + } + + consentOpts := cfgClient.Matrix.UserConsentOptions + data := make(map[string]string) + var err error + sentMessages := 0 + for _, userID := range res.OutdatedUsers { + if userID == cfgClient.Matrix.ServerNotices.LocalPart { + continue + } + userID = fmt.Sprintf("@%s:%s", userID, cfgClient.Matrix.ServerName) + data["ConsentURL"], err = buildConsentURI(cfgClient, userID) + if err != nil { + logrus.WithError(err).WithField("userID", userID).Error("unable to construct consentURI") + continue + } + logrus.Debugf("sending message to %s", userID) + msgBody := &bytes.Buffer{} + + if err = consentOpts.TextTemplates.ExecuteTemplate(msgBody, "serverNoticeTemplate", data); err != nil { + logrus.WithError(err).WithField("userID", userID).Error("unable to execute serverNoticeTemplate") + continue + } + + req := sendServerNoticeRequest{ + UserID: userID, + Content: struct { + MsgType string `json:"msgtype,omitempty"` + Body string `json:"body,omitempty"` + }{ + MsgType: consentOpts.ServerNoticeContent.MsgType, + Body: msgBody.String(), + }, + } + _, err = sendServerNotice(context.Background(), req, rsAPI, cfgNotices, cfgClient, senderDevice, accountsDB, asAPI, userAPI, nil, nil, nil) + if err != nil { + logrus.WithError(err).WithField("userID", userID).Error("failed to send server notice for consent to user") + continue + } + sentMessages++ + res := &userapi.UpdatePolicyVersionResponse{} + if err = userAPI.PerformUpdatePolicyVersion(context.Background(), &userapi.UpdatePolicyVersionRequest{ + PolicyVersion: consentOpts.Version, + LocalPart: userID, + ServerNoticeUpdate: true, + }, res); err != nil { + logrus.WithError(err).WithField("userID", userID).Error("failed to update policy version") + continue + } + } + logrus.Infof("Send messages to %d users", sentMessages) +} + +func buildConsentURI(cfgClient *config.ClientAPI, userID string) (string, error) { + consentOpts := cfgClient.Matrix.UserConsentOptions + + mac := hmac.New(sha256.New, []byte(consentOpts.FormSecret)) + _, err := mac.Write([]byte(userID)) + if err != nil { + return "", err + } + userMAC := mac.Sum(nil) + + return fmt.Sprintf("%s/_matrix/consent?u=%s&h=%s&v=%s", consentOpts.BaseURL, userID, userMAC, consentOpts.Version), nil +} + func validHMAC(username, userHMAC, secret string) (bool, error) { mac := hmac.New(sha256.New, []byte(secret)) _, err := mac.Write([]byte(username)) diff --git a/clientapi/routing/room_tagging.go b/clientapi/routing/room_tagging.go index c683cc949..85effcddc 100644 --- a/clientapi/routing/room_tagging.go +++ b/clientapi/routing/room_tagging.go @@ -15,6 +15,7 @@ package routing import ( + "context" "encoding/json" "net/http" @@ -93,7 +94,7 @@ func PutTag( } tagContent.Tags[tag] = properties - if err = saveTagData(req, userID, roomID, userAPI, tagContent); err != nil { + if err = saveTagData(req.Context(), userID, roomID, userAPI, tagContent); err != nil { util.GetLogger(req.Context()).WithError(err).Error("saveTagData failed") return jsonerror.InternalServerError() } @@ -145,7 +146,7 @@ func DeleteTag( } } - if err = saveTagData(req, userID, roomID, userAPI, tagContent); err != nil { + if err = saveTagData(req.Context(), userID, roomID, userAPI, tagContent); err != nil { util.GetLogger(req.Context()).WithError(err).Error("saveTagData failed") return jsonerror.InternalServerError() } @@ -191,7 +192,7 @@ func obtainSavedTags( // saveTagData saves the provided tag data into the database func saveTagData( - req *http.Request, + context context.Context, userID string, roomID string, userAPI api.UserInternalAPI, @@ -208,5 +209,5 @@ func saveTagData( AccountData: json.RawMessage(newTagData), } dataRes := api.InputAccountDataResponse{} - return userAPI.InputAccountData(req.Context(), &dataReq, &dataRes) + return userAPI.InputAccountData(context, &dataReq, &dataRes) } diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go index 230053f60..0fa4885bf 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -119,9 +119,13 @@ func Setup( } // server notifications + var ( + serverNotificationSender *userapi.Device + err error + ) 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) + serverNotificationSender, err = getSenderDevice(context.Background(), userAPI, accountDB, cfg) if err != nil { logrus.WithError(err).Fatal("unable to get account for sending sending server notices") } @@ -172,6 +176,12 @@ func Setup( // unspecced consent tracking if cfg.Matrix.UserConsentOptions.Enabled { + if !cfg.Matrix.ServerNotices.Enabled { + logrus.Warnf("Consent tracking is enabled, but server notes are not. No server notice will be sent to users") + } else { + // start a new go routine to send messages about consent + go sendServerNoticeForConsent(userAPI, rsAPI, &cfg.Matrix.ServerNotices, cfg, serverNotificationSender, accountDB, asAPI) + } consentAPIMux.Handle("/consent", httputil.MakeHTMLAPI("consent", func(writer http.ResponseWriter, request *http.Request) *util.JSONResponse { return consent(writer, request, userAPI, cfg) diff --git a/clientapi/routing/server_notices.go b/clientapi/routing/server_notices.go index 42a303a6b..40e42d72a 100644 --- a/clientapi/routing/server_notices.go +++ b/clientapi/routing/server_notices.go @@ -85,41 +85,38 @@ func SendServerNotice( if resErr != nil { return *resErr } + res, _ := sendServerNotice(ctx, r, rsAPI, cfgNotices, cfgClient, senderDevice, accountsDB, asAPI, userAPI, txnID, device, txnCache) + return res +} + +func sendServerNotice( + ctx context.Context, + serverNoticeRequest sendServerNoticeRequest, + rsAPI api.RoomserverInternalAPI, + cfgNotices *config.ServerNotices, + cfgClient *config.ClientAPI, + senderDevice *userapi.Device, + accountsDB userdb.Database, + asAPI appserviceAPI.AppServiceQueryAPI, + userAPI userapi.UserInternalAPI, + txnID *string, + device *userapi.Device, + txnCache *transactions.Cache, +) (util.JSONResponse, error) { // check that all required fields are set - if !r.valid() { + if !serverNoticeRequest.valid() { return util.JSONResponse{ Code: http.StatusBadRequest, JSON: jsonerror.BadJSON("Invalid request"), - } + }, nil } // 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, err := getAllUserRooms(ctx, rsAPI, serverNoticeRequest.UserID) + if err != nil { + return util.ErrorResponse(err), nil } - 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) @@ -128,7 +125,7 @@ func SendServerNotice( UserID: senderUserID, WantMembership: "join", }, &senderRooms); err != nil { - return util.ErrorResponse(err) + return util.ErrorResponse(err), nil } // check if we have rooms in common @@ -142,7 +139,7 @@ func SendServerNotice( } if len(commonRooms) > 1 { - return util.ErrorResponse(fmt.Errorf("expected to find one room, but got %d", len(commonRooms))) + return util.ErrorResponse(fmt.Errorf("expected to find one room, but got %d", len(commonRooms))), nil } var ( @@ -153,19 +150,19 @@ func SendServerNotice( // create a new room for the user if len(commonRooms) == 0 { powerLevelContent := eventutil.InitialPowerLevelsContent(senderUserID) - powerLevelContent.Users[r.UserID] = -10 // taken from Synapse + powerLevelContent.Users[serverNoticeRequest.UserID] = -10 // taken from Synapse pl, err := json.Marshal(powerLevelContent) if err != nil { - return util.ErrorResponse(err) + return util.ErrorResponse(err), nil } createContent := map[string]interface{}{} createContent["m.federate"] = false cc, err := json.Marshal(createContent) if err != nil { - return util.ErrorResponse(err) + return util.ErrorResponse(err), nil } crReq := createRoomRequest{ - Invite: []string{r.UserID}, + Invite: []string{serverNoticeRequest.UserID}, Name: cfgNotices.RoomName, Visibility: "private", Preset: presetPrivateChat, @@ -187,36 +184,35 @@ func SendServerNotice( Order: 1.0, }, }} - if err = saveTagData(req, r.UserID, roomID, userAPI, serverAlertTag); err != nil { + if err = saveTagData(ctx, serverNoticeRequest.UserID, roomID, userAPI, serverAlertTag); err != nil { util.GetLogger(ctx).WithError(err).Error("saveTagData failed") - return jsonerror.InternalServerError() + return jsonerror.InternalServerError(), nil } default: // if we didn't get a createRoomResponse, we probably received an error, so return that. - return roomRes + return roomRes, nil } } 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()) + res, err := sendInvite(ctx, accountsDB, senderDevice, roomID, serverNoticeRequest.UserID, "Server notice room", cfgClient, rsAPI, asAPI, time.Now()) if err != nil { - return res + return res, nil } } startedGeneratingEvent := time.Now() request := map[string]interface{}{ - "body": r.Content.Body, - "msgtype": r.Content.MsgType, + "body": serverNoticeRequest.Content.Body, + "msgtype": serverNoticeRequest.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 + return *resErr, nil } timeToGenerateEvent := time.Since(startedGeneratingEvent) @@ -243,7 +239,7 @@ func SendServerNotice( false, ); err != nil { util.GetLogger(ctx).WithError(err).Error("SendEvents failed") - return jsonerror.InternalServerError() + return jsonerror.InternalServerError(), nil } util.GetLogger(ctx).WithFields(logrus.Fields{ "event_id": e.EventID(), @@ -266,7 +262,36 @@ func SendServerNotice( sendEventDuration.With(prometheus.Labels{"action": "build"}).Observe(float64(timeToGenerateEvent.Milliseconds())) sendEventDuration.With(prometheus.Labels{"action": "submit"}).Observe(float64(timeToSubmitEvent.Milliseconds())) - return res + return res, nil +} + +func getAllUserRooms(ctx context.Context, rsAPI api.RoomserverInternalAPI, userID string) ([]string, error) { + allUserRooms := []string{} + userRooms := api.QueryRoomsForUserResponse{} + if err := rsAPI.QueryRoomsForUser(ctx, &api.QueryRoomsForUserRequest{ + UserID: userID, + WantMembership: "join", + }, &userRooms); err != nil { + return nil, err + } + allUserRooms = append(allUserRooms, userRooms.RoomIDs...) + // get invites for specified user + if err := rsAPI.QueryRoomsForUser(ctx, &api.QueryRoomsForUserRequest{ + UserID: userID, + WantMembership: "invite", + }, &userRooms); err != nil { + return nil, err + } + allUserRooms = append(allUserRooms, userRooms.RoomIDs...) + // get left rooms for specified user + if err := rsAPI.QueryRoomsForUser(ctx, &api.QueryRoomsForUserRequest{ + UserID: userID, + WantMembership: "leave", + }, &userRooms); err != nil { + return nil, err + } + allUserRooms = append(allUserRooms, userRooms.RoomIDs...) + return allUserRooms, nil } func (r sendServerNoticeRequest) valid() (ok bool) {