2017-08-04 15:32:10 +00:00
// Copyright 2017 Vector Creations Ltd
//
// 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.
2017-10-11 17:16:53 +00:00
package routing
2017-08-04 15:32:10 +00:00
import (
2017-09-13 12:37:50 +00:00
"context"
2017-09-11 18:18:19 +00:00
"errors"
2017-08-04 15:32:10 +00:00
"net/http"
2018-08-06 13:09:25 +00:00
"time"
2017-08-04 15:32:10 +00:00
2018-08-20 09:45:17 +00:00
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
2017-08-04 15:32:10 +00:00
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/jsonerror"
2017-09-01 09:13:10 +00:00
"github.com/matrix-org/dendrite/clientapi/threepid"
2020-06-12 13:55:57 +00:00
"github.com/matrix-org/dendrite/internal/eventutil"
2018-08-20 09:45:17 +00:00
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
2020-12-02 17:41:00 +00:00
"github.com/matrix-org/dendrite/setup/config"
2020-06-16 13:10:55 +00:00
userapi "github.com/matrix-org/dendrite/userapi/api"
2020-06-17 11:05:56 +00:00
"github.com/matrix-org/dendrite/userapi/storage/accounts"
2017-08-04 15:32:10 +00:00
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
)
2017-09-11 18:18:19 +00:00
var errMissingUserID = errors . New ( "'user_id' must be supplied" )
2020-06-24 17:19:54 +00:00
func SendBan (
2020-06-16 13:10:55 +00:00
req * http . Request , accountDB accounts . Database , device * userapi . Device ,
2020-08-10 13:18:04 +00:00
roomID string , cfg * config . ClientAPI ,
2020-05-01 09:48:17 +00:00
rsAPI roomserverAPI . RoomserverInternalAPI , asAPI appserviceAPI . AppServiceQueryAPI ,
2017-08-04 15:32:10 +00:00
) util . JSONResponse {
2020-06-24 17:19:54 +00:00
body , evTime , roomVer , reqErr := extractRequestData ( req , roomID , rsAPI )
if reqErr != nil {
return * reqErr
}
2021-07-19 17:33:05 +00:00
errRes := checkMemberInRoom ( req . Context ( ) , rsAPI , device . UserID , roomID )
if errRes != nil {
return * errRes
}
plEvent := roomserverAPI . GetStateEvent ( req . Context ( ) , rsAPI , roomID , gomatrixserverlib . StateKeyTuple {
EventType : gomatrixserverlib . MRoomPowerLevels ,
StateKey : "" ,
} )
if plEvent == nil {
return util . JSONResponse {
Code : 403 ,
JSON : jsonerror . Forbidden ( "You don't have permission to ban this user, no power_levels event in this room." ) ,
}
}
pl , err := plEvent . PowerLevels ( )
if err != nil {
return util . JSONResponse {
Code : 403 ,
JSON : jsonerror . Forbidden ( "You don't have permission to ban this user, the power_levels event for this room is malformed so auth checks cannot be performed." ) ,
}
}
allowedToBan := pl . UserLevel ( device . UserID ) >= pl . Ban
if ! allowedToBan {
return util . JSONResponse {
Code : 403 ,
JSON : jsonerror . Forbidden ( "You don't have permission to ban this user, power level too low." ) ,
}
}
2020-06-24 17:19:54 +00:00
return sendMembership ( req . Context ( ) , accountDB , device , roomID , "ban" , body . Reason , cfg , body . UserID , evTime , roomVer , rsAPI , asAPI )
}
func sendMembership ( ctx context . Context , accountDB accounts . Database , device * userapi . Device ,
2020-08-10 13:18:04 +00:00
roomID , membership , reason string , cfg * config . ClientAPI , targetUserID string , evTime time . Time ,
2020-06-24 17:19:54 +00:00
roomVer gomatrixserverlib . RoomVersion ,
rsAPI roomserverAPI . RoomserverInternalAPI , asAPI appserviceAPI . AppServiceQueryAPI ) util . JSONResponse {
event , err := buildMembershipEvent (
ctx , targetUserID , reason , accountDB , device , membership ,
roomID , false , cfg , evTime , rsAPI , asAPI ,
)
if err == errMissingUserID {
2020-03-27 16:28:22 +00:00
return util . JSONResponse {
Code : http . StatusBadRequest ,
2020-06-24 17:19:54 +00:00
JSON : jsonerror . BadJSON ( err . Error ( ) ) ,
2020-03-27 16:28:22 +00:00
}
2020-06-24 17:19:54 +00:00
} else if err == eventutil . ErrRoomNoExists {
return util . JSONResponse {
Code : http . StatusNotFound ,
JSON : jsonerror . NotFound ( err . Error ( ) ) ,
}
} else if err != nil {
util . GetLogger ( ctx ) . WithError ( err ) . Error ( "buildMembershipEvent failed" )
return jsonerror . InternalServerError ( )
2020-03-27 16:28:22 +00:00
}
2020-09-03 14:22:16 +00:00
if err = roomserverAPI . SendEvents (
2020-06-24 17:19:54 +00:00
ctx , rsAPI ,
2021-09-08 16:31:03 +00:00
roomserverAPI . KindNew ,
2020-11-16 15:44:53 +00:00
[ ] * gomatrixserverlib . HeaderedEvent { event . Event . Headered ( roomVer ) } ,
2020-06-24 17:19:54 +00:00
cfg . Matrix . ServerName ,
2022-01-27 14:29:14 +00:00
cfg . Matrix . ServerName ,
2020-06-24 17:19:54 +00:00
nil ,
2022-01-05 17:44:49 +00:00
false ,
2020-09-03 14:22:16 +00:00
) ; err != nil {
2020-06-24 17:19:54 +00:00
util . GetLogger ( ctx ) . WithError ( err ) . Error ( "SendEvents failed" )
return jsonerror . InternalServerError ( )
}
return util . JSONResponse {
Code : http . StatusOK ,
JSON : struct { } { } ,
}
}
func SendKick (
req * http . Request , accountDB accounts . Database , device * userapi . Device ,
2020-08-10 13:18:04 +00:00
roomID string , cfg * config . ClientAPI ,
2020-06-24 17:19:54 +00:00
rsAPI roomserverAPI . RoomserverInternalAPI , asAPI appserviceAPI . AppServiceQueryAPI ,
) util . JSONResponse {
body , evTime , roomVer , reqErr := extractRequestData ( req , roomID , rsAPI )
if reqErr != nil {
2017-08-29 14:17:26 +00:00
return * reqErr
}
2020-06-24 17:19:54 +00:00
if body . UserID == "" {
return util . JSONResponse {
Code : 400 ,
JSON : jsonerror . BadJSON ( "missing user_id" ) ,
}
}
2017-08-29 14:17:26 +00:00
2020-09-04 11:30:56 +00:00
errRes := checkMemberInRoom ( req . Context ( ) , rsAPI , device . UserID , roomID )
2020-07-27 08:20:09 +00:00
if errRes != nil {
return * errRes
}
2020-06-24 17:19:54 +00:00
var queryRes roomserverAPI . QueryMembershipForUserResponse
err := rsAPI . QueryMembershipForUser ( req . Context ( ) , & roomserverAPI . QueryMembershipForUserRequest {
RoomID : roomID ,
UserID : body . UserID ,
} , & queryRes )
2018-08-22 12:40:25 +00:00
if err != nil {
2020-06-24 17:19:54 +00:00
return util . ErrorResponse ( err )
}
2020-07-27 08:20:09 +00:00
// kick is only valid if the user is not currently banned or left (that is, they are joined or invited)
if queryRes . Membership != "join" && queryRes . Membership != "invite" {
2018-08-22 12:40:25 +00:00
return util . JSONResponse {
2020-06-24 17:19:54 +00:00
Code : 403 ,
2020-07-27 08:20:09 +00:00
JSON : jsonerror . Unknown ( "cannot /kick banned or left users" ) ,
2018-08-22 12:40:25 +00:00
}
}
2020-06-24 17:19:54 +00:00
// TODO: should we be using SendLeave instead?
return sendMembership ( req . Context ( ) , accountDB , device , roomID , "leave" , body . Reason , cfg , body . UserID , evTime , roomVer , rsAPI , asAPI )
}
func SendUnban (
req * http . Request , accountDB accounts . Database , device * userapi . Device ,
2020-08-10 13:18:04 +00:00
roomID string , cfg * config . ClientAPI ,
2020-06-24 17:19:54 +00:00
rsAPI roomserverAPI . RoomserverInternalAPI , asAPI appserviceAPI . AppServiceQueryAPI ,
) util . JSONResponse {
body , evTime , roomVer , reqErr := extractRequestData ( req , roomID , rsAPI )
if reqErr != nil {
return * reqErr
}
if body . UserID == "" {
return util . JSONResponse {
Code : 400 ,
JSON : jsonerror . BadJSON ( "missing user_id" ) ,
}
}
var queryRes roomserverAPI . QueryMembershipForUserResponse
err := rsAPI . QueryMembershipForUser ( req . Context ( ) , & roomserverAPI . QueryMembershipForUserRequest {
RoomID : roomID ,
UserID : body . UserID ,
} , & queryRes )
if err != nil {
return util . ErrorResponse ( err )
}
// unban is only valid if the user is currently banned
if queryRes . Membership != "ban" {
return util . JSONResponse {
Code : 400 ,
JSON : jsonerror . Unknown ( "can only /unban users that are banned" ) ,
}
}
// TODO: should we be using SendLeave instead?
return sendMembership ( req . Context ( ) , accountDB , device , roomID , "leave" , body . Reason , cfg , body . UserID , evTime , roomVer , rsAPI , asAPI )
}
func SendInvite (
req * http . Request , accountDB accounts . Database , device * userapi . Device ,
2020-08-10 13:18:04 +00:00
roomID string , cfg * config . ClientAPI ,
2020-06-24 17:19:54 +00:00
rsAPI roomserverAPI . RoomserverInternalAPI , asAPI appserviceAPI . AppServiceQueryAPI ,
) util . JSONResponse {
2020-08-17 10:40:49 +00:00
body , evTime , _ , reqErr := extractRequestData ( req , roomID , rsAPI )
2020-06-24 17:19:54 +00:00
if reqErr != nil {
return * reqErr
}
2018-08-22 12:40:25 +00:00
2019-07-12 13:29:30 +00:00
inviteStored , jsonErrResp := checkAndProcessThreepid (
2020-06-24 17:19:54 +00:00
req , device , body , cfg , rsAPI , accountDB , roomID , evTime ,
2017-09-11 18:18:19 +00:00
)
2019-07-12 13:29:30 +00:00
if jsonErrResp != nil {
return * jsonErrResp
2017-08-29 14:17:26 +00:00
}
2017-09-11 18:18:19 +00:00
// If an invite has been stored on an identity server, it means that a
// m.room.third_party_invite event has been emitted and that we shouldn't
// emit a m.room.member one.
if inviteStored {
return util . JSONResponse {
2018-03-13 15:55:45 +00:00
Code : http . StatusOK ,
2017-09-11 18:18:19 +00:00
JSON : struct { } { } ,
}
}
event , err := buildMembershipEvent (
2020-06-24 17:19:54 +00:00
req . Context ( ) , body . UserID , body . Reason , accountDB , device , "invite" ,
2020-05-07 15:46:11 +00:00
roomID , false , cfg , evTime , rsAPI , asAPI ,
2017-09-11 18:18:19 +00:00
)
if err == errMissingUserID {
return util . JSONResponse {
2018-03-13 15:55:45 +00:00
Code : http . StatusBadRequest ,
2017-09-11 18:18:19 +00:00
JSON : jsonerror . BadJSON ( err . Error ( ) ) ,
}
2020-06-12 13:55:57 +00:00
} else if err == eventutil . ErrRoomNoExists {
2017-09-11 18:18:19 +00:00
return util . JSONResponse {
2018-03-13 15:55:45 +00:00
Code : http . StatusNotFound ,
2017-09-11 18:18:19 +00:00
JSON : jsonerror . NotFound ( err . Error ( ) ) ,
}
} else if err != nil {
2020-03-02 16:20:44 +00:00
util . GetLogger ( req . Context ( ) ) . WithError ( err ) . Error ( "buildMembershipEvent failed" )
return jsonerror . InternalServerError ( )
2017-09-11 18:18:19 +00:00
}
2020-08-17 10:40:49 +00:00
err = roomserverAPI . SendInvite (
2020-06-24 17:19:54 +00:00
req . Context ( ) , rsAPI ,
2020-11-16 15:44:53 +00:00
event ,
2020-06-24 17:19:54 +00:00
nil , // ask the roomserver to draw up invite room state for us
cfg . Matrix . ServerName ,
nil ,
)
2020-08-17 10:40:49 +00:00
switch e := err . ( type ) {
case * roomserverAPI . PerformError :
return e . JSONResponse ( )
case nil :
return util . JSONResponse {
Code : http . StatusOK ,
JSON : struct { } { } ,
}
default :
util . GetLogger ( req . Context ( ) ) . WithError ( err ) . Error ( "roomserverAPI.SendInvite failed" )
return util . JSONResponse {
Code : http . StatusInternalServerError ,
JSON : jsonerror . InternalServerError ( ) ,
}
2017-09-11 18:18:19 +00:00
}
}
func buildMembershipEvent (
2018-08-06 13:09:25 +00:00
ctx context . Context ,
2020-06-24 17:19:54 +00:00
targetUserID , reason string , accountDB accounts . Database ,
2020-06-16 13:10:55 +00:00
device * userapi . Device ,
2020-05-07 15:46:11 +00:00
membership , roomID string , isDirect bool ,
2020-08-10 13:18:04 +00:00
cfg * config . ClientAPI , evTime time . Time ,
2020-05-01 09:48:17 +00:00
rsAPI roomserverAPI . RoomserverInternalAPI , asAPI appserviceAPI . AppServiceQueryAPI ,
2020-07-03 16:24:51 +00:00
) ( * gomatrixserverlib . HeaderedEvent , error ) {
2020-06-24 17:19:54 +00:00
profile , err := loadProfile ( ctx , targetUserID , cfg , accountDB , asAPI )
2017-08-04 15:32:10 +00:00
if err != nil {
2017-09-11 18:18:19 +00:00
return nil , err
2017-08-04 15:32:10 +00:00
}
builder := gomatrixserverlib . EventBuilder {
Sender : device . UserID ,
RoomID : roomID ,
Type : "m.room.member" ,
2020-06-24 17:19:54 +00:00
StateKey : & targetUserID ,
2017-08-04 15:32:10 +00:00
}
2019-08-15 17:45:11 +00:00
content := gomatrixserverlib . MemberContent {
2017-08-04 15:32:10 +00:00
Membership : membership ,
DisplayName : profile . DisplayName ,
AvatarURL : profile . AvatarURL ,
Reason : reason ,
2020-05-07 15:46:11 +00:00
IsDirect : isDirect ,
2017-08-04 15:32:10 +00:00
}
if err = builder . SetContent ( content ) ; err != nil {
2017-09-11 18:18:19 +00:00
return nil , err
2017-08-04 15:32:10 +00:00
}
2020-09-02 16:13:15 +00:00
return eventutil . QueryAndBuildEvent ( ctx , & builder , cfg . Matrix , evTime , rsAPI , nil )
2017-08-04 15:32:10 +00:00
}
2017-08-29 14:17:26 +00:00
// loadProfile lookups the profile of a given user from the database and returns
// it if the user is local to this server, or returns an empty profile if not.
// Returns an error if the retrieval failed or if the first parameter isn't a
// valid Matrix ID.
2017-09-18 13:15:27 +00:00
func loadProfile (
2018-08-20 09:45:17 +00:00
ctx context . Context ,
userID string ,
2020-08-10 13:18:04 +00:00
cfg * config . ClientAPI ,
2020-02-13 17:27:33 +00:00
accountDB accounts . Database ,
2018-08-20 09:45:17 +00:00
asAPI appserviceAPI . AppServiceQueryAPI ,
2017-09-18 13:15:27 +00:00
) ( * authtypes . Profile , error ) {
2018-08-20 09:45:17 +00:00
_ , serverName , err := gomatrixserverlib . SplitID ( '@' , userID )
2017-08-29 14:17:26 +00:00
if err != nil {
return nil , err
}
var profile * authtypes . Profile
if serverName == cfg . Matrix . ServerName {
2019-07-12 15:43:01 +00:00
profile , err = appserviceAPI . RetrieveUserProfile ( ctx , userID , asAPI , accountDB )
2017-08-29 14:17:26 +00:00
} else {
profile = & authtypes . Profile { }
}
return profile , err
}
2021-09-08 16:31:03 +00:00
func extractRequestData ( req * http . Request , roomID string , rsAPI roomserverAPI . RoomserverInternalAPI ) (
2020-06-24 17:19:54 +00:00
body * threepid . MembershipRequest , evTime time . Time , roomVer gomatrixserverlib . RoomVersion , resErr * util . JSONResponse ,
) {
2021-09-08 16:31:03 +00:00
verReq := roomserverAPI . QueryRoomVersionForRoomRequest { RoomID : roomID }
verRes := roomserverAPI . QueryRoomVersionForRoomResponse { }
2020-06-24 17:19:54 +00:00
if err := rsAPI . QueryRoomVersionForRoom ( req . Context ( ) , & verReq , & verRes ) ; err != nil {
resErr = & util . JSONResponse {
Code : http . StatusBadRequest ,
JSON : jsonerror . UnsupportedRoomVersion ( err . Error ( ) ) ,
2017-08-04 15:32:10 +00:00
}
2020-06-24 17:19:54 +00:00
return
}
roomVer = verRes . RoomVersion
2017-08-04 15:32:10 +00:00
2020-06-24 17:19:54 +00:00
if reqErr := httputil . UnmarshalJSONRequest ( req , & body ) ; reqErr != nil {
resErr = reqErr
return
2017-08-04 15:32:10 +00:00
}
2020-06-24 17:19:54 +00:00
evTime , err := httputil . ParseTSParam ( req )
if err != nil {
resErr = & util . JSONResponse {
Code : http . StatusBadRequest ,
JSON : jsonerror . InvalidArgumentValue ( err . Error ( ) ) ,
}
return
}
2017-08-04 15:32:10 +00:00
return
}
2019-07-12 13:29:30 +00:00
func checkAndProcessThreepid (
req * http . Request ,
2020-06-16 13:10:55 +00:00
device * userapi . Device ,
2019-07-12 13:29:30 +00:00
body * threepid . MembershipRequest ,
2020-08-10 13:18:04 +00:00
cfg * config . ClientAPI ,
2020-05-01 09:48:17 +00:00
rsAPI roomserverAPI . RoomserverInternalAPI ,
2020-02-13 17:27:33 +00:00
accountDB accounts . Database ,
2020-06-24 17:19:54 +00:00
roomID string ,
2019-07-12 13:29:30 +00:00
evTime time . Time ,
) ( inviteStored bool , errRes * util . JSONResponse ) {
inviteStored , err := threepid . CheckAndProcessInvite (
2020-06-10 11:17:54 +00:00
req . Context ( ) , device , body , cfg , rsAPI , accountDB ,
2020-06-24 17:19:54 +00:00
roomID , evTime ,
2019-07-12 13:29:30 +00:00
)
if err == threepid . ErrMissingParameter {
return inviteStored , & util . JSONResponse {
Code : http . StatusBadRequest ,
JSON : jsonerror . BadJSON ( err . Error ( ) ) ,
}
} else if err == threepid . ErrNotTrusted {
return inviteStored , & util . JSONResponse {
Code : http . StatusBadRequest ,
JSON : jsonerror . NotTrusted ( body . IDServer ) ,
}
2020-06-12 13:55:57 +00:00
} else if err == eventutil . ErrRoomNoExists {
2019-07-12 13:29:30 +00:00
return inviteStored , & util . JSONResponse {
Code : http . StatusNotFound ,
JSON : jsonerror . NotFound ( err . Error ( ) ) ,
}
2020-06-04 09:53:39 +00:00
} else if e , ok := err . ( gomatrixserverlib . BadJSONError ) ; ok {
return inviteStored , & util . JSONResponse {
Code : http . StatusBadRequest ,
JSON : jsonerror . BadJSON ( e . Error ( ) ) ,
}
}
if err != nil {
2020-03-02 16:20:44 +00:00
util . GetLogger ( req . Context ( ) ) . WithError ( err ) . Error ( "threepid.CheckAndProcessInvite failed" )
er := jsonerror . InternalServerError ( )
2019-07-12 13:29:30 +00:00
return inviteStored , & er
}
return
}
2020-07-02 14:41:18 +00:00
2021-09-08 16:31:03 +00:00
func checkMemberInRoom ( ctx context . Context , rsAPI roomserverAPI . RoomserverInternalAPI , userID , roomID string ) * util . JSONResponse {
2020-07-02 14:41:18 +00:00
tuple := gomatrixserverlib . StateKeyTuple {
EventType : gomatrixserverlib . MRoomMember ,
StateKey : userID ,
}
2021-09-08 16:31:03 +00:00
var membershipRes roomserverAPI . QueryCurrentStateResponse
err := rsAPI . QueryCurrentState ( ctx , & roomserverAPI . QueryCurrentStateRequest {
2020-07-02 14:41:18 +00:00
RoomID : roomID ,
StateTuples : [ ] gomatrixserverlib . StateKeyTuple { tuple } ,
} , & membershipRes )
if err != nil {
util . GetLogger ( ctx ) . WithError ( err ) . Error ( "QueryCurrentState: could not query membership for user" )
e := jsonerror . InternalServerError ( )
return & e
}
2020-08-25 20:04:35 +00:00
ev := membershipRes . StateEvents [ tuple ]
if ev == nil {
2020-07-02 14:41:18 +00:00
return & util . JSONResponse {
Code : http . StatusForbidden ,
JSON : jsonerror . Forbidden ( "user does not belong to room" ) ,
}
}
membership , err := ev . Membership ( )
2020-08-25 20:04:35 +00:00
if err != nil {
util . GetLogger ( ctx ) . WithError ( err ) . Error ( "Member event isn't valid" )
e := jsonerror . InternalServerError ( )
return & e
}
if membership != gomatrixserverlib . Join {
2020-07-02 14:41:18 +00:00
return & util . JSONResponse {
Code : http . StatusForbidden ,
JSON : jsonerror . Forbidden ( "user does not belong to room" ) ,
}
}
return nil
}
2020-11-05 10:19:23 +00:00
func SendForget (
req * http . Request , device * userapi . Device ,
roomID string , rsAPI roomserverAPI . RoomserverInternalAPI ,
) util . JSONResponse {
ctx := req . Context ( )
logger := util . GetLogger ( ctx ) . WithField ( "roomID" , roomID ) . WithField ( "userID" , device . UserID )
2021-09-08 16:31:03 +00:00
var membershipRes roomserverAPI . QueryMembershipForUserResponse
membershipReq := roomserverAPI . QueryMembershipForUserRequest {
2020-11-05 10:19:23 +00:00
RoomID : roomID ,
UserID : device . UserID ,
}
err := rsAPI . QueryMembershipForUser ( ctx , & membershipReq , & membershipRes )
if err != nil {
logger . WithError ( err ) . Error ( "QueryMembershipForUser: could not query membership for user" )
return jsonerror . InternalServerError ( )
}
if membershipRes . IsInRoom {
return util . JSONResponse {
Code : http . StatusBadRequest ,
JSON : jsonerror . Forbidden ( "user is still a member of the room" ) ,
}
}
if ! membershipRes . HasBeenInRoom {
return util . JSONResponse {
Code : http . StatusBadRequest ,
JSON : jsonerror . Forbidden ( "user did not belong to room" ) ,
}
}
2021-09-08 16:31:03 +00:00
request := roomserverAPI . PerformForgetRequest {
2020-11-05 10:19:23 +00:00
RoomID : roomID ,
UserID : device . UserID ,
}
2021-09-08 16:31:03 +00:00
response := roomserverAPI . PerformForgetResponse { }
2020-11-05 10:19:23 +00:00
if err := rsAPI . PerformForget ( ctx , & request , & response ) ; err != nil {
logger . WithError ( err ) . Error ( "PerformForget: unable to forget room" )
return jsonerror . InternalServerError ( )
}
return util . JSONResponse {
Code : http . StatusOK ,
JSON : struct { } { } ,
}
}