Skip to content
This repository was archived by the owner on Nov 25, 2024. It is now read-only.

Commit 8f68f1f

Browse files
authored
Move /joined_members back to the clientapi/roomserver (#3312)
Partly reverts #2827 by moving `/joined_members` back to the clientAPI/roomserver
1 parent a4817f3 commit 8f68f1f

File tree

6 files changed

+278
-107
lines changed

6 files changed

+278
-107
lines changed

clientapi/clientapi_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2151,3 +2151,130 @@ func TestKeyBackup(t *testing.T) {
21512151
}
21522152
})
21532153
}
2154+
2155+
func TestGetMembership(t *testing.T) {
2156+
alice := test.NewUser(t)
2157+
bob := test.NewUser(t)
2158+
2159+
testCases := []struct {
2160+
name string
2161+
roomID string
2162+
user *test.User
2163+
additionalEvents func(t *testing.T, room *test.Room)
2164+
request func(t *testing.T, room *test.Room, accessToken string) *http.Request
2165+
wantOK bool
2166+
wantMemberCount int
2167+
}{
2168+
2169+
{
2170+
name: "/joined_members - Bob never joined",
2171+
user: bob,
2172+
request: func(t *testing.T, room *test.Room, accessToken string) *http.Request {
2173+
return test.NewRequest(t, "GET", fmt.Sprintf("/_matrix/client/v3/rooms/%s/joined_members", room.ID), test.WithQueryParams(map[string]string{
2174+
"access_token": accessToken,
2175+
}))
2176+
},
2177+
wantOK: false,
2178+
},
2179+
{
2180+
name: "/joined_members - Alice joined",
2181+
user: alice,
2182+
request: func(t *testing.T, room *test.Room, accessToken string) *http.Request {
2183+
return test.NewRequest(t, "GET", fmt.Sprintf("/_matrix/client/v3/rooms/%s/joined_members", room.ID), test.WithQueryParams(map[string]string{
2184+
"access_token": accessToken,
2185+
}))
2186+
},
2187+
wantOK: true,
2188+
wantMemberCount: 1,
2189+
},
2190+
{
2191+
name: "/joined_members - Alice leaves, shouldn't be able to see members ",
2192+
user: alice,
2193+
request: func(t *testing.T, room *test.Room, accessToken string) *http.Request {
2194+
return test.NewRequest(t, "GET", fmt.Sprintf("/_matrix/client/v3/rooms/%s/joined_members", room.ID), test.WithQueryParams(map[string]string{
2195+
"access_token": accessToken,
2196+
}))
2197+
},
2198+
additionalEvents: func(t *testing.T, room *test.Room) {
2199+
room.CreateAndInsert(t, alice, spec.MRoomMember, map[string]interface{}{
2200+
"membership": "leave",
2201+
}, test.WithStateKey(alice.ID))
2202+
},
2203+
wantOK: false,
2204+
},
2205+
{
2206+
name: "/joined_members - Bob joins, Alice sees two members",
2207+
user: alice,
2208+
request: func(t *testing.T, room *test.Room, accessToken string) *http.Request {
2209+
return test.NewRequest(t, "GET", fmt.Sprintf("/_matrix/client/v3/rooms/%s/joined_members", room.ID), test.WithQueryParams(map[string]string{
2210+
"access_token": accessToken,
2211+
}))
2212+
},
2213+
additionalEvents: func(t *testing.T, room *test.Room) {
2214+
room.CreateAndInsert(t, bob, spec.MRoomMember, map[string]interface{}{
2215+
"membership": "join",
2216+
}, test.WithStateKey(bob.ID))
2217+
},
2218+
wantOK: true,
2219+
wantMemberCount: 2,
2220+
},
2221+
}
2222+
2223+
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
2224+
2225+
cfg, processCtx, close := testrig.CreateConfig(t, dbType)
2226+
routers := httputil.NewRouters()
2227+
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
2228+
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
2229+
defer close()
2230+
natsInstance := jetstream.NATSInstance{}
2231+
jsctx, _ := natsInstance.Prepare(processCtx, &cfg.Global.JetStream)
2232+
defer jetstream.DeleteAllStreams(jsctx, &cfg.Global.JetStream)
2233+
2234+
// Use an actual roomserver for this
2235+
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
2236+
rsAPI.SetFederationAPI(nil, nil)
2237+
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff)
2238+
2239+
// We mostly need the rsAPI for this test, so nil for other APIs/caches etc.
2240+
AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics)
2241+
2242+
accessTokens := map[*test.User]userDevice{
2243+
alice: {},
2244+
bob: {},
2245+
}
2246+
createAccessTokens(t, accessTokens, userAPI, processCtx.Context(), routers)
2247+
2248+
for _, tc := range testCases {
2249+
t.Run(tc.name, func(t *testing.T) {
2250+
room := test.NewRoom(t, alice)
2251+
t.Cleanup(func() {
2252+
t.Logf("running cleanup for %s", tc.name)
2253+
})
2254+
// inject additional events
2255+
if tc.additionalEvents != nil {
2256+
tc.additionalEvents(t, room)
2257+
}
2258+
if err := api.SendEvents(context.Background(), rsAPI, api.KindNew, room.Events(), "test", "test", "test", nil, false); err != nil {
2259+
t.Fatalf("failed to send events: %v", err)
2260+
}
2261+
2262+
w := httptest.NewRecorder()
2263+
routers.Client.ServeHTTP(w, tc.request(t, room, accessTokens[tc.user].accessToken))
2264+
if w.Code != 200 && tc.wantOK {
2265+
t.Logf("%s", w.Body.String())
2266+
t.Fatalf("got HTTP %d want %d", w.Code, 200)
2267+
}
2268+
t.Logf("[%s] Resp: %s", tc.name, w.Body.String())
2269+
2270+
// check we got the expected events
2271+
if tc.wantOK {
2272+
memberCount := len(gjson.GetBytes(w.Body.Bytes(), "joined").Map())
2273+
if memberCount != tc.wantMemberCount {
2274+
t.Fatalf("expected %d members, got %d", tc.wantMemberCount, memberCount)
2275+
}
2276+
}
2277+
})
2278+
}
2279+
})
2280+
}

clientapi/routing/memberships.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright 2024 The Matrix.org Foundation C.I.C.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package routing
16+
17+
import (
18+
"encoding/json"
19+
"net/http"
20+
21+
"github.com/matrix-org/dendrite/roomserver/api"
22+
userapi "github.com/matrix-org/dendrite/userapi/api"
23+
"github.com/matrix-org/gomatrixserverlib/spec"
24+
"github.com/matrix-org/util"
25+
)
26+
27+
// https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-rooms-roomid-joined-members
28+
type getJoinedMembersResponse struct {
29+
Joined map[string]joinedMember `json:"joined"`
30+
}
31+
32+
type joinedMember struct {
33+
DisplayName string `json:"display_name"`
34+
AvatarURL string `json:"avatar_url"`
35+
}
36+
37+
// The database stores 'displayname' without an underscore.
38+
// Deserialize into this and then change to the actual API response
39+
type databaseJoinedMember struct {
40+
DisplayName string `json:"displayname"`
41+
AvatarURL string `json:"avatar_url"`
42+
}
43+
44+
// GetJoinedMembers implements
45+
//
46+
// GET /rooms/{roomId}/joined_members
47+
func GetJoinedMembers(
48+
req *http.Request, device *userapi.Device, roomID string,
49+
rsAPI api.ClientRoomserverAPI,
50+
) util.JSONResponse {
51+
// Validate the userID
52+
userID, err := spec.NewUserID(device.UserID, true)
53+
if err != nil {
54+
return util.JSONResponse{
55+
Code: http.StatusBadRequest,
56+
JSON: spec.InvalidParam("Device UserID is invalid"),
57+
}
58+
}
59+
60+
// Validate the roomID
61+
validRoomID, err := spec.NewRoomID(roomID)
62+
if err != nil {
63+
return util.JSONResponse{
64+
Code: http.StatusBadRequest,
65+
JSON: spec.InvalidParam("RoomID is invalid"),
66+
}
67+
}
68+
69+
// Get the current memberships for the requesting user to determine
70+
// if they are allowed to query this endpoint.
71+
queryReq := api.QueryMembershipForUserRequest{
72+
RoomID: validRoomID.String(),
73+
UserID: *userID,
74+
}
75+
76+
var queryRes api.QueryMembershipForUserResponse
77+
if queryErr := rsAPI.QueryMembershipForUser(req.Context(), &queryReq, &queryRes); queryErr != nil {
78+
util.GetLogger(req.Context()).WithError(queryErr).Error("rsAPI.QueryMembershipsForRoom failed")
79+
return util.JSONResponse{
80+
Code: http.StatusInternalServerError,
81+
JSON: spec.InternalServerError{},
82+
}
83+
}
84+
85+
if !queryRes.HasBeenInRoom {
86+
return util.JSONResponse{
87+
Code: http.StatusForbidden,
88+
JSON: spec.Forbidden("You aren't a member of the room and weren't previously a member of the room."),
89+
}
90+
}
91+
92+
if !queryRes.IsInRoom {
93+
return util.JSONResponse{
94+
Code: http.StatusForbidden,
95+
JSON: spec.Forbidden("You aren't a member of the room and weren't previously a member of the room."),
96+
}
97+
}
98+
99+
// Get the current membership events
100+
var membershipsForRoomResp api.QueryMembershipsForRoomResponse
101+
if err = rsAPI.QueryMembershipsForRoom(req.Context(), &api.QueryMembershipsForRoomRequest{
102+
JoinedOnly: true,
103+
RoomID: validRoomID.String(),
104+
}, &membershipsForRoomResp); err != nil {
105+
util.GetLogger(req.Context()).WithError(err).Error("rsAPI.QueryEventsByID failed")
106+
return util.JSONResponse{
107+
Code: http.StatusInternalServerError,
108+
JSON: spec.InternalServerError{},
109+
}
110+
}
111+
112+
var res getJoinedMembersResponse
113+
res.Joined = make(map[string]joinedMember)
114+
for _, ev := range membershipsForRoomResp.JoinEvents {
115+
var content databaseJoinedMember
116+
if err := json.Unmarshal(ev.Content, &content); err != nil {
117+
util.GetLogger(req.Context()).WithError(err).Error("failed to unmarshal event content")
118+
return util.JSONResponse{
119+
Code: http.StatusInternalServerError,
120+
JSON: spec.InternalServerError{},
121+
}
122+
}
123+
124+
userID, err := rsAPI.QueryUserIDForSender(req.Context(), *validRoomID, spec.SenderID(ev.Sender))
125+
if err != nil || userID == nil {
126+
util.GetLogger(req.Context()).WithError(err).Error("rsAPI.QueryUserIDForSender failed")
127+
return util.JSONResponse{
128+
Code: http.StatusInternalServerError,
129+
JSON: spec.InternalServerError{},
130+
}
131+
}
132+
133+
res.Joined[userID.String()] = joinedMember(content)
134+
}
135+
return util.JSONResponse{
136+
Code: http.StatusOK,
137+
JSON: res,
138+
}
139+
}

clientapi/routing/routing.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1513,4 +1513,14 @@ func Setup(
15131513
return GetPresence(req, device, natsClient, cfg.Matrix.JetStream.Prefixed(jetstream.RequestPresence), vars["userId"])
15141514
}),
15151515
).Methods(http.MethodGet, http.MethodOptions)
1516+
1517+
v3mux.Handle("/rooms/{roomID}/joined_members",
1518+
httputil.MakeAuthAPI("rooms_members", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
1519+
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
1520+
if err != nil {
1521+
return util.ErrorResponse(err)
1522+
}
1523+
return GetJoinedMembers(req, device, vars["roomID"], rsAPI)
1524+
}),
1525+
).Methods(http.MethodGet, http.MethodOptions)
15161526
}

syncapi/routing/memberships.go

Lines changed: 1 addition & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
package routing
1616

1717
import (
18-
"encoding/json"
1918
"math"
2019
"net/http"
2120

@@ -33,31 +32,13 @@ type getMembershipResponse struct {
3332
Chunk []synctypes.ClientEvent `json:"chunk"`
3433
}
3534

36-
// https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-rooms-roomid-joined-members
37-
type getJoinedMembersResponse struct {
38-
Joined map[string]joinedMember `json:"joined"`
39-
}
40-
41-
type joinedMember struct {
42-
DisplayName string `json:"display_name"`
43-
AvatarURL string `json:"avatar_url"`
44-
}
45-
46-
// The database stores 'displayname' without an underscore.
47-
// Deserialize into this and then change to the actual API response
48-
type databaseJoinedMember struct {
49-
DisplayName string `json:"displayname"`
50-
AvatarURL string `json:"avatar_url"`
51-
}
52-
5335
// GetMemberships implements
5436
//
5537
// GET /rooms/{roomId}/members
56-
// GET /rooms/{roomId}/joined_members
5738
func GetMemberships(
5839
req *http.Request, device *userapi.Device, roomID string,
5940
syncDB storage.Database, rsAPI api.SyncRoomserverAPI,
60-
joinedOnly bool, membership, notMembership *string, at string,
41+
membership, notMembership *string, at string,
6142
) util.JSONResponse {
6243
userID, err := spec.NewUserID(device.UserID, true)
6344
if err != nil {
@@ -87,13 +68,6 @@ func GetMemberships(
8768
}
8869
}
8970

90-
if joinedOnly && !queryRes.IsInRoom {
91-
return util.JSONResponse{
92-
Code: http.StatusForbidden,
93-
JSON: spec.Forbidden("You aren't a member of the room and weren't previously a member of the room."),
94-
}
95-
}
96-
9771
db, err := syncDB.NewDatabaseSnapshot(req.Context())
9872
if err != nil {
9973
return util.JSONResponse{
@@ -139,40 +113,6 @@ func GetMemberships(
139113

140114
result := qryRes.Events
141115

142-
if joinedOnly {
143-
var res getJoinedMembersResponse
144-
res.Joined = make(map[string]joinedMember)
145-
for _, ev := range result {
146-
var content databaseJoinedMember
147-
if err := json.Unmarshal(ev.Content(), &content); err != nil {
148-
util.GetLogger(req.Context()).WithError(err).Error("failed to unmarshal event content")
149-
return util.JSONResponse{
150-
Code: http.StatusInternalServerError,
151-
JSON: spec.InternalServerError{},
152-
}
153-
}
154-
155-
userID, err := rsAPI.QueryUserIDForSender(req.Context(), ev.RoomID(), ev.SenderID())
156-
if err != nil || userID == nil {
157-
util.GetLogger(req.Context()).WithError(err).Error("rsAPI.QueryUserIDForSender failed")
158-
return util.JSONResponse{
159-
Code: http.StatusInternalServerError,
160-
JSON: spec.InternalServerError{},
161-
}
162-
}
163-
if err != nil {
164-
return util.JSONResponse{
165-
Code: http.StatusForbidden,
166-
JSON: spec.Forbidden("You don't have permission to kick this user, unknown senderID"),
167-
}
168-
}
169-
res.Joined[userID.String()] = joinedMember(content)
170-
}
171-
return util.JSONResponse{
172-
Code: http.StatusOK,
173-
JSON: res,
174-
}
175-
}
176116
return util.JSONResponse{
177117
Code: http.StatusOK,
178118
JSON: getMembershipResponse{synctypes.ToClientEvents(gomatrixserverlib.ToPDUs(result), synctypes.FormatAll, func(roomID spec.RoomID, senderID spec.SenderID) (*spec.UserID, error) {

0 commit comments

Comments
 (0)