mirror of
https://github.com/matrix-org/dendrite
synced 2024-11-10 07:04:24 +00:00
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]
This commit is contained in:
parent
1bdf0cc541
commit
79072c3dcd
11 changed files with 647 additions and 4 deletions
|
@ -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
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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!
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue