aboutsummaryrefslogtreecommitdiff
path: root/clientapi
diff options
context:
space:
mode:
authorTill <2353100+S7evinK@users.noreply.github.com>2024-03-22 22:32:30 +0100
committerGitHub <noreply@github.com>2024-03-22 22:32:30 +0100
commit79072c3dcdc88b77dd5a49c013a0c62624dd3224 (patch)
treeafba94c5d21fc9d64c8a0391becd21ab682f91cd /clientapi
parent1bdf0cc5414a47fbdab9ae76fa7d549349d648ec (diff)
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]
Diffstat (limited to 'clientapi')
-rw-r--r--clientapi/admin_test.go244
-rw-r--r--clientapi/routing/admin.go42
-rw-r--r--clientapi/routing/routing.go14
3 files changed, 300 insertions, 0 deletions
diff --git a/clientapi/admin_test.go b/clientapi/admin_test.go
index f0e5f004..2444f0be 100644
--- a/clientapi/admin_test.go
+++ b/clientapi/admin_test.go
@@ -2,10 +2,12 @@ package clientapi
import (
"context"
+ "encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"reflect"
+ "strings"
"testing"
"time"
@@ -1092,3 +1094,245 @@ func TestAdminMarkAsStale(t *testing.T) {
}
})
}
+
+func TestAdminQueryEventReports(t *testing.T) {
+ alice := test.NewUser(t, test.WithAccountType(uapi.AccountTypeAdmin))
+ bob := test.NewUser(t)
+ room := test.NewRoom(t, alice)
+ room2 := test.NewRoom(t, alice)
+
+ // room2 has a name and canonical alias
+ room2.CreateAndInsert(t, alice, spec.MRoomName, map[string]string{"name": "Testing"}, test.WithStateKey(""))
+ room2.CreateAndInsert(t, alice, spec.MRoomCanonicalAlias, map[string]string{"alias": "#testing"}, test.WithStateKey(""))
+
+ // Join the rooms with Bob
+ room.CreateAndInsert(t, bob, spec.MRoomMember, map[string]interface{}{
+ "membership": "join",
+ }, test.WithStateKey(bob.ID))
+ room2.CreateAndInsert(t, bob, spec.MRoomMember, map[string]interface{}{
+ "membership": "join",
+ }, test.WithStateKey(bob.ID))
+
+ // Create a few events to report
+ eventsToReportPerRoom := make(map[string][]string)
+ for i := 0; i < 10; i++ {
+ ev1 := room.CreateAndInsert(t, alice, "m.room.message", map[string]interface{}{"body": "hello world"})
+ ev2 := room2.CreateAndInsert(t, alice, "m.room.message", map[string]interface{}{"body": "hello world"})
+ eventsToReportPerRoom[room.ID] = append(eventsToReportPerRoom[room.ID], ev1.EventID())
+ eventsToReportPerRoom[room2.ID] = append(eventsToReportPerRoom[room2.ID], ev2.EventID())
+ }
+
+ test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
+ /*if dbType == test.DBTypeSQLite {
+ t.Skip()
+ }*/
+ cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+ routers := httputil.NewRouters()
+ cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+ caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
+ defer close()
+ natsInstance := jetstream.NATSInstance{}
+ jsctx, _ := natsInstance.Prepare(processCtx, &cfg.Global.JetStream)
+ defer jetstream.DeleteAllStreams(jsctx, &cfg.Global.JetStream)
+
+ // Use an actual roomserver for this
+ rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
+ rsAPI.SetFederationAPI(nil, nil)
+ userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff)
+
+ if err := api.SendEvents(context.Background(), rsAPI, api.KindNew, room.Events(), "test", "test", "test", nil, false); err != nil {
+ t.Fatalf("failed to send events: %v", err)
+ }
+ if err := api.SendEvents(context.Background(), rsAPI, api.KindNew, room2.Events(), "test", "test", "test", nil, false); err != nil {
+ t.Fatalf("failed to send events: %v", err)
+ }
+
+ // We mostly need the rsAPI for this test, so nil for other APIs/caches etc.
+ AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics)
+
+ accessTokens := map[*test.User]userDevice{
+ alice: {},
+ bob: {},
+ }
+ createAccessTokens(t, accessTokens, userAPI, processCtx.Context(), routers)
+
+ reqBody := map[string]any{
+ "reason": "baaad",
+ "score": -100,
+ }
+ body, err := json.Marshal(reqBody)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ w := httptest.NewRecorder()
+
+ var req *http.Request
+ // Report all events
+ for roomID, eventIDs := range eventsToReportPerRoom {
+ for _, eventID := range eventIDs {
+ req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/_matrix/client/v3/rooms/%s/report/%s", roomID, eventID), strings.NewReader(string(body)))
+ req.Header.Set("Authorization", "Bearer "+accessTokens[bob].accessToken)
+
+ routers.Client.ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected report to succeed, got HTTP %d instead: %s", w.Code, w.Body.String())
+ }
+ }
+ }
+
+ type response struct {
+ EventReports []api.QueryAdminEventReportsResponse `json:"event_reports"`
+ Total int64 `json:"total"`
+ NextToken *int64 `json:"next_token,omitempty"`
+ }
+
+ t.Run("Can query all reports", func(t *testing.T) {
+ w = httptest.NewRecorder()
+ req = httptest.NewRequest(http.MethodGet, "/_synapse/admin/v1/event_reports", strings.NewReader(string(body)))
+ req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
+
+ routers.SynapseAdmin.ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected getting reports to succeed, got HTTP %d instead: %s", w.Code, w.Body.String())
+ }
+ var resp response
+ if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
+ t.Fatal(err)
+ }
+ wantCount := 20
+ // Only validating the count
+ if len(resp.EventReports) != wantCount {
+ t.Fatalf("expected %d events, got %d", wantCount, len(resp.EventReports))
+ }
+ if resp.Total != int64(wantCount) {
+ t.Fatalf("expected total to be %d, got %d", wantCount, resp.Total)
+ }
+ })
+
+ t.Run("Can filter on room", func(t *testing.T) {
+ w = httptest.NewRecorder()
+ req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/_synapse/admin/v1/event_reports?room_id=%s", room.ID), strings.NewReader(string(body)))
+ req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
+
+ routers.SynapseAdmin.ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected getting reports to succeed, got HTTP %d instead: %s", w.Code, w.Body.String())
+ }
+ var resp response
+ if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
+ t.Fatal(err)
+ }
+ wantCount := 10
+ // Only validating the count
+ if len(resp.EventReports) != wantCount {
+ t.Fatalf("expected %d events, got %d", wantCount, len(resp.EventReports))
+ }
+ if resp.Total != int64(wantCount) {
+ t.Fatalf("expected total to be %d, got %d", wantCount, resp.Total)
+ }
+ })
+
+ t.Run("Can filter on user_id", func(t *testing.T) {
+ w = httptest.NewRecorder()
+ req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/_synapse/admin/v1/event_reports?user_id=%s", "@doesnotexist:test"), strings.NewReader(string(body)))
+ req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
+
+ routers.SynapseAdmin.ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected getting reports to succeed, got HTTP %d instead: %s", w.Code, w.Body.String())
+ }
+ var resp response
+ if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
+ t.Fatal(err)
+ }
+
+ // The user does not exist, so we expect no results
+ wantCount := 0
+ // Only validating the count
+ if len(resp.EventReports) != wantCount {
+ t.Fatalf("expected %d events, got %d", wantCount, len(resp.EventReports))
+ }
+ if resp.Total != int64(wantCount) {
+ t.Fatalf("expected total to be %d, got %d", wantCount, resp.Total)
+ }
+ })
+
+ t.Run("Can set direction=f", func(t *testing.T) {
+ w = httptest.NewRecorder()
+ req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/_synapse/admin/v1/event_reports?room_id=%s&dir=f", room.ID), strings.NewReader(string(body)))
+ req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
+
+ routers.SynapseAdmin.ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected getting reports to succeed, got HTTP %d instead: %s", w.Code, w.Body.String())
+ }
+ var resp response
+ if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
+ t.Fatal(err)
+ }
+ wantCount := 10
+ // Only validating the count
+ if len(resp.EventReports) != wantCount {
+ t.Fatalf("expected %d events, got %d", wantCount, len(resp.EventReports))
+ }
+ if resp.Total != int64(wantCount) {
+ t.Fatalf("expected total to be %d, got %d", wantCount, resp.Total)
+ }
+ // we now should have the first reported event
+ wantEventID := eventsToReportPerRoom[room.ID][0]
+ gotEventID := resp.EventReports[0].EventID
+ if gotEventID != wantEventID {
+ t.Fatalf("expected eventID to be %v, got %v", wantEventID, gotEventID)
+ }
+ })
+
+ t.Run("Can limit and paginate", func(t *testing.T) {
+ var from int64 = 0
+ var limit int64 = 5
+ var wantTotal int64 = 10 // We expect there to be 10 events in total
+ var resp response
+ for from+limit <= wantTotal {
+ resp = response{}
+ t.Logf("Getting reports starting from %d", from)
+ w = httptest.NewRecorder()
+ 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)))
+ req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
+
+ routers.SynapseAdmin.ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected getting reports to succeed, got HTTP %d instead: %s", w.Code, w.Body.String())
+ }
+
+ if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
+ t.Fatal(err)
+ }
+
+ wantCount := 5 // we are limited to 5
+ if len(resp.EventReports) != wantCount {
+ t.Fatalf("expected %d events, got %d", wantCount, len(resp.EventReports))
+ }
+ if resp.Total != int64(wantTotal) {
+ t.Fatalf("expected total to be %d, got %d", wantCount, resp.Total)
+ }
+
+ // We've reached the end
+ if (from + int64(len(resp.EventReports))) == wantTotal {
+ return
+ }
+
+ // The next_token should be set
+ if resp.NextToken == nil {
+ t.Fatal("expected nextToken to be set")
+ }
+ from = *resp.NextToken
+ }
+ })
+ })
+}
diff --git a/clientapi/routing/admin.go b/clientapi/routing/admin.go
index 51966607..e91e098a 100644
--- a/clientapi/routing/admin.go
+++ b/clientapi/routing/admin.go
@@ -495,3 +495,45 @@ func AdminDownloadState(req *http.Request, device *api.Device, rsAPI roomserverA
JSON: struct{}{},
}
}
+
+// GetEventReports returns reported events for a given user/room.
+func GetEventReports(
+ req *http.Request,
+ rsAPI roomserverAPI.ClientRoomserverAPI,
+ from, limit uint64,
+ backwards bool,
+ userID, roomID string,
+) util.JSONResponse {
+
+ eventReports, count, err := rsAPI.QueryAdminEventReports(req.Context(), from, limit, backwards, userID, roomID)
+ if err != nil {
+ logrus.WithError(err).Error("failed to query event reports")
+ return util.JSONResponse{
+ Code: http.StatusInternalServerError,
+ JSON: spec.InternalServerError{},
+ }
+ }
+
+ resp := map[string]any{
+ "event_reports": eventReports,
+ "total": count,
+ }
+
+ // Add a next_token if there are still reports
+ if int64(from+limit) < count {
+ resp["next_token"] = int(from) + len(eventReports)
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: resp,
+ }
+}
+
+func parseUint64OrDefault(input string, defaultValue uint64) uint64 {
+ v, err := strconv.ParseUint(input, 10, 64)
+ if err != nil {
+ return defaultValue
+ }
+ return v
+}
diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go
index 40e59822..dc63a2c2 100644
--- a/clientapi/routing/routing.go
+++ b/clientapi/routing/routing.go
@@ -1533,4 +1533,18 @@ func Setup(
return ReportEvent(req, device, vars["roomID"], vars["eventID"], rsAPI)
}),
).Methods(http.MethodPost, http.MethodOptions)
+
+ synapseAdminRouter.Handle("/admin/v1/event_reports",
+ httputil.MakeAdminAPI("admin_report_event", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
+ from := parseUint64OrDefault(req.URL.Query().Get("from"), 0)
+ limit := parseUint64OrDefault(req.URL.Query().Get("limit"), 100)
+ dir := req.URL.Query().Get("dir")
+ userID := req.URL.Query().Get("user_id")
+ roomID := req.URL.Query().Get("room_id")
+
+ // Go backwards if direction is empty or "b"
+ backwards := dir == "" || dir == "b"
+ return GetEventReports(req, rsAPI, from, limit, backwards, userID, roomID)
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
}