aboutsummaryrefslogtreecommitdiff
path: root/clientapi
diff options
context:
space:
mode:
authorDan <dan@globekeeper.com>2022-03-03 13:40:53 +0200
committerGitHub <noreply@github.com>2022-03-03 11:40:53 +0000
commitf05ce478f05dcaf650fbae68a39aaf5d9880a580 (patch)
treea6a47f77bba03ec7a05a8d98bea6791d47f3b48a /clientapi
parent111f01ddc81d775dfdaab6e6a3a6afa6fa5608ea (diff)
Implement Push Notifications (#1842)
* Add Pushserver component with Pushers API Co-authored-by: Tommie Gannert <tommie@gannert.se> Co-authored-by: Dan Peleg <dan@globekeeper.com> * Wire Pushserver component Co-authored-by: Neil Alexander <neilalexander@users.noreply.github.com> * Add PushGatewayClient. The full event format is required for Sytest. * Add a pushrules module. * Change user API account creation to use the new pushrules module's defaults. Introduces "scope" as required by client API, and some small field tweaks to make some 61push Sytests pass. * Add push rules query/put API in Pushserver. This manipulates account data over User API, and fires sync messages for changes. Those sync messages should, according to an existing TODO in clientapi, be moved to userapi. Forks clientapi/producers/syncapi.go to pushserver/ for later extension. * Add clientapi routes for push rules to Pushserver. A cleanup would be to move more of the name-splitting logic into pushrules.go, to depollute routing.go. * Output rooms.join.unread_notifications in /sync. This is the read-side. Pushserver will be the write-side. * Implement pushserver/storage for notifications. * Use PushGatewayClient and the pushrules module in Pushserver's room consumer. * Use one goroutine per user to avoid locking up the entire server for one bad push gateway. * Split pushing by format. * Send one device per push. Sytest does not support coalescing multiple devices into one push. Matches Synapse. Either we change Sytest, or remove the group-by-url-and-format logic. * Write OutputNotificationData from push server. Sync API is already the consumer. * Implement read receipt consumers in Pushserver. Supports m.read and m.fully_read receipts. * Add clientapi route for /unstable/notifications. * Rename to UpsertPusher for clarity and handle pusher update * Fix linter errors * Ignore body.Close() error check * Fix push server internal http wiring * Add 40 newly passing 61push tests to whitelist * Add next 12 newly passing 61push tests to whitelist * Send notification data before notifying users in EDU server consumer * NATS JetStream * Goodbye sarama * Fix `NewStreamTokenFromString` * Consume on the correct topic for the roomserver * Don't panic, NAK instead * Move push notifications into the User API * Don't set null values since that apparently causes Element upsetti * Also set omitempty on conditions * Fix bug so that we don't override the push rules unnecessarily * Tweak defaults * Update defaults * More tweaks * Move `/notifications` onto `r0`/`v3` mux * User API will consume events and read/fully read markers from the sync API with stream positions, instead of consuming directly Co-authored-by: Piotr Kozimor <p1996k@gmail.com> Co-authored-by: Tommie Gannert <tommie@gannert.se> Co-authored-by: Neil Alexander <neilalexander@users.noreply.github.com>
Diffstat (limited to 'clientapi')
-rw-r--r--clientapi/clientapi.go3
-rw-r--r--clientapi/producers/syncapi.go7
-rw-r--r--clientapi/routing/account_data.go12
-rw-r--r--clientapi/routing/notification.go63
-rw-r--r--clientapi/routing/password.go15
-rw-r--r--clientapi/routing/pusher.go114
-rw-r--r--clientapi/routing/pushrules.go386
-rw-r--r--clientapi/routing/room_tagging.go4
-rw-r--r--clientapi/routing/routing.go165
9 files changed, 741 insertions, 28 deletions
diff --git a/clientapi/clientapi.go b/clientapi/clientapi.go
index a65f3b70..91847667 100644
--- a/clientapi/clientapi.go
+++ b/clientapi/clientapi.go
@@ -59,6 +59,7 @@ func AddPublicRoutes(
routing.Setup(
router, synapseAdminRouter, cfg, eduInputAPI, rsAPI, asAPI,
accountsDB, userAPI, federation,
- syncProducer, transactionsCache, fsAPI, keyAPI, extRoomsProvider, mscCfg,
+ syncProducer, transactionsCache, fsAPI, keyAPI,
+ extRoomsProvider, mscCfg,
)
}
diff --git a/clientapi/producers/syncapi.go b/clientapi/producers/syncapi.go
index 9b1d6b1a..9ab90391 100644
--- a/clientapi/producers/syncapi.go
+++ b/clientapi/producers/syncapi.go
@@ -30,7 +30,7 @@ type SyncAPIProducer struct {
}
// SendData sends account data to the sync API server
-func (p *SyncAPIProducer) SendData(userID string, roomID string, dataType string) error {
+func (p *SyncAPIProducer) SendData(userID string, roomID string, dataType string, readMarker *eventutil.ReadMarkerJSON) error {
m := &nats.Msg{
Subject: p.Topic,
Header: nats.Header{},
@@ -38,8 +38,9 @@ func (p *SyncAPIProducer) SendData(userID string, roomID string, dataType string
m.Header.Set(jetstream.UserID, userID)
data := eventutil.AccountData{
- RoomID: roomID,
- Type: dataType,
+ RoomID: roomID,
+ Type: dataType,
+ ReadMarker: readMarker,
}
var err error
m.Data, err = json.Marshal(data)
diff --git a/clientapi/routing/account_data.go b/clientapi/routing/account_data.go
index 03025f1d..d8e98269 100644
--- a/clientapi/routing/account_data.go
+++ b/clientapi/routing/account_data.go
@@ -24,6 +24,7 @@ import (
"github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/clientapi/producers"
eduserverAPI "github.com/matrix-org/dendrite/eduserver/api"
+ "github.com/matrix-org/dendrite/internal/eventutil"
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/dendrite/userapi/api"
@@ -127,7 +128,7 @@ func SaveAccountData(
}
// TODO: user API should do this since it's account data
- if err := syncProducer.SendData(userID, roomID, dataType); err != nil {
+ if err := syncProducer.SendData(userID, roomID, dataType, nil); err != nil {
util.GetLogger(req.Context()).WithError(err).Error("syncProducer.SendData failed")
return jsonerror.InternalServerError()
}
@@ -138,11 +139,6 @@ func SaveAccountData(
}
}
-type readMarkerJSON struct {
- FullyRead string `json:"m.fully_read"`
- Read string `json:"m.read"`
-}
-
type fullyReadEvent struct {
EventID string `json:"event_id"`
}
@@ -159,7 +155,7 @@ func SaveReadMarker(
return *resErr
}
- var r readMarkerJSON
+ var r eventutil.ReadMarkerJSON
resErr = httputil.UnmarshalJSONRequest(req, &r)
if resErr != nil {
return *resErr
@@ -189,7 +185,7 @@ func SaveReadMarker(
return util.ErrorResponse(err)
}
- if err := syncProducer.SendData(device.UserID, roomID, "m.fully_read"); err != nil {
+ if err := syncProducer.SendData(device.UserID, roomID, "m.fully_read", &r); err != nil {
util.GetLogger(req.Context()).WithError(err).Error("syncProducer.SendData failed")
return jsonerror.InternalServerError()
}
diff --git a/clientapi/routing/notification.go b/clientapi/routing/notification.go
new file mode 100644
index 00000000..ee715d32
--- /dev/null
+++ b/clientapi/routing/notification.go
@@ -0,0 +1,63 @@
+// Copyright 2021 Dan Peleg <dan@globekeeper.com>
+//
+// 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 (
+ "net/http"
+ "strconv"
+
+ "github.com/matrix-org/dendrite/clientapi/jsonerror"
+ userapi "github.com/matrix-org/dendrite/userapi/api"
+ "github.com/matrix-org/gomatrixserverlib"
+ "github.com/matrix-org/util"
+)
+
+// GetNotifications handles /_matrix/client/r0/notifications
+func GetNotifications(
+ req *http.Request, device *userapi.Device,
+ userAPI userapi.UserInternalAPI,
+) util.JSONResponse {
+ var limit int64
+ if limitStr := req.URL.Query().Get("limit"); limitStr != "" {
+ var err error
+ limit, err = strconv.ParseInt(limitStr, 10, 64)
+ if err != nil {
+ util.GetLogger(req.Context()).WithError(err).Error("ParseInt(limit) failed")
+ return jsonerror.InternalServerError()
+ }
+ }
+
+ var queryRes userapi.QueryNotificationsResponse
+ localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
+ if err != nil {
+ util.GetLogger(req.Context()).WithError(err).Error("SplitID failed")
+ return jsonerror.InternalServerError()
+ }
+ err = userAPI.QueryNotifications(req.Context(), &userapi.QueryNotificationsRequest{
+ Localpart: localpart,
+ From: req.URL.Query().Get("from"),
+ Limit: int(limit),
+ Only: req.URL.Query().Get("only"),
+ }, &queryRes)
+ if err != nil {
+ util.GetLogger(req.Context()).WithError(err).Error("QueryNotifications failed")
+ return jsonerror.InternalServerError()
+ }
+ util.GetLogger(req.Context()).WithField("from", req.URL.Query().Get("from")).WithField("limit", limit).WithField("only", req.URL.Query().Get("only")).WithField("next", queryRes.NextToken).Infof("QueryNotifications: len %d", len(queryRes.Notifications))
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: queryRes,
+ }
+}
diff --git a/clientapi/routing/password.go b/clientapi/routing/password.go
index acac60fa..c63412d0 100644
--- a/clientapi/routing/password.go
+++ b/clientapi/routing/password.go
@@ -12,6 +12,7 @@ import (
userdb "github.com/matrix-org/dendrite/userapi/storage"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
+ "github.com/sirupsen/logrus"
)
type newPasswordRequest struct {
@@ -37,6 +38,11 @@ func Password(
var r newPasswordRequest
r.LogoutDevices = true
+ logrus.WithFields(logrus.Fields{
+ "sessionId": device.SessionID,
+ "userId": device.UserID,
+ }).Debug("Changing password")
+
// Unmarshal the request.
resErr := httputil.UnmarshalJSONRequest(req, &r)
if resErr != nil {
@@ -116,6 +122,15 @@ func Password(
util.GetLogger(req.Context()).WithError(err).Error("PerformDeviceDeletion failed")
return jsonerror.InternalServerError()
}
+
+ pushersReq := &api.PerformPusherDeletionRequest{
+ Localpart: localpart,
+ SessionID: device.SessionID,
+ }
+ if err := userAPI.PerformPusherDeletion(req.Context(), pushersReq, &struct{}{}); err != nil {
+ util.GetLogger(req.Context()).WithError(err).Error("PerformPusherDeletion failed")
+ return jsonerror.InternalServerError()
+ }
}
// Return a success code.
diff --git a/clientapi/routing/pusher.go b/clientapi/routing/pusher.go
new file mode 100644
index 00000000..9d6bef8b
--- /dev/null
+++ b/clientapi/routing/pusher.go
@@ -0,0 +1,114 @@
+// Copyright 2021 Dan Peleg <dan@globekeeper.com>
+//
+// 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 (
+ "net/http"
+ "net/url"
+
+ "github.com/matrix-org/dendrite/clientapi/httputil"
+ "github.com/matrix-org/dendrite/clientapi/jsonerror"
+ userapi "github.com/matrix-org/dendrite/userapi/api"
+ "github.com/matrix-org/gomatrixserverlib"
+ "github.com/matrix-org/util"
+)
+
+// GetPushers handles /_matrix/client/r0/pushers
+func GetPushers(
+ req *http.Request, device *userapi.Device,
+ userAPI userapi.UserInternalAPI,
+) util.JSONResponse {
+ var queryRes userapi.QueryPushersResponse
+ localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
+ if err != nil {
+ util.GetLogger(req.Context()).WithError(err).Error("SplitID failed")
+ return jsonerror.InternalServerError()
+ }
+ err = userAPI.QueryPushers(req.Context(), &userapi.QueryPushersRequest{
+ Localpart: localpart,
+ }, &queryRes)
+ if err != nil {
+ util.GetLogger(req.Context()).WithError(err).Error("QueryPushers failed")
+ return jsonerror.InternalServerError()
+ }
+ for i := range queryRes.Pushers {
+ queryRes.Pushers[i].SessionID = 0
+ }
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: queryRes,
+ }
+}
+
+// SetPusher handles /_matrix/client/r0/pushers/set
+// This endpoint allows the creation, modification and deletion of pushers for this user ID.
+// The behaviour of this endpoint varies depending on the values in the JSON body.
+func SetPusher(
+ req *http.Request, device *userapi.Device,
+ userAPI userapi.UserInternalAPI,
+) util.JSONResponse {
+ localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
+ if err != nil {
+ util.GetLogger(req.Context()).WithError(err).Error("SplitID failed")
+ return jsonerror.InternalServerError()
+ }
+ body := userapi.PerformPusherSetRequest{}
+ if resErr := httputil.UnmarshalJSONRequest(req, &body); resErr != nil {
+ return *resErr
+ }
+ if len(body.AppID) > 64 {
+ return invalidParam("length of app_id must be no more than 64 characters")
+ }
+ if len(body.PushKey) > 512 {
+ return invalidParam("length of pushkey must be no more than 512 bytes")
+ }
+ uInt := body.Data["url"]
+ if uInt != nil {
+ u, ok := uInt.(string)
+ if !ok {
+ return invalidParam("url must be string")
+ }
+ if u != "" {
+ var pushUrl *url.URL
+ pushUrl, err = url.Parse(u)
+ if err != nil {
+ return invalidParam("malformed url passed")
+ }
+ if pushUrl.Scheme != "https" {
+ return invalidParam("only https scheme is allowed")
+ }
+ }
+
+ }
+ body.Localpart = localpart
+ body.SessionID = device.SessionID
+ err = userAPI.PerformPusherSet(req.Context(), &body, &struct{}{})
+ if err != nil {
+ util.GetLogger(req.Context()).WithError(err).Error("PerformPusherSet failed")
+ return jsonerror.InternalServerError()
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct{}{},
+ }
+}
+
+func invalidParam(msg string) util.JSONResponse {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.InvalidParam(msg),
+ }
+}
diff --git a/clientapi/routing/pushrules.go b/clientapi/routing/pushrules.go
new file mode 100644
index 00000000..81a33b25
--- /dev/null
+++ b/clientapi/routing/pushrules.go
@@ -0,0 +1,386 @@
+package routing
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "reflect"
+
+ "github.com/matrix-org/dendrite/clientapi/jsonerror"
+ "github.com/matrix-org/dendrite/internal/pushrules"
+ userapi "github.com/matrix-org/dendrite/userapi/api"
+ "github.com/matrix-org/util"
+)
+
+func errorResponse(ctx context.Context, err error, msg string, args ...interface{}) util.JSONResponse {
+ if eerr, ok := err.(*jsonerror.MatrixError); ok {
+ var status int
+ switch eerr.ErrCode {
+ case "M_INVALID_ARGUMENT_VALUE":
+ status = http.StatusBadRequest
+ case "M_NOT_FOUND":
+ status = http.StatusNotFound
+ default:
+ status = http.StatusInternalServerError
+ }
+ return util.MatrixErrorResponse(status, eerr.ErrCode, eerr.Err)
+ }
+ util.GetLogger(ctx).WithError(err).Errorf(msg, args...)
+ return jsonerror.InternalServerError()
+}
+
+func GetAllPushRules(ctx context.Context, device *userapi.Device, userAPI userapi.UserInternalAPI) util.JSONResponse {
+ ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
+ if err != nil {
+ return errorResponse(ctx, err, "queryPushRulesJSON failed")
+ }
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: ruleSets,
+ }
+}
+
+func GetPushRulesByScope(ctx context.Context, scope string, device *userapi.Device, userAPI userapi.UserInternalAPI) util.JSONResponse {
+ ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
+ if err != nil {
+ return errorResponse(ctx, err, "queryPushRulesJSON failed")
+ }
+ ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
+ if ruleSet == nil {
+ return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rule set"), "pushRuleSetByScope failed")
+ }
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: ruleSet,
+ }
+}
+
+func GetPushRulesByKind(ctx context.Context, scope, kind string, device *userapi.Device, userAPI userapi.UserInternalAPI) util.JSONResponse {
+ ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
+ if err != nil {
+ return errorResponse(ctx, err, "queryPushRules failed")
+ }
+ ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
+ if ruleSet == nil {
+ return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rule set"), "pushRuleSetByScope failed")
+ }
+ rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
+ if rulesPtr == nil {
+ return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rules kind"), "pushRuleSetKindPointer failed")
+ }
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: *rulesPtr,
+ }
+}
+
+func GetPushRuleByRuleID(ctx context.Context, scope, kind, ruleID string, device *userapi.Device, userAPI userapi.UserInternalAPI) util.JSONResponse {
+ ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
+ if err != nil {
+ return errorResponse(ctx, err, "queryPushRules failed")
+ }
+ ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
+ if ruleSet == nil {
+ return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rule set"), "pushRuleSetByScope failed")
+ }
+ rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
+ if rulesPtr == nil {
+ return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rules kind"), "pushRuleSetKindPointer failed")
+ }
+ i := pushRuleIndexByID(*rulesPtr, ruleID)
+ if i < 0 {
+ return errorResponse(ctx, jsonerror.NotFound("push rule ID not found"), "pushRuleIndexByID failed")
+ }
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: (*rulesPtr)[i],
+ }
+}
+
+func PutPushRuleByRuleID(ctx context.Context, scope, kind, ruleID, afterRuleID, beforeRuleID string, body io.Reader, device *userapi.Device, userAPI userapi.UserInternalAPI) util.JSONResponse {
+ var newRule pushrules.Rule
+ if err := json.NewDecoder(body).Decode(&newRule); err != nil {
+ return errorResponse(ctx, err, "JSON Decode failed")
+ }
+ newRule.RuleID = ruleID
+
+ errs := pushrules.ValidateRule(pushrules.Kind(kind), &newRule)
+ if len(errs) > 0 {
+ return errorResponse(ctx, jsonerror.InvalidArgumentValue(errs[0].Error()), "rule sanity check failed: %v", errs)
+ }
+
+ ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
+ if err != nil {
+ return errorResponse(ctx, err, "queryPushRules failed")
+ }
+ ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
+ if ruleSet == nil {
+ return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rule set"), "pushRuleSetByScope failed")
+ }
+ rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
+ if rulesPtr == nil {
+ return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rules kind"), "pushRuleSetKindPointer failed")
+ }
+ i := pushRuleIndexByID(*rulesPtr, ruleID)
+ if i >= 0 && afterRuleID == "" && beforeRuleID == "" {
+ // Modify rule at the same index.
+
+ // TODO: The spec does not say what to do in this case, but
+ // this feels reasonable.
+ *((*rulesPtr)[i]) = newRule
+ util.GetLogger(ctx).Infof("Modified existing push rule at %d", i)
+ } else {
+ if i >= 0 {
+ // Delete old rule.
+ *rulesPtr = append((*rulesPtr)[:i], (*rulesPtr)[i+1:]...)
+ util.GetLogger(ctx).Infof("Deleted old push rule at %d", i)
+ } else {
+ // SPEC: When creating push rules, they MUST be enabled by default.
+ //
+ // TODO: it's unclear if we must reject disabled rules, or force
+ // the value to true. Sytests fail if we don't force it.
+ newRule.Enabled = true
+ }
+
+ // Add new rule.
+ i, err := findPushRuleInsertionIndex(*rulesPtr, afterRuleID, beforeRuleID)
+ if err != nil {
+ return errorResponse(ctx, err, "findPushRuleInsertionIndex failed")
+ }
+
+ *rulesPtr = append((*rulesPtr)[:i], append([]*pushrules.Rule{&newRule}, (*rulesPtr)[i:]...)...)
+ util.GetLogger(ctx).WithField("after", afterRuleID).WithField("before", beforeRuleID).Infof("Added new push rule at %d", i)
+ }
+
+ if err := putPushRules(ctx, device.UserID, ruleSets, userAPI); err != nil {
+ return errorResponse(ctx, err, "putPushRules failed")
+ }
+
+ return util.JSONResponse{Code: http.StatusOK, JSON: struct{}{}}
+}
+
+func DeletePushRuleByRuleID(ctx context.Context, scope, kind, ruleID string, device *userapi.Device, userAPI userapi.UserInternalAPI) util.JSONResponse {
+ ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
+ if err != nil {
+ return errorResponse(ctx, err, "queryPushRules failed")
+ }
+ ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
+ if ruleSet == nil {
+ return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rule set"), "pushRuleSetByScope failed")
+ }
+ rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
+ if rulesPtr == nil {
+ return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rules kind"), "pushRuleSetKindPointer failed")
+ }
+ i := pushRuleIndexByID(*rulesPtr, ruleID)
+ if i < 0 {
+ return errorResponse(ctx, jsonerror.NotFound("push rule ID not found"), "pushRuleIndexByID failed")
+ }
+
+ *rulesPtr = append((*rulesPtr)[:i], (*rulesPtr)[i+1:]...)
+
+ if err := putPushRules(ctx, device.UserID, ruleSets, userAPI); err != nil {
+ return errorResponse(ctx, err, "putPushRules failed")
+ }
+
+ return util.JSONResponse{Code: http.StatusOK, JSON: struct{}{}}
+}
+
+func GetPushRuleAttrByRuleID(ctx context.Context, scope, kind, ruleID, attr string, device *userapi.Device, userAPI userapi.UserInternalAPI) util.JSONResponse {
+ attrGet, err := pushRuleAttrGetter(attr)
+ if err != nil {
+ return errorResponse(ctx, err, "pushRuleAttrGetter failed")
+ }
+ ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
+ if err != nil {
+ return errorResponse(ctx, err, "queryPushRules failed")
+ }
+ ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
+ if ruleSet == nil {
+ return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rule set"), "pushRuleSetByScope failed")
+ }
+ rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
+ if rulesPtr == nil {
+ return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rules kind"), "pushRuleSetKindPointer failed")
+ }
+ i := pushRuleIndexByID(*rulesPtr, ruleID)
+ if i < 0 {
+ return errorResponse(ctx, jsonerror.NotFound("push rule ID not found"), "pushRuleIndexByID failed")
+ }
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: map[string]interface{}{
+ attr: attrGet((*rulesPtr)[i]),
+ },
+ }
+}
+
+func PutPushRuleAttrByRuleID(ctx context.Context, scope, kind, ruleID, attr string, body io.Reader, device *userapi.Device, userAPI userapi.UserInternalAPI) util.JSONResponse {
+ var newPartialRule pushrules.Rule
+ if err := json.NewDecoder(body).Decode(&newPartialRule); err != nil {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON(err.Error()),
+ }
+ }
+ if newPartialRule.Actions == nil {
+ // This ensures json.Marshal encodes the empty list as [] rather than null.
+ newPartialRule.Actions = []*pushrules.Action{}
+ }
+
+ attrGet, err := pushRuleAttrGetter(attr)
+ if err != nil {
+ return errorResponse(ctx, err, "pushRuleAttrGetter failed")
+ }
+ attrSet, err := pushRuleAttrSetter(attr)
+ if err != nil {
+ return errorResponse(ctx, err, "pushRuleAttrSetter failed")
+ }
+
+ ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
+ if err != nil {
+ return errorResponse(ctx, err, "queryPushRules failed")
+ }
+ ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
+ if ruleSet == nil {
+ return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rule set"), "pushRuleSetByScope failed")
+ }
+ rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
+ if rulesPtr == nil {
+ return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rules kind"), "pushRuleSetKindPointer failed")
+ }
+ i := pushRuleIndexByID(*rulesPtr, ruleID)
+ if i < 0 {
+ return errorResponse(ctx, jsonerror.NotFound("push rule ID not found"), "pushRuleIndexByID failed")
+ }
+
+ if !reflect.DeepEqual(attrGet((*rulesPtr)[i]), attrGet(&newPartialRule)) {
+ attrSet((*rulesPtr)[i], &newPartialRule)
+
+ if err := putPushRules(ctx, device.UserID, ruleSets, userAPI); err != nil {
+ return errorResponse(ctx, err, "putPushRules failed")
+ }
+ }
+
+ return util.JSONResponse{Code: http.StatusOK, JSON: struct{}{}}
+}
+
+func queryPushRules(ctx context.Context, userID string, userAPI userapi.UserInternalAPI) (*pushrules.AccountRuleSets, error) {
+ var res userapi.QueryPushRulesResponse
+ if err := userAPI.QueryPushRules(ctx, &userapi.QueryPushRulesRequest{UserID: userID}, &res); err != nil {
+ util.GetLogger(ctx).WithError(err).Error("userAPI.QueryPushRules failed")
+ return nil, err
+ }
+ return res.RuleSets, nil
+}
+
+func putPushRules(ctx context.Context, userID string, ruleSets *pushrules.AccountRuleSets, userAPI userapi.UserInternalAPI) error {
+ req := userapi.PerformPushRulesPutRequest{
+ UserID: userID,
+ RuleSets: ruleSets,
+ }
+ var res struct{}
+ if err := userAPI.PerformPushRulesPut(ctx, &req, &res); err != nil {
+ util.GetLogger(ctx).WithError(err).Error("userAPI.PerformPushRulesPut failed")
+ return err
+ }
+ return nil
+}
+
+func pushRuleSetByScope(ruleSets *pushrules.AccountRuleSets, scope pushrules.Scope) *pushrules.RuleSet {
+ switch scope {
+ case pushrules.GlobalScope:
+ return &ruleSets.Global
+ default:
+ return nil
+ }
+}
+
+func pushRuleSetKindPointer(ruleSet *pushrules.RuleSet, kind pushrules.Kind) *[]*pushrules.Rule {
+ switch kind {
+ case pushrules.OverrideKind:
+ return &ruleSet.Override
+ case pushrules.ContentKind:
+ return &ruleSet.Content
+ case pushrules.RoomKind:
+ return &ruleSet.Room
+ case pushrules.SenderKind:
+ return &ruleSet.Sender
+ case pushrules.UnderrideKind:
+ return &ruleSet.Underride
+ default:
+ return nil
+ }
+}
+
+func pushRuleIndexByID(rules []*pushrules.Rule, id string) int {
+ for i, rule := range rules {
+ if rule.RuleID == id {
+ return i
+ }
+ }
+ return -1
+}
+
+func pushRuleAttrGetter(attr string) (func(*pushrules.Rule) interface{}, error) {
+ switch attr {
+ case "actions":
+ return func(rule *pushrules.Rule) interface{} { return rule.Actions }, nil
+ case "enabled":
+ return func(rule *pushrules.Rule) interface{} { return rule.Enabled }, nil
+ default:
+ return nil, jsonerror.InvalidArgumentValue("invalid push rule attribute")
+ }
+}
+
+func pushRuleAttrSetter(attr string) (func(dest, src *pushrules.Rule), error) {
+ switch attr {
+ case "actions":
+ return func(dest, src *pushrules.Rule) { dest.Actions = src.Actions }, nil
+ case "enabled":
+ return func(dest, src *pushrules.Rule) { dest.Enabled = src.Enabled }, nil
+ default:
+ return nil, jsonerror.InvalidArgumentValue("invalid push rule attribute")
+ }
+}
+
+func findPushRuleInsertionIndex(rules []*pushrules.Rule, afterID, beforeID string) (int, error) {
+ var i int
+
+ if afterID != "" {
+ for ; i < len(rules); i++ {
+ if rules[i].RuleID == afterID {
+ break
+ }
+ }
+ if i == len(rules) {
+ return 0, jsonerror.NotFound("after: rule ID not found")
+ }
+ if rules[i].Default {
+ return 0, jsonerror.NotFound("after: rule ID must not be a default rule")
+ }
+ // We stopped on the "after" match to differentiate
+ // not-found from is-last-entry. Now we move to the earliest
+ // insertion point.
+ i++
+ }
+
+ if beforeID != "" {
+ for ; i < len(rules); i++ {
+ if rules[i].RuleID == beforeID {
+ break
+ }
+ }
+ if i == len(rules) {
+ return 0, jsonerror.NotFound("before: rule ID not found")
+ }
+ if rules[i].Default {
+ return 0, jsonerror.NotFound("before: rule ID must not be a default rule")
+ }
+ }
+
+ // UNSPEC: The spec does not say what to do if no after/before is
+ // given. Sytest fails if it doesn't go first.
+ return i, nil
+}
diff --git a/clientapi/routing/room_tagging.go b/clientapi/routing/room_tagging.go
index c683cc94..83294b18 100644
--- a/clientapi/routing/room_tagging.go
+++ b/clientapi/routing/room_tagging.go
@@ -98,7 +98,7 @@ func PutTag(
return jsonerror.InternalServerError()
}
- if err = syncProducer.SendData(userID, roomID, "m.tag"); err != nil {
+ if err = syncProducer.SendData(userID, roomID, "m.tag", nil); err != nil {
logrus.WithError(err).Error("Failed to send m.tag account data update to syncapi")
}
@@ -151,7 +151,7 @@ func DeleteTag(
}
// TODO: user API should do this since it's account data
- if err := syncProducer.SendData(userID, roomID, "m.tag"); err != nil {
+ if err := syncProducer.SendData(userID, roomID, "m.tag", nil); err != nil {
logrus.WithError(err).Error("Failed to send m.tag account data update to syncapi")
}
diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go
index d75f58b8..d22fbd80 100644
--- a/clientapi/routing/routing.go
+++ b/clientapi/routing/routing.go
@@ -16,7 +16,6 @@ package routing
import (
"context"
- "encoding/json"
"net/http"
"strings"
@@ -561,25 +560,142 @@ func Setup(
}),
).Methods(http.MethodGet, http.MethodPost, http.MethodOptions)
+ // Push rules
+
+ v3mux.Handle("/pushrules",
+ httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.InvalidArgumentValue("missing trailing slash"),
+ }
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
v3mux.Handle("/pushrules/",
- httputil.MakeExternalAPI("push_rules", func(req *http.Request) util.JSONResponse {
- // TODO: Implement push rules API
- res := json.RawMessage(`{
- "global": {
- "content": [],
- "override": [],
- "room": [],
- "sender": [],
- "underride": []
- }
- }`)
+ httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
+ return GetAllPushRules(req.Context(), device, userAPI)
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ v3mux.Handle("/pushrules/",
+ httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
return util.JSONResponse{
- Code: http.StatusOK,
- JSON: &res,
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.InvalidArgumentValue("scope, kind and rule ID must be specified"),
+ }
+ }),
+ ).Methods(http.MethodPut)
+
+ v3mux.Handle("/pushrules/{scope}/",
+ httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
+ vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
+ if err != nil {
+ return util.ErrorResponse(err)
+ }
+ return GetPushRulesByScope(req.Context(), vars["scope"], device, userAPI)
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ v3mux.Handle("/pushrules/{scope}",
+ httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.InvalidArgumentValue("missing trailing slash after scope"),
+ }
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ v3mux.Handle("/pushrules/{scope:[^/]+/?}",
+ httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.InvalidArgumentValue("kind and rule ID must be specified"),
+ }
+ }),
+ ).Methods(http.MethodPut)
+
+ v3mux.Handle("/pushrules/{scope}/{kind}/",
+ httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
+ vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
+ if err != nil {
+ return util.ErrorResponse(err)
+ }
+ return GetPushRulesByKind(req.Context(), vars["scope"], vars["kind"], device, userAPI)
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ v3mux.Handle("/pushrules/{scope}/{kind}",
+ httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.InvalidArgumentValue("missing trailing slash after kind"),
+ }
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ v3mux.Handle("/pushrules/{scope}/{kind:[^/]+/?}",
+ httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.InvalidArgumentValue("rule ID must be specified"),
+ }
+ }),
+ ).Methods(http.MethodPut)
+
+ v3mux.Handle("/pushrules/{scope}/{kind}/{ruleID}",
+ httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
+ vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
+ if err != nil {
+ return util.ErrorResponse(err)
}
+ return GetPushRuleByRuleID(req.Context(), vars["scope"], vars["kind"], vars["ruleID"], device, userAPI)
}),
).Methods(http.MethodGet, http.MethodOptions)
+ v3mux.Handle("/pushrules/{scope}/{kind}/{ruleID}",
+ httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
+ if r := rateLimits.Limit(req); r != nil {
+ return *r
+ }
+ vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
+ if err != nil {
+ return util.ErrorResponse(err)
+ }
+ query := req.URL.Query()
+ return PutPushRuleByRuleID(req.Context(), vars["scope"], vars["kind"], vars["ruleID"], query.Get("after"), query.Get("before"), req.Body, device, userAPI)
+ }),
+ ).Methods(http.MethodPut)
+
+ v3mux.Handle("/pushrules/{scope}/{kind}/{ruleID}",
+ httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
+ vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
+ if err != nil {
+ return util.ErrorResponse(err)
+ }
+ return DeletePushRuleByRuleID(req.Context(), vars["scope"], vars["kind"], vars["ruleID"], device, userAPI)
+ }),
+ ).Methods(http.MethodDelete)
+
+ v3mux.Handle("/pushrules/{scope}/{kind}/{ruleID}/{attr}",
+ httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
+ vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
+ if err != nil {
+ return util.ErrorResponse(err)
+ }
+ return GetPushRuleAttrByRuleID(req.Context(), vars["scope"], vars["kind"], vars["ruleID"], vars["attr"], device, userAPI)
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ v3mux.Handle("/pushrules/{scope}/{kind}/{ruleID}/{attr}",
+ httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
+ vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
+ if err != nil {
+ return util.ErrorResponse(err)
+ }
+ return PutPushRuleAttrByRuleID(req.Context(), vars["scope"], vars["kind"], vars["ruleID"], vars["attr"], req.Body, device, userAPI)
+ }),
+ ).Methods(http.MethodPut)
+
// Element user settings
v3mux.Handle("/profile/{userID}",
@@ -885,6 +1001,27 @@ func Setup(
}),
).Methods(http.MethodPost, http.MethodOptions)
+ v3mux.Handle("/notifications",
+ httputil.MakeAuthAPI("get_notifications", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
+ return GetNotifications(req, device, userAPI)
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ v3mux.Handle("/pushers",
+ httputil.MakeAuthAPI("get_pushers", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
+ return GetPushers(req, device, userAPI)
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ v3mux.Handle("/pushers/set",
+ httputil.MakeAuthAPI("set_pushers", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
+ if r := rateLimits.Limit(req); r != nil {
+ return *r
+ }
+ return SetPusher(req, device, userAPI)
+ }),
+ ).Methods(http.MethodPost, http.MethodOptions)
+
// Stub implementations for sytest
v3mux.Handle("/events",
httputil.MakeExternalAPI("events", func(req *http.Request) util.JSONResponse {