diff --git a/README.md b/README.md index 42547e5..4b7bc6b 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. -- [X] the core is a stream of events +- [X] 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,14 +21,14 @@ feature discussions. - [X] logout button - [ ] profile editing - [X] avatar uploads -- [X] chat channels +- [X] chat topics - [X] chat messages - [ ] membership and presence - - [ ] add memberships to channels + - [ ] add memberships to topics - [ ] join to get notifications - [ ] join for additional permissions - - [ ] filters for allowing joining a channel based on criteria on the user? - - [ ] display channel members somehwere + - [ ] filters for allowing joining a topic based on criteria on the user? + - [ ] display topic members somehwere - [ ] emit presence events on join/leave - [ ] display user presence - [ ] chat message actions @@ -88,7 +88,7 @@ feature discussions. - [ ] if web notifications are enabled, emit on events - [ ] ability to mute - [ ] users - - [ ] channels + - [ ] topics - [ ] tags (#tags?) - [ ] admin panel - [ ] add invite code generation diff --git a/deno.json b/deno.json index a69554d..812bc80 100644 --- a/deno.json +++ b/deno.json @@ -32,7 +32,7 @@ } }, "imports": { - "@andyburke/fsdb": "jsr:@andyburke/fsdb@^1.2.4", + "@andyburke/fsdb": "jsr:@andyburke/fsdb@^1.1.0", "@andyburke/lurid": "jsr:@andyburke/lurid@^0.2.0", "@andyburke/serverus": "jsr:@andyburke/serverus@^0.13.0", "@da/bcrypt": "jsr:@da/bcrypt@^1.0.1", diff --git a/deno.lock b/deno.lock index e96cc4d..6397343 100644 --- a/deno.lock +++ b/deno.lock @@ -1,7 +1,7 @@ { "version": "5", "specifiers": { - "jsr:@andyburke/fsdb@^1.2.4": "1.2.4", + "jsr:@andyburke/fsdb@^1.1.0": "1.1.0", "jsr:@andyburke/lurid@0.2": "0.2.0", "jsr:@andyburke/serverus@0.13": "0.13.0", "jsr:@da/bcrypt@*": "1.0.1", @@ -31,8 +31,8 @@ "npm:@types/node@*": "22.15.15" }, "jsr": { - "@andyburke/fsdb@1.2.4": { - "integrity": "3437078a5627d4c72d677e41c20293a47d58a3af19eda72869a12acb011064d2", + "@andyburke/fsdb@1.1.0": { + "integrity": "ad2d062672137ca96df19df032b51f1c7aa3133c973a0b86eb8eaab3b4c2d47b", "dependencies": [ "jsr:@std/cli@^1.0.20", "jsr:@std/fs@^1.0.18", @@ -133,7 +133,7 @@ }, "workspace": { "dependencies": [ - "jsr:@andyburke/fsdb@^1.2.4", + "jsr:@andyburke/fsdb@^1.1.0", "jsr:@andyburke/lurid@0.2", "jsr:@andyburke/serverus@0.13", "jsr:@da/bcrypt@^1.0.1", diff --git a/models/channel.ts b/models/channel.ts deleted file mode 100644 index cd8c011..0000000 --- a/models/channel.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { by_character, by_lurid } from '@andyburke/fsdb/organizers'; -import { FSDB_COLLECTION } from '@andyburke/fsdb'; -import { FSDB_INDEXER_SYMLINKS } from '@andyburke/fsdb/indexers'; - -/** - * @typedef {object} CHANNEL_EVENT_PERMISSIONS - * @property {string[]} read a list of user_ids with read permission for the channel events - * @property {string[]} write a list of user_ids with write permission for the channel events - */ - -/** - * @typedef {object} CHANNEL_PERMISSIONS - * @property {string[]} read a list of user_ids with read permission for the channel - * @property {string[]} write a list of user_ids with write permission for the channel - * @property {CHANNEL_EVENT_PERMISSIONS} events - */ - -/** - * @typedef {object} CHANNEL_TIMESTAMPS - * @property {string} created when the channel was created - * @property {string} updated the last time the channel was updated - * @property {string} [archived] an option time the channel was archived - */ - -/** - * CHANNEL - * - * @property {string} id - lurid (stable) - * @property {string} name - channel name (max 64 characters, unique, unstable) - * @property {string} creator_id - user id of the channel creator - * @property {CHANNEL_PERMISSIONS} permissions - permissions setup for the channel - * @property {string} [icon] - optional url for channel icon - * @property {string} [topic] - optional topic for the channel - * @property {string} [rules] - optional channel rules (Markdown/text) - * @property {string[]} [tags] - optional tags for the channel - * @property {Record} [meta] - optional metadata about the channel - * @property {CHANNEL_TIMESTAMPS} timestamps - timestamps - */ - -export type CHANNEL = { - id: string; - name: string; - creator_id: string; - permissions: { - read: string[]; - write: string[]; - events: { - read: string[]; - write: string[]; - }; - }; - icon?: string; - topic?: string; - rules?: string; - tags?: string[]; - meta?: Record; - timestamps: { - created: string; - updated: string; - archived: string | undefined; - }; -}; - -export const CHANNELS = new FSDB_COLLECTION({ - name: 'channels', - id_field: 'id', - organize: by_lurid, - indexers: { - creator_id: new FSDB_INDEXER_SYMLINKS({ - name: 'creator_id', - field: 'creator_id', - to_many: true, - organize: by_lurid - }), - - name: new FSDB_INDEXER_SYMLINKS({ - name: 'name', - get_values_to_index: (channel) => [channel.name.toLowerCase()], - organize: by_character - }), - - tags: new FSDB_INDEXER_SYMLINKS({ - name: 'tags', - get_values_to_index: (channel): string[] => { - return (channel.tags ?? []).map((tag) => tag.toLowerCase()); - }, - to_many: true, - organize: by_character - }) - } -}); diff --git a/models/event.ts b/models/event.ts index bd66ac7..7eefbd8 100644 --- a/models/event.ts +++ b/models/event.ts @@ -1,4 +1,4 @@ -import { by_lurid } from '@andyburke/fsdb/organizers'; +import { by_character, by_lurid } from '@andyburke/fsdb/organizers'; import { FSDB_COLLECTION } from '@andyburke/fsdb'; import { FSDB_INDEXER_SYMLINKS } from '@andyburke/fsdb/indexers'; import { EMOJIS } from '../public/js/emojis/en.ts'; @@ -16,9 +16,7 @@ import { EMOJIS } from '../public/js/emojis/en.ts'; * @property {string} creator_id - id of the source user * @property {string} type - event type * @property {string} [parent_id] - optional parent event id - * @property {string} [channel] - optional channel - * @property {string} [topic] - optional topic - * @property {string[]} [tags] - optional tags + * @property {string[]} [tags] - optional event tags * @property {Record} [data] - optional data payload of the event * @property {TIMESTAMPS} timestamps - timestamps that will be set by the server */ @@ -27,8 +25,6 @@ export type EVENT = { creator_id: string; type: string; parent_id?: string; - channel?: string; - topic?: string; tags?: string[]; data?: Record; timestamps: { @@ -37,6 +33,11 @@ export type EVENT = { }; }; +type TOPIC_EVENT_CACHE_ENTRY = { + collection: FSDB_COLLECTION; + eviction_timeout: number; +}; + // TODO: separate out these different validators somewhere? export function VALIDATE_EVENT(event: EVENT) { const errors: any[] = []; @@ -110,8 +111,6 @@ export function VALIDATE_EVENT(event: EVENT) { }); } break; - case 'presence': - break; case 'reaction': if (typeof event.parent_id !== 'string') { errors.push({ @@ -149,76 +148,78 @@ export function VALIDATE_EVENT(event: EVENT) { return errors.length ? errors : undefined; } -const EVENT_ID_EXTRACTOR = /^(?.*):(?.*)$/; +const TOPIC_EVENT_ID_MATCHER = /^(?.*):(?.*)$/; -function smart_event_id_organizer(id: string) { - const [event_type, event_id] = id.split(':', 2); - const event_dirs = by_lurid(event_id).slice(0, -1); - return [event_type, ...event_dirs, `${id}.json`]; +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: `topics/${topic_id.slice(0, 14)}/${topic_id.slice(0, 34)}/${topic_id}/events`, + id_field: 'id', + organize: (id) => { + TOPIC_EVENT_ID_MATCHER.lastIndex = 0; + + const groups: Record | undefined = TOPIC_EVENT_ID_MATCHER.exec(id ?? '')?.groups; + + if (!groups) { + throw new Error('Could not parse event id: ' + id); + } + + const event_type = groups.event_type; + const event_id = groups.event_id; + + return [ + event_type, + event_id.slice(0, 14), + event_id.slice(0, 34), + event_id, + `${event_id}.json` /* TODO: this should be ${id}.json - need to write a converter */ + ]; + }, + indexers: { + creator_id: new FSDB_INDEXER_SYMLINKS({ + name: 'creator_id', + field: 'creator_id', + to_many: true, + organize: by_lurid + }), + + parent_id: new FSDB_INDEXER_SYMLINKS({ + name: 'parent_id', + field: 'parent_id', + to_many: true, + organize: by_lurid + }), + + tags: new FSDB_INDEXER_SYMLINKS({ + name: 'tags', + get_values_to_index: (event: EVENT): string[] => { + return (event.tags ?? []).map((tag: string) => tag.toLowerCase()); + }, + to_many: true, + organize: by_character + }) + } + }), + eviction_timeout: 0 + }; + + if (TOPIC_EVENTS[topic_id].eviction_timeout) { + clearTimeout(TOPIC_EVENTS[topic_id].eviction_timeout); + } + + TOPIC_EVENTS[topic_id].eviction_timeout = setTimeout(() => { + delete TOPIC_EVENTS[topic_id]; + }, 60_000 * 5); + + return TOPIC_EVENTS[topic_id].collection; } -export const EVENTS = new FSDB_COLLECTION({ - name: `events`, - id_field: 'id', - organize: (id) => { - EVENT_ID_EXTRACTOR.lastIndex = 0; - - const groups: Record | undefined = EVENT_ID_EXTRACTOR.exec(id ?? '')?.groups; - - if (!groups) { - throw new Error('Could not parse event id: ' + id); +export function clear_topic_events_cache() { + for (const [topic_id, cached] of Object.entries(TOPIC_EVENTS)) { + if (cached.eviction_timeout) { + clearTimeout(cached.eviction_timeout); } - - const event_type = groups.event_type; - const event_id = groups.event_id; - - return [ - event_type, - event_id.slice(0, 14), - event_id.slice(0, 34), - event_id, - `${id}.json` - ]; - }, - indexers: { - creator_id: new FSDB_INDEXER_SYMLINKS({ - name: 'creator_id', - field: 'creator_id', - to_many: true, - organize: by_lurid - }), - - parent_id: new FSDB_INDEXER_SYMLINKS({ - name: 'parent_id', - field: 'parent_id', - to_many: true, - organize: smart_event_id_organizer - }), - - channel: new FSDB_INDEXER_SYMLINKS({ - name: 'channel', - field: 'channel', - to_many: true, - organize: (channel: string) => [channel], - organize_id: smart_event_id_organizer - }), - - topic: new FSDB_INDEXER_SYMLINKS({ - name: 'topic', - field: 'topic', - to_many: true, - organize: (topic: string) => [topic], - organize_id: smart_event_id_organizer - }), - - tags: new FSDB_INDEXER_SYMLINKS({ - name: 'tags', - get_values_to_index: (event: EVENT): string[] => { - return (event.tags ?? []).map((tag: string) => tag.toLowerCase()); - }, - to_many: true, - organize: (tag: string) => tag.length > 3 ? [tag.substring(0, 3), tag] : [tag], - organize_id: smart_event_id_organizer - }) + delete TOPIC_EVENTS[topic_id]; } -}); +} diff --git a/models/topic.ts b/models/topic.ts new file mode 100644 index 0000000..fa01e66 --- /dev/null +++ b/models/topic.ts @@ -0,0 +1,87 @@ +import { by_character, by_lurid } from '@andyburke/fsdb/organizers'; +import { FSDB_COLLECTION } from '@andyburke/fsdb'; +import { FSDB_INDEXER_SYMLINKS } from '@andyburke/fsdb/indexers'; + +/** + * @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 + */ + +/** + * TOPIC + * + * @property {string} id - lurid (stable) + * @property {string} name - channel name (max 64 characters, unique, unstable) + * @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 TOPIC = { + id: string; + name: string; + creator_id: string; + permissions: { + read: string[]; + write: string[]; + read_events: string[]; + write_events: string[]; + }; + icon_url?: string; + topic?: string; + rules?: string; + tags?: string[]; + meta?: Record; + emojis?: Record; // either: string: emoji eg: { 'rofl: 🤣, ... } or { 'rofl': 🤣, 'blap': 'https://somewhere.someplace/image.jpg' } + timestamps: { + created: string; + updated: string; + archived: string | undefined; + }; +}; + +export const TOPICS = new FSDB_COLLECTION({ + name: 'topics', + id_field: 'id', + organize: by_lurid, + indexers: { + creator_id: new FSDB_INDEXER_SYMLINKS({ + name: 'creator_id', + field: 'creator_id', + to_many: true, + organize: by_lurid + }), + + name: new FSDB_INDEXER_SYMLINKS({ + name: 'name', + get_values_to_index: (topic) => [topic.name.toLowerCase()], + organize: by_character + }), + + tags: new FSDB_INDEXER_SYMLINKS({ + name: 'tags', + get_values_to_index: (topic): string[] => { + return (topic.tags ?? []).map((tag) => tag.toLowerCase()); + }, + to_many: true, + organize: by_character + }), + + topic: new FSDB_INDEXER_SYMLINKS({ + name: 'topic', + get_values_to_index: (topic): string[] => { + return (topic.topic ?? '').split(/\W/); + }, + to_many: true, + organize: by_character + }) + } +}); diff --git a/models/watch.ts b/models/watch.ts index 2d460ec..97e13b4 100644 --- a/models/watch.ts +++ b/models/watch.ts @@ -2,30 +2,32 @@ import { FSDB_COLLECTION } from '@andyburke/fsdb'; import { by_lurid } from '@andyburke/fsdb/organizers'; import { FSDB_INDEXER_SYMLINKS } from '@andyburke/fsdb/indexers'; +/** + * @typedef {object} WATCH_TYPE_INFO + * @property {boolean} ignored if true, this type should NOT produce any indications or notifications + * @property {string} last_id_seen the last event id the user has seen for this event type + * @property {string} last_id_notified the last event id the user was notified about for this type + */ + +export type WATCH_TYPE_INFO = { + ignored: boolean; + last_id_seen: string; + last_id_notified: string; +}; + /** * @typedef {object} WATCH_TIMESTAMPS * @property {string} created the created date of the watch * @property {string} updated the last updated date, usually coinciding with the last seen id being changed */ -export type WATCH_TIMESTAMPS = { - created: string; - updated: string; -}; - /** * WATCH * * @property {string} id - lurid (stable) * @property {string} creator_id - user id of the watch creator - * @property {string} [type] - a filter for event type - * @property {string} [parent_id] - a filter for event parent_id - * @property {string} [channel] - a filter for event channel - * @property {string} [topic] - a filter for event topic - * @property {string[]} [tags] - a filter for event tags - * @property {Record} [data] - a filter on event data, each leaf should be a RegExp - * @property {string} last_id_seen - the last id the user has seen for this watch - * @property {string} [last_id_notified] - the last id the user was notified about for this watch + * @property {string} topic_id - the topic_id being watched + * @property {[WATCH_TYPE_INFO]} types - information for types being watched within this topic * @property {Record} [meta] - optional metadata about the watch * @property {WATCH_TIMESTAMPS} timestamps - timestamps for the watch */ @@ -33,16 +35,13 @@ export type WATCH_TIMESTAMPS = { export type WATCH = { id: string; creator_id: string; - type?: string; - parent_id?: string; - channel?: string; - topic?: string; - tags?: string[]; - data?: Record; - last_id_seen: string; - last_id_notified?: string; + topic_id: string; + types: [WATCH_TYPE_INFO]; meta?: Record; - timestamps: WATCH_TIMESTAMPS; + timestamps: { + created: string; + updated: string; + }; }; export const WATCHES = new FSDB_COLLECTION({ @@ -57,44 +56,11 @@ export const WATCHES = new FSDB_COLLECTION({ organize: by_lurid }), - type: new FSDB_INDEXER_SYMLINKS({ - name: 'type', - field: 'type', + topic_id: new FSDB_INDEXER_SYMLINKS({ + name: 'topic_id', + field: 'topic_id', to_many: true, - organize: (type: string) => [type], - organize_id: by_lurid - }), - - parent_id: new FSDB_INDEXER_SYMLINKS({ - name: 'parent_id', - field: 'parent_id', - to_many: true, - organize: by_lurid, - organize_id: by_lurid - }), - - channel: new FSDB_INDEXER_SYMLINKS({ - name: 'channel', - field: 'channel', - to_many: true, - organize: (channel: string) => channel.length > 3 ? [channel.substring(0, 3), channel] : [channel], - organize_id: by_lurid - }), - - topic: new FSDB_INDEXER_SYMLINKS({ - name: 'topic', - field: 'topic', - to_many: true, - organize: (topic: string) => topic.length > 3 ? [topic.substring(0, 3), topic] : [topic], - organize_id: by_lurid - }), - - tags: new FSDB_INDEXER_SYMLINKS({ - name: 'tags', - field: 'tags', - to_many: true, - organize: (tag: string) => tag.length > 3 ? [tag.substring(0, 3), tag] : [tag], - organize_id: by_lurid + organize: by_lurid }) } }); diff --git a/public/api/channels/:channel_id/README.md b/public/api/channels/:channel_id/README.md deleted file mode 100644 index f3e8787..0000000 --- a/public/api/channels/:channel_id/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# /api/channels/:channel_id - -Interact with a specific channel. - -## GET /api/channels/:channel_id - -Get the channel specified by `:channel_id`. - -## PUT /api/channels/:channel_id - -Update the channels specified by `:channel_id`. - -Eg: - -``` -{ - name?: string; -} -``` - -## DELETE /api/channels/:channel_id - -Delete the channel specified by `:channel_id`. diff --git a/public/api/channels/:channel_id/events/:event_id/index.ts b/public/api/channels/:channel_id/events/:event_id/index.ts deleted file mode 100644 index 2759b9d..0000000 --- a/public/api/channels/:channel_id/events/:event_id/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { EVENT, EVENTS } from '../../../../../../models/event.ts'; -import { CHANNEL, CHANNELS } from '../../../../../../models/channel.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/channels/:channel_id/events/:id - Get an event -PRECHECKS.GET = [get_session, get_user, require_user, async (_req: Request, meta: Record): Promise => { - const channel_id: string = meta.params?.channel_id?.toLowerCase().trim() ?? ''; - - // lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same" - const channel: CHANNEL | null = channel_id.length === 49 ? await CHANNELS.get(channel_id) : null; - - if (!channel) { - return CANNED_RESPONSES.not_found(); - } - - meta.channel = channel; - const channel_is_public = channel.permissions.read.length === 0; - const user_has_read_for_channel = channel_is_public || channel.permissions.read.includes(meta.user.id); - const channel_has_public_events = user_has_read_for_channel && (channel.permissions.events.read.length === 0); - const user_has_read_events_for_channel = user_has_read_for_channel && - (channel_has_public_events || channel.permissions.events.read.includes(meta.user.id)); - - if (!user_has_read_events_for_channel) { - return CANNED_RESPONSES.permission_denied(); - } -}]; -export async function GET(_req: Request, meta: Record): Promise { - const event: EVENT | null = await EVENTS.get(meta.params.event_id); - - if (!event) { - return CANNED_RESPONSES.not_found(); - } - - return Response.json(event, { - status: 200 - }); -} diff --git a/public/api/channels/:channel_id/events/index.ts b/public/api/channels/:channel_id/events/index.ts deleted file mode 100644 index 1d42417..0000000 --- a/public/api/channels/:channel_id/events/index.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { get_session, get_user, PRECHECK_TABLE, require_user } from '../../../../../utils/prechecks.ts'; -import { CHANNEL, CHANNELS } from '../../../../../models/channel.ts'; -import * as CANNED_RESPONSES from '../../../../../utils/canned_responses.ts'; -import { EVENT, EVENTS } from '../../../../../models/event.ts'; -import { FSDB_SEARCH_OPTIONS, WALK_ENTRY } from '@andyburke/fsdb'; - -export const PRECHECKS: PRECHECK_TABLE = {}; - -// GET /api/channels/:channel_id/events - get channel 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 channel_id: string = meta.params?.channel_id?.toLowerCase().trim() ?? ''; - - // lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same" - const channel: CHANNEL | null = channel_id.length === 49 ? await CHANNELS.get(channel_id) : null; - - if (!channel) { - return CANNED_RESPONSES.not_found(); - } - - meta.channel = channel; - - const channel_is_public: boolean = meta.channel.permissions.read.length === 0; - const user_has_read_for_channel = channel_is_public || meta.channel.permissions.read.includes(meta.user.id); - - if (!user_has_read_for_channel) { - return CANNED_RESPONSES.permission_denied(); - } -}]; -export async function GET(request: Request, meta: Record): Promise { - const sorts = EVENTS.sorts; - const sort_name: string = meta.query.sort ?? 'newest'; - const key = sort_name as keyof typeof sorts; - const sort: any = sorts[key]; - if (!sort) { - return Response.json({ - error: { - message: 'You must specify a sort: newest, oldest, latest, stalest', - cause: 'invalid_sort' - } - }, { - status: 400 - }); - } - - const options: FSDB_SEARCH_OPTIONS = { - ...(meta.query ?? {}), - limit: Math.min(parseInt(meta.query?.limit ?? '10', 10), 1_000), - offset: Math.max(parseInt(meta.query?.offset ?? '0', 10), 0), - sort, - filter: (entry: WALK_ENTRY) => { - const { - event_type, - event_id - } = /^.*\/(?.*?):(?[A-Za-z-]+)\.json$/.exec(entry.path)?.groups ?? {}; - - if (meta.query.after_id && event_id <= meta.query.after_id) { - return false; - } - - if (meta.query.before_id && event_id >= meta.query.before_id) { - return false; - } - - if (meta.query.type && !meta.query.type.split(',').includes(event_type)) { - return false; - } - - return true; - } - }; - - const headers = { - 'Cache-Control': 'no-cache, must-revalidate' - }; - - const results = (await EVENTS.find({ - channel: meta.channel.id - }, options)) - .map((entry: WALK_ENTRY) => entry.load()) - .sort((lhs_item: EVENT, rhs_item: EVENT) => rhs_item.timestamps.created.localeCompare(lhs_item.timestamps.created)); - - // long-polling support - if (results.length === 0 && meta.query.wait) { - return new Promise((resolve, reject) => { - function on_create(create_event: any) { - if (create_event.item.channel !== meta.channel.id) { - return; - } - - if (meta.query.type && !meta.query.type.split(',').includes(create_event.item.type)) { - return; - } - - results.push(create_event.item); - clearTimeout(timeout); - EVENTS.off('create', on_create); - - return resolve(Response.json(results, { - status: 200, - headers - })); - } - - const timeout = setTimeout(() => { - EVENTS.off('create', on_create); - return resolve(Response.json(results, { - status: 200, - headers - })); - }, 60_000); // 60 seconds - EVENTS.on('create', on_create); - request.signal.addEventListener('abort', () => { - EVENTS.off('create', on_create); - clearTimeout(timeout); - reject(new Error('request aborted')); - }); - Deno.addSignalListener('SIGINT', () => { - EVENTS.off('create', on_create); - clearTimeout(timeout); - return resolve(Response.json(results, { - status: 200, - headers - })); - }); - }); - } - - return Response.json(results, { - status: 200, - headers - }); -} diff --git a/public/api/channels/README.md b/public/api/channels/README.md deleted file mode 100644 index c16c81b..0000000 --- a/public/api/channels/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# /api/channels - -Interact with channels. - -## POST /api/channels - -Create a new channel. - -``` -export type CHANNEL = { - id: string; // unique id for this channel - name: string; // the name of the channel (max 128 characters) - icon?: string; // optional url for a channel icon - topic?: string; // optional channel topic - tags?: string[]; // optional tags for the channel - meta?: Record; // optional metadata - 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/channels - -Get channels. diff --git a/public/api/events/:event_id/README.md b/public/api/events/:event_id/README.md deleted file mode 100644 index 269a9f4..0000000 --- a/public/api/events/:event_id/README.md +++ /dev/null @@ -1,7 +0,0 @@ -## PUT /api/events/:event_id - -Update an event. - -## DELETE /api/events/:event_id - -Delete an event. diff --git a/public/api/events/:event_id/index.ts b/public/api/events/:event_id/index.ts deleted file mode 100644 index 557b231..0000000 --- a/public/api/events/:event_id/index.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { CHANNEL, CHANNELS } from '../../../../models/channel.ts'; -import { EVENT, EVENTS } from '../../../../models/event.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, user_has_write_permission_for_event } from '../../../../utils/prechecks.ts'; - -export const PRECHECKS: PRECHECK_TABLE = {}; - -// GET /api/events/:id - Get an event -PRECHECKS.GET = [get_session, get_user, require_user]; -export async function GET(_req: Request, meta: Record): Promise { - const event: EVENT | null = await EVENTS.get(meta.params.event_id); - - if (!event) { - return CANNED_RESPONSES.not_found(); - } - - return Response.json(event, { - status: 200 - }); -} - -// PUT /api/events/:event_id - Update event -PRECHECKS.PUT = [ - get_session, - get_user, - require_user, - (_req: Request, _meta: Record): Response | undefined => { - if (Deno.env.get('APPEND_ONLY_EVENTS')) { - return CANNED_RESPONSES.append_only_events(); - } - }, - (_req: Request, meta: Record): Response | undefined => { - if (!meta.user.permissions.some((permission: string) => permission.indexOf('events.write') === 0)) { - return CANNED_RESPONSES.permission_denied(); - } - } -]; -export async function PUT(req: Request, meta: Record): Promise { - const now = new Date().toISOString(); - - try { - const event: EVENT | null = await EVENTS.get(meta.params.event_id); - - if (!event) { - return CANNED_RESPONSES.not_found(); - } - - if (event.creator_id !== meta.user.id) { - return CANNED_RESPONSES.permission_denied(); - } - - const body = await parse_body(req); - const updated: EVENT = { - ...event, - ...body, - id: event.id, - creator_id: event.creator_id, - channel: event.channel, - timestamps: { - created: event.timestamps.created, - updated: now - } - }; - - if (updated.channel) { - const channel: CHANNEL | null = await CHANNELS.get(updated.channel); - if (!channel) { - return Response.json({ - errors: [{ - cause: 'missing_channel', - message: 'No such channel exists.' - }] - }, { - status: 400 - }); - } - - const user_can_write_events_to_channel = channel.permissions.events.write.length === 0 ? true : channel.permissions.events.write.includes(meta.user.id); - - if (!user_can_write_events_to_channel) { - return CANNED_RESPONSES.permission_denied(); - } - } - - if (!user_has_write_permission_for_event(meta.user, updated)) { - return CANNED_RESPONSES.permission_denied(); - } - - await EVENTS.update(updated); - return Response.json(updated, { - status: 200 - }); - } catch (err) { - return Response.json({ - error: { - message: (err as Error)?.message ?? 'Unknown error due to invalid data.', - cause: (err as Error)?.cause ?? 'invalid_data' - } - }, { - status: 400 - }); - } -} - -// DELETE /api/events/:event_id - Delete event -PRECHECKS.DELETE = [ - get_session, - get_user, - require_user, - (_req: Request, _meta: Record): Response | undefined => { - if (Deno.env.get('APPEND_ONLY_EVENTS')) { - return CANNED_RESPONSES.append_only_events(); - } - }, - (_req: Request, meta: Record): Response | undefined => { - if (!meta.user.permissions.some((permission: string) => permission.indexOf('events.write') === 0)) { - return CANNED_RESPONSES.permission_denied(); - } - } -]; -export async function DELETE(_req: Request, meta: Record): Promise { - const event: EVENT | null = await EVENTS.get(meta.params.event_id); - if (!event) { - return CANNED_RESPONSES.not_found(); - } - - if (event.channel) { - const channel: CHANNEL | null = await CHANNELS.get(event.channel); - if (!channel) { - return Response.json({ - errors: [{ - cause: 'missing_channel', - message: 'No such channel exists.' - }] - }, { - status: 400 - }); - } - - const user_can_write_events_to_channel = channel.permissions.events.write.length === 0 ? true : channel.permissions.events.write.includes(meta.user.id); - - if (!user_can_write_events_to_channel) { - return CANNED_RESPONSES.permission_denied(); - } - } - - if (!user_has_write_permission_for_event(meta.user, event)) { - return CANNED_RESPONSES.permission_denied(); - } - - await EVENTS.delete(event); - - return Response.json({ - deleted: true - }, { - status: 200 - }); -} diff --git a/public/api/events/README.md b/public/api/events/README.md deleted file mode 100644 index 71c7345..0000000 --- a/public/api/events/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# /api/events - -Interact with events. - -## GET /api/events - -Get events. - -## POST /api/events - -Create an event. 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/topics/:topic_id/events/:event_id/README.md b/public/api/topics/:topic_id/events/:event_id/README.md new file mode 100644 index 0000000..e69de29 diff --git a/public/api/topics/:topic_id/events/:event_id/index.ts b/public/api/topics/:topic_id/events/:event_id/index.ts new file mode 100644 index 0000000..6149d6f --- /dev/null +++ b/public/api/topics/:topic_id/events/:event_id/index.ts @@ -0,0 +1,166 @@ +import { FSDB_COLLECTION } from '@andyburke/fsdb'; +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/topics/:topic_id/events/:id - Get an event +PRECHECKS.GET = [get_session, get_user, require_user, async (_req: Request, meta: Record): Promise => { + 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 topic: TOPIC | null = topic_id.length === 49 ? await TOPICS.get(topic_id) : null; + + if (!topic) { + return CANNED_RESPONSES.not_found(); + } + + 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_topic) { + return CANNED_RESPONSES.permission_denied(); + } +}]; +export async function GET(_req: Request, meta: Record): Promise { + 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(); + } + + return Response.json(event, { + status: 200 + }); +} + +// PUT /api/topics/:topic_id/events/:event_id - Update event +PRECHECKS.PUT = [ + get_session, + get_user, + require_user, + (_req: Request, _meta: Record): Response | undefined => { + if (Deno.env.get('APPEND_ONLY_EVENTS')) { + return CANNED_RESPONSES.append_only_events(); + } + }, + async (_req: Request, meta: Record): Promise => { + 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 topic: TOPIC | null = topic_id.length === 49 ? await TOPICS.get(topic_id) : null; + + if (!topic) { + return CANNED_RESPONSES.not_found(); + } + + 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_topic) { + return CANNED_RESPONSES.permission_denied(); + } + } +]; +export async function PUT(req: Request, meta: Record): Promise { + const now = new Date().toISOString(); + + try { + 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(); + } + + if (event.creator_id !== meta.user.id) { + return CANNED_RESPONSES.permission_denied(); + } + + const body = await parse_body(req); + const updated = { + ...event, + ...body, + id: event.id, + creator_id: event.creator_id, + timestamps: { + created: event.timestamps.created, + updated: now + } + }; + + await events.update(updated); + return Response.json(updated, { + status: 200 + }); + } catch (err) { + return Response.json({ + error: { + message: (err as Error)?.message ?? 'Unknown error due to invalid data.', + cause: (err as Error)?.cause ?? 'invalid_data' + } + }, { + status: 400 + }); + } +} + +// DELETE /api/topics/:topic_id/events/:event_id - Delete event +PRECHECKS.DELETE = [ + get_session, + get_user, + require_user, + (_req: Request, _meta: Record): Response | undefined => { + if (Deno.env.get('APPEND_ONLY_EVENTS')) { + return CANNED_RESPONSES.append_only_events(); + } + }, + async (_req: Request, meta: Record): Promise => { + 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 topic: TOPIC | null = topic_id.length === 49 ? await TOPICS.get(topic_id) : null; + + if (!topic) { + return CANNED_RESPONSES.not_found(); + } + + 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_topic) { + return CANNED_RESPONSES.permission_denied(); + } + } +]; +export async function DELETE(_req: Request, meta: Record): Promise { + 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(); + } + + await events.delete(event); + + return Response.json({ + deleted: true + }, { + status: 200 + }); +} 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/events/index.ts b/public/api/topics/:topic_id/events/index.ts similarity index 51% rename from public/api/events/index.ts rename to public/api/topics/:topic_id/events/index.ts index ca2fe45..fe99165 100644 --- a/public/api/events/index.ts +++ b/public/api/topics/:topic_id/events/index.ts @@ -1,21 +1,42 @@ import lurid from '@andyburke/lurid'; -import { get_session, get_user, PRECHECK_TABLE, require_user, user_has_write_permission_for_event } from '../../../utils/prechecks.ts'; -import * as CANNED_RESPONSES from '../../../utils/canned_responses.ts'; -import { EVENT, EVENTS, VALIDATE_EVENT } from '../../../models/event.ts'; -import parse_body from '../../../utils/bodyparser.ts'; -import { FSDB_SEARCH_OPTIONS, WALK_ENTRY } from '@andyburke/fsdb'; -import { WATCH, WATCHES } from '../../../models/watch.ts'; -import { flatten } from '../../../utils/object_helpers.ts'; -import { CHANNEL, CHANNELS } from '../../../models/channel.ts'; +import { get_session, get_user, PRECHECK_TABLE, require_user } from '../../../../../utils/prechecks.ts'; +import { TOPIC, TOPICS } from '../../../../../models/topic.ts'; +import * as CANNED_RESPONSES from '../../../../../utils/canned_responses.ts'; +import { EVENT, get_events_collection_for_topic, VALIDATE_EVENT } from '../../../../../models/event.ts'; +import parse_body from '../../../../../utils/bodyparser.ts'; +import { FSDB_COLLECTION, FSDB_SEARCH_OPTIONS, WALK_ENTRY } from '@andyburke/fsdb'; +import { WATCH, WATCHES } from '../../../../../models/watch.ts'; export const PRECHECKS: PRECHECK_TABLE = {}; -// GET /api/events - get 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]; +PRECHECKS.GET = [get_session, get_user, require_user, async (_req: Request, meta: Record): Promise => { + 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 topic: TOPIC | null = topic_id.length === 49 ? await TOPICS.get(topic_id) : null; + + if (!topic) { + return CANNED_RESPONSES.not_found(); + } + + 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_topic) { + return CANNED_RESPONSES.permission_denied(); + } +}]; export async function GET(request: Request, meta: Record): Promise { - const sorts = EVENTS.sorts; + const events: FSDB_COLLECTION = get_events_collection_for_topic(meta.topic.id); + + const sorts = events.sorts; const sort_name: string = meta.query.sort ?? 'newest'; const key = sort_name as keyof typeof sorts; const sort: any = sorts[key]; @@ -32,7 +53,7 @@ export async function GET(request: Request, meta: Record): Promise< const options: FSDB_SEARCH_OPTIONS = { ...(meta.query ?? {}), - limit: Math.min(parseInt(meta.query?.limit ?? '100', 10), 1_000), + limit: Math.min(parseInt(meta.query?.limit ?? '10', 10), 1_000), offset: Math.max(parseInt(meta.query?.offset ?? '0', 10), 0), sort, filter: (entry: WALK_ENTRY) => { @@ -61,9 +82,8 @@ export async function GET(request: Request, meta: Record): Promise< 'Cache-Control': 'no-cache, must-revalidate' }; - const results = (await EVENTS.all(options)) + const results = (await events.all(options)) .map((entry: WALK_ENTRY) => entry.load()) - .filter((event) => typeof event.channel === 'undefined') // channel events must be queried via the channel's api .sort((lhs_item: EVENT, rhs_item: EVENT) => rhs_item.timestamps.created.localeCompare(lhs_item.timestamps.created)); // long-polling support @@ -76,7 +96,7 @@ export async function GET(request: Request, meta: Record): Promise< results.push(create_event.item); clearTimeout(timeout); - EVENTS.off('create', on_create); + events.off('create', on_create); return resolve(Response.json(results, { status: 200, @@ -85,20 +105,20 @@ export async function GET(request: Request, meta: Record): Promise< } const timeout = setTimeout(() => { - EVENTS.off('create', on_create); + events.off('create', on_create); return resolve(Response.json(results, { status: 200, headers })); }, 60_000); // 60 seconds - EVENTS.on('create', on_create); + events.on('create', on_create); request.signal.addEventListener('abort', () => { - EVENTS.off('create', on_create); + events.off('create', on_create); clearTimeout(timeout); reject(new Error('request aborted')); }); Deno.addSignalListener('SIGINT', () => { - EVENTS.off('create', on_create); + events.off('create', on_create); clearTimeout(timeout); return resolve(Response.json(results, { status: 200, @@ -114,78 +134,52 @@ export async function GET(request: Request, meta: Record): Promise< }); } -async function update_watches(event: EVENT) { +async function update_watches(topic: TOPIC, event: EVENT) { const limit = 100; let more_to_process; let offset = 0; do { - const watches: WATCH[] = (await WATCHES.all({ + const watches: WATCH[] = (await WATCHES.find({ + topic_id: topic.id + }, { limit, offset })).map((entry) => entry.load()); - for (const watch of watches) { - if (typeof watch.type === 'string' && event.type !== watch.type) { - continue; - } - - if (typeof watch.parent_id === 'string' && event.parent_id !== watch.parent_id) { - continue; - } - - if (typeof watch.channel === 'string' && event.channel !== watch.channel) { - continue; - } - - if (typeof watch.topic === 'string' && event.topic !== watch.topic) { - continue; - } - - if (typeof watch.tags !== 'undefined' && !watch.tags.every((tag) => event.tags?.includes(tag))) { - continue; - } - - if (typeof watch.data !== 'undefined') { - const event_data = flatten(event.data ?? {}); - for (const [key, value] of Object.entries(flatten(watch.data))) { - const matcher = new RegExp(value); - if (!matcher.test(event_data[key])) { - continue; - } - } - } - - if (event.id < watch.last_id_seen) { - continue; - } - - watch.last_id_seen = event.id; - - // TODO: send a notification - console.dir({ - notification: { - watch, - event - } - }); - } + // TODO: look at the watch .types[] and send notifications offset += watches.length; more_to_process = watches.length === limit; } while (more_to_process); } -// POST /api/events - Create an event -PRECHECKS.POST = [get_session, get_user, require_user, (_req: Request, meta: Record): Response | undefined => { - const user_can_create_events = meta.user.permissions.some((permission: string) => permission.indexOf('events.create') === 0); +// POST /api/topics/:topic_id/events - Create an event +PRECHECKS.POST = [get_session, get_user, require_user, async (_req: Request, meta: Record): Promise => { + const topic_id: string = meta.params?.topic_id?.toLowerCase().trim() ?? ''; - if (!user_can_create_events) { + // lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same" + const topic: TOPIC | null = topic_id.length === 49 ? await TOPICS.get(topic_id) : null; + + if (!topic) { + return CANNED_RESPONSES.not_found(); + } + + 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_topic) { return CANNED_RESPONSES.permission_denied(); } }]; export async function POST(req: Request, meta: Record): Promise { try { + const events: FSDB_COLLECTION = get_events_collection_for_topic(meta.topic.id); + const now = new Date().toISOString(); const body = await parse_body(req); @@ -210,33 +204,7 @@ export async function POST(req: Request, meta: Record): Promise): Promise => { - const channel_id: string = meta.params?.channel_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 channel: CHANNEL | null = channel_id.length === 49 ? await CHANNELS.get(channel_id) : null; + const topic: TOPIC | null = topic_id.length === 49 ? await TOPICS.get(topic_id) : null; - if (!channel) { + if (!topic) { return CANNED_RESPONSES.not_found(); } - meta.channel = channel; - const channel_is_public = channel.permissions.read.length === 0; - const user_has_read_for_channel = channel_is_public || channel.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_channel) { + if (!user_has_read_for_topic) { return CANNED_RESPONSES.permission_denied(); } }]; export function GET(_req: Request, meta: Record): Response { - return Response.json(meta.channel, { + return Response.json(meta.topic, { status: 200 }); } -// PUT /api/channels/:id - Update channel +// PUT /api/topics/:id - Update topic PRECHECKS.PUT = [get_session, get_user, require_user, async (_req: Request, meta: Record): Promise => { - const channel_id: string = meta.params?.channel_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 channel: CHANNEL | null = channel_id.length === 49 ? await CHANNELS.get(channel_id) : null; + const topic: TOPIC | null = topic_id.length === 49 ? await TOPICS.get(topic_id) : null; - if (!channel) { + if (!topic) { return CANNED_RESPONSES.not_found(); } - meta.channel = channel; - const user_has_write_for_channel = channel.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_channel) { + 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 channel_id: string = meta.params?.channel_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 channel: CHANNEL | null = channel_id.length === 49 ? await CHANNELS.get(channel_id) : null; + const topic: TOPIC | null = topic_id.length === 49 ? await TOPICS.get(topic_id) : null; - if (!channel) { + if (!topic) { return CANNED_RESPONSES.not_found(); } - meta.channel = channel; - const user_has_write_for_channel = channel.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_channel) { + if (!user_has_write_for_topic) { return CANNED_RESPONSES.permission_denied(); } } ]; export async function DELETE(_req: Request, meta: Record): Promise { - await CHANNELS.delete(meta.channel); + 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/channels/index.ts b/public/api/topics/index.ts similarity index 61% rename from public/api/channels/index.ts rename to public/api/topics/index.ts index 30fde2a..a8120b7 100644 --- a/public/api/channels/index.ts +++ b/public/api/topics/index.ts @@ -3,34 +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 { CHANNEL, CHANNELS } from '../../../models/channel.ts'; +import { TOPIC, TOPICS } from '../../../models/topic.ts'; +import { WALK_ENTRY } from '@andyburke/fsdb'; export const PRECHECKS: PRECHECK_TABLE = {}; -// GET /api/channels - get channels +// GET /api/topics - get topics PRECHECKS.GET = [get_session, get_user, require_user, (_req: Request, meta: Record): Response | undefined => { - const can_read_channels = meta.user.permissions.includes('channels.read'); + const can_read_topics = meta.user.permissions.includes('topics.read'); - if (!can_read_channels) { + 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 channels = (await CHANNELS.all({ - limit - })).map((channel_entry) => channel_entry.load()); + const topics = (await TOPICS.all({ + limit, + 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((topic_entry) => topic_entry.load()); - return Response.json(channels, { + return Response.json(topics, { status: 200 }); } -// POST /api/channels - Create a channel +// POST /api/topics - Create a topic PRECHECKS.POST = [get_session, get_user, require_user, (_req: Request, meta: Record): Response | undefined => { - const can_create_channels = meta.user.permissions.includes('channels.create'); + const can_create_topics = meta.user.permissions.includes('topics.create'); - if (!can_create_channels) { + if (!can_create_topics) { return CANNED_RESPONSES.permission_denied(); } }]; @@ -43,8 +49,8 @@ export async function POST(req: Request, meta: Record): Promise): Promise 64) { return Response.json({ error: { - cause: 'invalid_channel_name', - message: 'channel names must be 64 characters or fewer.' + cause: 'invalid_topic_name', + message: 'topic names must be 64 characters or fewer.' } }, { status: 400 @@ -64,31 +70,29 @@ export async function POST(req: Request, meta: Record): Promise): Promise): Promise => { const watch_id: string = meta.params?.watch_id?.toLowerCase().trim() ?? ''; @@ -69,7 +69,7 @@ PRECHECKS.DELETE = [ return CANNED_RESPONSES.not_found(); } - meta.watch = watch; + meta.topic = watch; const user_owns_watch = watch.creator_id === meta.user.id; if (!user_owns_watch) { diff --git a/public/api/users/:user_id/watches/index.ts b/public/api/users/:user_id/watches/index.ts index 4950a33..0808a98 100644 --- a/public/api/users/:user_id/watches/index.ts +++ b/public/api/users/:user_id/watches/index.ts @@ -4,7 +4,7 @@ import * as CANNED_RESPONSES from '../../../../../utils/canned_responses.ts'; import { get_session, get_user, PRECHECK_TABLE, require_user } from '../../../../../utils/prechecks.ts'; import parse_body from '../../../../../utils/bodyparser.ts'; import lurid from '@andyburke/lurid'; -import { CHANNELS } from '../../../../../models/channel.ts'; +import { TOPICS } from '../../../../../models/topic.ts'; export const PRECHECKS: PRECHECK_TABLE = {}; @@ -99,18 +99,34 @@ export async function POST(req: Request, meta: Record): Promise): Promise\w+)(?:\/channel\/(?[A-Za-z\-]+)\/?)?/gm; -const UPDATE_CHANNELS_FREQUENCY = 60_000; +const HASH_EXTRACTOR = /^\#\/topic\/(?[A-Za-z\-]+)\/?(?\w+)?/gm; +const UPDATE_TOPICS_FREQUENCY = 60_000; const APP = { user: undefined, @@ -52,52 +52,69 @@ const APP = { extract_url_hash_info: async function () { HASH_EXTRACTOR.lastIndex = 0; // ugh, need this to have this work on multiple exec calls const { - groups: { view, channel_id }, + groups: { topic_id, view }, } = HASH_EXTRACTOR.exec(window.location.hash ?? "") ?? { groups: {}, }; + /* + console.dir({ + url: window.location.href, + hash: window.location.hash, + topic_id, + view, + }); + */ + + if (!document.body.dataset.topic || document.body.dataset.topic !== topic_id) { + const previous = document.body.dataset.topic; + + /* + console.dir({ + topic_changed: { + detail: { + previous, + topic_id, + }, + }, + }); + */ + + document.body.dataset.topic = topic_id; + + this._emit( 'topic_changed', { + previous, + topic_id + }); + + if (!topic_id) { + const first_topic_id = this.TOPICS.TOPIC_LIST[0]?.id; + if (first_topic_id) { + window.location.hash = `/topic/${first_topic_id}/chat`; // TODO: allow a different default than chat + } + } + } + if (!document.body.dataset.view || document.body.dataset.view !== view) { - const previous = typeof document.body.dataset.view === 'string' ? document.body.dataset.view : undefined; - if ( view ) { - document.body.dataset.view = view; - } - else { - delete document.body.dataset.view; - } + const previous = document.body.dataset.view; + document.body.dataset.view = view; + + /* + console.dir({ + view_changed: { + detail: { + previous, + view, + }, + }, + }); + */ this._emit( 'view_changed', { previous, view }); } - - if (!document.body.dataset.channel || document.body.dataset.channel !== channel_id) { - const previous = typeof document.body.dataset.channel === 'string' ? document.body.dataset.channel : undefined; - - if ( channel_id ) { - document.body.dataset.channel = channel_id; - } - else { - delete document.body.dataset.channel; - } - - const target_channel_id = channel_id ?? this.CHANNELS.CHANNEL_LIST[0]?.id; - - // TODO: allow a different default than chat - const hash_target = `/${ view ? view : 'chat' }` + ( target_channel_id ? `/channel/${ target_channel_id }` : '' ); - - if ( window.location.hash?.slice( 1 ) !== hash_target ) { - if ( previous !== target_channel_id ) { - this._emit( 'channel_changed', { - previous, - channel_id: target_channel_id - }); - } - - window.location.hash = hash_target; - } - } }, load: async function() { @@ -132,7 +149,7 @@ const APP = { } window.addEventListener("locationchange", this.extract_url_hash_info.bind( this )); - window.addEventListener("locationchange", this.CHANNELS.update ); + window.addEventListener("locationchange", this.TOPICS.update ); this.check_if_logged_in(); this.extract_url_hash_info(); @@ -145,7 +162,7 @@ const APP = { document.body.dataset.user = JSON.stringify(user); document.body.dataset.perms = user.permissions.join(":"); - this.CHANNELS.update(); + this.TOPICS.update(); this.user_servers = []; try { @@ -214,57 +231,56 @@ const APP = { }, }, - CHANNELS: { - _last_channel_update: undefined, - _update_channels_timeout: undefined, - CHANNEL_LIST: [], + TOPICS: { + _last_topic_update: undefined, + _update_topics_timeout: undefined, + TOPIC_LIST: [], - update: async ( force = false ) => { + update: async () => { const now = new Date(); - const time_since_last_update = now - (APP.CHANNELS._last_channel_update ?? 0); - const sufficient_time_has_passed_since_last_update = time_since_last_update > UPDATE_CHANNELS_FREQUENCY / 2; - if ( !force && !sufficient_time_has_passed_since_last_update ) { + const time_since_last_update = now - (APP.TOPICS._last_topic_update ?? 0); + if (time_since_last_update < UPDATE_TOPICS_FREQUENCY / 2) { return; } - if (APP.CHANNELS._update_channels_timeout) { - clearTimeout(APP.CHANNELS._update_channels_timeout); - APP.CHANNELS._update_channels_timeout = undefined; + if (APP.TOPICS._update_topics_timeout) { + clearTimeout(APP.TOPICS._update_topics_timeout); + APP.TOPICS._update_topics_timeout = undefined; } try { - const channels_response = await api.fetch("/api/channels"); - if (channels_response.ok) { - const new_channels = await channels_response.json(); + const topics_response = await api.fetch("/api/topics"); + if (topics_response.ok) { + const new_topics = await topics_response.json(); const has_differences = - APP.CHANNELS.CHANNEL_LIST.length !== new_channels.length || - new_channels.some((channel, index) => { + APP.TOPICS.TOPIC_LIST.length !== new_topics.length || + new_topics.some((topic, index) => { return ( - APP.CHANNELS.CHANNEL_LIST[index]?.id !== channel.id || - APP.CHANNELS.CHANNEL_LIST[index]?.name !== channel.name + APP.TOPICS.TOPIC_LIST[index]?.id !== topic.id || + APP.TOPICS.TOPIC_LIST[index]?.name !== topic.name ); }); if (has_differences) { - APP.CHANNELS.CHANNEL_LIST = [...new_channels]; + APP.TOPICS.TOPIC_LIST = [...new_topics]; - APP._emit( 'channels_updated', { - channels: APP.CHANNELS.CHANNEL_LIST + APP._emit( 'topics_updated', { + topics: APP.TOPICS.TOPIC_LIST }); } - APP.CHANNELS._last_channel_update = now; + APP.TOPICS._last_topic_update = now; } } catch (error) { console.error(error); } - APP.CHANNELS._update_channels_timeout = setTimeout( - APP.CHANNELS.update, - UPDATE_CHANNELS_FREQUENCY, + APP.TOPICS._update_topics_timeout = setTimeout( + APP.TOPICS.update, + UPDATE_TOPICS_FREQUENCY, ); - // now that we have channels, make sure our url is all good + // now that we have topics, make sure our url is all good APP.extract_url_hash_info(); }, }, diff --git a/public/js/reactions.js b/public/js/reactions.js index 441ecaf..230be01 100644 --- a/public/js/reactions.js +++ b/public/js/reactions.js @@ -139,7 +139,6 @@ document.addEventListener("DOMContentLoaded", () => {
{ generator="() => { return APP.user?.id; }" /> - - { document.body.appendChild(reactions_popup); reactions_popup_form = document.getElementById("reactions-selection-form"); + APP.on("topic_changed", ({ topic_id }) => { + const reaction_topic_id = topic_id ?? document.body.dataset.topic; + reactions_popup_form.action = reaction_topic_id + ? `/api/topics/${reaction_topic_id}/events` + : ""; + }); reactions_popup_search_input = document.getElementById("reactions-search-input"); reactions_popup_parent_id_input = reactions_popup_form.querySelector('[name="parent_id"]'); diff --git a/public/sidebar/sidebar.html b/public/sidebar/sidebar.html index 83e7560..d118b03 100644 --- a/public/sidebar/sidebar.html +++ b/public/sidebar/sidebar.html @@ -1,4 +1,38 @@ -
+
diff --git a/public/tabs/calendar/README.md b/public/tabs/calendar/README.md index 50fe4db..d64b1dd 100644 --- a/public/tabs/calendar/README.md +++ b/public/tabs/calendar/README.md @@ -1,3 +1,3 @@ # Calendar -The calendar should help people coordinate events. +The calendar should help people coordinate events around a topic. diff --git a/public/tabs/chat/channel_sidebar.html b/public/tabs/chat/channel_sidebar.html deleted file mode 100644 index a22b793..0000000 --- a/public/tabs/chat/channel_sidebar.html +++ /dev/null @@ -1,187 +0,0 @@ - - - diff --git a/public/tabs/chat/chat.html b/public/tabs/chat/chat.html index 83367ed..bc66136 100644 --- a/public/tabs/chat/chat.html +++ b/public/tabs/chat/chat.html @@ -21,8 +21,8 @@
{ feed.__reset && feed.__reset(); }); + APP.on("topic_changed", () => { feed.__reset && feed.__reset(); }); APP.on("user_logged_in", () => { feed.__reset && feed.__reset(); }); const time_tick_tock_timeout = 60_000; @@ -109,7 +109,6 @@ user avatar
@@ -148,8 +147,7 @@ { await document.getElementById( 'chat-content' ).__render(event); document.getElementById(event.id)?.classList.remove('sending'); }" on_parsed="async (event) => { await document.getElementById( 'chat-content' ).__render(event); document.getElementById(event.id)?.classList.add('sending'); }" > + + - -
- diff --git a/public/tabs/essays/essays.html b/public/tabs/essays/essays.html index 638d311..4eb0945 100644 --- a/public/tabs/essays/essays.html +++ b/public/tabs/essays/essays.html @@ -115,8 +115,8 @@
{ feed.__reset && feed.__reset(); }); APP.on("user_logged_in", () => { feed.__reset && feed.__reset(); }); feed.__target_element = (item) => { @@ -170,7 +171,6 @@ ${context.essay.data?.media?.length ? context.essay.data.media.map(function(url) { return `` }).join('\n') : ''}
@@ -179,7 +179,6 @@ user avatar
diff --git a/public/tabs/essays/new_essay.html b/public/tabs/essays/new_essay.html index fead5bb..209d70a 100644 --- a/public/tabs/essays/new_essay.html +++ b/public/tabs/essays/new_essay.html @@ -22,7 +22,7 @@ display: inline-block; } -
+
diff --git a/public/tabs/forum/new_post.html b/public/tabs/forum/new_post.html index 520a164..2560b9c 100644 --- a/public/tabs/forum/new_post.html +++ b/public/tabs/forum/new_post.html @@ -6,15 +6,15 @@ diff --git a/public/tabs/resources/README.md b/public/tabs/resources/README.md index 2771877..dcd6aa4 100644 --- a/public/tabs/resources/README.md +++ b/public/tabs/resources/README.md @@ -1,3 +1,3 @@ # Resources -Resources should be a wiki for organizing community knowledge. +Resources should be a wiki for organizing community knowledge on a topic. diff --git a/public/tabs/tabs.html b/public/tabs/tabs.html index db75630..b036fd3 100644 --- a/public/tabs/tabs.html +++ b/public/tabs/tabs.html @@ -14,7 +14,7 @@ const tab_selector = event.target; const view = tab_selector.dataset.view; if (view) { - window.location.hash = `/${view}${ document.body.dataset.channel ? `/channel/${ document.body.dataset.channel }` : '' }`; + window.location.hash = `/topic/${document.body.dataset.topic}/${view}`; } }); } diff --git a/tests/01_create_user.test.ts b/tests/01_create_user.test.ts index fbea069..9309566 100644 --- a/tests/01_create_user.test.ts +++ b/tests/01_create_user.test.ts @@ -3,6 +3,7 @@ import * as asserts from '@std/assert'; import { USER } from '../models/user.ts'; import { delete_user, EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, random_username } from './helpers.ts'; import { encodeBase64 } from '@std/encoding'; +import { generateTotp } from '../utils/totp.ts'; Deno.test({ name: 'API - USERS - Create', @@ -38,7 +39,7 @@ Deno.test({ asserts.assert(info.session); asserts.assert(info.headers); - const user: USER = info.user; + const user = info.user; asserts.assertEquals(user.username, username); diff --git a/tests/02_update_user.test.ts b/tests/02_update_user.test.ts index 2076649..240d4fc 100644 --- a/tests/02_update_user.test.ts +++ b/tests/02_update_user.test.ts @@ -2,6 +2,9 @@ import { api, API_CLIENT } from '../utils/api.ts'; import * as asserts from '@std/assert'; import { USER } from '../models/user.ts'; import { delete_user, EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, random_username } from './helpers.ts'; +import { Cookie, getSetCookies } from '@std/http/cookie'; +import { encodeBase64 } from '@std/encoding'; +import { generateTotp } from '../utils/totp.ts'; Deno.test({ name: 'API - USERS - Update', diff --git a/tests/04_create_channel.test.ts b/tests/04_create_topic.test.ts similarity index 74% rename from tests/04_create_channel.test.ts rename to tests/04_create_topic.test.ts index 5b28f72..5b248e9 100644 --- a/tests/04_create_channel.test.ts +++ b/tests/04_create_topic.test.ts @@ -2,9 +2,10 @@ import { api, API_CLIENT } from '../utils/api.ts'; import * as asserts from '@std/assert'; import { delete_user, EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from './helpers.ts'; import { generateTotp } from '../utils/totp.ts'; +import { clear_topic_events_cache } from '../models/event.ts'; Deno.test({ - name: 'API - CHANNELS - Create', + name: 'API - TOPICS - Create', permissions: { env: true, read: true, @@ -24,18 +25,18 @@ Deno.test({ const root_user_info = await get_new_user(client); try { - const root_user_channel = await client.fetch('/channels', { + const root_user_topic = await client.fetch('/topics', { method: 'POST', headers: { 'x-session_id': root_user_info.session.id, 'x-totp': await generateTotp(root_user_info.session.secret) }, json: { - name: 'this is the root user channel' + name: 'this is the root user topic' } }); - asserts.assert(root_user_channel); + asserts.assert(root_user_topic); } catch (error) { const reason: string = (error as Error).cause as string ?? (error as Error).toString(); asserts.fail(reason); @@ -44,7 +45,7 @@ Deno.test({ const regular_user_info = await get_new_user(client, {}, root_user_info); try { - const _permission_denied_channel = await client.fetch('/channels', { + const _permission_denied_topic = await client.fetch('/topics', { method: 'POST', headers: { 'x-session_id': regular_user_info.session.id, @@ -55,15 +56,15 @@ Deno.test({ } }); - asserts.fail('allowed creation of a channel without channel creation permissions'); + asserts.fail('allowed creation of a topic without topic creation permissions'); } catch (error) { asserts.assertEquals((error as Error).cause, 'permission_denied'); } - await set_user_permissions(client, regular_user_info.user, regular_user_info.session, [...regular_user_info.user.permissions, 'channels.create']); + await set_user_permissions(client, regular_user_info.user, regular_user_info.session, [...regular_user_info.user.permissions, 'topics.create']); try { - const _too_long_name_channel = await client.fetch('/channels', { + const _too_long_name_topic = await client.fetch('/topics', { method: 'POST', headers: { 'x-session_id': regular_user_info.session.id, @@ -74,27 +75,28 @@ Deno.test({ } }); - asserts.fail('allowed creation of a channel with an excessively long name'); + asserts.fail('allowed creation of a topic with an excessively long name'); } catch (error) { - asserts.assertEquals((error as Error).cause, 'invalid_channel_name'); + asserts.assertEquals((error as Error).cause, 'invalid_topic_name'); } - const new_channel = await client.fetch('/channels', { + const new_topic = await client.fetch('/topics', { method: 'POST', headers: { 'x-session_id': regular_user_info.session.id, 'x-totp': await generateTotp(regular_user_info.session.secret) }, json: { - name: 'test channel' + name: 'test topic' } }); - asserts.assert(new_channel); + asserts.assert(new_topic); await delete_user(client, regular_user_info); await delete_user(client, root_user_info); } finally { + clear_topic_events_cache(); if (test_server_info) { await test_server_info?.server?.stop(); } diff --git a/tests/05_update_channel.test.ts b/tests/05_update_topic.test.ts similarity index 59% rename from tests/05_update_channel.test.ts rename to tests/05_update_topic.test.ts index e0ea8c0..e258aed 100644 --- a/tests/05_update_channel.test.ts +++ b/tests/05_update_topic.test.ts @@ -2,9 +2,10 @@ import { api, API_CLIENT } from '../utils/api.ts'; import * as asserts from '@std/assert'; import { delete_user, EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from './helpers.ts'; import { generateTotp } from '../utils/totp.ts'; +import { clear_topic_events_cache } from '../models/event.ts'; Deno.test({ - name: 'API - CHANNELS - Update', + name: 'API - TOPICS - Update', permissions: { env: true, read: true, @@ -23,25 +24,25 @@ Deno.test({ const info = await get_new_user(client); - await set_user_permissions(client, info.user, info.session, [...info.user.permissions, 'channels.create']); + await set_user_permissions(client, info.user, info.session, [...info.user.permissions, 'topics.create']); - const new_channel = await client.fetch('/channels', { + const new_topic = await client.fetch('/topics', { method: 'POST', headers: { 'x-session_id': info.session.id, 'x-totp': await generateTotp(info.session.secret) }, json: { - name: 'test update channel' + name: 'test update topic' } }); - asserts.assert(new_channel); + asserts.assert(new_topic); const other_user_info = await get_new_user(client, {}, info); try { - const _permission_denied_channel = await client.fetch(`/channels/${new_channel.id}`, { + const _permission_denied_topic = await client.fetch(`/topics/${new_topic.id}`, { method: 'PUT', headers: { 'x-session_id': other_user_info.session.id, @@ -52,48 +53,49 @@ Deno.test({ } }); - asserts.fail('allowed updating a channel owned by someone else'); + asserts.fail('allowed updating a topic owned by someone else'); } catch (error) { asserts.assertEquals((error as Error).cause, 'permission_denied'); } - const updated_by_owner_channel = await client.fetch(`/channels/${new_channel.id}`, { + const updated_by_owner_topic = await client.fetch(`/topics/${new_topic.id}`, { method: 'PUT', headers: { 'x-session_id': info.session.id, 'x-totp': await generateTotp(info.session.secret) }, json: { - channel: 'this is a new channel', + topic: 'this is a new topic', permissions: { - ...new_channel.permissions, - write: [...new_channel.permissions.write, other_user_info.user.id] + ...new_topic.permissions, + write: [...new_topic.permissions.write, other_user_info.user.id] } } }); - asserts.assert(updated_by_owner_channel); - asserts.assertEquals(updated_by_owner_channel.channel, 'this is a new channel'); - asserts.assertEquals(updated_by_owner_channel.permissions.write, [info.user.id, other_user_info.user.id]); + asserts.assert(updated_by_owner_topic); + asserts.assertEquals(updated_by_owner_topic.topic, 'this is a new topic'); + asserts.assertEquals(updated_by_owner_topic.permissions.write, [info.user.id, other_user_info.user.id]); - const updated_by_other_user_channel = await client.fetch(`/channels/${new_channel.id}`, { + const updated_by_other_user_topic = await client.fetch(`/topics/${new_topic.id}`, { method: 'PUT', headers: { 'x-session_id': other_user_info.session.id, 'x-totp': await generateTotp(other_user_info.session.secret) }, json: { - channel: 'this is a newer channel' + topic: 'this is a newer topic' } }); - asserts.assert(updated_by_other_user_channel); - asserts.assertEquals(updated_by_other_user_channel.channel, 'this is a newer channel'); - asserts.assertEquals(updated_by_other_user_channel.permissions.write, [info.user.id, other_user_info.user.id]); + asserts.assert(updated_by_other_user_topic); + asserts.assertEquals(updated_by_other_user_topic.topic, 'this is a newer topic'); + asserts.assertEquals(updated_by_other_user_topic.permissions.write, [info.user.id, other_user_info.user.id]); await delete_user(client, other_user_info); await delete_user(client, info); } finally { + clear_topic_events_cache(); if (test_server_info) { await test_server_info?.server?.stop(); } diff --git a/tests/06_delete_channel.test.ts b/tests/06_delete_topic.test.ts similarity index 75% rename from tests/06_delete_channel.test.ts rename to tests/06_delete_topic.test.ts index 37e8cd4..46e6014 100644 --- a/tests/06_delete_channel.test.ts +++ b/tests/06_delete_topic.test.ts @@ -2,9 +2,10 @@ import { api, API_CLIENT } from '../utils/api.ts'; import * as asserts from '@std/assert'; import { delete_user, EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from './helpers.ts'; import { generateTotp } from '../utils/totp.ts'; +import { clear_topic_events_cache } from '../models/event.ts'; Deno.test({ - name: 'API - CHANNELS - Delete', + name: 'API - TOPICS - Delete', permissions: { env: true, read: true, @@ -23,22 +24,22 @@ Deno.test({ const info = await get_new_user(client); - await set_user_permissions(client, info.user, info.session, [...info.user.permissions, 'channels.create']); + await set_user_permissions(client, info.user, info.session, [...info.user.permissions, 'topics.create']); - const new_channel = await client.fetch('/channels', { + const new_topic = await client.fetch('/topics', { method: 'POST', headers: { 'x-session_id': info.session.id, 'x-totp': await generateTotp(info.session.secret) }, json: { - name: 'test delete channel' + name: 'test delete topic' } }); - asserts.assert(new_channel); + asserts.assert(new_topic); - const deleted_channel = await client.fetch(`/channels/${new_channel.id}`, { + const deleted_topic = await client.fetch(`/topics/${new_topic.id}`, { method: 'DELETE', headers: { 'x-session_id': info.session.id, @@ -46,10 +47,11 @@ Deno.test({ } }); - asserts.assert(deleted_channel); + asserts.assert(deleted_topic); await delete_user(client, info); } finally { + clear_topic_events_cache(); if (test_server_info) { await test_server_info?.server?.stop(); } diff --git a/tests/07_create_channel_events.test.ts b/tests/07_create_topic_events.test.ts similarity index 70% rename from tests/07_create_channel_events.test.ts rename to tests/07_create_topic_events.test.ts index 63f4469..84eaeda 100644 --- a/tests/07_create_channel_events.test.ts +++ b/tests/07_create_topic_events.test.ts @@ -2,9 +2,10 @@ import * as asserts from '@std/assert'; import { delete_user, EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from './helpers.ts'; import { api, API_CLIENT } from '../utils/api.ts'; import { generateTotp } from '../utils/totp.ts'; +import { clear_topic_events_cache } from '../models/event.ts'; Deno.test({ - name: 'API - CHANNELS - EVENTS - Create', + name: 'API - TOPICS - EVENTS - Create', permissions: { env: true, read: true, @@ -23,27 +24,25 @@ Deno.test({ const owner_info = await get_new_user(client); - await set_user_permissions(client, owner_info.user, owner_info.session, [...owner_info.user.permissions, 'channels.create']); + await set_user_permissions(client, owner_info.user, owner_info.session, [...owner_info.user.permissions, 'topics.create']); - const channel = await client.fetch('/channels', { + const topic = await client.fetch('/topics', { method: 'POST', headers: { 'x-session_id': owner_info.session.id, 'x-totp': await generateTotp(owner_info.session.secret) }, json: { - name: 'test events channel', + name: 'test events topic', permissions: { - events: { - write: [owner_info.user.id] - } + write_events: [owner_info.user.id] } } }); - asserts.assert(channel); + asserts.assert(topic); - const event_from_owner = await client.fetch(`/events`, { + const event_from_owner = await client.fetch(`/topics/${topic.id}/events`, { method: 'POST', headers: { 'x-session_id': owner_info.session.id, @@ -51,7 +50,6 @@ Deno.test({ }, json: { type: 'test', - channel: channel.id, data: { foo: 'bar' } @@ -63,7 +61,7 @@ Deno.test({ const other_user_info = await get_new_user(client, {}, owner_info); try { - const _permission_denied_channel = await client.fetch(`/events`, { + const _permission_denied_topic = await client.fetch(`/topics/${topic.id}/events`, { method: 'POST', headers: { 'x-session_id': other_user_info.session.id, @@ -71,20 +69,19 @@ Deno.test({ }, json: { type: 'test', - channel: channel.id, data: { other_user: true } } }); - asserts.fail('allowed adding an event to a channel without permission'); + asserts.fail('allowed adding an event to a topic without permission'); } catch (error) { asserts.assertEquals((error as Error).cause, 'permission_denied'); } - // make the channel public write - const updated_by_owner_channel = await client.fetch(`/channels/${channel.id}`, { + // make the topic public write + const updated_by_owner_topic = await client.fetch(`/topics/${topic.id}`, { method: 'PUT', headers: { 'x-session_id': owner_info.session.id, @@ -92,18 +89,16 @@ Deno.test({ }, json: { permissions: { - ...channel.permissions, - events: { - write: [] - } + ...topic.permissions, + write_events: [] } } }); - asserts.assert(updated_by_owner_channel); - asserts.assertEquals(updated_by_owner_channel.permissions.events.write, []); + asserts.assert(updated_by_owner_topic); + asserts.assertEquals(updated_by_owner_topic.permissions.write_events, []); - const event_from_other_user = await client.fetch(`/events`, { + const event_from_other_user = await client.fetch(`/topics/${topic.id}/events`, { method: 'POST', headers: { 'x-session_id': other_user_info.session.id, @@ -111,7 +106,6 @@ Deno.test({ }, json: { type: 'test', - channel: channel.id, data: { other_user: true } @@ -123,6 +117,7 @@ Deno.test({ await delete_user(client, other_user_info); await delete_user(client, owner_info); } finally { + clear_topic_events_cache(); if (test_server_info) { await test_server_info?.server?.stop(); } diff --git a/tests/08_get_channel_events.test.ts b/tests/08_get_topic_events.test.ts similarity index 82% rename from tests/08_get_channel_events.test.ts rename to tests/08_get_topic_events.test.ts index 13e4749..d9ce1ff 100644 --- a/tests/08_get_channel_events.test.ts +++ b/tests/08_get_topic_events.test.ts @@ -2,9 +2,10 @@ import * as asserts from '@std/assert'; import { delete_user, EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from './helpers.ts'; import { api, API_CLIENT } from '../utils/api.ts'; import { generateTotp } from '../utils/totp.ts'; +import { clear_topic_events_cache } from '../models/event.ts'; Deno.test({ - name: 'API - CHANNELS - EVENTS - Get', + name: 'API - TOPICS - EVENTS - Get', permissions: { env: true, read: true, @@ -28,25 +29,25 @@ Deno.test({ const owner_info = await get_new_user(client); - await set_user_permissions(client, owner_info.user, owner_info.session, [...owner_info.user.permissions, 'channels.create']); + await set_user_permissions(client, owner_info.user, owner_info.session, [...owner_info.user.permissions, 'topics.create']); - const channel = await client.fetch('/channels', { + const topic = await client.fetch('/topics', { method: 'POST', headers: { 'x-session_id': owner_info.session.id, 'x-totp': await generateTotp(owner_info.session.secret) }, json: { - name: 'test get events channel' + name: 'test get events topic' } }); - asserts.assert(channel); + asserts.assert(topic); const NUM_INITIAL_EVENTS = 5; const events_initial_batch: any[] = []; for (let i = 0; i < NUM_INITIAL_EVENTS; ++i) { - const event = await client.fetch(`/events`, { + const event = await client.fetch(`/topics/${topic.id}/events`, { method: 'POST', headers: { 'x-session_id': owner_info.session.id, @@ -54,7 +55,6 @@ Deno.test({ }, json: { type: 'test', - channel: channel.id, data: { i } @@ -69,7 +69,7 @@ Deno.test({ const other_user_info = await get_new_user(client, {}, owner_info); - const events_from_server = await client.fetch(`/channels/${channel.id}/events`, { + const events_from_server = await client.fetch(`/topics/${topic.id}/events`, { method: 'GET', headers: { 'x-session_id': other_user_info.session.id, @@ -82,7 +82,7 @@ Deno.test({ const newest_event = events_from_server[0]; asserts.assert(newest_event); - const long_poll_request_promise = client.fetch(`/channels/${channel.id}/events?wait=true&after_id=${newest_event.id.split(':', 2)[1]}`, { + const long_poll_request_promise = client.fetch(`/topics/${topic.id}/events?wait=true&after_id=${newest_event.id}`, { method: 'GET', headers: { 'x-session_id': other_user_info.session.id, @@ -92,7 +92,7 @@ Deno.test({ const wait_and_then_create_an_event = new Promise((resolve) => { setTimeout(async () => { - await client.fetch(`/events`, { + await client.fetch(`/topics/${topic.id}/events`, { method: 'POST', headers: { 'x-session_id': owner_info.session.id, @@ -100,7 +100,6 @@ Deno.test({ }, json: { type: 'test', - channel: channel.id, data: { i: 12345 } @@ -121,6 +120,7 @@ Deno.test({ await delete_user(client, other_user_info); await delete_user(client, owner_info); } finally { + clear_topic_events_cache(); if (test_server_info) { await test_server_info.server.stop(); } diff --git a/tests/09_update_channel_events.test.ts b/tests/09_update_topic_events.test.ts similarity index 69% rename from tests/09_update_channel_events.test.ts rename to tests/09_update_topic_events.test.ts index f67d104..9c26058 100644 --- a/tests/09_update_channel_events.test.ts +++ b/tests/09_update_topic_events.test.ts @@ -2,9 +2,10 @@ import * as asserts from '@std/assert'; import { delete_user, EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from './helpers.ts'; import { api, API_CLIENT } from '../utils/api.ts'; import { generateTotp } from '../utils/totp.ts'; +import { clear_topic_events_cache } from '../models/event.ts'; Deno.test({ - name: 'API - CHANNELS - EVENTS - Update', + name: 'API - TOPICS - EVENTS - Update', permissions: { env: true, read: true, @@ -23,22 +24,22 @@ Deno.test({ const owner_info = await get_new_user(client); - await set_user_permissions(client, owner_info.user, owner_info.session, [...owner_info.user.permissions, 'channels.create']); + await set_user_permissions(client, owner_info.user, owner_info.session, [...owner_info.user.permissions, 'topics.create']); - const channel = await client.fetch('/channels', { + const topic = await client.fetch('/topics', { method: 'POST', headers: { 'x-session_id': owner_info.session.id, 'x-totp': await generateTotp(owner_info.session.secret) }, json: { - name: 'test update events channel' + name: 'test update events topic' } }); - asserts.assert(channel); + asserts.assert(topic); - const event_from_owner = await client.fetch(`/events`, { + const event_from_owner = await client.fetch(`/topics/${topic.id}/events`, { method: 'POST', headers: { 'x-session_id': owner_info.session.id, @@ -46,7 +47,6 @@ Deno.test({ }, json: { type: 'test', - channel: channel.id, data: { foo: 'bar' } @@ -55,7 +55,7 @@ Deno.test({ asserts.assert(event_from_owner); - const fetched_event_from_owner = await client.fetch(`/channels/${channel.id}/events/${event_from_owner.id}`, { + const fetched_event_from_owner = await client.fetch(`/topics/${topic.id}/events/${event_from_owner.id}`, { method: 'GET', headers: { 'x-session_id': owner_info.session.id, @@ -65,23 +65,25 @@ Deno.test({ asserts.assertEquals(fetched_event_from_owner, event_from_owner); - const updated_event_from_owner = await client.fetch(`/events/${event_from_owner.id}`, { + const updated_event_from_owner = await client.fetch(`/topics/${topic.id}/events/${event_from_owner.id}`, { method: 'PUT', headers: { 'x-session_id': owner_info.session.id, 'x-totp': await generateTotp(owner_info.session.secret) }, json: { - meta: { + type: 'other', + data: { foo: 'baz' } } }); asserts.assertNotEquals(updated_event_from_owner, event_from_owner); - asserts.assertEquals(updated_event_from_owner.meta?.foo, 'baz'); + asserts.assertEquals(updated_event_from_owner.type, 'other'); + asserts.assertEquals(updated_event_from_owner.data.foo, 'baz'); - const fetched_updated_event_from_owner = await client.fetch(`/channels/${channel.id}/events/${event_from_owner.id}`, { + const fetched_updated_event_from_owner = await client.fetch(`/topics/${topic.id}/events/${event_from_owner.id}`, { method: 'GET', headers: { 'x-session_id': owner_info.session.id, @@ -95,7 +97,7 @@ Deno.test({ const other_user_info = await get_new_user(client, {}, owner_info); - const event_from_other_user = await client.fetch(`/events`, { + const event_from_other_user = await client.fetch(`/topics/${topic.id}/events`, { method: 'POST', headers: { 'x-session_id': other_user_info.session.id, @@ -103,7 +105,6 @@ Deno.test({ }, json: { type: 'test', - channel: channel.id, data: { other_user: true } @@ -112,7 +113,7 @@ Deno.test({ asserts.assert(event_from_other_user); - const fetched_event_from_other_user = await client.fetch(`/channels/${channel.id}/events/${event_from_other_user.id}`, { + const fetched_event_from_other_user = await client.fetch(`/topics/${topic.id}/events/${event_from_other_user.id}`, { method: 'GET', headers: { 'x-session_id': other_user_info.session.id, @@ -122,13 +123,14 @@ Deno.test({ asserts.assertEquals(fetched_event_from_other_user, event_from_other_user); - const updated_event_from_other_user = await client.fetch(`/events/${event_from_other_user.id}`, { + const updated_event_from_other_user = await client.fetch(`/topics/${topic.id}/events/${event_from_other_user.id}`, { method: 'PUT', headers: { 'x-session_id': other_user_info.session.id, 'x-totp': await generateTotp(other_user_info.session.secret) }, json: { + type: 'other', data: { other_user: 'bloop' } @@ -136,9 +138,10 @@ Deno.test({ }); asserts.assertNotEquals(updated_event_from_other_user, event_from_other_user); + asserts.assertEquals(updated_event_from_other_user.type, 'other'); asserts.assertEquals(updated_event_from_other_user.data.other_user, 'bloop'); - const fetched_updated_event_from_other_user = await client.fetch(`/channels/${channel.id}/events/${event_from_other_user.id}`, { + const fetched_updated_event_from_other_user = await client.fetch(`/topics/${topic.id}/events/${event_from_other_user.id}`, { method: 'GET', headers: { 'x-session_id': other_user_info.session.id, @@ -150,7 +153,7 @@ Deno.test({ asserts.assertNotEquals(fetched_updated_event_from_other_user, fetched_event_from_other_user); asserts.assertEquals(fetched_updated_event_from_other_user, updated_event_from_other_user); - const updated_by_owner_channel = await client.fetch(`/channels/${channel.id}`, { + const updated_by_owner_topic = await client.fetch(`/topics/${topic.id}`, { method: 'PUT', headers: { 'x-session_id': owner_info.session.id, @@ -158,38 +161,33 @@ Deno.test({ }, json: { permissions: { - ...channel.permissions, - events: { - read: [], - write: [owner_info.user.id] - } + ...topic.permissions, + write_events: [owner_info.user.id] } } }); - asserts.assertEquals(updated_by_owner_channel.permissions.events.write, [owner_info.user.id]); + asserts.assertEquals(updated_by_owner_topic.permissions.write_events, [owner_info.user.id]); try { - await client.fetch(`/events/${event_from_other_user.id}`, { + await client.fetch(`/topics/${topic.id}/events/${event_from_other_user.id}`, { method: 'PUT', headers: { 'x-session_id': other_user_info.session.id, 'x-totp': await generateTotp(other_user_info.session.secret) }, json: { - data: { - other_user: 'glop' - } + type: 'new' } }); - asserts.fail('allowed updating an event in a channel with a events.write allowed only by owner'); + asserts.fail('allowed updating an event in a topic with a write_events allowed only by owner'); } catch (error) { asserts.assertEquals((error as Error).cause, 'permission_denied'); } try { - await client.fetch(`/events/${event_from_other_user.id}`, { + await client.fetch(`/topics/${topic.id}/events/${event_from_other_user.id}`, { method: 'DELETE', headers: { 'x-session_id': other_user_info.session.id, @@ -197,12 +195,12 @@ Deno.test({ } }); - asserts.fail('allowed deleting an event in a channel with a events.write allowed only by owner'); + asserts.fail('allowed deleting an event in a topic with a write_events allowed only by owner'); } catch (error) { asserts.assertEquals((error as Error).cause, 'permission_denied'); } - const publicly_writable_channel = await client.fetch(`/channels/${channel.id}`, { + const publicly_writable_topic = await client.fetch(`/topics/${topic.id}`, { method: 'PUT', headers: { 'x-session_id': owner_info.session.id, @@ -210,18 +208,15 @@ Deno.test({ }, json: { permissions: { - ...channel.permissions, - events: { - read: [], - write: [] - } + ...topic.permissions, + write_events: [] } } }); - asserts.assertEquals(publicly_writable_channel.permissions.events.write, []); + asserts.assertEquals(publicly_writable_topic.permissions.write_events, []); - const delete_other_user_event_response = await client.fetch(`/events/${event_from_other_user.id}`, { + const delete_other_user_event_response = await client.fetch(`/topics/${topic.id}/events/${event_from_other_user.id}`, { method: 'DELETE', headers: { 'x-session_id': other_user_info.session.id, @@ -231,7 +226,7 @@ Deno.test({ asserts.assertEquals(delete_other_user_event_response.deleted, true); - const delete_owner_event_response = await client.fetch(`/events/${event_from_owner.id}`, { + const delete_owner_event_response = await client.fetch(`/topics/${topic.id}/events/${event_from_owner.id}`, { method: 'DELETE', headers: { 'x-session_id': owner_info.session.id, @@ -244,6 +239,7 @@ Deno.test({ await delete_user(client, other_user_info); await delete_user(client, owner_info); } finally { + clear_topic_events_cache(); if (test_server_info) { await test_server_info?.server?.stop(); } diff --git a/tests/10_update_channel_events_when_append_only.test.ts b/tests/10_update_topic_events_when_append_only.test.ts similarity index 70% rename from tests/10_update_channel_events_when_append_only.test.ts rename to tests/10_update_topic_events_when_append_only.test.ts index 3d46add..3ce4024 100644 --- a/tests/10_update_channel_events_when_append_only.test.ts +++ b/tests/10_update_topic_events_when_append_only.test.ts @@ -2,9 +2,10 @@ import * as asserts from '@std/assert'; import { delete_user, EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from './helpers.ts'; import { api, API_CLIENT } from '../utils/api.ts'; import { generateTotp } from '../utils/totp.ts'; +import { clear_topic_events_cache } from '../models/event.ts'; Deno.test({ - name: 'API - CHANNELS - EVENTS - Update (APPEND_ONLY_EVENTS)', + name: 'API - TOPICS - EVENTS - Update (APPEND_ONLY_EVENTS)', permissions: { env: true, read: true, @@ -26,22 +27,22 @@ Deno.test({ const owner_info = await get_new_user(client); - await set_user_permissions(client, owner_info.user, owner_info.session, [...owner_info.user.permissions, 'channels.create']); + await set_user_permissions(client, owner_info.user, owner_info.session, [...owner_info.user.permissions, 'topics.create']); - const channel = await client.fetch('/channels', { + const topic = await client.fetch('/topics', { method: 'POST', headers: { 'x-session_id': owner_info.session.id, 'x-totp': await generateTotp(owner_info.session.secret) }, json: { - name: 'test update events channel in append only mode' + name: 'test update events topic in append only mode' } }); - asserts.assert(channel); + asserts.assert(topic); - const event_from_owner = await client.fetch(`/events`, { + const event_from_owner = await client.fetch(`/topics/${topic.id}/events`, { method: 'POST', headers: { 'x-session_id': owner_info.session.id, @@ -49,7 +50,6 @@ Deno.test({ }, json: { type: 'test', - channel: channel.id, data: { foo: 'bar' } @@ -58,7 +58,7 @@ Deno.test({ asserts.assert(event_from_owner); - const fetched_event_from_owner = await client.fetch(`/channels/${channel.id}/events/${event_from_owner.id}`, { + const fetched_event_from_owner = await client.fetch(`/topics/${topic.id}/events/${event_from_owner.id}`, { method: 'GET', headers: { 'x-session_id': owner_info.session.id, @@ -69,26 +69,24 @@ Deno.test({ asserts.assertEquals(fetched_event_from_owner, event_from_owner); try { - await client.fetch(`/events/${event_from_owner.id}`, { + await client.fetch(`/topics/${topic.id}/events/${event_from_owner.id}`, { method: 'PUT', headers: { 'x-session_id': owner_info.session.id, 'x-totp': await generateTotp(owner_info.session.secret) }, json: { - meta: { - foo: 'bar' - } + type: 'new' } }); - asserts.fail('allowed updating an event in a channel with APPEND_ONLY_EVENTS on'); + asserts.fail('allowed updating an event in a topic with APPEND_ONLY_EVENTS on'); } catch (error) { asserts.assertEquals((error as Error).cause, 'append_only_events'); } try { - await client.fetch(`/events/${event_from_owner.id}`, { + await client.fetch(`/topics/${topic.id}/events/${event_from_owner.id}`, { method: 'DELETE', headers: { 'x-session_id': owner_info.session.id, @@ -96,14 +94,14 @@ Deno.test({ } }); - asserts.fail('allowed deleting an event in a channel with APPEND_ONLY_EVENTS on'); + asserts.fail('allowed deleting an event in a topic with APPEND_ONLY_EVENTS on'); } catch (error) { asserts.assertEquals((error as Error).cause, 'append_only_events'); } const other_user_info = await get_new_user(client, {}, owner_info); - const event_from_other_user = await client.fetch(`/events`, { + const event_from_other_user = await client.fetch(`/topics/${topic.id}/events`, { method: 'POST', headers: { 'x-session_id': other_user_info.session.id, @@ -111,7 +109,6 @@ Deno.test({ }, json: { type: 'test', - channel: channel.id, data: { other_user: true } @@ -120,7 +117,7 @@ Deno.test({ asserts.assert(event_from_other_user); - const fetched_event_from_other_user = await client.fetch(`/channels/${channel.id}/events/${event_from_other_user.id}`, { + const fetched_event_from_other_user = await client.fetch(`/topics/${topic.id}/events/${event_from_other_user.id}`, { method: 'GET', headers: { 'x-session_id': other_user_info.session.id, @@ -131,26 +128,24 @@ Deno.test({ asserts.assertEquals(fetched_event_from_other_user, event_from_other_user); try { - await client.fetch(`/events/${event_from_other_user.id}`, { + await client.fetch(`/topics/${topic.id}/events/${event_from_other_user.id}`, { method: 'PUT', headers: { 'x-session_id': other_user_info.session.id, 'x-totp': await generateTotp(other_user_info.session.secret) }, json: { - meta: { - foo: 'bar' - } + type: 'new' } }); - asserts.fail('allowed updating an event in a channel with APPEND_ONLY_EVENTS on'); + asserts.fail('allowed updating an event in a topic with APPEND_ONLY_EVENTS on'); } catch (error) { asserts.assertEquals((error as Error).cause, 'append_only_events'); } try { - await client.fetch(`/events/${event_from_other_user.id}`, { + await client.fetch(`/topics/${topic.id}/events/${event_from_other_user.id}`, { method: 'DELETE', headers: { 'x-session_id': other_user_info.session.id, @@ -158,7 +153,7 @@ Deno.test({ } }); - asserts.fail('allowed deleting an event in a channel with APPEND_ONLY_EVENTS on'); + asserts.fail('allowed deleting an event in a topic with APPEND_ONLY_EVENTS on'); } catch (error) { asserts.assertEquals((error as Error).cause, 'append_only_events'); } @@ -168,6 +163,7 @@ Deno.test({ } finally { Deno.env.delete('APPEND_ONLY_EVENTS'); + clear_topic_events_cache(); if (test_server_info) { await test_server_info?.server?.stop(); } diff --git a/utils/object_helpers.ts b/utils/object_helpers.ts deleted file mode 100644 index 5566375..0000000 --- a/utils/object_helpers.ts +++ /dev/null @@ -1,30 +0,0 @@ -export function flatten(obj: Record, path?: string, result?: Record) { - result = result ?? {}; - - for (const [key, value] of Object.entries(obj)) { - if (typeof value === 'object') { - flatten(value, (path ?? '') + key + '.', result); - } else { - result[(path ?? '') + key] = value; - } - } - - return result; -} - -export function expand(obj: Record) { - const result: Record = {}; - - for (const [key, value] of Object.entries(obj)) { - const elements = key.split('.'); - - let current = result; - for (const element of elements.slice(0, elements.length - 1)) { - current[element] = current[element] ?? {}; - current = current[element]; - } - current[elements[elements.length - 1]] = value; - } - - return result; -} diff --git a/utils/prechecks.ts b/utils/prechecks.ts index 2c910b9..3d0f82d 100644 --- a/utils/prechecks.ts +++ b/utils/prechecks.ts @@ -1,9 +1,8 @@ import { getCookies } from '@std/http/cookie'; import { SESSIONS } from '../models/session.ts'; import { verifyTotp } from './totp.ts'; -import { USER, USERS } from '../models/user.ts'; +import { USERS } from '../models/user.ts'; import * as CANNED_RESPONSES from './canned_responses.ts'; -import { EVENT } from '../models/event.ts'; export type PRECHECK = (req: Request, meta: Record) => Promise | Response | undefined; export type PRECHECK_TABLE = Record; @@ -42,7 +41,3 @@ export function require_user( return CANNED_RESPONSES.permission_denied(); } } - -export function user_has_write_permission_for_event(user: USER, event: EVENT) { - return user.permissions.includes('events.create.' + event.type) || (Deno.env.get('DENO_ENV') === 'test' && event.type === 'test'); -}