diff --git a/clientapi/routing/notification.go b/clientapi/routing/notification.go new file mode 100644 index 000000000..277248158 --- /dev/null +++ b/clientapi/routing/notification.go @@ -0,0 +1,64 @@ +// Copyright 2021 Dan Peleg +// +// 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" + pushserverapi "github.com/matrix-org/dendrite/pushserver/api" + 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, + psAPI pushserverapi.PushserverInternalAPI, +) 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 pushserverapi.QueryNotificationsResponse + localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID) + if err != nil { + util.GetLogger(req.Context()).WithError(err).Error("SplitID failed") + return jsonerror.InternalServerError() + } + err = psAPI.QueryNotifications(req.Context(), &pushserverapi.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/routing.go b/clientapi/routing/routing.go index 67814bf88..ee65629d0 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -951,6 +951,12 @@ func Setup( }), ).Methods(http.MethodPost, http.MethodOptions) + unstableMux.Handle("/notifications", + httputil.MakeAuthAPI("get_notifications", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + return GetNotifications(req, device, psAPI) + }), + ).Methods(http.MethodGet, http.MethodOptions) + r0mux.Handle("/pushers", httputil.MakeAuthAPI("get_pushers", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { return GetPushers(req, device, psAPI) diff --git a/pushserver/api/api.go b/pushserver/api/api.go index b3dee52f8..19eda3b6f 100644 --- a/pushserver/api/api.go +++ b/pushserver/api/api.go @@ -14,6 +14,8 @@ type PushserverInternalAPI interface { PerformPushRulesPut(ctx context.Context, req *PerformPushRulesPutRequest, res *struct{}) error QueryPushRules(ctx context.Context, req *QueryPushRulesRequest, res *QueryPushRulesResponse) error + + QueryNotifications(ctx context.Context, req *QueryNotificationsRequest, res *QueryNotificationsResponse) error } type QueryPushersRequest struct { @@ -69,6 +71,18 @@ type QueryPushRulesResponse struct { RuleSets *pushrules.AccountRuleSets `json:"rule_sets"` } +type QueryNotificationsRequest struct { + Localpart string `json:"localpart"` // Required. + From string `json:"from,omitempty"` + Limit int `json:"limit,omitempty"` + Only string `json:"only,omitempty"` +} + +type QueryNotificationsResponse struct { + NextToken string `json:"next_token"` + Notifications []*Notification `json:"notifications"` // Required. +} + type Notification struct { Actions []*pushrules.Action `json:"actions"` // Required. Event gomatrixserverlib.ClientEvent `json:"event"` // Required. diff --git a/pushserver/internal/api.go b/pushserver/internal/api.go index a1bdfac75..2c595c33e 100644 --- a/pushserver/internal/api.go +++ b/pushserver/internal/api.go @@ -4,12 +4,14 @@ import ( "context" "encoding/json" "fmt" + "strconv" "time" "github.com/matrix-org/dendrite/internal/pushrules" "github.com/matrix-org/dendrite/pushserver/api" "github.com/matrix-org/dendrite/pushserver/producers" "github.com/matrix-org/dendrite/pushserver/storage" + "github.com/matrix-org/dendrite/pushserver/storage/tables" "github.com/matrix-org/dendrite/setup/config" uapi "github.com/matrix-org/dendrite/userapi/api" "github.com/matrix-org/gomatrixserverlib" @@ -37,6 +39,38 @@ func NewPushserverAPI( return a } +func (a *PushserverInternalAPI) QueryNotifications(ctx context.Context, req *api.QueryNotificationsRequest, res *api.QueryNotificationsResponse) error { + if req.Limit == 0 || req.Limit > 1000 { + req.Limit = 1000 + } + + var fromID int64 + var err error + if req.From != "" { + fromID, err = strconv.ParseInt(req.From, 10, 64) + if err != nil { + return fmt.Errorf("QueryNotifications: parsing 'from': %w", err) + } + } + var filter storage.NotificationFilter = tables.AllNotifications + if req.Only == "highlight" { + filter = tables.HighlightNotifications + } + notifs, lastID, err := a.DB.GetNotifications(ctx, req.Localpart, fromID, req.Limit, filter) + if err != nil { + return err + } + if notifs == nil { + // This ensures empty is JSON-encoded as [] instead of null. + notifs = []*api.Notification{} + } + res.Notifications = notifs + if lastID >= 0 { + res.NextToken = strconv.FormatInt(lastID+1, 10) + } + return nil +} + func (a *PushserverInternalAPI) PerformPusherSet(ctx context.Context, req *api.PerformPusherSetRequest, res *struct{}) error { util.GetLogger(ctx).WithFields(logrus.Fields{ "localpart": req.Localpart, diff --git a/pushserver/inthttp/client.go b/pushserver/inthttp/client.go index 1de62fbe8..1c528d69f 100644 --- a/pushserver/inthttp/client.go +++ b/pushserver/inthttp/client.go @@ -41,6 +41,13 @@ func NewPushserverClient( }, nil } +func (h *httpPushserverInternalAPI) QueryNotifications(ctx context.Context, req *api.QueryNotificationsRequest, res *api.QueryNotificationsResponse) error { + span, ctx := opentracing.StartSpanFromContext(ctx, "QueryNotifications") + defer span.Finish() + + return httputil.PostJSON(ctx, span, h.httpClient, h.pushserverURL+QueryNotificationsPath, req, res) +} + func (h *httpPushserverInternalAPI) PerformPusherSet( ctx context.Context, request *api.PerformPusherSetRequest, diff --git a/pushserver/inthttp/server.go b/pushserver/inthttp/server.go index 9482d1955..0e7451848 100644 --- a/pushserver/inthttp/server.go +++ b/pushserver/inthttp/server.go @@ -13,6 +13,19 @@ import ( // AddRoutes adds the PushserverInternalAPI handlers to the http.ServeMux. // nolint: gocyclo func AddRoutes(r api.PushserverInternalAPI, internalAPIMux *mux.Router) { + internalAPIMux.Handle(QueryNotificationsPath, + httputil.MakeInternalAPI("queryNotifications", func(req *http.Request) util.JSONResponse { + var request api.QueryNotificationsRequest + var response api.QueryNotificationsResponse + if err := json.NewDecoder(req.Body).Decode(&request); err != nil { + return util.MessageResponse(http.StatusBadRequest, err.Error()) + } + if err := r.QueryNotifications(req.Context(), &request, &response); err != nil { + return util.ErrorResponse(err) + } + return util.JSONResponse{Code: http.StatusOK, JSON: &response} + }), + ) internalAPIMux.Handle(PerformPusherSetPath, httputil.MakeInternalAPI("performPusherSet", func(req *http.Request) util.JSONResponse {