From 6500d9a9be75d293991b13d68b57b0458d1be4ee Mon Sep 17 00:00:00 2001 From: Andy Burke Date: Sun, 12 Oct 2025 16:25:42 -0700 Subject: [PATCH] feature: start to lay groundwork for reactions --- models/event.ts | 33 ++++++++++++++++++++++ models/invites.ts | 20 +++++++++++-- public/api/users/:user_id/invites/index.ts | 3 ++ public/api/users/index.ts | 5 ++-- 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/models/event.ts b/models/event.ts index 5dcb873..a11827a 100644 --- a/models/event.ts +++ b/models/event.ts @@ -1,6 +1,9 @@ import { by_character, by_lurid } from '@andyburke/fsdb/organizers'; import { FSDB_COLLECTION } from '@andyburke/fsdb'; import { FSDB_INDEXER_SYMLINKS } from '@andyburke/fsdb/indexers'; +import { EMOJI_MAP as JS_EMOJI_MAP } from '../public/js/emojis/en.js'; + +const EMOJI_MAP: Record = JS_EMOJI_MAP; /** * @typedef {object} TIMESTAMPS @@ -110,7 +113,37 @@ export function VALIDATE_EVENT(event: EVENT) { }); } break; + case 'reaction': + if (typeof event.parent_id !== 'string') { + errors.push({ + cause: 'reaction_missing_parent_id', + message: 'A reaction must have a parent_id that refers to another event.' + }); + } else if (typeof event.data?.reaction !== 'string') { + errors.push({ + cause: 'reaction_must_be_an_emoji', + message: 'A reaction event must have a `data.reaction` that is an emoji.' + }); + } else if (typeof EMOJI_MAP[event.data.reaction] === 'undefined') { + errors.push({ + cause: 'reaction_must_be_an_emoji', + message: 'A reaction event must have a `data.reaction` that is an emoji.' + }); + } + break; + case 'test': + if (Deno.env.get('DENO_ENV') !== 'test') { + errors.push({ + cause: 'unknown_event_type', + message: 'Event types allowed: ' + ['chat', 'post', 'blurb', 'essay', 'reaction'].join(', ') + }); + } + break; default: + errors.push({ + cause: 'unknown_event_type', + message: 'Event types allowed: ' + ['chat', 'post', 'blurb', 'essay', 'reaction'].join(', ') + }); break; } diff --git a/models/invites.ts b/models/invites.ts index d81088b..b7a07e7 100644 --- a/models/invites.ts +++ b/models/invites.ts @@ -8,7 +8,7 @@ export type INVITE_CODE = { code: string; timestamps: { created: string; - expires?: string; + expires: string; cancelled?: string; }; }; @@ -30,7 +30,7 @@ export const INVITE_CODES = new FSDB_COLLECTION({ } }); -// TODO: separate out these different validators somewhere? +const MAX_INVITE_EXPIRE_TIME = 28 * 24 * 60 * 60 * 1_000; export function VALIDATE_INVITE_CODE(invite_code: INVITE_CODE) { const errors: any[] = []; @@ -41,7 +41,6 @@ export function VALIDATE_INVITE_CODE(invite_code: INVITE_CODE) { }); } - // TODO: further invite code validation if (typeof invite_code.code !== 'string' || invite_code.id.length < 3) { errors.push({ cause: 'invalid_invite_code_code', @@ -49,5 +48,20 @@ export function VALIDATE_INVITE_CODE(invite_code: INVITE_CODE) { }); } + if (typeof invite_code.timestamps?.expires !== 'string') { + errors.push({ + cause: 'missing_invite_expiration', + message: 'Invite codes must have an expiration set.' + }); + } + + const expiration_delta = new Date(invite_code.timestamps.expires).valueOf() - new Date(invite_code.timestamps.created).valueOf(); + if (expiration_delta > MAX_INVITE_EXPIRE_TIME || expiration_delta < 0) { + errors.push({ + cause: 'invite_expiration_invalid', + message: 'Invite codes must expire within a limited window after they are created.' + }); + } + return errors.length ? errors : undefined; } diff --git a/public/api/users/:user_id/invites/index.ts b/public/api/users/:user_id/invites/index.ts index 991167c..d4a74e5 100644 --- a/public/api/users/:user_id/invites/index.ts +++ b/public/api/users/:user_id/invites/index.ts @@ -8,6 +8,7 @@ import { INVITE_CODE, INVITE_CODES, VALIDATE_INVITE_CODE } from '../../../../../ export const PRECHECKS: PRECHECK_TABLE = {}; const INVITE_CODES_ALLOWED_PER_MINUTE = parseInt(Deno.env.get('INVITE_CODES_ALLOWED_PER_MINUTE') ?? '3', 10); +const DEFAULT_INVITE_EXPIRE_TIME: number = 28 * 24 * 60 * 60 * 1_000; // 28 days max invite length // GET /api/users/:user_id/invites - get invites this user has created // query parameters: @@ -94,6 +95,8 @@ export async function POST(req: Request, meta: Record): Promise): Promise): Promise= (invite_code?.timestamps.expires ?? '0000-01-01T00:00:00.000Z'); const is_used = (await SIGNUPS.find({ referring_invite_code_id: invite_code?.id }, {