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

Commit 79072c3

Browse files
authored
Add /_synapse/admin/v1/event_reports endpoint (#3342)
Based on #3340 This adds a `/_synapse/admin/v1/event_reports` endpoint, the same Synapse has. This way existing tools also work with Dendrite. Given this is already getting huge (even though many test lines), splitting this into two PRs. (The next adds "getting one report" and "deleting reports") [skip ci]
1 parent 1bdf0cc commit 79072c3

File tree

11 files changed

+647
-4
lines changed

11 files changed

+647
-4
lines changed

clientapi/admin_test.go

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package clientapi
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"net/http"
78
"net/http/httptest"
89
"reflect"
10+
"strings"
911
"testing"
1012
"time"
1113

@@ -1092,3 +1094,245 @@ func TestAdminMarkAsStale(t *testing.T) {
10921094
}
10931095
})
10941096
}
1097+
1098+
func TestAdminQueryEventReports(t *testing.T) {
1099+
alice := test.NewUser(t, test.WithAccountType(uapi.AccountTypeAdmin))
1100+
bob := test.NewUser(t)
1101+
room := test.NewRoom(t, alice)
1102+
room2 := test.NewRoom(t, alice)
1103+
1104+
// room2 has a name and canonical alias
1105+
room2.CreateAndInsert(t, alice, spec.MRoomName, map[string]string{"name": "Testing"}, test.WithStateKey(""))
1106+
room2.CreateAndInsert(t, alice, spec.MRoomCanonicalAlias, map[string]string{"alias": "#testing"}, test.WithStateKey(""))
1107+
1108+
// Join the rooms with Bob
1109+
room.CreateAndInsert(t, bob, spec.MRoomMember, map[string]interface{}{
1110+
"membership": "join",
1111+
}, test.WithStateKey(bob.ID))
1112+
room2.CreateAndInsert(t, bob, spec.MRoomMember, map[string]interface{}{
1113+
"membership": "join",
1114+
}, test.WithStateKey(bob.ID))
1115+
1116+
// Create a few events to report
1117+
eventsToReportPerRoom := make(map[string][]string)
1118+
for i := 0; i < 10; i++ {
1119+
ev1 := room.CreateAndInsert(t, alice, "m.room.message", map[string]interface{}{"body": "hello world"})
1120+
ev2 := room2.CreateAndInsert(t, alice, "m.room.message", map[string]interface{}{"body": "hello world"})
1121+
eventsToReportPerRoom[room.ID] = append(eventsToReportPerRoom[room.ID], ev1.EventID())
1122+
eventsToReportPerRoom[room2.ID] = append(eventsToReportPerRoom[room2.ID], ev2.EventID())
1123+
}
1124+
1125+
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
1126+
/*if dbType == test.DBTypeSQLite {
1127+
t.Skip()
1128+
}*/
1129+
cfg, processCtx, close := testrig.CreateConfig(t, dbType)
1130+
routers := httputil.NewRouters()
1131+
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
1132+
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
1133+
defer close()
1134+
natsInstance := jetstream.NATSInstance{}
1135+
jsctx, _ := natsInstance.Prepare(processCtx, &cfg.Global.JetStream)
1136+
defer jetstream.DeleteAllStreams(jsctx, &cfg.Global.JetStream)
1137+
1138+
// Use an actual roomserver for this
1139+
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
1140+
rsAPI.SetFederationAPI(nil, nil)
1141+
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff)
1142+
1143+
if err := api.SendEvents(context.Background(), rsAPI, api.KindNew, room.Events(), "test", "test", "test", nil, false); err != nil {
1144+
t.Fatalf("failed to send events: %v", err)
1145+
}
1146+
if err := api.SendEvents(context.Background(), rsAPI, api.KindNew, room2.Events(), "test", "test", "test", nil, false); err != nil {
1147+
t.Fatalf("failed to send events: %v", err)
1148+
}
1149+
1150+
// We mostly need the rsAPI for this test, so nil for other APIs/caches etc.
1151+
AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics)
1152+
1153+
accessTokens := map[*test.User]userDevice{
1154+
alice: {},
1155+
bob: {},
1156+
}
1157+
createAccessTokens(t, accessTokens, userAPI, processCtx.Context(), routers)
1158+
1159+
reqBody := map[string]any{
1160+
"reason": "baaad",
1161+
"score": -100,
1162+
}
1163+
body, err := json.Marshal(reqBody)
1164+
if err != nil {
1165+
t.Fatal(err)
1166+
}
1167+
1168+
w := httptest.NewRecorder()
1169+
1170+
var req *http.Request
1171+
// Report all events
1172+
for roomID, eventIDs := range eventsToReportPerRoom {
1173+
for _, eventID := range eventIDs {
1174+
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/_matrix/client/v3/rooms/%s/report/%s", roomID, eventID), strings.NewReader(string(body)))
1175+
req.Header.Set("Authorization", "Bearer "+accessTokens[bob].accessToken)
1176+
1177+
routers.Client.ServeHTTP(w, req)
1178+
1179+
if w.Code != http.StatusOK {
1180+
t.Fatalf("expected report to succeed, got HTTP %d instead: %s", w.Code, w.Body.String())
1181+
}
1182+
}
1183+
}
1184+
1185+
type response struct {
1186+
EventReports []api.QueryAdminEventReportsResponse `json:"event_reports"`
1187+
Total int64 `json:"total"`
1188+
NextToken *int64 `json:"next_token,omitempty"`
1189+
}
1190+
1191+
t.Run("Can query all reports", func(t *testing.T) {
1192+
w = httptest.NewRecorder()
1193+
req = httptest.NewRequest(http.MethodGet, "/_synapse/admin/v1/event_reports", strings.NewReader(string(body)))
1194+
req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
1195+
1196+
routers.SynapseAdmin.ServeHTTP(w, req)
1197+
1198+
if w.Code != http.StatusOK {
1199+
t.Fatalf("expected getting reports to succeed, got HTTP %d instead: %s", w.Code, w.Body.String())
1200+
}
1201+
var resp response
1202+
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
1203+
t.Fatal(err)
1204+
}
1205+
wantCount := 20
1206+
// Only validating the count
1207+
if len(resp.EventReports) != wantCount {
1208+
t.Fatalf("expected %d events, got %d", wantCount, len(resp.EventReports))
1209+
}
1210+
if resp.Total != int64(wantCount) {
1211+
t.Fatalf("expected total to be %d, got %d", wantCount, resp.Total)
1212+
}
1213+
})
1214+
1215+
t.Run("Can filter on room", func(t *testing.T) {
1216+
w = httptest.NewRecorder()
1217+
req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/_synapse/admin/v1/event_reports?room_id=%s", room.ID), strings.NewReader(string(body)))
1218+
req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
1219+
1220+
routers.SynapseAdmin.ServeHTTP(w, req)
1221+
1222+
if w.Code != http.StatusOK {
1223+
t.Fatalf("expected getting reports to succeed, got HTTP %d instead: %s", w.Code, w.Body.String())
1224+
}
1225+
var resp response
1226+
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
1227+
t.Fatal(err)
1228+
}
1229+
wantCount := 10
1230+
// Only validating the count
1231+
if len(resp.EventReports) != wantCount {
1232+
t.Fatalf("expected %d events, got %d", wantCount, len(resp.EventReports))
1233+
}
1234+
if resp.Total != int64(wantCount) {
1235+
t.Fatalf("expected total to be %d, got %d", wantCount, resp.Total)
1236+
}
1237+
})
1238+
1239+
t.Run("Can filter on user_id", func(t *testing.T) {
1240+
w = httptest.NewRecorder()
1241+
req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/_synapse/admin/v1/event_reports?user_id=%s", "@doesnotexist:test"), strings.NewReader(string(body)))
1242+
req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
1243+
1244+
routers.SynapseAdmin.ServeHTTP(w, req)
1245+
1246+
if w.Code != http.StatusOK {
1247+
t.Fatalf("expected getting reports to succeed, got HTTP %d instead: %s", w.Code, w.Body.String())
1248+
}
1249+
var resp response
1250+
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
1251+
t.Fatal(err)
1252+
}
1253+
1254+
// The user does not exist, so we expect no results
1255+
wantCount := 0
1256+
// Only validating the count
1257+
if len(resp.EventReports) != wantCount {
1258+
t.Fatalf("expected %d events, got %d", wantCount, len(resp.EventReports))
1259+
}
1260+
if resp.Total != int64(wantCount) {
1261+
t.Fatalf("expected total to be %d, got %d", wantCount, resp.Total)
1262+
}
1263+
})
1264+
1265+
t.Run("Can set direction=f", func(t *testing.T) {
1266+
w = httptest.NewRecorder()
1267+
req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/_synapse/admin/v1/event_reports?room_id=%s&dir=f", room.ID), strings.NewReader(string(body)))
1268+
req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
1269+
1270+
routers.SynapseAdmin.ServeHTTP(w, req)
1271+
1272+
if w.Code != http.StatusOK {
1273+
t.Fatalf("expected getting reports to succeed, got HTTP %d instead: %s", w.Code, w.Body.String())
1274+
}
1275+
var resp response
1276+
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
1277+
t.Fatal(err)
1278+
}
1279+
wantCount := 10
1280+
// Only validating the count
1281+
if len(resp.EventReports) != wantCount {
1282+
t.Fatalf("expected %d events, got %d", wantCount, len(resp.EventReports))
1283+
}
1284+
if resp.Total != int64(wantCount) {
1285+
t.Fatalf("expected total to be %d, got %d", wantCount, resp.Total)
1286+
}
1287+
// we now should have the first reported event
1288+
wantEventID := eventsToReportPerRoom[room.ID][0]
1289+
gotEventID := resp.EventReports[0].EventID
1290+
if gotEventID != wantEventID {
1291+
t.Fatalf("expected eventID to be %v, got %v", wantEventID, gotEventID)
1292+
}
1293+
})
1294+
1295+
t.Run("Can limit and paginate", func(t *testing.T) {
1296+
var from int64 = 0
1297+
var limit int64 = 5
1298+
var wantTotal int64 = 10 // We expect there to be 10 events in total
1299+
var resp response
1300+
for from+limit <= wantTotal {
1301+
resp = response{}
1302+
t.Logf("Getting reports starting from %d", from)
1303+
w = httptest.NewRecorder()
1304+
req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/_synapse/admin/v1/event_reports?room_id=%s&limit=%d&from=%d", room2.ID, limit, from), strings.NewReader(string(body)))
1305+
req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
1306+
1307+
routers.SynapseAdmin.ServeHTTP(w, req)
1308+
1309+
if w.Code != http.StatusOK {
1310+
t.Fatalf("expected getting reports to succeed, got HTTP %d instead: %s", w.Code, w.Body.String())
1311+
}
1312+
1313+
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
1314+
t.Fatal(err)
1315+
}
1316+
1317+
wantCount := 5 // we are limited to 5
1318+
if len(resp.EventReports) != wantCount {
1319+
t.Fatalf("expected %d events, got %d", wantCount, len(resp.EventReports))
1320+
}
1321+
if resp.Total != int64(wantTotal) {
1322+
t.Fatalf("expected total to be %d, got %d", wantCount, resp.Total)
1323+
}
1324+
1325+
// We've reached the end
1326+
if (from + int64(len(resp.EventReports))) == wantTotal {
1327+
return
1328+
}
1329+
1330+
// The next_token should be set
1331+
if resp.NextToken == nil {
1332+
t.Fatal("expected nextToken to be set")
1333+
}
1334+
from = *resp.NextToken
1335+
}
1336+
})
1337+
})
1338+
}

clientapi/routing/admin.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,3 +495,45 @@ func AdminDownloadState(req *http.Request, device *api.Device, rsAPI roomserverA
495495
JSON: struct{}{},
496496
}
497497
}
498+
499+
// GetEventReports returns reported events for a given user/room.
500+
func GetEventReports(
501+
req *http.Request,
502+
rsAPI roomserverAPI.ClientRoomserverAPI,
503+
from, limit uint64,
504+
backwards bool,
505+
userID, roomID string,
506+
) util.JSONResponse {
507+
508+
eventReports, count, err := rsAPI.QueryAdminEventReports(req.Context(), from, limit, backwards, userID, roomID)
509+
if err != nil {
510+
logrus.WithError(err).Error("failed to query event reports")
511+
return util.JSONResponse{
512+
Code: http.StatusInternalServerError,
513+
JSON: spec.InternalServerError{},
514+
}
515+
}
516+
517+
resp := map[string]any{
518+
"event_reports": eventReports,
519+
"total": count,
520+
}
521+
522+
// Add a next_token if there are still reports
523+
if int64(from+limit) < count {
524+
resp["next_token"] = int(from) + len(eventReports)
525+
}
526+
527+
return util.JSONResponse{
528+
Code: http.StatusOK,
529+
JSON: resp,
530+
}
531+
}
532+
533+
func parseUint64OrDefault(input string, defaultValue uint64) uint64 {
534+
v, err := strconv.ParseUint(input, 10, 64)
535+
if err != nil {
536+
return defaultValue
537+
}
538+
return v
539+
}

clientapi/routing/routing.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1533,4 +1533,18 @@ func Setup(
15331533
return ReportEvent(req, device, vars["roomID"], vars["eventID"], rsAPI)
15341534
}),
15351535
).Methods(http.MethodPost, http.MethodOptions)
1536+
1537+
synapseAdminRouter.Handle("/admin/v1/event_reports",
1538+
httputil.MakeAdminAPI("admin_report_event", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
1539+
from := parseUint64OrDefault(req.URL.Query().Get("from"), 0)
1540+
limit := parseUint64OrDefault(req.URL.Query().Get("limit"), 100)
1541+
dir := req.URL.Query().Get("dir")
1542+
userID := req.URL.Query().Get("user_id")
1543+
roomID := req.URL.Query().Get("room_id")
1544+
1545+
// Go backwards if direction is empty or "b"
1546+
backwards := dir == "" || dir == "b"
1547+
return GetEventReports(req, rsAPI, from, limit, backwards, userID, roomID)
1548+
}),
1549+
).Methods(http.MethodGet, http.MethodOptions)
15361550
}

roomserver/api/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ type ClientRoomserverAPI interface {
271271
roomID, eventID, reportingUserID, reason string,
272272
score int64,
273273
) (int64, error)
274+
QueryAdminEventReports(ctx context.Context, from, limit uint64, backwards bool, userID, roomID string) ([]QueryAdminEventReportsResponse, int64, error)
274275
}
275276

276277
type UserRoomserverAPI interface {

roomserver/api/query.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,23 @@ type QueryServerBannedFromRoomResponse struct {
346346
Banned bool `json:"banned"`
347347
}
348348

349+
type QueryAdminEventReportsResponse struct {
350+
ID int64 `json:"id"`
351+
Score int64 `json:"score"`
352+
EventNID types.EventNID `json:"-"` // only used to query the state
353+
RoomNID types.RoomNID `json:"-"` // only used to query the state
354+
ReportingUserNID types.EventStateKeyNID `json:"-"` // only used in the DB
355+
SenderNID types.EventStateKeyNID `json:"-"` // only used in the DB
356+
RoomID string `json:"room_id"`
357+
EventID string `json:"event_id"`
358+
UserID string `json:"user_id"` // the user reporting the event
359+
Reason string `json:"reason"`
360+
Sender string `json:"sender"` // the user sending the reported event
361+
CanonicalAlias string `json:"canonical_alias"`
362+
RoomName string `json:"name"`
363+
ReceivedTS spec.Timestamp `json:"received_ts"`
364+
}
365+
349366
// MarshalJSON stringifies the room ID and StateKeyTuple keys so they can be sent over the wire in HTTP API mode.
350367
func (r *QueryBulkStateContentResponse) MarshalJSON() ([]byte, error) {
351368
se := make(map[string]string)

roomserver/internal/query/query.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,3 +1104,8 @@ func (r *Queryer) QueryUserIDForSender(ctx context.Context, roomID spec.RoomID,
11041104
func (r *Queryer) RoomsWithACLs(ctx context.Context) ([]string, error) {
11051105
return r.DB.RoomsWithACLs(ctx)
11061106
}
1107+
1108+
// QueryAdminEventReports returns event reports given a filter.
1109+
func (r *Queryer) QueryAdminEventReports(ctx context.Context, from uint64, limit uint64, backwards bool, userID, roomID string) ([]api.QueryAdminEventReportsResponse, int64, error) {
1110+
return r.DB.QueryAdminEventReports(ctx, from, limit, backwards, userID, roomID)
1111+
}

roomserver/storage/interface.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ type Database interface {
195195

196196
// RoomsWithACLs returns all room IDs for rooms with ACLs
197197
RoomsWithACLs(ctx context.Context) ([]string, error)
198+
QueryAdminEventReports(ctx context.Context, from uint64, limit uint64, backwards bool, userID string, roomID string) ([]api.QueryAdminEventReportsResponse, int64, error)
198199
}
199200

200201
type UserRoomKeys interface {

0 commit comments

Comments
 (0)