Add admin endpoint to query empty rooms (#3663)
Some checks failed
Dendrite / WASM build test (push) Has been cancelled
Dendrite / Linting (push) Has been cancelled
Dendrite / Unit tests (push) Has been cancelled
Dendrite / Build for Linux (push) Has been cancelled
Dendrite / Build for Windows (push) Has been cancelled
Dendrite / Sytest (SQLite Cgo) (push) Has been cancelled
Dendrite / Initial tests passed (push) Has been cancelled
Dendrite / Integration tests (push) Has been cancelled
Dendrite / Upgrade tests (push) Has been cancelled
Dendrite / Upgrade tests from HEAD-2 (push) Has been cancelled
Dendrite / Sytest (PostgreSQL) (push) Has been cancelled
Dendrite / Sytest (SQLite native) (push) Has been cancelled
Dendrite / Complement (PostgreSQL) (push) Has been cancelled
Dendrite / Update Docker images (push) Has been cancelled
Dendrite / Complement (SQLite native) (push) Has been cancelled
Dendrite / Complement (SQLite Cgo) (push) Has been cancelled
Dendrite / Integration tests passed (push) Has been cancelled

This is to complement the existing [Purge Room Admin
API](https://element-hq.github.io/dendrite/administration/adminapi#post-_dendriteadminpurgeroomroomid)
### Pull Request Checklist

<!-- Please read
https://matrix-org.github.io/dendrite/development/contributing before
submitting your pull request -->

* [x] I have added Go unit tests or [Complement integration
tests](https://github.com/matrix-org/complement) for this PR _or_ I have
justified why this PR doesn't need tests
* [x] Pull request includes a [sign off
below](https://element-hq.github.io/dendrite/development/contributing#sign-off)
_or_ I have already signed off privately

---------

Signed-off-by: Till Faelligen <2353100+S7evinK@users.noreply.github.com>
This commit is contained in:
Till 2025-12-10 22:56:04 +01:00 committed by GitHub
parent 68458c139d
commit a042861df5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 111 additions and 2 deletions

View file

@ -73,7 +73,7 @@ func AdminCreateNewRegistrationToken(req *http.Request, cfg *config.ClientAPI, u
}
if len(token) > 64 {
//Token present in request body, but is too long.
// Token present in request body, but is too long.
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.BadJSON("token must not be longer than 64"),
@ -578,6 +578,23 @@ func DeleteEventReport(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAP
}
}
func QueryEmptyRooms(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
emptyRooms, err := rsAPI.AdminQueryEmptyRooms(req.Context())
if err != nil {
logrus.WithError(err).Error("failed to query empty rooms")
return util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: spec.InternalServerError{},
}
}
return util.JSONResponse{
Code: http.StatusOK,
JSON: map[string]any{
"empty_rooms": emptyRooms,
},
}
}
func parseUint64OrDefault(input string, defaultValue uint64) uint64 {
v, err := strconv.ParseUint(input, 10, 64)
if err != nil {

View file

@ -243,6 +243,12 @@ func Setup(
}),
).Methods(http.MethodPost, http.MethodOptions)
dendriteAdminRouter.Handle("/admin/emptyRooms",
httputil.MakeAdminAPI("admin_empty_rooms", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
return QueryEmptyRooms(req, rsAPI)
}),
).Methods(http.MethodGet, http.MethodOptions)
// server notifications
if cfg.Matrix.ServerNotices.Enabled {
logrus.Info("Enabling server notices at /_synapse/admin/v1/send_server_notice")

View file

@ -77,6 +77,19 @@ This endpoint instructs Dendrite to immediately query `/devices/{userID}` on a f
This endpoint instructs Dendrite to remove the given room from its database. It does **NOT** remove media files. Depending on the size of the room, this may take a while. Will return an empty JSON once other components were instructed to delete the room.
## GET `/_dendrite/admin/emptyRooms`
Returns a list of all rooms which have zero (locally) joined members. Response format:
```json
{
"empty_rooms": [
"!roomid1:server_name",
"!roomid2:server_name"
]
}
```
## POST `/_synapse/admin/v1/send_server_notice`
Request body format:

View file

@ -89,6 +89,8 @@ type RoomserverInternalAPI interface {
// RoomsWithACLs returns all room IDs for rooms with ACLs
RoomsWithACLs(ctx context.Context) ([]string, error)
// EmptyRooms returns all rooms that the local server has left.
EmptyRooms(ctx context.Context) ([]string, error)
}
type UserRoomPrivateKeyCreator interface {
@ -248,6 +250,7 @@ type ClientRoomserverAPI interface {
PerformAdminEvacuateUser(ctx context.Context, userID string) (affected []string, err error)
PerformAdminPurgeRoom(ctx context.Context, roomID string) error
PerformAdminDownloadState(ctx context.Context, roomID, userID string, serverName spec.ServerName) error
AdminQueryEmptyRooms(ctx context.Context) ([]string, error)
PerformPeek(ctx context.Context, req *PerformPeekRequest) (roomID string, err error)
PerformUnpeek(ctx context.Context, roomID, userID, deviceID string) error
PerformInvite(ctx context.Context, req *PerformInviteRequest) error
@ -263,7 +266,7 @@ type ClientRoomserverAPI interface {
// If true, then the alias has not been set to the provided room, as it already in use.
SetRoomAlias(ctx context.Context, senderID spec.SenderID, roomID spec.RoomID, alias string) (aliasAlreadyExists bool, err error)
//RemoveRoomAlias(ctx context.Context, req *RemoveRoomAliasRequest, res *RemoveRoomAliasResponse) error
// RemoveRoomAlias(ctx context.Context, req *RemoveRoomAliasRequest, res *RemoveRoomAliasResponse) error
// Removes a room alias, as provided sender.
//
// Returns whether the alias was found, whether it was removed, and an error (if any occurred)

View file

@ -350,3 +350,7 @@ func (r *Admin) PerformAdminDownloadState(
func (r *Admin) PerformAdminDeleteEventReport(ctx context.Context, reportID uint64) error {
return r.DB.AdminDeleteEventReport(ctx, reportID)
}
func (r *Admin) AdminQueryEmptyRooms(ctx context.Context) ([]string, error) {
return r.DB.EmptyRooms(ctx)
}

View file

@ -1097,6 +1097,11 @@ func (r *Queryer) RoomsWithACLs(ctx context.Context) ([]string, error) {
return r.DB.RoomsWithACLs(ctx)
}
// EmptyRooms returns all rooms that the local server has left.
func (r *Queryer) EmptyRooms(ctx context.Context) ([]string, error) {
return r.DB.EmptyRooms(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)

View file

@ -1319,3 +1319,35 @@ func TestRoomsWithACLs(t *testing.T) {
assert.Equal(t, []string{aclRoom.ID}, roomsWithACLs)
})
}
func TestEmptyRooms(t *testing.T) {
ctx := context.Background()
alice := test.NewUser(t)
r1 := test.NewRoom(t, alice)
r2 := test.NewRoom(t, alice)
r2.CreateAndInsert(t, alice, spec.MRoomMember, map[string]interface{}{"membership": spec.Leave}, test.WithStateKey(alice.ID))
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
cfg, processCtx, closeDB := testrig.CreateConfig(t, dbType)
defer closeDB()
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
natsInstance := &jetstream.NATSInstance{}
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
// start JetStream listeners
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, natsInstance, caches, caching.DisableMetrics)
rsAPI.SetFederationAPI(nil, nil)
for _, room := range []*test.Room{r1, r2} {
// Create the rooms
err := api.SendEvents(ctx, rsAPI, api.KindNew, room.Events(), "test", "test", "test", nil, false)
assert.NoError(t, err)
}
// We should only have r2 as an empty room
emptyRooms, err := rsAPI.EmptyRooms(ctx)
assert.NoError(t, err)
assert.Equal(t, []string{r2.ID}, emptyRooms)
})
}

View file

@ -187,6 +187,9 @@ type Database interface {
// RoomsWithACLs returns all room IDs for rooms with ACLs
RoomsWithACLs(ctx context.Context) ([]string, error)
// EmptyRooms returns all rooms that the local server has left.
EmptyRooms(ctx context.Context) ([]string, error)
// GetBulkStateACLs returns all server ACLs for the given rooms.
GetBulkStateACLs(ctx context.Context, roomIDs []string) ([]tables.StrippedEvent, error)
QueryAdminEventReports(ctx context.Context, from uint64, limit uint64, backwards bool, userID string, roomID string) ([]api.QueryAdminEventReportsResponse, int64, error)

View file

@ -1707,6 +1707,32 @@ func (d *Database) RoomsWithACLs(ctx context.Context) ([]string, error) {
return roomIDs, nil
}
// EmptyRooms returns all rooms that the local server has left.
func (d *Database) EmptyRooms(ctx context.Context) ([]string, error) {
eventTypeNID := types.EventTypeNID(5)
roomNIDs, err := d.EventsTable.SelectRoomsWithEventTypeNID(ctx, nil, eventTypeNID)
if err != nil {
return nil, err
}
// Figure out if we are joined to the rooms
leftRoomsNIDs := make([]types.RoomNID, 0, len(roomNIDs))
for i := 0; i < len(roomNIDs); i++ {
inRoom, err := d.GetLocalServerInRoom(ctx, roomNIDs[i])
if err != nil {
return nil, err
}
if inRoom {
continue
}
// Server is not in the room anymore
leftRoomsNIDs = append(leftRoomsNIDs, roomNIDs[i])
}
return d.RoomsTable.BulkSelectRoomIDs(ctx, nil, leftRoomsNIDs)
}
// ForgetRoom sets a users room to forgotten
func (d *Database) ForgetRoom(ctx context.Context, userID, roomID string, forget bool) error {
roomNIDs, err := d.RoomsTable.BulkSelectRoomNIDs(ctx, nil, []string{roomID})