aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--clientapi/admin_test.go244
-rw-r--r--clientapi/routing/admin.go42
-rw-r--r--clientapi/routing/routing.go14
-rw-r--r--roomserver/api/api.go1
-rw-r--r--roomserver/api/query.go17
-rw-r--r--roomserver/internal/query/query.go5
-rw-r--r--roomserver/storage/interface.go1
-rw-r--r--roomserver/storage/postgres/reported_events_table.go98
-rw-r--r--roomserver/storage/shared/storage.go125
-rw-r--r--roomserver/storage/sqlite3/reported_events_table.go95
-rw-r--r--roomserver/storage/tables/interface.go9
11 files changed, 647 insertions, 4 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)
}
diff --git a/roomserver/api/api.go b/roomserver/api/api.go
index 62aac144..ac4ea5ba 100644
--- a/roomserver/api/api.go
+++ b/roomserver/api/api.go
@@ -271,6 +271,7 @@ type ClientRoomserverAPI interface {
roomID, eventID, reportingUserID, reason string,
score int64,
) (int64, error)
+ QueryAdminEventReports(ctx context.Context, from, limit uint64, backwards bool, userID, roomID string) ([]QueryAdminEventReportsResponse, int64, error)
}
type UserRoomserverAPI interface {
diff --git a/roomserver/api/query.go b/roomserver/api/query.go
index 893d5dcc..9a7acab9 100644
--- a/roomserver/api/query.go
+++ b/roomserver/api/query.go
@@ -346,6 +346,23 @@ type QueryServerBannedFromRoomResponse struct {
Banned bool `json:"banned"`
}
+type QueryAdminEventReportsResponse struct {
+ ID int64 `json:"id"`
+ Score int64 `json:"score"`
+ EventNID types.EventNID `json:"-"` // only used to query the state
+ RoomNID types.RoomNID `json:"-"` // only used to query the state
+ ReportingUserNID types.EventStateKeyNID `json:"-"` // only used in the DB
+ SenderNID types.EventStateKeyNID `json:"-"` // only used in the DB
+ RoomID string `json:"room_id"`
+ EventID string `json:"event_id"`
+ UserID string `json:"user_id"` // the user reporting the event
+ Reason string `json:"reason"`
+ Sender string `json:"sender"` // the user sending the reported event
+ CanonicalAlias string `json:"canonical_alias"`
+ RoomName string `json:"name"`
+ ReceivedTS spec.Timestamp `json:"received_ts"`
+}
+
// MarshalJSON stringifies the room ID and StateKeyTuple keys so they can be sent over the wire in HTTP API mode.
func (r *QueryBulkStateContentResponse) MarshalJSON() ([]byte, error) {
se := make(map[string]string)
diff --git a/roomserver/internal/query/query.go b/roomserver/internal/query/query.go
index c7f8d100..b1b11df3 100644
--- a/roomserver/internal/query/query.go
+++ b/roomserver/internal/query/query.go
@@ -1104,3 +1104,8 @@ func (r *Queryer) QueryUserIDForSender(ctx context.Context, roomID spec.RoomID,
func (r *Queryer) RoomsWithACLs(ctx context.Context) ([]string, error) {
return r.DB.RoomsWithACLs(ctx)
}
+
+// QueryAdminEventReports returns event reports given a filter.
+func (r *Queryer) QueryAdminEventReports(ctx context.Context, from uint64, limit uint64, backwards bool, userID, roomID string) ([]api.QueryAdminEventReportsResponse, int64, error) {
+ return r.DB.QueryAdminEventReports(ctx, from, limit, backwards, userID, roomID)
+}
diff --git a/roomserver/storage/interface.go b/roomserver/storage/interface.go
index 5f9b5b2b..eb169cbb 100644
--- a/roomserver/storage/interface.go
+++ b/roomserver/storage/interface.go
@@ -195,6 +195,7 @@ type Database interface {
// RoomsWithACLs returns all room IDs for rooms with ACLs
RoomsWithACLs(ctx context.Context) ([]string, error)
+ QueryAdminEventReports(ctx context.Context, from uint64, limit uint64, backwards bool, userID string, roomID string) ([]api.QueryAdminEventReportsResponse, int64, error)
}
type UserRoomKeys interface {
diff --git a/roomserver/storage/postgres/reported_events_table.go b/roomserver/storage/postgres/reported_events_table.go
index 01debcf9..70393833 100644
--- a/roomserver/storage/postgres/reported_events_table.go
+++ b/roomserver/storage/postgres/reported_events_table.go
@@ -19,7 +19,9 @@ import (
"database/sql"
"time"
+ "github.com/matrix-org/dendrite/internal"
"github.com/matrix-org/dendrite/internal/sqlutil"
+ "github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/dendrite/roomserver/storage/tables"
"github.com/matrix-org/dendrite/roomserver/types"
"github.com/matrix-org/gomatrixserverlib/spec"
@@ -32,8 +34,8 @@ CREATE TABLE IF NOT EXISTS roomserver_reported_events
id BIGINT PRIMARY KEY DEFAULT nextval('roomserver_reported_events_id_seq'),
room_nid BIGINT NOT NULL,
event_nid BIGINT NOT NULL,
- reporting_user_nid INTEGER NOT NULL, -- the user reporting the event
- event_sender_nid INTEGER NOT NULL, -- the user who sent the reported event
+ reporting_user_nid BIGINT NOT NULL, -- the user reporting the event
+ event_sender_nid BIGINT NOT NULL, -- the user who sent the reported event
reason TEXT,
score INTEGER,
received_ts BIGINT NOT NULL
@@ -45,8 +47,38 @@ const insertReportedEventSQL = `
RETURNING id
`
+const selectReportedEventsDescSQL = `
+WITH countReports AS (
+ SELECT count(*) as report_count
+ FROM roomserver_reported_events
+ WHERE ($1::BIGINT IS NULL OR room_nid = $1::BIGINT) AND ($2::TEXT IS NULL OR reporting_user_nid = $2::BIGINT)
+)
+SELECT report_count, id, room_nid, event_nid, reporting_user_nid, event_sender_nid, reason, score, received_ts
+FROM roomserver_reported_events, countReports
+WHERE ($1::BIGINT IS NULL OR room_nid = $1::BIGINT) AND ($2::TEXT IS NULL OR reporting_user_nid = $2::BIGINT)
+ORDER BY received_ts DESC
+OFFSET $3
+LIMIT $4
+`
+
+const selectReportedEventsAscSQL = `
+WITH countReports AS (
+ SELECT count(*) as report_count
+ FROM roomserver_reported_events
+ WHERE ($1::BIGINT IS NULL OR room_nid = $1::BIGINT) AND ($2::TEXT IS NULL OR reporting_user_nid = $2::BIGINT)
+)
+SELECT report_count, id, room_nid, event_nid, reporting_user_nid, event_sender_nid, reason, score, received_ts
+FROM roomserver_reported_events, countReports
+WHERE ($1::BIGINT IS NULL OR room_nid = $1::BIGINT) AND ($2::TEXT IS NULL OR reporting_user_nid = $2::BIGINT)
+ORDER BY received_ts ASC
+OFFSET $3
+LIMIT $4
+`
+
type reportedEventsStatements struct {
- insertReportedEventsStmt *sql.Stmt
+ insertReportedEventsStmt *sql.Stmt
+ selectReportedEventsDescStmt *sql.Stmt
+ selectReportedEventsAscStmt *sql.Stmt
}
func CreateReportedEventsTable(db *sql.DB) error {
@@ -59,6 +91,8 @@ func PrepareReportedEventsTable(db *sql.DB) (tables.ReportedEvents, error) {
return s, sqlutil.StatementList{
{&s.insertReportedEventsStmt, insertReportedEventSQL},
+ {&s.selectReportedEventsDescStmt, selectReportedEventsDescSQL},
+ {&s.selectReportedEventsAscStmt, selectReportedEventsAscSQL},
}.Prepare(db)
}
@@ -86,3 +120,61 @@ func (r *reportedEventsStatements) InsertReportedEvent(
).Scan(&reportID)
return reportID, err
}
+
+func (r *reportedEventsStatements) SelectReportedEvents(
+ ctx context.Context,
+ txn *sql.Tx,
+ from, limit uint64,
+ backwards bool,
+ reportingUserID types.EventStateKeyNID,
+ roomNID types.RoomNID,
+) ([]api.QueryAdminEventReportsResponse, int64, error) {
+ var stmt *sql.Stmt
+ if backwards {
+ stmt = sqlutil.TxStmt(txn, r.selectReportedEventsDescStmt)
+ } else {
+ stmt = sqlutil.TxStmt(txn, r.selectReportedEventsAscStmt)
+ }
+
+ var qryRoomNID *types.RoomNID
+ if roomNID > 0 {
+ qryRoomNID = &roomNID
+ }
+ var qryReportingUser *types.EventStateKeyNID
+ if reportingUserID > 0 {
+ qryReportingUser = &reportingUserID
+ }
+
+ rows, err := stmt.QueryContext(ctx,
+ qryRoomNID,
+ qryReportingUser,
+ from,
+ limit,
+ )
+ if err != nil {
+ return nil, 0, err
+ }
+ defer internal.CloseAndLogIfError(ctx, rows, "SelectReportedEvents: failed to close rows")
+
+ var result []api.QueryAdminEventReportsResponse
+ var row api.QueryAdminEventReportsResponse
+ var count int64
+ for rows.Next() {
+ if err = rows.Scan(
+ &count,
+ &row.ID,
+ &row.RoomNID,
+ &row.EventNID,
+ &row.ReportingUserNID,
+ &row.SenderNID,
+ &row.Reason,
+ &row.Score,
+ &row.ReceivedTS,
+ ); err != nil {
+ return nil, 0, err
+ }
+ result = append(result, row)
+ }
+
+ return result, count, rows.Err()
+}
diff --git a/roomserver/storage/shared/storage.go b/roomserver/storage/shared/storage.go
index f1fb3cf9..c8c34907 100644
--- a/roomserver/storage/shared/storage.go
+++ b/roomserver/storage/shared/storage.go
@@ -1936,6 +1936,131 @@ func (d *Database) InsertReportedEvent(
return reportID, err
}
+// QueryAdminEventReports returns event reports given a filter.
+func (d *Database) QueryAdminEventReports(ctx context.Context, from uint64, limit uint64, backwards bool, userID string, roomID string) ([]api.QueryAdminEventReportsResponse, int64, error) {
+ // Filter on roomID, if requested
+ var roomNID types.RoomNID
+ if roomID != "" {
+ roomInfo, err := d.RoomInfo(ctx, roomID)
+ if err != nil {
+ return nil, 0, err
+ }
+ roomNID = roomInfo.RoomNID
+ }
+
+ // Same as above, but for userID
+ var userNID types.EventStateKeyNID
+ if userID != "" {
+ stateKeysMap, err := d.EventStateKeyNIDs(ctx, []string{userID})
+ if err != nil {
+ return nil, 0, err
+ }
+ if len(stateKeysMap) != 1 {
+ return nil, 0, fmt.Errorf("failed to get eventStateKeyNID for %s", userID)
+ }
+ userNID = stateKeysMap[userID]
+ }
+
+ // Query all reported events matching the filters
+ reports, count, err := d.ReportedEventsTable.SelectReportedEvents(ctx, nil, from, limit, backwards, userNID, roomNID)
+ if err != nil {
+ return nil, 0, fmt.Errorf("failed to SelectReportedEvents: %w", err)
+ }
+
+ // TODO: The below code may be inefficient due to many DB round trips and needs to be revisited.
+ // For the time being, this is "good enough".
+ qryRoomNIDs := make([]types.RoomNID, 0, len(reports))
+ qryEventNIDs := make([]types.EventNID, 0, len(reports))
+ qryStateKeyNIDs := make([]types.EventStateKeyNID, 0, len(reports))
+ for _, report := range reports {
+ qryRoomNIDs = append(qryRoomNIDs, report.RoomNID)
+ qryEventNIDs = append(qryEventNIDs, report.EventNID)
+ qryStateKeyNIDs = append(qryStateKeyNIDs, report.ReportingUserNID, report.SenderNID)
+ }
+
+ // This also de-dupes the roomIDs, otherwise we would query the same
+ // roomIDs in GetBulkStateContent multiple times
+ roomIDs, err := d.RoomsTable.BulkSelectRoomIDs(ctx, nil, qryRoomNIDs)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ // TODO: replace this with something more efficient, as it loads the entire state snapshot.
+ stateContent, err := d.GetBulkStateContent(ctx, roomIDs, []gomatrixserverlib.StateKeyTuple{
+ {EventType: spec.MRoomName, StateKey: ""},
+ {EventType: spec.MRoomCanonicalAlias, StateKey: ""},
+ }, false)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ eventIDMap, err := d.EventIDs(ctx, qryEventNIDs)
+ if err != nil {
+ logrus.WithError(err).Error("unable to map eventNIDs to eventIDs")
+ return nil, 0, err
+ }
+ if len(eventIDMap) != len(qryEventNIDs) {
+ return nil, 0, fmt.Errorf("expected %d eventIDs, got %d", len(qryEventNIDs), len(eventIDMap))
+ }
+
+ // Get a map from EventStateKeyNID to userID
+ userNIDMap, err := d.EventStateKeys(ctx, qryStateKeyNIDs)
+ if err != nil {
+ logrus.WithError(err).Error("unable to map userNIDs to userIDs")
+ return nil, 0, err
+ }
+
+ // Create a cache from roomNID to roomID to avoid hitting the DB again
+ roomNIDIDCache := make(map[types.RoomNID]string, len(roomIDs))
+ for i := 0; i < len(reports); i++ {
+ cachedRoomID := roomNIDIDCache[reports[i].RoomNID]
+ if cachedRoomID == "" {
+ // We need to query this again, as we otherwise don't have a way to match roomNID -> roomID
+ roomIDs, err = d.RoomsTable.BulkSelectRoomIDs(ctx, nil, []types.RoomNID{reports[i].RoomNID})
+ if err != nil {
+ return nil, 0, err
+ }
+ if len(roomIDs) == 0 || len(roomIDs) > 1 {
+ logrus.Warnf("unable to map roomNID %d to a roomID, was this room deleted?", roomNID)
+ continue
+ }
+ roomNIDIDCache[reports[i].RoomNID] = roomIDs[0]
+ cachedRoomID = roomIDs[0]
+ }
+
+ reports[i].EventID = eventIDMap[reports[i].EventNID]
+ reports[i].RoomID = cachedRoomID
+ roomName, canonicalAlias := findRoomNameAndCanonicalAlias(stateContent, cachedRoomID)
+ reports[i].RoomName = roomName
+ reports[i].CanonicalAlias = canonicalAlias
+ reports[i].Sender = userNIDMap[reports[i].SenderNID]
+ reports[i].UserID = userNIDMap[reports[i].ReportingUserNID]
+ }
+
+ return reports, count, nil
+}
+
+// findRoomNameAndCanonicalAlias loops over events to find the corresponding room name and canonicalAlias
+// for a given roomID.
+func findRoomNameAndCanonicalAlias(events []tables.StrippedEvent, roomID string) (name, canonicalAlias string) {
+ for _, ev := range events {
+ if ev.RoomID != roomID {
+ continue
+ }
+ if ev.EventType == spec.MRoomName {
+ name = ev.ContentValue
+ }
+ if ev.EventType == spec.MRoomCanonicalAlias {
+ canonicalAlias = ev.ContentValue
+ }
+ // We found both wanted values, break the loop
+ if name != "" && canonicalAlias != "" {
+ break
+ }
+ }
+ return name, canonicalAlias
+}
+
// FIXME TODO: Remove all this - horrible dupe with roomserver/state. Can't use the original impl because of circular loops
// it should live in this package!
diff --git a/roomserver/storage/sqlite3/reported_events_table.go b/roomserver/storage/sqlite3/reported_events_table.go
index 4a8582fc..65584f4c 100644
--- a/roomserver/storage/sqlite3/reported_events_table.go
+++ b/roomserver/storage/sqlite3/reported_events_table.go
@@ -19,7 +19,9 @@ import (
"database/sql"
"time"
+ "github.com/matrix-org/dendrite/internal"
"github.com/matrix-org/dendrite/internal/sqlutil"
+ "github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/dendrite/roomserver/storage/tables"
"github.com/matrix-org/dendrite/roomserver/types"
"github.com/matrix-org/gomatrixserverlib/spec"
@@ -44,8 +46,38 @@ const insertReportedEventSQL = `
RETURNING id
`
+const selectReportedEventsDescSQL = `
+WITH countReports AS (
+ SELECT count(*) as report_count
+ FROM roomserver_reported_events
+ WHERE ($1 IS NULL OR room_nid = $1) AND ($2 IS NULL OR reporting_user_nid = $2)
+)
+SELECT report_count, id, room_nid, event_nid, reporting_user_nid, event_sender_nid, reason, score, received_ts
+FROM roomserver_reported_events, countReports
+WHERE ($1 IS NULL OR room_nid = $1) AND ($2 IS NULL OR reporting_user_nid = $2)
+ORDER BY received_ts DESC
+LIMIT $3
+OFFSET $4
+`
+
+const selectReportedEventsAscSQL = `
+WITH countReports AS (
+ SELECT count(*) as report_count
+ FROM roomserver_reported_events
+ WHERE ($1 IS NULL OR room_nid = $1) AND ($2 IS NULL OR reporting_user_nid = $2)
+)
+SELECT report_count, id, room_nid, event_nid, reporting_user_nid, event_sender_nid, reason, score, received_ts
+FROM roomserver_reported_events, countReports
+WHERE ($1 IS NULL OR room_nid = $1) AND ($2 IS NULL OR reporting_user_nid = $2)
+ORDER BY received_ts ASC
+LIMIT $3
+OFFSET $4
+`
+
type reportedEventsStatements struct {
- insertReportedEventsStmt *sql.Stmt
+ insertReportedEventsStmt *sql.Stmt
+ selectReportedEventsDescStmt *sql.Stmt
+ selectReportedEventsAscStmt *sql.Stmt
}
func CreateReportedEventsTable(db *sql.DB) error {
@@ -58,6 +90,8 @@ func PrepareReportedEventsTable(db *sql.DB) (tables.ReportedEvents, error) {
return s, sqlutil.StatementList{
{&s.insertReportedEventsStmt, insertReportedEventSQL},
+ {&s.selectReportedEventsDescStmt, selectReportedEventsDescSQL},
+ {&s.selectReportedEventsAscStmt, selectReportedEventsAscSQL},
}.Prepare(db)
}
@@ -85,3 +119,62 @@ func (r *reportedEventsStatements) InsertReportedEvent(
).Scan(&reportID)
return reportID, err
}
+
+func (r *reportedEventsStatements) SelectReportedEvents(
+ ctx context.Context,
+ txn *sql.Tx,
+ from, limit uint64,
+ backwards bool,
+ reportingUserID types.EventStateKeyNID,
+ roomNID types.RoomNID,
+) ([]api.QueryAdminEventReportsResponse, int64, error) {
+
+ var stmt *sql.Stmt
+ if backwards {
+ stmt = sqlutil.TxStmt(txn, r.selectReportedEventsDescStmt)
+ } else {
+ stmt = sqlutil.TxStmt(txn, r.selectReportedEventsAscStmt)
+ }
+
+ var qryRoomNID *types.RoomNID
+ if roomNID > 0 {
+ qryRoomNID = &roomNID
+ }
+ var qryReportingUser *types.EventStateKeyNID
+ if reportingUserID > 0 {
+ qryReportingUser = &reportingUserID
+ }
+
+ rows, err := stmt.QueryContext(ctx,
+ qryRoomNID,
+ qryReportingUser,
+ limit,
+ from,
+ )
+ if err != nil {
+ return nil, 0, err
+ }
+ defer internal.CloseAndLogIfError(ctx, rows, "SelectReportedEvents: failed to close rows")
+
+ var result []api.QueryAdminEventReportsResponse
+ var row api.QueryAdminEventReportsResponse
+ var count int64
+ for rows.Next() {
+ if err = rows.Scan(
+ &count,
+ &row.ID,
+ &row.RoomNID,
+ &row.EventNID,
+ &row.ReportingUserNID,
+ &row.SenderNID,
+ &row.Reason,
+ &row.Score,
+ &row.ReceivedTS,
+ ); err != nil {
+ return nil, 0, err
+ }
+ result = append(result, row)
+ }
+
+ return result, count, rows.Err()
+}
diff --git a/roomserver/storage/tables/interface.go b/roomserver/storage/tables/interface.go
index cc011437..d41b5032 100644
--- a/roomserver/storage/tables/interface.go
+++ b/roomserver/storage/tables/interface.go
@@ -6,6 +6,7 @@ import (
"database/sql"
"errors"
+ "github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/gomatrixserverlib/spec"
"github.com/tidwall/gjson"
@@ -138,6 +139,14 @@ type ReportedEvents interface {
reason string,
score int64,
) (int64, error)
+ SelectReportedEvents(
+ ctx context.Context,
+ txn *sql.Tx,
+ from, limit uint64,
+ backwards bool,
+ reportingUserID types.EventStateKeyNID,
+ roomNID types.RoomNID,
+ ) ([]api.QueryAdminEventReportsResponse, int64, error)
}
type MembershipState int64