Implements room tagging. (#694)
parent
3578d77d25
commit
d283676b9a
|
@ -0,0 +1,234 @@
|
||||||
|
// 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))
|
||||||
|
}
|
|
@ -483,4 +483,34 @@ func Setup(
|
||||||
}}
|
}}
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodGet, http.MethodOptions)
|
).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)
|
||||||
}
|
}
|
||||||
|
|
6
testfile
6
testfile
|
@ -159,3 +159,9 @@ Inbound federation rejects remote attempts to kick local users to rooms
|
||||||
An event which redacts itself should be ignored
|
An event which redacts itself should be ignored
|
||||||
A pair of events which redact each other should be ignored
|
A pair of events which redact each other should be ignored
|
||||||
Full state sync includes joined rooms
|
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
|
||||||
|
|
Loading…
Reference in New Issue