Add sending server notices on startup

This commit is contained in:
Till Faelligen 2022-02-21 14:27:59 +01:00
parent 219a15c4c3
commit 6622fda08c
4 changed files with 170 additions and 48 deletions

View file

@ -1,14 +1,20 @@
package routing package routing
import ( import (
"bytes"
"context"
"crypto/hmac" "crypto/hmac"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt"
"net/http" "net/http"
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
"github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/jsonerror"
"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"
userdb "github.com/matrix-org/dendrite/userapi/storage"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util" "github.com/matrix-org/util"
"github.com/sirupsen/logrus" "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} 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) { func validHMAC(username, userHMAC, secret string) (bool, error) {
mac := hmac.New(sha256.New, []byte(secret)) mac := hmac.New(sha256.New, []byte(secret))
_, err := mac.Write([]byte(username)) _, err := mac.Write([]byte(username))

View file

@ -15,6 +15,7 @@
package routing package routing
import ( import (
"context"
"encoding/json" "encoding/json"
"net/http" "net/http"
@ -93,7 +94,7 @@ func PutTag(
} }
tagContent.Tags[tag] = properties 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") util.GetLogger(req.Context()).WithError(err).Error("saveTagData failed")
return jsonerror.InternalServerError() 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") util.GetLogger(req.Context()).WithError(err).Error("saveTagData failed")
return jsonerror.InternalServerError() return jsonerror.InternalServerError()
} }
@ -191,7 +192,7 @@ func obtainSavedTags(
// saveTagData saves the provided tag data into the database // saveTagData saves the provided tag data into the database
func saveTagData( func saveTagData(
req *http.Request, context context.Context,
userID string, userID string,
roomID string, roomID string,
userAPI api.UserInternalAPI, userAPI api.UserInternalAPI,
@ -208,5 +209,5 @@ func saveTagData(
AccountData: json.RawMessage(newTagData), AccountData: json.RawMessage(newTagData),
} }
dataRes := api.InputAccountDataResponse{} dataRes := api.InputAccountDataResponse{}
return userAPI.InputAccountData(req.Context(), &dataReq, &dataRes) return userAPI.InputAccountData(context, &dataReq, &dataRes)
} }

View file

@ -119,9 +119,13 @@ func Setup(
} }
// server notifications // server notifications
var (
serverNotificationSender *userapi.Device
err error
)
if cfg.Matrix.ServerNotices.Enabled { if cfg.Matrix.ServerNotices.Enabled {
logrus.Info("Enabling server notices at /_synapse/admin/v1/send_server_notice") 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 { if err != nil {
logrus.WithError(err).Fatal("unable to get account for sending sending server notices") logrus.WithError(err).Fatal("unable to get account for sending sending server notices")
} }
@ -172,6 +176,12 @@ func Setup(
// unspecced consent tracking // unspecced consent tracking
if cfg.Matrix.UserConsentOptions.Enabled { 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", consentAPIMux.Handle("/consent",
httputil.MakeHTMLAPI("consent", func(writer http.ResponseWriter, request *http.Request) *util.JSONResponse { httputil.MakeHTMLAPI("consent", func(writer http.ResponseWriter, request *http.Request) *util.JSONResponse {
return consent(writer, request, userAPI, cfg) return consent(writer, request, userAPI, cfg)

View file

@ -85,41 +85,38 @@ func SendServerNotice(
if resErr != nil { if resErr != nil {
return *resErr 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 // check that all required fields are set
if !r.valid() { if !serverNoticeRequest.valid() {
return util.JSONResponse{ return util.JSONResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
JSON: jsonerror.BadJSON("Invalid request"), JSON: jsonerror.BadJSON("Invalid request"),
} }, nil
} }
// get rooms for specified user // get rooms for specified user
allUserRooms := []string{} allUserRooms, err := getAllUserRooms(ctx, rsAPI, serverNoticeRequest.UserID)
userRooms := api.QueryRoomsForUserResponse{} if err != nil {
if err := rsAPI.QueryRoomsForUser(ctx, &api.QueryRoomsForUserRequest{ return util.ErrorResponse(err), nil
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 // get rooms of the sender
senderUserID := fmt.Sprintf("@%s:%s", cfgNotices.LocalPart, cfgClient.Matrix.ServerName) senderUserID := fmt.Sprintf("@%s:%s", cfgNotices.LocalPart, cfgClient.Matrix.ServerName)
@ -128,7 +125,7 @@ func SendServerNotice(
UserID: senderUserID, UserID: senderUserID,
WantMembership: "join", WantMembership: "join",
}, &senderRooms); err != nil { }, &senderRooms); err != nil {
return util.ErrorResponse(err) return util.ErrorResponse(err), nil
} }
// check if we have rooms in common // check if we have rooms in common
@ -142,7 +139,7 @@ func SendServerNotice(
} }
if len(commonRooms) > 1 { 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 ( var (
@ -153,19 +150,19 @@ func SendServerNotice(
// create a new room for the user // create a new room for the user
if len(commonRooms) == 0 { if len(commonRooms) == 0 {
powerLevelContent := eventutil.InitialPowerLevelsContent(senderUserID) 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) pl, err := json.Marshal(powerLevelContent)
if err != nil { if err != nil {
return util.ErrorResponse(err) return util.ErrorResponse(err), nil
} }
createContent := map[string]interface{}{} createContent := map[string]interface{}{}
createContent["m.federate"] = false createContent["m.federate"] = false
cc, err := json.Marshal(createContent) cc, err := json.Marshal(createContent)
if err != nil { if err != nil {
return util.ErrorResponse(err) return util.ErrorResponse(err), nil
} }
crReq := createRoomRequest{ crReq := createRoomRequest{
Invite: []string{r.UserID}, Invite: []string{serverNoticeRequest.UserID},
Name: cfgNotices.RoomName, Name: cfgNotices.RoomName,
Visibility: "private", Visibility: "private",
Preset: presetPrivateChat, Preset: presetPrivateChat,
@ -187,36 +184,35 @@ func SendServerNotice(
Order: 1.0, 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") util.GetLogger(ctx).WithError(err).Error("saveTagData failed")
return jsonerror.InternalServerError() return jsonerror.InternalServerError(), nil
} }
default: default:
// if we didn't get a createRoomResponse, we probably received an error, so return that. // if we didn't get a createRoomResponse, we probably received an error, so return that.
return roomRes return roomRes, nil
} }
} else { } else {
// we've found a room in common, check the membership
roomID = commonRooms[0] roomID = commonRooms[0]
// re-invite the user // 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 { if err != nil {
return res return res, nil
} }
} }
startedGeneratingEvent := time.Now() startedGeneratingEvent := time.Now()
request := map[string]interface{}{ request := map[string]interface{}{
"body": r.Content.Body, "body": serverNoticeRequest.Content.Body,
"msgtype": r.Content.MsgType, "msgtype": serverNoticeRequest.Content.MsgType,
} }
e, resErr := generateSendEvent(ctx, request, senderDevice, roomID, "m.room.message", nil, cfgClient, rsAPI, time.Now()) e, resErr := generateSendEvent(ctx, request, senderDevice, roomID, "m.room.message", nil, cfgClient, rsAPI, time.Now())
if resErr != nil { if resErr != nil {
logrus.Errorf("failed to send message: %+v", resErr) logrus.Errorf("failed to send message: %+v", resErr)
return *resErr return *resErr, nil
} }
timeToGenerateEvent := time.Since(startedGeneratingEvent) timeToGenerateEvent := time.Since(startedGeneratingEvent)
@ -243,7 +239,7 @@ func SendServerNotice(
false, false,
); err != nil { ); err != nil {
util.GetLogger(ctx).WithError(err).Error("SendEvents failed") util.GetLogger(ctx).WithError(err).Error("SendEvents failed")
return jsonerror.InternalServerError() return jsonerror.InternalServerError(), nil
} }
util.GetLogger(ctx).WithFields(logrus.Fields{ util.GetLogger(ctx).WithFields(logrus.Fields{
"event_id": e.EventID(), "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": "build"}).Observe(float64(timeToGenerateEvent.Milliseconds()))
sendEventDuration.With(prometheus.Labels{"action": "submit"}).Observe(float64(timeToSubmitEvent.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) { func (r sendServerNoticeRequest) valid() (ok bool) {