Implements room tagging.
ADD
clientapi/routing/room_tagging.go
+// Copyright 2019 Sumukha PK
+//
+// 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 (
+ "encoding/json"
+ "net/http"
+
+ "github.com/sirupsen/logrus"
+
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+ "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
+ "github.com/matrix-org/dendrite/clientapi/httputil"
+ "github.com/matrix-org/dendrite/clientapi/jsonerror"
+ "github.com/matrix-org/dendrite/clientapi/producers"
+ "github.com/matrix-org/gomatrix"
+ "github.com/matrix-org/gomatrixserverlib"
+ "github.com/matrix-org/util"
+)
+
+// newTag creates and returns a new gomatrix.TagContent
+func newTag() gomatrix.TagContent {
+ return gomatrix.TagContent{
+ Tags: make(map[string]gomatrix.TagProperties),
+ }
+}
+
+// GetTags implements GET /_matrix/client/r0/user/{userID}/rooms/{roomID}/tags
+func GetTags(
+ req *http.Request,
+ accountDB *accounts.Database,
+ device *authtypes.Device,
+ userID string,
+ roomID string,
+ syncProducer *producers.SyncAPIProducer,
+) util.JSONResponse {
+
+ if device.UserID != userID {
+ return util.JSONResponse{
+ Code: http.StatusForbidden,
+ JSON: jsonerror.Forbidden("Cannot retrieve another user's tags"),
+ }
+ }
+
+ _, data, err := obtainSavedTags(req, userID, roomID, accountDB)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ if len(data) == 0 {
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct{}{},
+ }
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: data[0].Content,
+ }
+}
+
+// PutTag implements PUT /_matrix/client/r0/user/{userID}/rooms/{roomID}/tags/{tag}
+// Put functionality works by getting existing data from the DB (if any), adding
+// the tag to the "map" and saving the new "map" to the DB
+func PutTag(
+ req *http.Request,
+ accountDB *accounts.Database,
+ device *authtypes.Device,
+ userID string,
+ roomID string,
+ tag string,
+ syncProducer *producers.SyncAPIProducer,
+) util.JSONResponse {
+
+ if device.UserID != userID {
+ return util.JSONResponse{
+ Code: http.StatusForbidden,
+ JSON: jsonerror.Forbidden("Cannot modify another user's tags"),
+ }
+ }
+
+ var properties gomatrix.TagProperties
+ if reqErr := httputil.UnmarshalJSONRequest(req, &properties); reqErr != nil {
+ return *reqErr
+ }
+
+ localpart, data, err := obtainSavedTags(req, userID, roomID, accountDB)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ var tagContent gomatrix.TagContent
+ if len(data) > 0 {
+ if err = json.Unmarshal(data[0].Content, &tagContent); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+ } else {
+ tagContent = newTag()
+ }
+ tagContent.Tags[tag] = properties
+ if err = saveTagData(req, localpart, roomID, accountDB, tagContent); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ // Send data to syncProducer in order to inform clients of changes
+ // Run in a goroutine in order to prevent blocking the tag request response
+ go func() {
+ if err := syncProducer.SendData(userID, roomID, "m.tag"); err != nil {
+ logrus.WithError(err).Error("Failed to send m.tag account data update to syncapi")
+ }
+ }()
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct{}{},
+ }
+}
+
+// DeleteTag implements DELETE /_matrix/client/r0/user/{userID}/rooms/{roomID}/tags/{tag}
+// Delete functionality works by obtaining the saved tags, removing the intended tag from
+// the "map" and then saving the new "map" in the DB
+func DeleteTag(
+ req *http.Request,
+ accountDB *accounts.Database,
+ device *authtypes.Device,
+ userID string,
+ roomID string,
+ tag string,
+ syncProducer *producers.SyncAPIProducer,
+) util.JSONResponse {
+
+ if device.UserID != userID {
+ return util.JSONResponse{
+ Code: http.StatusForbidden,
+ JSON: jsonerror.Forbidden("Cannot modify another user's tags"),
+ }
+ }
+
+ localpart, data, err := obtainSavedTags(req, userID, roomID, accountDB)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ // If there are no tags in the database, exit
+ if len(data) == 0 {
+ // Spec only defines 200 responses for this endpoint so we don't return anything else.
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct{}{},
+ }
+ }
+
+ var tagContent gomatrix.TagContent
+ err = json.Unmarshal(data[0].Content, &tagContent)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ // Check whether the tag to be deleted exists
+ if _, ok := tagContent.Tags[tag]; ok {
+ delete(tagContent.Tags, tag)
+ } else {
+ // Spec only defines 200 responses for this endpoint so we don't return anything else.
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct{}{},
+ }
+ }
+ if err = saveTagData(req, localpart, roomID, accountDB, tagContent); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ // Send data to syncProducer in order to inform clients of changes
+ // Run in a goroutine in order to prevent blocking the tag request response
+ go func() {
+ if err := syncProducer.SendData(userID, roomID, "m.tag"); err != nil {
+ logrus.WithError(err).Error("Failed to send m.tag account data update to syncapi")
+ }
+ }()
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct{}{},
+ }
+}
+
+// obtainSavedTags gets all tags scoped to a userID and roomID
+// from the database
+func obtainSavedTags(
+ req *http.Request,
+ userID string,
+ roomID string,
+ accountDB *accounts.Database,
+) (string, []gomatrixserverlib.ClientEvent, error) {
+ localpart, _, err := gomatrixserverlib.SplitID('@', userID)
+ if err != nil {
+ return "", nil, err
+ }
+
+ data, err := accountDB.GetAccountDataByType(
+ req.Context(), localpart, roomID, "m.tag",
+ )
+
+ return localpart, data, err
+}
+
+// saveTagData saves the provided tag data into the database
+func saveTagData(
+ req *http.Request,
+ localpart string,
+ roomID string,
+ accountDB *accounts.Database,
+ Tag gomatrix.TagContent,
+) error {
+ newTagData, err := json.Marshal(Tag)
+ if err != nil {
+ return err
+ }
+
+ return accountDB.SaveAccountData(req.Context(), localpart, roomID, "m.tag", string(newTagData))
+}

MODIFY
clientapi/routing/routing.go
clientapi/routing/routing.go
@@ -483,4 +483,34 @@ func Setup(
}}
}),
).Methods(http.MethodGet, http.MethodOptions)
+
+ r0mux.Handle("/user/{userId}/rooms/{roomId}/tags",
+ common.MakeAuthAPI("get_tags", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ vars, err := common.URLDecodeMapValues(mux.Vars(req))
+ if err != nil {
+ return util.ErrorResponse(err)
+ }
+ return GetTags(req, accountDB, device, vars["userId"], vars["roomId"], syncProducer)
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ r0mux.Handle("/user/{userId}/rooms/{roomId}/tags/{tag}",
+ common.MakeAuthAPI("put_tag", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ vars, err := common.URLDecodeMapValues(mux.Vars(req))
+ if err != nil {
+ return util.ErrorResponse(err)
+ }
+ return PutTag(req, accountDB, device, vars["userId"], vars["roomId"], vars["tag"], syncProducer)
+ }),
+ ).Methods(http.MethodPut, http.MethodOptions)
+
+ r0mux.Handle("/user/{userId}/rooms/{roomId}/tags/{tag}",
+ common.MakeAuthAPI("delete_tag", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ vars, err := common.URLDecodeMapValues(mux.Vars(req))
+ if err != nil {
+ return util.ErrorResponse(err)
+ }
+ return DeleteTag(req, accountDB, device, vars["userId"], vars["roomId"], vars["tag"], syncProducer)
+ }),
+ ).Methods(http.MethodDelete, http.MethodOptions)
}

MODIFY
testfile
testfile
@@ -159,3 +159,9 @@ Inbound federation rejects remote attempts to kick local users to rooms
An event which redacts itself should be ignored
A pair of events which redact each other should be ignored
Full state sync includes joined rooms
+Can add tag
+Can remove tag
+Can list tags for a room
+Tags appear in an initial v2 /sync
+Newly updated tags appear in an incremental v2 /sync
+Deleted tags appear in an incremental v2 /sync

Implements room tagging.  (#694)