mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 15:09:18 -07:00
[rooms] obey excludefromlists
This commit is contained in:
@@ -45,7 +45,10 @@ export const ROOM_SCHEMA_DDL: string[] = [
|
||||
-- rest so the blob stays the only copy; they exist to be INDEXED — see
|
||||
-- {@link LISTABLE_WHERE}.
|
||||
accessibility INTEGER GENERATED ALWAYS AS (json_extract(data, '$.Accessibility')) VIRTUAL,
|
||||
exclude_from_lists INTEGER GENERATED ALWAYS AS (json_extract(data, '$.ExcludeFromLists')) VIRTUAL
|
||||
exclude_from_lists INTEGER GENERATED ALWAYS AS (json_extract(data, '$.ExcludeFromLists')) VIRTUAL,
|
||||
-- The flag SEARCH filters on (migrations/0027_room_searchable.sql) — see
|
||||
-- {@link SEARCHABLE_WHERE}.
|
||||
exclude_from_search INTEGER GENERATED ALWAYS AS (json_extract(data, '$.ExcludeFromSearch')) VIRTUAL
|
||||
)`,
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS idx_rooms_room_id ON room (room_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_rooms_name_lower ON room (name_lower)`,
|
||||
@@ -2069,11 +2072,23 @@ const LISTABLE_WHERE = 'is_dorm IS NOT 1 AND accessibility = 1 AND exclude_from_
|
||||
|
||||
/**
|
||||
* The wider half of {@link LISTABLE_WHERE}: public and not a dorm, without the
|
||||
* `ExcludeFromLists` term. What SEARCH considers — a room can opt out of the browse feeds
|
||||
* and still be findable by name — so the two searching reads select on this instead.
|
||||
* `ExcludeFromLists` term. What `idx_room_public` covers, and the base of
|
||||
* {@link SEARCHABLE_WHERE}.
|
||||
*/
|
||||
const PUBLIC_WHERE = 'is_dorm IS NOT 1 AND accessibility = 1'
|
||||
|
||||
/**
|
||||
* What SEARCH considers — the same test {@link isSearchable} makes in memory, pushed down
|
||||
* like {@link LISTABLE_WHERE} is for the feeds. `ExcludeFromSearch` is a room's own flag,
|
||||
* independent of `ExcludeFromLists`: a room can opt out of the browse feeds and still be
|
||||
* findable by name, or be in every feed and unfindable by name. So the two searching reads
|
||||
* ({@link searchRooms}, {@link autocompleteRoomSearch}) select on this and NOT on the
|
||||
* listable predicate; `idx_room_public` still narrows them to the public rooms, and the
|
||||
* search term is checked against the rows it fetched. The in-memory filter stays, for the
|
||||
* same reason it does with the feeds.
|
||||
*/
|
||||
const SEARCHABLE_WHERE = `${PUBLIC_WHERE} AND exclude_from_search IS NOT 1`
|
||||
|
||||
/**
|
||||
* The columns a scan-then-rank feed selects in place of {@link ROOM_COLUMNS}: a STUB room
|
||||
* holding only the fields the feeds filter and sort on, not the whole blob. The feeds rank
|
||||
@@ -2473,7 +2488,7 @@ function roomsByTagsQuery(
|
||||
where = '',
|
||||
columns = ROOM_COLUMNS
|
||||
): { sql: string; binds: string[] } {
|
||||
// `where` is the caller's row filter ({@link LISTABLE_WHERE} or {@link PUBLIC_WHERE}) —
|
||||
// `where` is the caller's row filter ({@link LISTABLE_WHERE} or {@link SEARCHABLE_WHERE}) —
|
||||
// unqualified, which is unambiguous under either shape below. It matters most when
|
||||
// `tagSets` is EMPTY: that branch is the full scan every pseudo-tag feed still runs.
|
||||
const filter = where === '' ? '' : ` WHERE ${where}`
|
||||
@@ -3338,11 +3353,11 @@ function roomHasAnyTag(room: Room, tags: Set<string>): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Search public, non-dorm rooms. The query is split into terms (space/`+`):
|
||||
* `#tag` terms match the room's Tags; plain terms match the room name
|
||||
* Search public, non-dorm rooms not opted out of search. The query is split into terms
|
||||
* (space/`+`): `#tag` terms match the room's Tags; plain terms match the room name
|
||||
* (substring). All terms must match. Returns a paginated `{ Results, TotalResults }`.
|
||||
* The rows narrow in SQL — public, non-dorm ({@link PUBLIC_WHERE}) — and the name terms
|
||||
* match in memory over what comes back.
|
||||
* The rows narrow in SQL — public, non-dorm, searchable ({@link SEARCHABLE_WHERE}) — and
|
||||
* the name terms match in memory over what comes back.
|
||||
*
|
||||
* `#community` is the one tag term that isn't a tag lookup — see {@link COMMUNITY_TAG}.
|
||||
*/
|
||||
@@ -3370,12 +3385,12 @@ export async function searchRooms(
|
||||
const tagSets = tagTerms
|
||||
.filter((tag) => tag !== COMMUNITY_TAG)
|
||||
.map((tag) => [tag, ...(TAG_ALIASES[tag] ?? [])])
|
||||
const { sql, binds } = roomsByTagsQuery(tagSets, PUBLIC_WHERE)
|
||||
const { sql, binds } = roomsByTagsQuery(tagSets, SEARCHABLE_WHERE)
|
||||
const { results } = await db
|
||||
.prepare(sql)
|
||||
.bind(...binds)
|
||||
.all<RoomRow>()
|
||||
let rooms = parseAll(results).filter((r) => r.IsDorm !== true && r.Accessibility === 1)
|
||||
let rooms = parseAll(results).filter(isSearchable)
|
||||
|
||||
// The same test the hot feed's `community` chip applies: every room a player made, which
|
||||
// is every room the Coach account doesn't own. It narrows the other terms rather than
|
||||
@@ -3403,8 +3418,9 @@ export async function searchRooms(
|
||||
* which is the whole point of the endpoint: a suggestion that returns nothing is worse
|
||||
* than no suggestion. So the candidates are drawn from the two things that search matches
|
||||
* — room NAMES for a plain term, and TAGS for a `#tag` term — over the same public,
|
||||
* non-dorm rooms search itself considers. A tag comes back with its `#` so submitting the
|
||||
* suggestion verbatim searches by tag rather than for a room called "horror".
|
||||
* non-dorm, searchable rooms search itself considers. A room opted out of search must not
|
||||
* leak through its name or tags as a suggestion. A tag comes back with its `#` so
|
||||
* submitting the suggestion verbatim searches by tag rather than for a room called "horror".
|
||||
*
|
||||
* A query starting with `#` is asking for tags, so only tags are suggested. Otherwise
|
||||
* names come first (the likelier intent), then tags, and within each, matches that START
|
||||
@@ -3425,13 +3441,11 @@ export async function autocompleteRoomSearch(
|
||||
if (q === '' || take <= 0) return []
|
||||
|
||||
const { results } = await db
|
||||
.prepare(`SELECT ${ROOM_COLUMNS} FROM room WHERE ${PUBLIC_WHERE}`)
|
||||
.prepare(`SELECT ${ROOM_COLUMNS} FROM room WHERE ${SEARCHABLE_WHERE}`)
|
||||
.all<RoomRow>()
|
||||
// Tags attached up front: suggestions are drawn from them, and this reads every candidate
|
||||
// room for its NAME regardless, so the tags cost one extra query rather than a second scan.
|
||||
const rooms = (await parseAllWithTags(db, results)).filter(
|
||||
(r) => r.IsDorm !== true && r.Accessibility === 1
|
||||
)
|
||||
const rooms = (await parseAllWithTags(db, results)).filter(isSearchable)
|
||||
|
||||
const tagQuery = q.startsWith('#')
|
||||
const term = tagQuery ? q.slice(1) : q
|
||||
@@ -3515,6 +3529,15 @@ function isListable(room: Room): boolean {
|
||||
return room.IsDorm !== true && room.Accessibility === 1 && room.ExcludeFromLists !== true
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the room may come back from a SEARCH: public, not a dorm, and not opted out of
|
||||
* search. The search-side twin of {@link isListable} — `ExcludeFromSearch` is its own flag,
|
||||
* so neither test implies the other; see {@link SEARCHABLE_WHERE}.
|
||||
*/
|
||||
function isSearchable(room: Room): boolean {
|
||||
return room.IsDorm !== true && room.Accessibility === 1 && room.ExcludeFromSearch !== true
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the room is a PLAYER's rather than one of this server's stock ones — the same
|
||||
* test {@link COMMUNITY_TAG} applies, since the Coach account owns every seeded room.
|
||||
|
||||
Reference in New Issue
Block a user