diff options
Diffstat (limited to 'clientapi')
-rw-r--r-- | clientapi/admin_test.go | 244 | ||||
-rw-r--r-- | clientapi/routing/admin.go | 42 | ||||
-rw-r--r-- | clientapi/routing/routing.go | 14 |
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) } |