From fac8f409f4eefc69395b740c32ea9e1c24d0d76a Mon Sep 17 00:00:00 2001 From: Andy Burke Date: Wed, 10 Sep 2025 12:51:27 -0700 Subject: [PATCH] refactor: zones => topics --- README.md | 6 +- models/event.ts | 26 +-- models/{zone.ts => topic.ts} | 50 +++--- public/api/topics/:topic_id/README.md | 23 +++ .../:topic_id}/events/:event_id/README.md | 0 .../:topic_id}/events/:event_id/index.ts | 76 ++++----- public/api/topics/:topic_id/events/README.md | 15 ++ .../:topic_id}/events/index.ts | 52 +++--- .../:zone_id => topics/:topic_id}/index.ts | 58 +++---- public/api/topics/README.md | 28 ++++ public/api/{zones => topics}/index.ts | 46 +++--- public/api/users/index.ts | 2 +- public/api/zones/:zone_id/README.md | 23 --- public/api/zones/:zone_id/events/README.md | 15 -- public/api/zones/README.md | 28 ---- public/base.css | 2 +- public/tabs/chat/chat.css | 34 ++-- public/tabs/chat/chat.html | 75 ++++----- public/tabs/chat/chat.js | 154 +++++++++--------- .../create_topic.test.ts} | 24 +-- .../delete_topic.test.ts} | 18 +- .../events/create_events.test.ts | 32 ++-- .../events/get_events.test.ts | 22 +-- .../events/update_events.test.ts | 54 +++--- .../update_events_when_append_only.test.ts | 38 ++--- .../update_topic.test.ts} | 38 ++--- 26 files changed, 470 insertions(+), 469 deletions(-) rename models/{zone.ts => topic.ts} (59%) create mode 100644 public/api/topics/:topic_id/README.md rename public/api/{zones/:zone_id => topics/:topic_id}/events/:event_id/README.md (100%) rename public/api/{zones/:zone_id => topics/:topic_id}/events/:event_id/index.ts (57%) create mode 100644 public/api/topics/:topic_id/events/README.md rename public/api/{zones/:zone_id => topics/:topic_id}/events/index.ts (72%) rename public/api/{zones/:zone_id => topics/:topic_id}/index.ts (59%) create mode 100644 public/api/topics/README.md rename public/api/{zones => topics}/index.ts (69%) delete mode 100644 public/api/zones/:zone_id/README.md delete mode 100644 public/api/zones/:zone_id/events/README.md delete mode 100644 public/api/zones/README.md rename tests/api/{zones/create_zone.test.ts => topics/create_topic.test.ts} (70%) rename tests/api/{zones/delete_zone.test.ts => topics/delete_topic.test.ts} (74%) rename tests/api/{zones => topics}/events/create_events.test.ts (71%) rename tests/api/{zones => topics}/events/get_events.test.ts (81%) rename tests/api/{zones => topics}/events/update_events.test.ts (73%) rename tests/api/{zones => topics}/events/update_events_when_append_only.test.ts (70%) rename tests/api/{zones/update_zone.test.ts => topics/update_topic.test.ts} (60%) diff --git a/README.md b/README.md index 36bccb7..331d047 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Bringing the BBS back. These are in no particular order. Pull requests updating this section welcome for feature discussions. -- [ ] should everything be an event in a zone? +- [ ] should everything be an event in a topic? - [X] get a first-pass podman/docker setup up - [X] sign up - [X] check for logged in user session @@ -21,7 +21,7 @@ feature discussions. - [X] logout button - [ ] profile editing - [X] avatar uploads -- [X] chat zones +- [X] chat topics - [X] chat messages - [ ] chat message actions - [X] '...' button to show actions @@ -76,7 +76,7 @@ feature discussions. - [ ] if web notifications are enabled, emit on events - [ ] ability to mute - [ ] users - - [ ] zones + - [ ] topics - [ ] tags (#tags?) - [ ] admin panel - [ ] add invite code generation diff --git a/models/event.ts b/models/event.ts index 42c9fe9..19880ae 100644 --- a/models/event.ts +++ b/models/event.ts @@ -32,16 +32,16 @@ export type EVENT = { }; }; -type ZONE_EVENT_CACHE_ENTRY = { +type TOPIC_EVENT_CACHE_ENTRY = { collection: FSDB_COLLECTION; eviction_timeout: number; }; -const ZONE_EVENTS: Record = {}; -export function get_events_collection_for_zone(zone_id: string): FSDB_COLLECTION { - ZONE_EVENTS[zone_id] = ZONE_EVENTS[zone_id] ?? { +const TOPIC_EVENTS: Record = {}; +export function get_events_collection_for_topic(topic_id: string): FSDB_COLLECTION { + TOPIC_EVENTS[topic_id] = TOPIC_EVENTS[topic_id] ?? { collection: new FSDB_COLLECTION({ - name: `zones/${zone_id.slice(0, 14)}/${zone_id.slice(0, 34)}/${zone_id}/events`, + name: `topics/${topic_id.slice(0, 14)}/${topic_id.slice(0, 34)}/${topic_id}/events`, id_field: 'id', organize: by_lurid, indexers: { @@ -65,22 +65,22 @@ export function get_events_collection_for_zone(zone_id: string): FSDB_COLLECTION eviction_timeout: 0 }; - if (ZONE_EVENTS[zone_id].eviction_timeout) { - clearTimeout(ZONE_EVENTS[zone_id].eviction_timeout); + if (TOPIC_EVENTS[topic_id].eviction_timeout) { + clearTimeout(TOPIC_EVENTS[topic_id].eviction_timeout); } - ZONE_EVENTS[zone_id].eviction_timeout = setTimeout(() => { - delete ZONE_EVENTS[zone_id]; + TOPIC_EVENTS[topic_id].eviction_timeout = setTimeout(() => { + delete TOPIC_EVENTS[topic_id]; }, 60_000 * 5); - return ZONE_EVENTS[zone_id].collection; + return TOPIC_EVENTS[topic_id].collection; } -export function clear_zone_events_cache() { - for (const [zone_id, cached] of Object.entries(ZONE_EVENTS)) { +export function clear_topic_events_cache() { + for (const [topic_id, cached] of Object.entries(TOPIC_EVENTS)) { if (cached.eviction_timeout) { clearTimeout(cached.eviction_timeout); } - delete ZONE_EVENTS[zone_id]; + delete TOPIC_EVENTS[topic_id]; } } diff --git a/models/zone.ts b/models/topic.ts similarity index 59% rename from models/zone.ts rename to models/topic.ts index 36ae5f9..fa01e66 100644 --- a/models/zone.ts +++ b/models/topic.ts @@ -3,29 +3,29 @@ import { FSDB_COLLECTION } from '@andyburke/fsdb'; import { FSDB_INDEXER_SYMLINKS } from '@andyburke/fsdb/indexers'; /** - * @typedef {object} ZONE_PERMISSIONS - * @property {string[]} read a list of user_ids with read permission for the zone - * @property {string[]} write a list of user_ids with write permission for the zone - * @property {string[]} read_events a list of user_ids with read_events permission for this zone - * @property {string[]} write_events a list of user_ids with write_events permission for this zone + * @typedef {object} TOPIC_PERMISSIONS + * @property {string[]} read a list of user_ids with read permission for the topic + * @property {string[]} write a list of user_ids with write permission for the topic + * @property {string[]} read_events a list of user_ids with read_events permission for this topic + * @property {string[]} write_events a list of user_ids with write_events permission for this topic */ /** - * ZONE + * TOPIC * * @property {string} id - lurid (stable) * @property {string} name - channel name (max 64 characters, unique, unstable) - * @property {string} creator_id - user id of the zone creator - * @property {ZONE_PERMISSIONS} permissions - permissions setup for the zone - * @property {string} [icon_url] - optional url for zone icon - * @property {string} [topic] - optional topic for the zone - * @property {string} [rules] - optional zone rules (Markdown/text) - * @property {string[]} [tags] - optional tags for the zone - * @property {Record} [meta] - optional metadata about the zone + * @property {string} creator_id - user id of the topic creator + * @property {TOPIC_PERMISSIONS} permissions - permissions setup for the topic + * @property {string} [icon_url] - optional url for topic icon + * @property {string} [topic] - optional topic for the topic + * @property {string} [rules] - optional topic rules (Markdown/text) + * @property {string[]} [tags] - optional tags for the topic + * @property {Record} [meta] - optional metadata about the topic * @property {Record} [emojis] - optional emojis table, eg: { 'rofl': 🤣, 'blap': 'https://somewhere.someplace/image.jpg' } */ -export type ZONE = { +export type TOPIC = { id: string; name: string; creator_id: string; @@ -48,37 +48,37 @@ export type ZONE = { }; }; -export const ZONES = new FSDB_COLLECTION({ - name: 'zones', +export const TOPICS = new FSDB_COLLECTION({ + name: 'topics', id_field: 'id', organize: by_lurid, indexers: { - creator_id: new FSDB_INDEXER_SYMLINKS({ + creator_id: new FSDB_INDEXER_SYMLINKS({ name: 'creator_id', field: 'creator_id', to_many: true, organize: by_lurid }), - name: new FSDB_INDEXER_SYMLINKS({ + name: new FSDB_INDEXER_SYMLINKS({ name: 'name', - get_values_to_index: (zone) => [zone.name.toLowerCase()], + get_values_to_index: (topic) => [topic.name.toLowerCase()], organize: by_character }), - tags: new FSDB_INDEXER_SYMLINKS({ + tags: new FSDB_INDEXER_SYMLINKS({ name: 'tags', - get_values_to_index: (zone): string[] => { - return (zone.tags ?? []).map((tag) => tag.toLowerCase()); + get_values_to_index: (topic): string[] => { + return (topic.tags ?? []).map((tag) => tag.toLowerCase()); }, to_many: true, organize: by_character }), - topic: new FSDB_INDEXER_SYMLINKS({ + topic: new FSDB_INDEXER_SYMLINKS({ name: 'topic', - get_values_to_index: (zone): string[] => { - return (zone.topic ?? '').split(/\W/); + get_values_to_index: (topic): string[] => { + return (topic.topic ?? '').split(/\W/); }, to_many: true, organize: by_character diff --git a/public/api/topics/:topic_id/README.md b/public/api/topics/:topic_id/README.md new file mode 100644 index 0000000..ec62541 --- /dev/null +++ b/public/api/topics/:topic_id/README.md @@ -0,0 +1,23 @@ +# /api/topics/:topic_id + +Interact with a specific topic. + +## GET /api/topics/:topic_id + +Get the topic specified by `:topic_id`. + +## PUT /api/topics/:topic_id + +Update the topics specified by `:topic_id`. + +Eg: + +``` +{ + name?: string; +} +``` + +## DELETE /api/topics/:topic_id + +Delete the topic specified by `:topic_id`. diff --git a/public/api/zones/:zone_id/events/:event_id/README.md b/public/api/topics/:topic_id/events/:event_id/README.md similarity index 100% rename from public/api/zones/:zone_id/events/:event_id/README.md rename to public/api/topics/:topic_id/events/:event_id/README.md diff --git a/public/api/zones/:zone_id/events/:event_id/index.ts b/public/api/topics/:topic_id/events/:event_id/index.ts similarity index 57% rename from public/api/zones/:zone_id/events/:event_id/index.ts rename to public/api/topics/:topic_id/events/:event_id/index.ts index f6693d8..6149d6f 100644 --- a/public/api/zones/:zone_id/events/:event_id/index.ts +++ b/public/api/topics/:topic_id/events/:event_id/index.ts @@ -1,36 +1,36 @@ import { FSDB_COLLECTION } from '@andyburke/fsdb'; -import { EVENT, get_events_collection_for_zone } from '../../../../../../models/event.ts'; -import { ZONE, ZONES } from '../../../../../../models/zone.ts'; +import { EVENT, get_events_collection_for_topic } from '../../../../../../models/event.ts'; +import { TOPIC, TOPICS } from '../../../../../../models/topic.ts'; import parse_body from '../../../../../../utils/bodyparser.ts'; import * as CANNED_RESPONSES from '../../../../../../utils/canned_responses.ts'; import { get_session, get_user, PRECHECK_TABLE, require_user } from '../../../../../../utils/prechecks.ts'; export const PRECHECKS: PRECHECK_TABLE = {}; -// GET /api/zones/:zone_id/events/:id - Get an event +// GET /api/topics/:topic_id/events/:id - Get an event PRECHECKS.GET = [get_session, get_user, require_user, async (_req: Request, meta: Record): Promise => { - const zone_id: string = meta.params?.zone_id?.toLowerCase().trim() ?? ''; + const topic_id: string = meta.params?.topic_id?.toLowerCase().trim() ?? ''; // lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same" - const zone: ZONE | null = zone_id.length === 49 ? await ZONES.get(zone_id) : null; + const topic: TOPIC | null = topic_id.length === 49 ? await TOPICS.get(topic_id) : null; - if (!zone) { + if (!topic) { return CANNED_RESPONSES.not_found(); } - meta.zone = zone; - const zone_is_public = zone.permissions.read.length === 0; - const user_has_read_for_zone = zone_is_public || zone.permissions.read.includes(meta.user.id); - const zone_has_public_events = user_has_read_for_zone && (zone.permissions.read_events.length === 0); - const user_has_read_events_for_zone = user_has_read_for_zone && - (zone_has_public_events || zone.permissions.read_events.includes(meta.user.id)); + meta.topic = topic; + const topic_is_public = topic.permissions.read.length === 0; + const user_has_read_for_topic = topic_is_public || topic.permissions.read.includes(meta.user.id); + const topic_has_public_events = user_has_read_for_topic && (topic.permissions.read_events.length === 0); + const user_has_read_events_for_topic = user_has_read_for_topic && + (topic_has_public_events || topic.permissions.read_events.includes(meta.user.id)); - if (!user_has_read_events_for_zone) { + if (!user_has_read_events_for_topic) { return CANNED_RESPONSES.permission_denied(); } }]; export async function GET(_req: Request, meta: Record): Promise { - const events: FSDB_COLLECTION = get_events_collection_for_zone(meta.zone.id); + const events: FSDB_COLLECTION = get_events_collection_for_topic(meta.topic.id); const event: EVENT | null = await events.get(meta.params.event_id); if (!event) { @@ -42,7 +42,7 @@ export async function GET(_req: Request, meta: Record): Promise): Promise => { - const zone_id: string = meta.params?.zone_id?.toLowerCase().trim() ?? ''; + const topic_id: string = meta.params?.topic_id?.toLowerCase().trim() ?? ''; // lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same" - const zone: ZONE | null = zone_id.length === 49 ? await ZONES.get(zone_id) : null; + const topic: TOPIC | null = topic_id.length === 49 ? await TOPICS.get(topic_id) : null; - if (!zone) { + if (!topic) { return CANNED_RESPONSES.not_found(); } - meta.zone = zone; - const zone_is_public: boolean = meta.zone.permissions.read.length === 0; - const user_has_read_for_zone = zone_is_public || meta.zone.permissions.read.includes(meta.user.id); - const zone_events_are_publicly_writable = meta.zone.permissions.write_events.length === 0; - const user_has_write_events_for_zone = user_has_read_for_zone && - (zone_events_are_publicly_writable || meta.zone.permissions.write_events.includes(meta.user.id)); + meta.topic = topic; + const topic_is_public: boolean = meta.topic.permissions.read.length === 0; + const user_has_read_for_topic = topic_is_public || meta.topic.permissions.read.includes(meta.user.id); + const topic_events_are_publicly_writable = meta.topic.permissions.write_events.length === 0; + const user_has_write_events_for_topic = user_has_read_for_topic && + (topic_events_are_publicly_writable || meta.topic.permissions.write_events.includes(meta.user.id)); - if (!user_has_write_events_for_zone) { + if (!user_has_write_events_for_topic) { return CANNED_RESPONSES.permission_denied(); } } @@ -78,7 +78,7 @@ export async function PUT(req: Request, meta: Record): Promise = get_events_collection_for_zone(meta.zone.id); + const events: FSDB_COLLECTION = get_events_collection_for_topic(meta.topic.id); const event: EVENT | null = await events.get(meta.params.event_id); if (!event) { @@ -117,7 +117,7 @@ export async function PUT(req: Request, meta: Record): Promise): Promise => { - const zone_id: string = meta.params?.zone_id?.toLowerCase().trim() ?? ''; + const topic_id: string = meta.params?.topic_id?.toLowerCase().trim() ?? ''; // lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same" - const zone: ZONE | null = zone_id.length === 49 ? await ZONES.get(zone_id) : null; + const topic: TOPIC | null = topic_id.length === 49 ? await TOPICS.get(topic_id) : null; - if (!zone) { + if (!topic) { return CANNED_RESPONSES.not_found(); } - meta.zone = zone; - const zone_is_public: boolean = meta.zone.permissions.read.length === 0; - const user_has_read_for_zone = zone_is_public || meta.zone.permissions.read.includes(meta.user.id); - const zone_events_are_publicly_writable = meta.zone.permissions.write_events.length === 0; - const user_has_write_events_for_zone = user_has_read_for_zone && - (zone_events_are_publicly_writable || meta.zone.permissions.write_events.includes(meta.user.id)); + meta.topic = topic; + const topic_is_public: boolean = meta.topic.permissions.read.length === 0; + const user_has_read_for_topic = topic_is_public || meta.topic.permissions.read.includes(meta.user.id); + const topic_events_are_publicly_writable = meta.topic.permissions.write_events.length === 0; + const user_has_write_events_for_topic = user_has_read_for_topic && + (topic_events_are_publicly_writable || meta.topic.permissions.write_events.includes(meta.user.id)); - if (!user_has_write_events_for_zone) { + if (!user_has_write_events_for_topic) { return CANNED_RESPONSES.permission_denied(); } } ]; export async function DELETE(_req: Request, meta: Record): Promise { - const events: FSDB_COLLECTION = get_events_collection_for_zone(meta.zone.id); + const events: FSDB_COLLECTION = get_events_collection_for_topic(meta.topic.id); const event: EVENT | null = await events.get(meta.params.event_id); if (!event) { return CANNED_RESPONSES.not_found(); diff --git a/public/api/topics/:topic_id/events/README.md b/public/api/topics/:topic_id/events/README.md new file mode 100644 index 0000000..13582fa --- /dev/null +++ b/public/api/topics/:topic_id/events/README.md @@ -0,0 +1,15 @@ +# /api/topics/:topic_id/events + +Interact with a events for a topic. + +## GET /api/topics/:topic_id/events + +Get events for the given topic. + +## PUT /api/topics/:topic_id/events/:event_id + +Update an event. + +## DELETE /api/topics/:topic_id/events/:event_id + +Delete an event. diff --git a/public/api/zones/:zone_id/events/index.ts b/public/api/topics/:topic_id/events/index.ts similarity index 72% rename from public/api/zones/:zone_id/events/index.ts rename to public/api/topics/:topic_id/events/index.ts index 4597b0a..20e5dc8 100644 --- a/public/api/zones/:zone_id/events/index.ts +++ b/public/api/topics/:topic_id/events/index.ts @@ -1,40 +1,40 @@ import lurid from '@andyburke/lurid'; import { get_session, get_user, PRECHECK_TABLE, require_user } from '../../../../../utils/prechecks.ts'; -import { ZONE, ZONES } from '../../../../../models/zone.ts'; +import { TOPIC, TOPICS } from '../../../../../models/topic.ts'; import * as CANNED_RESPONSES from '../../../../../utils/canned_responses.ts'; -import { EVENT, get_events_collection_for_zone } from '../../../../../models/event.ts'; +import { EVENT, get_events_collection_for_topic } from '../../../../../models/event.ts'; import parse_body from '../../../../../utils/bodyparser.ts'; import { FSDB_COLLECTION, FSDB_SEARCH_OPTIONS, WALK_ENTRY } from '@andyburke/fsdb'; import * as path from '@std/path'; export const PRECHECKS: PRECHECK_TABLE = {}; -// GET /api/zones/:zone_id/events - get zone events +// GET /api/topics/:topic_id/events - get topic events // query parameters: // partial_id: the partial id subset you would like to match (remember, lurids are lexigraphically sorted) PRECHECKS.GET = [get_session, get_user, require_user, async (_req: Request, meta: Record): Promise => { - const zone_id: string = meta.params?.zone_id?.toLowerCase().trim() ?? ''; + const topic_id: string = meta.params?.topic_id?.toLowerCase().trim() ?? ''; // lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same" - const zone: ZONE | null = zone_id.length === 49 ? await ZONES.get(zone_id) : null; + const topic: TOPIC | null = topic_id.length === 49 ? await TOPICS.get(topic_id) : null; - if (!zone) { + if (!topic) { return CANNED_RESPONSES.not_found(); } - meta.zone = zone; - const zone_is_public: boolean = meta.zone.permissions.read.length === 0; - const user_has_read_for_zone = zone_is_public || meta.zone.permissions.read.includes(meta.user.id); - const zone_events_are_public = meta.zone.permissions.read_events.length === 0; - const user_has_read_events_for_zone = user_has_read_for_zone && - (zone_events_are_public || meta.zone.permissions.read_events.includes(meta.user.id)); + meta.topic = topic; + const topic_is_public: boolean = meta.topic.permissions.read.length === 0; + const user_has_read_for_topic = topic_is_public || meta.topic.permissions.read.includes(meta.user.id); + const topic_events_are_public = meta.topic.permissions.read_events.length === 0; + const user_has_read_events_for_topic = user_has_read_for_topic && + (topic_events_are_public || meta.topic.permissions.read_events.includes(meta.user.id)); - if (!user_has_read_events_for_zone) { + if (!user_has_read_events_for_topic) { return CANNED_RESPONSES.permission_denied(); } }]; export async function GET(request: Request, meta: Record): Promise { - const events: FSDB_COLLECTION = get_events_collection_for_zone(meta.zone.id); + const events: FSDB_COLLECTION = get_events_collection_for_topic(meta.topic.id); const sorts = events.sorts; const sort_name: string = meta.query.sort ?? 'newest'; @@ -126,31 +126,31 @@ export async function GET(request: Request, meta: Record): Promise< }); } -// POST /api/zones/:zone_id/events - Create an event +// POST /api/topics/:topic_id/events - Create an event PRECHECKS.POST = [get_session, get_user, require_user, async (_req: Request, meta: Record): Promise => { - const zone_id: string = meta.params?.zone_id?.toLowerCase().trim() ?? ''; + const topic_id: string = meta.params?.topic_id?.toLowerCase().trim() ?? ''; // lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same" - const zone: ZONE | null = zone_id.length === 49 ? await ZONES.get(zone_id) : null; + const topic: TOPIC | null = topic_id.length === 49 ? await TOPICS.get(topic_id) : null; - if (!zone) { + if (!topic) { return CANNED_RESPONSES.not_found(); } - meta.zone = zone; - const zone_is_public: boolean = meta.zone.permissions.read.length === 0; - const user_has_read_for_zone = zone_is_public || meta.zone.permissions.read.includes(meta.user.id); - const zone_events_are_publicly_writable = meta.zone.permissions.write_events.length === 0; - const user_has_write_events_for_zone = user_has_read_for_zone && - (zone_events_are_publicly_writable || meta.zone.permissions.write_events.includes(meta.user.id)); + meta.topic = topic; + const topic_is_public: boolean = meta.topic.permissions.read.length === 0; + const user_has_read_for_topic = topic_is_public || meta.topic.permissions.read.includes(meta.user.id); + const topic_events_are_publicly_writable = meta.topic.permissions.write_events.length === 0; + const user_has_write_events_for_topic = user_has_read_for_topic && + (topic_events_are_publicly_writable || meta.topic.permissions.write_events.includes(meta.user.id)); - if (!user_has_write_events_for_zone) { + if (!user_has_write_events_for_topic) { return CANNED_RESPONSES.permission_denied(); } }]; export async function POST(req: Request, meta: Record): Promise { try { - const events: FSDB_COLLECTION = get_events_collection_for_zone(meta.zone.id); + const events: FSDB_COLLECTION = get_events_collection_for_topic(meta.topic.id); const now = new Date().toISOString(); diff --git a/public/api/zones/:zone_id/index.ts b/public/api/topics/:topic_id/index.ts similarity index 59% rename from public/api/zones/:zone_id/index.ts rename to public/api/topics/:topic_id/index.ts index 9708110..dd656bc 100644 --- a/public/api/zones/:zone_id/index.ts +++ b/public/api/topics/:topic_id/index.ts @@ -1,50 +1,50 @@ import { get_session, get_user, PRECHECK_TABLE, require_user } from '../../../../utils/prechecks.ts'; import parse_body from '../../../../utils/bodyparser.ts'; import * as CANNED_RESPONSES from '../../../../utils/canned_responses.ts'; -import { ZONE, ZONES } from '../../../../models/zone.ts'; +import { TOPIC, TOPICS } from '../../../../models/topic.ts'; export const PRECHECKS: PRECHECK_TABLE = {}; -// GET /api/zones/:id - Get a zone +// GET /api/topics/:id - Get a topic PRECHECKS.GET = [get_session, get_user, require_user, async (_req: Request, meta: Record): Promise => { - const zone_id: string = meta.params?.zone_id?.toLowerCase().trim() ?? ''; + const topic_id: string = meta.params?.topic_id?.toLowerCase().trim() ?? ''; // lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same" - const zone: ZONE | null = zone_id.length === 49 ? await ZONES.get(zone_id) : null; + const topic: TOPIC | null = topic_id.length === 49 ? await TOPICS.get(topic_id) : null; - if (!zone) { + if (!topic) { return CANNED_RESPONSES.not_found(); } - meta.zone = zone; - const zone_is_public = zone.permissions.read.length === 0; - const user_has_read_for_zone = zone_is_public || zone.permissions.read.includes(meta.user.id); + meta.topic = topic; + const topic_is_public = topic.permissions.read.length === 0; + const user_has_read_for_topic = topic_is_public || topic.permissions.read.includes(meta.user.id); - if (!user_has_read_for_zone) { + if (!user_has_read_for_topic) { return CANNED_RESPONSES.permission_denied(); } }]; export function GET(_req: Request, meta: Record): Response { - return Response.json(meta.zone, { + return Response.json(meta.topic, { status: 200 }); } -// PUT /api/zones/:id - Update zone +// PUT /api/topics/:id - Update topic PRECHECKS.PUT = [get_session, get_user, require_user, async (_req: Request, meta: Record): Promise => { - const zone_id: string = meta.params?.zone_id?.toLowerCase().trim() ?? ''; + const topic_id: string = meta.params?.topic_id?.toLowerCase().trim() ?? ''; // lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same" - const zone: ZONE | null = zone_id.length === 49 ? await ZONES.get(zone_id) : null; + const topic: TOPIC | null = topic_id.length === 49 ? await TOPICS.get(topic_id) : null; - if (!zone) { + if (!topic) { return CANNED_RESPONSES.not_found(); } - meta.zone = zone; - const user_has_write_for_zone = zone.permissions.write.includes(meta.user.id); + meta.topic = topic; + const user_has_write_for_topic = topic.permissions.write.includes(meta.user.id); - if (!user_has_write_for_zone) { + if (!user_has_write_for_topic) { return CANNED_RESPONSES.permission_denied(); } }]; @@ -54,16 +54,16 @@ export async function PUT(req: Request, meta: Record): Promise): Promise): Promise => { - const zone_id: string = meta.params?.zone_id?.toLowerCase().trim() ?? ''; + const topic_id: string = meta.params?.topic_id?.toLowerCase().trim() ?? ''; // lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same" - const zone: ZONE | null = zone_id.length === 49 ? await ZONES.get(zone_id) : null; + const topic: TOPIC | null = topic_id.length === 49 ? await TOPICS.get(topic_id) : null; - if (!zone) { + if (!topic) { return CANNED_RESPONSES.not_found(); } - meta.zone = zone; - const user_has_write_for_zone = zone.permissions.write.includes(meta.user.id); + meta.topic = topic; + const user_has_write_for_topic = topic.permissions.write.includes(meta.user.id); - if (!user_has_write_for_zone) { + if (!user_has_write_for_topic) { return CANNED_RESPONSES.permission_denied(); } } ]; export async function DELETE(_req: Request, meta: Record): Promise { - await ZONES.delete(meta.zone); + await TOPICS.delete(meta.topic); return Response.json({ deleted: true diff --git a/public/api/topics/README.md b/public/api/topics/README.md new file mode 100644 index 0000000..7783a6d --- /dev/null +++ b/public/api/topics/README.md @@ -0,0 +1,28 @@ +# /api/topics + +Interact with topics. + +## POST /api/topics + +Create a new topic. + +``` +export type TOPIC = { + id: string; // unique id for this topic + name: string; // the name of the topic (max 128 characters) + icon_url?: string; // optional url for a topic icon + topic?: string; // optional topic topic + tags: string[]; // a list of tags for the topic + meta: Record; + limits: { + users: number; + user_messages_per_minute: number; + }; + creator_id: string; // user_id of the topic creator + emojis: Record; // either: string: emoji eg: { 'rofl: 🤣, ... } or { 'rofl': 🤣, 'blap': 'https://somewhere.someplace/image.jpg' } +}; +``` + +## GET /api/topics + +Get topics. diff --git a/public/api/zones/index.ts b/public/api/topics/index.ts similarity index 69% rename from public/api/zones/index.ts rename to public/api/topics/index.ts index 34aa8f7..a8120b7 100644 --- a/public/api/zones/index.ts +++ b/public/api/topics/index.ts @@ -3,40 +3,40 @@ import parse_body from '../../../utils/bodyparser.ts'; import { get_session, get_user, require_user } from '../../../utils/prechecks.ts'; import * as CANNED_RESPONSES from '../../../utils/canned_responses.ts'; import { PRECHECK_TABLE } from '../../../utils/prechecks.ts'; -import { ZONE, ZONES } from '../../../models/zone.ts'; +import { TOPIC, TOPICS } from '../../../models/topic.ts'; import { WALK_ENTRY } from '@andyburke/fsdb'; export const PRECHECKS: PRECHECK_TABLE = {}; -// GET /api/zones - get zones +// GET /api/topics - get topics PRECHECKS.GET = [get_session, get_user, require_user, (_req: Request, meta: Record): Response | undefined => { - const can_read_zones = meta.user.permissions.includes('zones.read'); + const can_read_topics = meta.user.permissions.includes('topics.read'); - if (!can_read_zones) { + if (!can_read_topics) { return CANNED_RESPONSES.permission_denied(); } }]; export async function GET(_req: Request, meta: Record): Promise { const limit = Math.min(parseInt(meta.query.limit ?? '100'), 100); - const zones = (await ZONES.all({ + const topics = (await TOPICS.all({ limit, - filter: (entry: WALK_ENTRY) => { - // we push our event collections into the zones, and fsdb + filter: (entry: WALK_ENTRY) => { + // we push our event collections into the topics, and fsdb // doesn't yet filter that out in its all() logic return entry.path.indexOf('/events/') === -1; } - })).map((zone_entry) => zone_entry.load()); + })).map((topic_entry) => topic_entry.load()); - return Response.json(zones, { + return Response.json(topics, { status: 200 }); } -// POST /api/zones - Create a zone +// POST /api/topics - Create a topic PRECHECKS.POST = [get_session, get_user, require_user, (_req: Request, meta: Record): Response | undefined => { - const can_create_zones = meta.user.permissions.includes('zones.create'); + const can_create_topics = meta.user.permissions.includes('topics.create'); - if (!can_create_zones) { + if (!can_create_topics) { return CANNED_RESPONSES.permission_denied(); } }]; @@ -49,8 +49,8 @@ export async function POST(req: Request, meta: Record): Promise): Promise 64) { return Response.json({ error: { - cause: 'invalid_zone_name', - message: 'zone names must be 64 characters or fewer.' + cause: 'invalid_topic_name', + message: 'topic names must be 64 characters or fewer.' } }, { status: 400 @@ -70,21 +70,21 @@ export async function POST(req: Request, meta: Record): Promise): Promise; - limits: { - users: number; - user_messages_per_minute: number; - }; - creator_id: string; // user_id of the zone creator - emojis: Record; // either: string: emoji eg: { 'rofl: 🤣, ... } or { 'rofl': 🤣, 'blap': 'https://somewhere.someplace/image.jpg' } -}; -``` - -## GET /api/zones - -Get zones. diff --git a/public/base.css b/public/base.css index 3244e53..d9c54d5 100644 --- a/public/base.css +++ b/public/base.css @@ -200,7 +200,7 @@ button.primary { } body[data-perms*="users.write"] [data-requires-permission="users.write"], -body[data-perms*="zones.create"] [data-requires-permission="zones.create"] { +body[data-perms*="topics.create"] [data-requires-permission="topics.create"] { visibility: visible; opacity: 1; height: unset; diff --git a/public/tabs/chat/chat.css b/public/tabs/chat/chat.css index ec81110..b5af9eb 100644 --- a/public/tabs/chat/chat.css +++ b/public/tabs/chat/chat.css @@ -3,12 +3,12 @@ grid-template-columns: auto 1fr; } -.zone-list { +.topic-list { margin: 1rem 0; list-style-type: none; } -.zone-list > li.zone a:before { +.topic-list > li.topic a:before { position: absolute; left: -1.75rem; top: 0; @@ -18,7 +18,7 @@ color: var(--text); } -.zone-list > li.zone a { +.topic-list > li.topic a { position: relative; display: block; width: 100%; @@ -30,7 +30,7 @@ text-decoration: none; } -.zone-list > li.zone.active a { +.topic-list > li.topic.active a { color: var(--accent); } @@ -56,11 +56,11 @@ line-height: 2rem; } -#chat #zone-chat-container { +#chat #topic-chat-container { position: relative; } -#chat #zone-chat-content { +#chat #topic-chat-content { overflow-y: scroll; position: absolute; top: 0; @@ -70,7 +70,7 @@ padding: 0.5rem; } -#chat #zone-chat-entry-container { +#chat #topic-chat-entry-container { position: absolute; bottom: 0; left: 0; @@ -79,7 +79,7 @@ padding: 1rem; } -#chat #zone-chat-entry-container form { +#chat #topic-chat-entry-container form { position: absolute; top: 0; left: 0; @@ -90,13 +90,13 @@ padding: 0.75rem; } -#chat #zone-chat-entry-container form input[type="file"] { +#chat #topic-chat-entry-container form input[type="file"] { opacity: 0; display: none; } -#chat #zone-chat-entry-container form button, -#chat #zone-chat-entry-container form label { +#chat #topic-chat-entry-container form button, +#chat #topic-chat-entry-container form label { position: relative; top: inherit; font-size: inherit; @@ -110,7 +110,7 @@ border: 1px solid var(--text); } -#chat #zone-chat-entry-container form textarea { +#chat #topic-chat-entry-container form textarea { width: 100%; flex-grow: 1; background: inherit; @@ -373,7 +373,7 @@ rotate: 180deg; } - #chat #zone-chat-container { + #chat #topic-chat-container { position: absolute; top: 0; left: 0; @@ -381,13 +381,13 @@ bottom: 0; } - #chat #zone-chat-container #zone-chat-entry-container, - #chat #zone-chat-container #zone-chat-entry-container form { + #chat #topic-chat-container #topic-chat-entry-container, + #chat #topic-chat-container #topic-chat-entry-container form { padding: 0.25rem; } - #chat #zone-chat-container #zone-chat-entry-container form button, - #chat #zone-chat-container #zone-chat-entry-container form label { + #chat #topic-chat-container #topic-chat-entry-container form button, + #chat #topic-chat-container #topic-chat-entry-container form label { margin: 0 0.5rem; } diff --git a/public/tabs/chat/chat.html b/public/tabs/chat/chat.html index a408f4a..a32c15b 100644 --- a/public/tabs/chat/chat.html +++ b/public/tabs/chat/chat.html @@ -22,21 +22,21 @@ -
+