refactor: rooms => zones

This commit is contained in:
Andy Burke 2025-09-09 15:32:07 -07:00
parent 525568d368
commit 20a5d1bc88
27 changed files with 2466 additions and 2044 deletions

View file

@ -7,7 +7,7 @@ Bringing the BBS back.
These are in no particular order. Pull requests updating this section welcome for These are in no particular order. Pull requests updating this section welcome for
feature discussions. feature discussions.
- [ ] should everything be an event in a room? - [ ] should everything be an event in a zone?
- [X] get a first-pass podman/docker setup up - [X] get a first-pass podman/docker setup up
- [X] sign up - [X] sign up
- [X] check for logged in user session - [X] check for logged in user session
@ -21,7 +21,7 @@ feature discussions.
- [X] logout button - [X] logout button
- [ ] profile editing - [ ] profile editing
- [X] avatar uploads - [X] avatar uploads
- [X] chat rooms - [X] chat zones
- [X] chat messages - [X] chat messages
- [ ] chat message actions - [ ] chat message actions
- [X] '...' button to show actions - [X] '...' button to show actions
@ -66,6 +66,7 @@ feature discussions.
- [ ] hide control - [ ] hide control
- [ ] inline image support - [ ] inline image support
- [X] inline images - [X] inline images
- [ ] fullscreen images on click to show largest version
- [ ] hide control - [ ] hide control
- [ ] custom emoji support - [ ] custom emoji support
- [ ] upload custom gif emoji - [ ] upload custom gif emoji
@ -75,7 +76,7 @@ feature discussions.
- [ ] if web notifications are enabled, emit on events - [ ] if web notifications are enabled, emit on events
- [ ] ability to mute - [ ] ability to mute
- [ ] users - [ ] users
- [ ] rooms - [ ] zones
- [ ] tags (#tags?) - [ ] tags (#tags?)
- [ ] admin panel - [ ] admin panel
- [ ] add invite code generation - [ ] add invite code generation

View file

@ -13,8 +13,8 @@ import { FSDB_INDEXER_SYMLINKS } from '@andyburke/fsdb/indexers';
* *
* @property {string} id - lurid * @property {string} id - lurid
* @property {string} creator_id - id of the source user * @property {string} creator_id - id of the source user
* @property {string} room_id - id of the target room
* @property {string} type - event type * @property {string} type - event type
* @property {string} [parent_id] - optional parent event id
* @property {string[]} [tags] - optional event tags * @property {string[]} [tags] - optional event tags
* @property {Record<string,any>} [data] - optional data payload of the event * @property {Record<string,any>} [data] - optional data payload of the event
* @property {TIMESTAMPS} timestamps - timestamps that will be set by the server * @property {TIMESTAMPS} timestamps - timestamps that will be set by the server
@ -23,6 +23,7 @@ export type EVENT = {
id: string; id: string;
creator_id: string; creator_id: string;
type: string; type: string;
parent_id?: string;
tags?: string[]; tags?: string[];
data?: Record<string, any>; data?: Record<string, any>;
timestamps: { timestamps: {
@ -31,16 +32,16 @@ export type EVENT = {
}; };
}; };
type ROOM_EVENT_CACHE_ENTRY = { type ZONE_EVENT_CACHE_ENTRY = {
collection: FSDB_COLLECTION<EVENT>; collection: FSDB_COLLECTION<EVENT>;
eviction_timeout: number; eviction_timeout: number;
}; };
const ROOM_EVENTS: Record<string, ROOM_EVENT_CACHE_ENTRY> = {}; const ZONE_EVENTS: Record<string, ZONE_EVENT_CACHE_ENTRY> = {};
export function get_events_collection_for_room(room_id: string): FSDB_COLLECTION<EVENT> { export function get_events_collection_for_zone(zone_id: string): FSDB_COLLECTION<EVENT> {
ROOM_EVENTS[room_id] = ROOM_EVENTS[room_id] ?? { ZONE_EVENTS[zone_id] = ZONE_EVENTS[zone_id] ?? {
collection: new FSDB_COLLECTION<EVENT>({ collection: new FSDB_COLLECTION<EVENT>({
name: `rooms/${room_id.slice(0, 14)}/${room_id.slice(0, 34)}/${room_id}/events`, name: `zones/${zone_id.slice(0, 14)}/${zone_id.slice(0, 34)}/${zone_id}/events`,
id_field: 'id', id_field: 'id',
organize: by_lurid, organize: by_lurid,
indexers: { indexers: {
@ -64,22 +65,22 @@ export function get_events_collection_for_room(room_id: string): FSDB_COLLECTION
eviction_timeout: 0 eviction_timeout: 0
}; };
if (ROOM_EVENTS[room_id].eviction_timeout) { if (ZONE_EVENTS[zone_id].eviction_timeout) {
clearTimeout(ROOM_EVENTS[room_id].eviction_timeout); clearTimeout(ZONE_EVENTS[zone_id].eviction_timeout);
} }
ROOM_EVENTS[room_id].eviction_timeout = setTimeout(() => { ZONE_EVENTS[zone_id].eviction_timeout = setTimeout(() => {
delete ROOM_EVENTS[room_id]; delete ZONE_EVENTS[zone_id];
}, 60_000 * 5); }, 60_000 * 5);
return ROOM_EVENTS[room_id].collection; return ZONE_EVENTS[zone_id].collection;
} }
export function clear_room_events_cache() { export function clear_zone_events_cache() {
for (const [room_id, cached] of Object.entries(ROOM_EVENTS)) { for (const [zone_id, cached] of Object.entries(ZONE_EVENTS)) {
if (cached.eviction_timeout) { if (cached.eviction_timeout) {
clearTimeout(cached.eviction_timeout); clearTimeout(cached.eviction_timeout);
} }
delete ROOM_EVENTS[room_id]; delete ZONE_EVENTS[zone_id];
} }
} }

View file

@ -3,29 +3,29 @@ import { FSDB_COLLECTION } from '@andyburke/fsdb';
import { FSDB_INDEXER_SYMLINKS } from '@andyburke/fsdb/indexers'; import { FSDB_INDEXER_SYMLINKS } from '@andyburke/fsdb/indexers';
/** /**
* @typedef {object} ROOM_PERMISSIONS * @typedef {object} ZONE_PERMISSIONS
* @property {string[]} read a list of user_ids with read permission for the room * @property {string[]} read a list of user_ids with read permission for the zone
* @property {string[]} write a list of user_ids with write permission for the room * @property {string[]} write a list of user_ids with write permission for the zone
* @property {string[]} read_events a list of user_ids with read_events permission for this room * @property {string[]} read_events a list of user_ids with read_events permission for this zone
* @property {string[]} write_events a list of user_ids with write_events permission for this room * @property {string[]} write_events a list of user_ids with write_events permission for this zone
*/ */
/** /**
* ROOM * ZONE
* *
* @property {string} id - lurid (stable) * @property {string} id - lurid (stable)
* @property {string} name - channel name (max 64 characters, unique, unstable) * @property {string} name - channel name (max 64 characters, unique, unstable)
* @property {string} creator_id - user id of the room creator * @property {string} creator_id - user id of the zone creator
* @property {ROOM_PERMISSIONS} permissions - permissions setup for the room * @property {ZONE_PERMISSIONS} permissions - permissions setup for the zone
* @property {string} [icon_url] - optional url for room icon * @property {string} [icon_url] - optional url for zone icon
* @property {string} [topic] - optional topic for the room * @property {string} [topic] - optional topic for the zone
* @property {string} [rules] - optional room rules (Markdown/text) * @property {string} [rules] - optional zone rules (Markdown/text)
* @property {string[]} [tags] - optional tags for the room * @property {string[]} [tags] - optional tags for the zone
* @property {Record<string,any>} [meta] - optional metadata about the room * @property {Record<string,any>} [meta] - optional metadata about the zone
* @property {Record<string,string>} [emojis] - optional emojis table, eg: { 'rofl': 🤣, 'blap': 'https://somewhere.someplace/image.jpg' } * @property {Record<string,string>} [emojis] - optional emojis table, eg: { 'rofl': 🤣, 'blap': 'https://somewhere.someplace/image.jpg' }
*/ */
export type ROOM = { export type ZONE = {
id: string; id: string;
name: string; name: string;
creator_id: string; creator_id: string;
@ -48,37 +48,37 @@ export type ROOM = {
}; };
}; };
export const ROOMS = new FSDB_COLLECTION<ROOM>({ export const ZONES = new FSDB_COLLECTION<ZONE>({
name: 'rooms', name: 'zones',
id_field: 'id', id_field: 'id',
organize: by_lurid, organize: by_lurid,
indexers: { indexers: {
creator_id: new FSDB_INDEXER_SYMLINKS<ROOM>({ creator_id: new FSDB_INDEXER_SYMLINKS<ZONE>({
name: 'creator_id', name: 'creator_id',
field: 'creator_id', field: 'creator_id',
to_many: true, to_many: true,
organize: by_lurid organize: by_lurid
}), }),
name: new FSDB_INDEXER_SYMLINKS<ROOM>({ name: new FSDB_INDEXER_SYMLINKS<ZONE>({
name: 'name', name: 'name',
get_values_to_index: (room) => [room.name.toLowerCase()], get_values_to_index: (zone) => [zone.name.toLowerCase()],
organize: by_character organize: by_character
}), }),
tags: new FSDB_INDEXER_SYMLINKS<ROOM>({ tags: new FSDB_INDEXER_SYMLINKS<ZONE>({
name: 'tags', name: 'tags',
get_values_to_index: (room): string[] => { get_values_to_index: (zone): string[] => {
return (room.tags ?? []).map((tag) => tag.toLowerCase()); return (zone.tags ?? []).map((tag) => tag.toLowerCase());
}, },
to_many: true, to_many: true,
organize: by_character organize: by_character
}), }),
topic: new FSDB_INDEXER_SYMLINKS<ROOM>({ topic: new FSDB_INDEXER_SYMLINKS<ZONE>({
name: 'topic', name: 'topic',
get_values_to_index: (room): string[] => { get_values_to_index: (zone): string[] => {
return (room.topic ?? '').split(/\W/); return (zone.topic ?? '').split(/\W/);
}, },
to_many: true, to_many: true,
organize: by_character organize: by_character

View file

@ -1,23 +0,0 @@
# /api/rooms/:room_id
Interact with a specific room.
## GET /api/rooms/:room_id
Get the room specified by `:room_id`.
## PUT /api/rooms/:room_id
Update the rooms specified by `:room_id`.
Eg:
```
{
name?: string;
}
```
## DELETE /api/rooms/:room_id
Delete the room specified by `:room_id`.

View file

@ -1,15 +0,0 @@
# /api/rooms/:room_id/events
Interact with a events for a room.
## GET /api/rooms/:room_id/events
Get events for the given room.
## PUT /api/rooms/:room_id/events/:event_id
Update an event.
## DELETE /api/rooms/:room_id/events/:event_id
Delete an event.

View file

@ -1,28 +0,0 @@
# /api/rooms
Interact with rooms.
## POST /api/rooms
Create a new room.
```
export type ROOM = {
id: string; // unique id for this room
name: string; // the name of the room (max 128 characters)
icon_url?: string; // optional url for a room icon
topic?: string; // optional room topic
tags: string[]; // a list of tags for the room
meta: Record<string, any>;
limits: {
users: number;
user_messages_per_minute: number;
};
creator_id: string; // user_id of the room creator
emojis: Record<string, string>; // either: string: emoji eg: { 'rofl: 🤣, ... } or { 'rofl': 🤣, 'blap': 'https://somewhere.someplace/image.jpg' }
};
```
## GET /api/rooms
Get rooms.

View file

@ -13,7 +13,7 @@ const DEFAULT_USER_PERMISSIONS: string[] = [
'files.write.own', 'files.write.own',
'self.read', 'self.read',
'self.write', 'self.write',
'rooms.read', 'zones.read',
'users.read' 'users.read'
]; ];

View file

@ -0,0 +1,23 @@
# /api/zones/:zone_id
Interact with a specific zone.
## GET /api/zones/:zone_id
Get the zone specified by `:zone_id`.
## PUT /api/zones/:zone_id
Update the zones specified by `:zone_id`.
Eg:
```
{
name?: string;
}
```
## DELETE /api/zones/:zone_id
Delete the zone specified by `:zone_id`.

View file

@ -1,36 +1,36 @@
import { FSDB_COLLECTION } from '@andyburke/fsdb'; import { FSDB_COLLECTION } from '@andyburke/fsdb';
import { EVENT, get_events_collection_for_room } from '../../../../../../models/event.ts'; import { EVENT, get_events_collection_for_zone } from '../../../../../../models/event.ts';
import { ROOM, ROOMS } from '../../../../../../models/room.ts'; import { ZONE, ZONES } from '../../../../../../models/zone.ts';
import parse_body from '../../../../../../utils/bodyparser.ts'; import parse_body from '../../../../../../utils/bodyparser.ts';
import * as CANNED_RESPONSES from '../../../../../../utils/canned_responses.ts'; import * as CANNED_RESPONSES from '../../../../../../utils/canned_responses.ts';
import { get_session, get_user, PRECHECK_TABLE, require_user } from '../../../../../../utils/prechecks.ts'; import { get_session, get_user, PRECHECK_TABLE, require_user } from '../../../../../../utils/prechecks.ts';
export const PRECHECKS: PRECHECK_TABLE = {}; export const PRECHECKS: PRECHECK_TABLE = {};
// GET /api/rooms/:room_id/events/:id - Get an event // GET /api/zones/:zone_id/events/:id - Get an event
PRECHECKS.GET = [get_session, get_user, require_user, async (_req: Request, meta: Record<string, any>): Promise<Response | undefined> => { PRECHECKS.GET = [get_session, get_user, require_user, async (_req: Request, meta: Record<string, any>): Promise<Response | undefined> => {
const room_id: string = meta.params?.room_id?.toLowerCase().trim() ?? ''; const zone_id: string = meta.params?.zone_id?.toLowerCase().trim() ?? '';
// lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same" // lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same"
const room: ROOM | null = room_id.length === 49 ? await ROOMS.get(room_id) : null; const zone: ZONE | null = zone_id.length === 49 ? await ZONES.get(zone_id) : null;
if (!room) { if (!zone) {
return CANNED_RESPONSES.not_found(); return CANNED_RESPONSES.not_found();
} }
meta.room = room; meta.zone = zone;
const room_is_public = room.permissions.read.length === 0; const zone_is_public = zone.permissions.read.length === 0;
const user_has_read_for_room = room_is_public || room.permissions.read.includes(meta.user.id); const user_has_read_for_zone = zone_is_public || zone.permissions.read.includes(meta.user.id);
const room_has_public_events = user_has_read_for_room && (room.permissions.read_events.length === 0); const zone_has_public_events = user_has_read_for_zone && (zone.permissions.read_events.length === 0);
const user_has_read_events_for_room = user_has_read_for_room && const user_has_read_events_for_zone = user_has_read_for_zone &&
(room_has_public_events || room.permissions.read_events.includes(meta.user.id)); (zone_has_public_events || zone.permissions.read_events.includes(meta.user.id));
if (!user_has_read_events_for_room) { if (!user_has_read_events_for_zone) {
return CANNED_RESPONSES.permission_denied(); return CANNED_RESPONSES.permission_denied();
} }
}]; }];
export async function GET(_req: Request, meta: Record<string, any>): Promise<Response> { export async function GET(_req: Request, meta: Record<string, any>): Promise<Response> {
const events: FSDB_COLLECTION<EVENT> = get_events_collection_for_room(meta.room.id); const events: FSDB_COLLECTION<EVENT> = get_events_collection_for_zone(meta.zone.id);
const event: EVENT | null = await events.get(meta.params.event_id); const event: EVENT | null = await events.get(meta.params.event_id);
if (!event) { if (!event) {
@ -42,7 +42,7 @@ export async function GET(_req: Request, meta: Record<string, any>): Promise<Res
}); });
} }
// PUT /api/rooms/:room_id/events/:event_id - Update event // PUT /api/zones/:zone_id/events/:event_id - Update event
PRECHECKS.PUT = [ PRECHECKS.PUT = [
get_session, get_session,
get_user, get_user,
@ -53,23 +53,23 @@ PRECHECKS.PUT = [
} }
}, },
async (_req: Request, meta: Record<string, any>): Promise<Response | undefined> => { async (_req: Request, meta: Record<string, any>): Promise<Response | undefined> => {
const room_id: string = meta.params?.room_id?.toLowerCase().trim() ?? ''; const zone_id: string = meta.params?.zone_id?.toLowerCase().trim() ?? '';
// lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same" // lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same"
const room: ROOM | null = room_id.length === 49 ? await ROOMS.get(room_id) : null; const zone: ZONE | null = zone_id.length === 49 ? await ZONES.get(zone_id) : null;
if (!room) { if (!zone) {
return CANNED_RESPONSES.not_found(); return CANNED_RESPONSES.not_found();
} }
meta.room = room; meta.zone = zone;
const room_is_public: boolean = meta.room.permissions.read.length === 0; const zone_is_public: boolean = meta.zone.permissions.read.length === 0;
const user_has_read_for_room = room_is_public || meta.room.permissions.read.includes(meta.user.id); const user_has_read_for_zone = zone_is_public || meta.zone.permissions.read.includes(meta.user.id);
const room_events_are_publicly_writable = meta.room.permissions.write_events.length === 0; const zone_events_are_publicly_writable = meta.zone.permissions.write_events.length === 0;
const user_has_write_events_for_room = user_has_read_for_room && const user_has_write_events_for_zone = user_has_read_for_zone &&
(room_events_are_publicly_writable || meta.room.permissions.write_events.includes(meta.user.id)); (zone_events_are_publicly_writable || meta.zone.permissions.write_events.includes(meta.user.id));
if (!user_has_write_events_for_room) { if (!user_has_write_events_for_zone) {
return CANNED_RESPONSES.permission_denied(); return CANNED_RESPONSES.permission_denied();
} }
} }
@ -78,7 +78,7 @@ export async function PUT(req: Request, meta: Record<string, any>): Promise<Resp
const now = new Date().toISOString(); const now = new Date().toISOString();
try { try {
const events: FSDB_COLLECTION<EVENT> = get_events_collection_for_room(meta.room.id); const events: FSDB_COLLECTION<EVENT> = get_events_collection_for_zone(meta.zone.id);
const event: EVENT | null = await events.get(meta.params.event_id); const event: EVENT | null = await events.get(meta.params.event_id);
if (!event) { if (!event) {
@ -117,7 +117,7 @@ export async function PUT(req: Request, meta: Record<string, any>): Promise<Resp
} }
} }
// DELETE /api/rooms/:room_id/events/:event_id - Delete event // DELETE /api/zones/:zone_id/events/:event_id - Delete event
PRECHECKS.DELETE = [ PRECHECKS.DELETE = [
get_session, get_session,
get_user, get_user,
@ -128,29 +128,29 @@ PRECHECKS.DELETE = [
} }
}, },
async (_req: Request, meta: Record<string, any>): Promise<Response | undefined> => { async (_req: Request, meta: Record<string, any>): Promise<Response | undefined> => {
const room_id: string = meta.params?.room_id?.toLowerCase().trim() ?? ''; const zone_id: string = meta.params?.zone_id?.toLowerCase().trim() ?? '';
// lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same" // lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same"
const room: ROOM | null = room_id.length === 49 ? await ROOMS.get(room_id) : null; const zone: ZONE | null = zone_id.length === 49 ? await ZONES.get(zone_id) : null;
if (!room) { if (!zone) {
return CANNED_RESPONSES.not_found(); return CANNED_RESPONSES.not_found();
} }
meta.room = room; meta.zone = zone;
const room_is_public: boolean = meta.room.permissions.read.length === 0; const zone_is_public: boolean = meta.zone.permissions.read.length === 0;
const user_has_read_for_room = room_is_public || meta.room.permissions.read.includes(meta.user.id); const user_has_read_for_zone = zone_is_public || meta.zone.permissions.read.includes(meta.user.id);
const room_events_are_publicly_writable = meta.room.permissions.write_events.length === 0; const zone_events_are_publicly_writable = meta.zone.permissions.write_events.length === 0;
const user_has_write_events_for_room = user_has_read_for_room && const user_has_write_events_for_zone = user_has_read_for_zone &&
(room_events_are_publicly_writable || meta.room.permissions.write_events.includes(meta.user.id)); (zone_events_are_publicly_writable || meta.zone.permissions.write_events.includes(meta.user.id));
if (!user_has_write_events_for_room) { if (!user_has_write_events_for_zone) {
return CANNED_RESPONSES.permission_denied(); return CANNED_RESPONSES.permission_denied();
} }
} }
]; ];
export async function DELETE(_req: Request, meta: Record<string, any>): Promise<Response> { export async function DELETE(_req: Request, meta: Record<string, any>): Promise<Response> {
const events: FSDB_COLLECTION<EVENT> = get_events_collection_for_room(meta.room.id); const events: FSDB_COLLECTION<EVENT> = get_events_collection_for_zone(meta.zone.id);
const event: EVENT | null = await events.get(meta.params.event_id); const event: EVENT | null = await events.get(meta.params.event_id);
if (!event) { if (!event) {
return CANNED_RESPONSES.not_found(); return CANNED_RESPONSES.not_found();

View file

@ -0,0 +1,15 @@
# /api/zones/:zone_id/events
Interact with a events for a zone.
## GET /api/zones/:zone_id/events
Get events for the given zone.
## PUT /api/zones/:zone_id/events/:event_id
Update an event.
## DELETE /api/zones/:zone_id/events/:event_id
Delete an event.

View file

@ -1,40 +1,40 @@
import lurid from '@andyburke/lurid'; import lurid from '@andyburke/lurid';
import { get_session, get_user, PRECHECK_TABLE, require_user } from '../../../../../utils/prechecks.ts'; import { get_session, get_user, PRECHECK_TABLE, require_user } from '../../../../../utils/prechecks.ts';
import { ROOM, ROOMS } from '../../../../../models/room.ts'; import { ZONE, ZONES } from '../../../../../models/zone.ts';
import * as CANNED_RESPONSES from '../../../../../utils/canned_responses.ts'; import * as CANNED_RESPONSES from '../../../../../utils/canned_responses.ts';
import { EVENT, get_events_collection_for_room } from '../../../../../models/event.ts'; import { EVENT, get_events_collection_for_zone } from '../../../../../models/event.ts';
import parse_body from '../../../../../utils/bodyparser.ts'; import parse_body from '../../../../../utils/bodyparser.ts';
import { FSDB_COLLECTION, FSDB_SEARCH_OPTIONS, WALK_ENTRY } from '@andyburke/fsdb'; import { FSDB_COLLECTION, FSDB_SEARCH_OPTIONS, WALK_ENTRY } from '@andyburke/fsdb';
import * as path from '@std/path'; import * as path from '@std/path';
export const PRECHECKS: PRECHECK_TABLE = {}; export const PRECHECKS: PRECHECK_TABLE = {};
// GET /api/rooms/:room_id/events - get room events // GET /api/zones/:zone_id/events - get zone events
// query parameters: // query parameters:
// partial_id: the partial id subset you would like to match (remember, lurids are lexigraphically sorted) // 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<string, any>): Promise<Response | undefined> => { PRECHECKS.GET = [get_session, get_user, require_user, async (_req: Request, meta: Record<string, any>): Promise<Response | undefined> => {
const room_id: string = meta.params?.room_id?.toLowerCase().trim() ?? ''; const zone_id: string = meta.params?.zone_id?.toLowerCase().trim() ?? '';
// lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same" // lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same"
const room: ROOM | null = room_id.length === 49 ? await ROOMS.get(room_id) : null; const zone: ZONE | null = zone_id.length === 49 ? await ZONES.get(zone_id) : null;
if (!room) { if (!zone) {
return CANNED_RESPONSES.not_found(); return CANNED_RESPONSES.not_found();
} }
meta.room = room; meta.zone = zone;
const room_is_public: boolean = meta.room.permissions.read.length === 0; const zone_is_public: boolean = meta.zone.permissions.read.length === 0;
const user_has_read_for_room = room_is_public || meta.room.permissions.read.includes(meta.user.id); const user_has_read_for_zone = zone_is_public || meta.zone.permissions.read.includes(meta.user.id);
const room_events_are_public = meta.room.permissions.read_events.length === 0; const zone_events_are_public = meta.zone.permissions.read_events.length === 0;
const user_has_read_events_for_room = user_has_read_for_room && const user_has_read_events_for_zone = user_has_read_for_zone &&
(room_events_are_public || meta.room.permissions.read_events.includes(meta.user.id)); (zone_events_are_public || meta.zone.permissions.read_events.includes(meta.user.id));
if (!user_has_read_events_for_room) { if (!user_has_read_events_for_zone) {
return CANNED_RESPONSES.permission_denied(); return CANNED_RESPONSES.permission_denied();
} }
}]; }];
export async function GET(request: Request, meta: Record<string, any>): Promise<Response> { export async function GET(request: Request, meta: Record<string, any>): Promise<Response> {
const events: FSDB_COLLECTION<EVENT> = get_events_collection_for_room(meta.room.id); const events: FSDB_COLLECTION<EVENT> = get_events_collection_for_zone(meta.zone.id);
const sorts = events.sorts; const sorts = events.sorts;
const sort_name: string = meta.query.sort ?? 'newest'; const sort_name: string = meta.query.sort ?? 'newest';
@ -126,31 +126,31 @@ export async function GET(request: Request, meta: Record<string, any>): Promise<
}); });
} }
// POST /api/rooms/:room_id/events - Create an event // POST /api/zones/:zone_id/events - Create an event
PRECHECKS.POST = [get_session, get_user, require_user, async (_req: Request, meta: Record<string, any>): Promise<Response | undefined> => { PRECHECKS.POST = [get_session, get_user, require_user, async (_req: Request, meta: Record<string, any>): Promise<Response | undefined> => {
const room_id: string = meta.params?.room_id?.toLowerCase().trim() ?? ''; const zone_id: string = meta.params?.zone_id?.toLowerCase().trim() ?? '';
// lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same" // lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same"
const room: ROOM | null = room_id.length === 49 ? await ROOMS.get(room_id) : null; const zone: ZONE | null = zone_id.length === 49 ? await ZONES.get(zone_id) : null;
if (!room) { if (!zone) {
return CANNED_RESPONSES.not_found(); return CANNED_RESPONSES.not_found();
} }
meta.room = room; meta.zone = zone;
const room_is_public: boolean = meta.room.permissions.read.length === 0; const zone_is_public: boolean = meta.zone.permissions.read.length === 0;
const user_has_read_for_room = room_is_public || meta.room.permissions.read.includes(meta.user.id); const user_has_read_for_zone = zone_is_public || meta.zone.permissions.read.includes(meta.user.id);
const room_events_are_publicly_writable = meta.room.permissions.write_events.length === 0; const zone_events_are_publicly_writable = meta.zone.permissions.write_events.length === 0;
const user_has_write_events_for_room = user_has_read_for_room && const user_has_write_events_for_zone = user_has_read_for_zone &&
(room_events_are_publicly_writable || meta.room.permissions.write_events.includes(meta.user.id)); (zone_events_are_publicly_writable || meta.zone.permissions.write_events.includes(meta.user.id));
if (!user_has_write_events_for_room) { if (!user_has_write_events_for_zone) {
return CANNED_RESPONSES.permission_denied(); return CANNED_RESPONSES.permission_denied();
} }
}]; }];
export async function POST(req: Request, meta: Record<string, any>): Promise<Response> { export async function POST(req: Request, meta: Record<string, any>): Promise<Response> {
try { try {
const events: FSDB_COLLECTION<EVENT> = get_events_collection_for_room(meta.room.id); const events: FSDB_COLLECTION<EVENT> = get_events_collection_for_zone(meta.zone.id);
const now = new Date().toISOString(); const now = new Date().toISOString();

View file

@ -1,50 +1,50 @@
import { get_session, get_user, PRECHECK_TABLE, require_user } from '../../../../utils/prechecks.ts'; import { get_session, get_user, PRECHECK_TABLE, require_user } from '../../../../utils/prechecks.ts';
import parse_body from '../../../../utils/bodyparser.ts'; import parse_body from '../../../../utils/bodyparser.ts';
import * as CANNED_RESPONSES from '../../../../utils/canned_responses.ts'; import * as CANNED_RESPONSES from '../../../../utils/canned_responses.ts';
import { ROOM, ROOMS } from '../../../../models/room.ts'; import { ZONE, ZONES } from '../../../../models/zone.ts';
export const PRECHECKS: PRECHECK_TABLE = {}; export const PRECHECKS: PRECHECK_TABLE = {};
// GET /api/rooms/:id - Get a room // GET /api/zones/:id - Get a zone
PRECHECKS.GET = [get_session, get_user, require_user, async (_req: Request, meta: Record<string, any>): Promise<Response | undefined> => { PRECHECKS.GET = [get_session, get_user, require_user, async (_req: Request, meta: Record<string, any>): Promise<Response | undefined> => {
const room_id: string = meta.params?.room_id?.toLowerCase().trim() ?? ''; const zone_id: string = meta.params?.zone_id?.toLowerCase().trim() ?? '';
// lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same" // lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same"
const room: ROOM | null = room_id.length === 49 ? await ROOMS.get(room_id) : null; const zone: ZONE | null = zone_id.length === 49 ? await ZONES.get(zone_id) : null;
if (!room) { if (!zone) {
return CANNED_RESPONSES.not_found(); return CANNED_RESPONSES.not_found();
} }
meta.room = room; meta.zone = zone;
const room_is_public = room.permissions.read.length === 0; const zone_is_public = zone.permissions.read.length === 0;
const user_has_read_for_room = room_is_public || room.permissions.read.includes(meta.user.id); const user_has_read_for_zone = zone_is_public || zone.permissions.read.includes(meta.user.id);
if (!user_has_read_for_room) { if (!user_has_read_for_zone) {
return CANNED_RESPONSES.permission_denied(); return CANNED_RESPONSES.permission_denied();
} }
}]; }];
export function GET(_req: Request, meta: Record<string, any>): Response { export function GET(_req: Request, meta: Record<string, any>): Response {
return Response.json(meta.room, { return Response.json(meta.zone, {
status: 200 status: 200
}); });
} }
// PUT /api/rooms/:id - Update room // PUT /api/zones/:id - Update zone
PRECHECKS.PUT = [get_session, get_user, require_user, async (_req: Request, meta: Record<string, any>): Promise<Response | undefined> => { PRECHECKS.PUT = [get_session, get_user, require_user, async (_req: Request, meta: Record<string, any>): Promise<Response | undefined> => {
const room_id: string = meta.params?.room_id?.toLowerCase().trim() ?? ''; const zone_id: string = meta.params?.zone_id?.toLowerCase().trim() ?? '';
// lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same" // lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same"
const room: ROOM | null = room_id.length === 49 ? await ROOMS.get(room_id) : null; const zone: ZONE | null = zone_id.length === 49 ? await ZONES.get(zone_id) : null;
if (!room) { if (!zone) {
return CANNED_RESPONSES.not_found(); return CANNED_RESPONSES.not_found();
} }
meta.room = room; meta.zone = zone;
const user_has_write_for_room = room.permissions.write.includes(meta.user.id); const user_has_write_for_zone = zone.permissions.write.includes(meta.user.id);
if (!user_has_write_for_room) { if (!user_has_write_for_zone) {
return CANNED_RESPONSES.permission_denied(); return CANNED_RESPONSES.permission_denied();
} }
}]; }];
@ -54,16 +54,16 @@ export async function PUT(req: Request, meta: Record<string, any>): Promise<Resp
try { try {
const body = await parse_body(req); const body = await parse_body(req);
const updated = { const updated = {
...meta.room, ...meta.zone,
...body, ...body,
id: meta.room.id, id: meta.zone.id,
timestamps: { timestamps: {
created: meta.room.timestamps.created, created: meta.zone.timestamps.created,
updated: now updated: now
} }
}; };
await ROOMS.update(updated); await ZONES.update(updated);
return Response.json(updated, { return Response.json(updated, {
status: 200 status: 200
}); });
@ -79,31 +79,31 @@ export async function PUT(req: Request, meta: Record<string, any>): Promise<Resp
} }
} }
// DELETE /api/rooms/:id - Delete room // DELETE /api/zones/:id - Delete zone
PRECHECKS.DELETE = [ PRECHECKS.DELETE = [
get_session, get_session,
get_user, get_user,
require_user, require_user,
async (_req: Request, meta: Record<string, any>): Promise<Response | undefined> => { async (_req: Request, meta: Record<string, any>): Promise<Response | undefined> => {
const room_id: string = meta.params?.room_id?.toLowerCase().trim() ?? ''; const zone_id: string = meta.params?.zone_id?.toLowerCase().trim() ?? '';
// lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same" // lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same"
const room: ROOM | null = room_id.length === 49 ? await ROOMS.get(room_id) : null; const zone: ZONE | null = zone_id.length === 49 ? await ZONES.get(zone_id) : null;
if (!room) { if (!zone) {
return CANNED_RESPONSES.not_found(); return CANNED_RESPONSES.not_found();
} }
meta.room = room; meta.zone = zone;
const user_has_write_for_room = room.permissions.write.includes(meta.user.id); const user_has_write_for_zone = zone.permissions.write.includes(meta.user.id);
if (!user_has_write_for_room) { if (!user_has_write_for_zone) {
return CANNED_RESPONSES.permission_denied(); return CANNED_RESPONSES.permission_denied();
} }
} }
]; ];
export async function DELETE(_req: Request, meta: Record<string, any>): Promise<Response> { export async function DELETE(_req: Request, meta: Record<string, any>): Promise<Response> {
await ROOMS.delete(meta.room); await ZONES.delete(meta.zone);
return Response.json({ return Response.json({
deleted: true deleted: true

View file

@ -0,0 +1,28 @@
# /api/zones
Interact with zones.
## POST /api/zones
Create a new zone.
```
export type ZONE = {
id: string; // unique id for this zone
name: string; // the name of the zone (max 128 characters)
icon_url?: string; // optional url for a zone icon
topic?: string; // optional zone topic
tags: string[]; // a list of tags for the zone
meta: Record<string, any>;
limits: {
users: number;
user_messages_per_minute: number;
};
creator_id: string; // user_id of the zone creator
emojis: Record<string, string>; // either: string: emoji eg: { 'rofl: 🤣, ... } or { 'rofl': 🤣, 'blap': 'https://somewhere.someplace/image.jpg' }
};
```
## GET /api/zones
Get zones.

View file

@ -3,40 +3,40 @@ import parse_body from '../../../utils/bodyparser.ts';
import { get_session, get_user, require_user } from '../../../utils/prechecks.ts'; import { get_session, get_user, require_user } from '../../../utils/prechecks.ts';
import * as CANNED_RESPONSES from '../../../utils/canned_responses.ts'; import * as CANNED_RESPONSES from '../../../utils/canned_responses.ts';
import { PRECHECK_TABLE } from '../../../utils/prechecks.ts'; import { PRECHECK_TABLE } from '../../../utils/prechecks.ts';
import { ROOM, ROOMS } from '../../../models/room.ts'; import { ZONE, ZONES } from '../../../models/zone.ts';
import { WALK_ENTRY } from '@andyburke/fsdb'; import { WALK_ENTRY } from '@andyburke/fsdb';
export const PRECHECKS: PRECHECK_TABLE = {}; export const PRECHECKS: PRECHECK_TABLE = {};
// GET /api/rooms - get rooms // GET /api/zones - get zones
PRECHECKS.GET = [get_session, get_user, require_user, (_req: Request, meta: Record<string, any>): Response | undefined => { PRECHECKS.GET = [get_session, get_user, require_user, (_req: Request, meta: Record<string, any>): Response | undefined => {
const can_read_rooms = meta.user.permissions.includes('rooms.read'); const can_read_zones = meta.user.permissions.includes('zones.read');
if (!can_read_rooms) { if (!can_read_zones) {
return CANNED_RESPONSES.permission_denied(); return CANNED_RESPONSES.permission_denied();
} }
}]; }];
export async function GET(_req: Request, meta: Record<string, any>): Promise<Response> { export async function GET(_req: Request, meta: Record<string, any>): Promise<Response> {
const limit = Math.min(parseInt(meta.query.limit ?? '100'), 100); const limit = Math.min(parseInt(meta.query.limit ?? '100'), 100);
const rooms = (await ROOMS.all({ const zones = (await ZONES.all({
limit, limit,
filter: (entry: WALK_ENTRY<ROOM>) => { filter: (entry: WALK_ENTRY<ZONE>) => {
// we push our event collections into the rooms, and fsdb // we push our event collections into the zones, and fsdb
// doesn't yet filter that out in its all() logic // doesn't yet filter that out in its all() logic
return entry.path.indexOf('/events/') === -1; return entry.path.indexOf('/events/') === -1;
} }
})).map((room_entry) => room_entry.load()); })).map((zone_entry) => zone_entry.load());
return Response.json(rooms, { return Response.json(zones, {
status: 200 status: 200
}); });
} }
// POST /api/rooms - Create a room // POST /api/zones - Create a zone
PRECHECKS.POST = [get_session, get_user, require_user, (_req: Request, meta: Record<string, any>): Response | undefined => { PRECHECKS.POST = [get_session, get_user, require_user, (_req: Request, meta: Record<string, any>): Response | undefined => {
const can_create_rooms = meta.user.permissions.includes('rooms.create'); const can_create_zones = meta.user.permissions.includes('zones.create');
if (!can_create_rooms) { if (!can_create_zones) {
return CANNED_RESPONSES.permission_denied(); return CANNED_RESPONSES.permission_denied();
} }
}]; }];
@ -49,8 +49,8 @@ export async function POST(req: Request, meta: Record<string, any>): Promise<Res
if (typeof body.name !== 'string' || body.name.length === 0) { if (typeof body.name !== 'string' || body.name.length === 0) {
return Response.json({ return Response.json({
error: { error: {
cause: 'missing_room_name', cause: 'missing_zone_name',
message: 'You must specify a unique name for a room.' message: 'You must specify a unique name for a zone.'
} }
}, { }, {
status: 400 status: 400
@ -60,8 +60,8 @@ export async function POST(req: Request, meta: Record<string, any>): Promise<Res
if (body.name.length > 64) { if (body.name.length > 64) {
return Response.json({ return Response.json({
error: { error: {
cause: 'invalid_room_name', cause: 'invalid_zone_name',
message: 'Room names must be 64 characters or fewer.' message: 'zone names must be 64 characters or fewer.'
} }
}, { }, {
status: 400 status: 400
@ -70,21 +70,21 @@ export async function POST(req: Request, meta: Record<string, any>): Promise<Res
const normalized_name = body.name.toLowerCase(); const normalized_name = body.name.toLowerCase();
const existing_room = (await ROOMS.find({ const existing_zone = (await ZONES.find({
name: normalized_name name: normalized_name
})).shift(); })).shift();
if (existing_room) { if (existing_zone) {
return Response.json({ return Response.json({
error: { error: {
cause: 'room_name_conflict', cause: 'zone_name_conflict',
message: 'There is already a room with this name.' message: 'There is already a zone with this name.'
} }
}, { }, {
status: 400 status: 400
}); });
} }
const room: ROOM = { const zone: ZONE = {
...body, ...body,
id: lurid(), id: lurid(),
creator_id: meta.user.id, creator_id: meta.user.id,
@ -101,9 +101,9 @@ export async function POST(req: Request, meta: Record<string, any>): Promise<Res
} }
}; };
await ROOMS.create(room); await ZONES.create(zone);
return Response.json(room, { return Response.json(zone, {
status: 201 status: 201
}); });
} catch (error) { } catch (error) {

View file

@ -6,7 +6,7 @@
--border-subtle: #555; --border-subtle: #555;
--border-normal: #888; --border-normal: #888;
--border-highlight: #bbb; --border-highlight: #bbb;
--icon-scale: 1.25; --icon-scale: 1;
--border-radius: 4px; --border-radius: 4px;
} }
@ -200,7 +200,7 @@ button.primary {
} }
body[data-perms*="users.write"] [data-requires-permission="users.write"], body[data-perms*="users.write"] [data-requires-permission="users.write"],
body[data-perms*="rooms.create"] [data-requires-permission="rooms.create"] { body[data-perms*="zones.create"] [data-requires-permission="zones.create"] {
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
height: unset; height: unset;
@ -548,12 +548,41 @@ body[data-perms*="rooms.create"] [data-requires-permission="rooms.create"] {
top: 4px; top: 4px;
} }
/* ICON - CONTROLLER */
.icon.controller {
box-sizing: border-box;
position: relative;
display: block;
transform: scale(var(--icon-scale, 1));
width: 8px;
height: 8px;
border: 2px solid;
border-radius: 100px;
}
.icon.controller::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
width: 14px;
height: 14px;
box-shadow:
-6px -6px 0 -4px,
6px 6px 0 -4px,
6px -6px 0 -4px,
-6px 6px 0 -4px;
left: -5px;
top: -5px;
transform: rotate(45deg);
}
/* ICON - DOWNLOAD */ /* ICON - DOWNLOAD */
.icon.download { .icon.download {
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
display: block; display: block;
transform: scale(var(--ggs, 1)); transform: scale(var(--icon-scale, 1));
width: 16px; width: 16px;
height: 6px; height: 6px;
border: 2px solid; border: 2px solid;
@ -735,6 +764,62 @@ body[data-perms*="rooms.create"] [data-requires-permission="rooms.create"] {
5px 0 0; 5px 0 0;
} }
/* ICON - MOREBORDERLESS */
.icon.more-borderless {
transform: scale(var(--icon-scale, 1));
}
.icon.more-borderless,
.icon.more-borderless::after,
.icon.more-borderless::before {
box-sizing: border-box;
position: relative;
display: block;
width: 4px;
height: 4px;
background: currentColor;
border-radius: 100%;
}
.icon.more-borderless::after,
.icon.more-borderless::before {
content: "";
position: absolute;
top: 0;
}
.icon.more-borderless::after {
left: -6px;
}
.icon.more-borderless::before {
right: -6px;
}
/* ICON - MORECIRCLE */
.icon.more-circle {
box-sizing: border-box;
position: relative;
display: block;
transform: scale(var(--icon-scale, 1));
width: 24px;
height: 24px;
border: 2px solid;
border-radius: 24px;
}
.icon.more-circle::before {
content: "";
position: absolute;
box-sizing: border-box;
display: block;
width: 4px;
height: 4px;
background-color: currentColor;
border-radius: 20px;
top: 8px;
left: 8px;
box-shadow:
-5px 0 0,
5px 0 0;
}
/* ICON - PLUS */ /* ICON - PLUS */
.icon.plus, .icon.plus,
.icon.plus::after, .icon.plus::after,
@ -1062,7 +1147,7 @@ body[data-perms*="rooms.create"] [data-requires-permission="rooms.create"] {
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
display: block; display: block;
transform: scale(var(--ggs, 1)); transform: scale(var(--icon-scale, 1));
width: 22px; width: 22px;
height: 22px; height: 22px;
border: 2px solid; border: 2px solid;
@ -1095,7 +1180,7 @@ body[data-perms*="rooms.create"] [data-requires-permission="rooms.create"] {
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
display: block; display: block;
transform: scale(var(--ggs, 1)); transform: scale(var(--icon-scale, 1));
border: 2px solid; border: 2px solid;
border-radius: 4px; border-radius: 4px;
width: 22px; width: 22px;
@ -1124,7 +1209,7 @@ body[data-perms*="rooms.create"] [data-requires-permission="rooms.create"] {
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
display: block; display: block;
transform: scale(var(--ggs, 1)); transform: scale(var(--icon-scale, 1));
width: 22px; width: 22px;
height: 22px; height: 22px;
border: 2px solid; border: 2px solid;
@ -1148,7 +1233,7 @@ body[data-perms*="rooms.create"] [data-requires-permission="rooms.create"] {
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
display: block; display: block;
transform: scale(var(--ggs, 1)); transform: scale(var(--icon-scale, 1));
width: 22px; width: 22px;
height: 22px; height: 22px;
border: 2px solid; border: 2px solid;
@ -1171,7 +1256,7 @@ body[data-perms*="rooms.create"] [data-requires-permission="rooms.create"] {
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
display: block; display: block;
transform: scale(var(--ggs, 1)); transform: scale(var(--icon-scale, 1));
border: 2px solid; border: 2px solid;
border-radius: 4px; border-radius: 4px;
width: 22px; width: 22px;
@ -1200,7 +1285,7 @@ body[data-perms*="rooms.create"] [data-requires-permission="rooms.create"] {
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
display: block; display: block;
transform: scale(var(--ggs, 1)); transform: scale(var(--icon-scale, 1));
width: 22px; width: 22px;
height: 22px; height: 22px;
border: 2px solid; border: 2px solid;

View file

@ -3,12 +3,12 @@
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
} }
.room-list { .zone-list {
margin: 1rem 0; margin: 1rem 0;
list-style-type: none; list-style-type: none;
} }
.room-list > li.room a:before { .zone-list > li.zone a:before {
position: absolute; position: absolute;
left: -1.75rem; left: -1.75rem;
top: 0; top: 0;
@ -18,7 +18,7 @@
color: var(--text); color: var(--text);
} }
.room-list > li.room a { .zone-list > li.zone a {
position: relative; position: relative;
display: block; display: block;
width: 100%; width: 100%;
@ -30,7 +30,7 @@
text-decoration: none; text-decoration: none;
} }
.room-list > li.room.active a { .zone-list > li.zone.active a {
color: var(--accent); color: var(--accent);
} }
@ -56,11 +56,11 @@
line-height: 2rem; line-height: 2rem;
} }
#chat #room-chat-container { #chat #zone-chat-container {
position: relative; position: relative;
} }
#chat #room-chat-content { #chat #zone-chat-content {
overflow-y: scroll; overflow-y: scroll;
position: absolute; position: absolute;
top: 0; top: 0;
@ -70,7 +70,7 @@
padding: 0.5rem; padding: 0.5rem;
} }
#chat #room-chat-entry-container { #chat #zone-chat-entry-container {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;
@ -79,7 +79,7 @@
padding: 1rem; padding: 1rem;
} }
#chat #room-chat-entry-container form { #chat #zone-chat-entry-container form {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@ -90,13 +90,13 @@
padding: 0.75rem; padding: 0.75rem;
} }
#chat #room-chat-entry-container form input[type="file"] { #chat #zone-chat-entry-container form input[type="file"] {
opacity: 0; opacity: 0;
display: none; display: none;
} }
#chat #room-chat-entry-container form button, #chat #zone-chat-entry-container form button,
#chat #room-chat-entry-container form label { #chat #zone-chat-entry-container form label {
position: relative; position: relative;
top: inherit; top: inherit;
font-size: inherit; font-size: inherit;
@ -110,7 +110,7 @@
border: 1px solid var(--text); border: 1px solid var(--text);
} }
#chat #room-chat-entry-container form textarea { #chat #zone-chat-entry-container form textarea {
width: 100%; width: 100%;
flex-grow: 1; flex-grow: 1;
background: inherit; background: inherit;
@ -163,27 +163,43 @@
#chat .message-container .message-actions-container { #chat .message-container .message-actions-container {
position: absolute; position: absolute;
top: 0.5rem; top: 0.1rem;
right: 0.5rem; right: 0.1rem;
display: none; /* flex; */ display: flex;
flex-direction: row; flex-direction: row-reverse;
border: 1px solid;
border-color: rgba(0, 0, 0, 0);
border-radius: var(--border-radius);
color: rgb(from var(--text) r g b / 0.7);
z-index: 10;
}
#chat .message-container .message-actions-container:has(input[type="checkbox"]:checked) {
background: rgb(from var(--bg) r g b / 0.9);
border-color: var(--border-subtle);
z-index: 11;
} }
#chat .message-container .message-actions-container .message-action { #chat .message-container .message-actions-container .message-action {
border: none;
opacity: 0; opacity: 0;
transition: 0.2s ease-in-out; transition: 0.2s ease-in-out;
width: 0; width: 0;
height: 4rem; align-content: center;
height: 3.5rem;
overflow: hidden; overflow: hidden;
order: -1;
cursor: pointer; cursor: pointer;
margin-right: 1.25rem; margin-right: 0;
padding-top: 0.25rem; padding: 0.25rem 0 0 0;
text-align: center; text-align: center;
text-wrap: nowrap;
} }
#chat .message-container .message-actions-container label { #chat .message-container .message-actions-container label {
cursor: pointer; cursor: pointer;
width: 2rem;
text-align: right;
padding-top: 0.5rem;
} }
#chat .message-container .message-actions-container input[type="checkbox"] { #chat .message-container .message-actions-container input[type="checkbox"] {
@ -197,7 +213,8 @@
input[type="checkbox"]:checked input[type="checkbox"]:checked
~ .message-action { ~ .message-action {
opacity: 1; opacity: 1;
width: 4rem; width: 3.25rem;
margin-right: 1.25rem;
} }
#chat .message-container .message-actions-container .message-action .action-name { #chat .message-container .message-actions-container .message-action .action-name {
@ -356,7 +373,7 @@
rotate: 180deg; rotate: 180deg;
} }
#chat #room-chat-container { #chat #zone-chat-container {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@ -364,13 +381,13 @@
bottom: 0; bottom: 0;
} }
#chat #room-chat-container #room-chat-entry-container, #chat #zone-chat-container #zone-chat-entry-container,
#chat #room-chat-container #room-chat-entry-container form { #chat #zone-chat-container #zone-chat-entry-container form {
padding: 0.25rem; padding: 0.25rem;
} }
#chat #room-chat-container #room-chat-entry-container form button, #chat #zone-chat-container #zone-chat-entry-container form button,
#chat #room-chat-container #room-chat-entry-container form label { #chat #zone-chat-container #zone-chat-entry-container form label {
margin: 0 0.5rem; margin: 0 0.5rem;
} }

View file

@ -22,21 +22,21 @@
<label id="sidebar-toggle-icon" for="sidebar-toggle"> <label id="sidebar-toggle-icon" for="sidebar-toggle">
<div class="icon right"></div> <div class="icon right"></div>
</label> </label>
<div id="room-creation-container" data-requires-permission="rooms.create"> <div id="zone-creation-container" data-requires-permission="zones.create">
<button <button
id="toggle-room-creation-form-button" id="toggle-zone-creation-form-button"
onclick="((event) => { onclick="((event) => {
event.preventDefault(); event.preventDefault();
const room_create_form = document.getElementById( 'room-create' ); const zone_create_form = document.getElementById( 'zone-create' );
room_create_form.style[ 'height' ] = room_create_form.style[ 'height' ] === '5rem' ? '0' : '5rem'; zone_create_form.style[ 'height' ] = zone_create_form.style[ 'height' ] === '5rem' ? '0' : '5rem';
})(event)" })(event)"
> >
<div class="icon plus"></div> <div class="icon plus"></div>
</button> </button>
<form <form
id="room-create" id="zone-create"
data-smart="true" data-smart="true"
action="/api/rooms" action="/api/zones"
method="POST" method="POST"
style=" style="
margin-top: 1rem; margin-top: 1rem;
@ -48,11 +48,11 @@
" "
> >
<input <input
id="new-room-name-input" id="new-zone-name-input"
type="text" type="text"
name="name" name="name"
value="" value=""
placeholder="new room" placeholder="new zone"
/> />
<!-- <button class="primary" type="submit"> <!-- <button class="primary" type="submit">
<div class="icon plus"></div> <div class="icon plus"></div>
@ -61,34 +61,35 @@
<script> <script>
{ {
const form = document.currentScript.closest("form"); const form = document.currentScript.closest("form");
const room_create_form = document.getElementById("room-create"); const zone_create_form = document.getElementById("zone-create");
const new_room_name_input = const new_zone_name_input =
document.getElementById("new-room-name-input"); document.getElementById("new-zone-name-input");
form.on_reply = (new_room) => { form.on_reply = (new_zone) => {
const room_list = document.getElementById("room-list"); const zone_list = document.getElementById("zone-list");
room_list.insertAdjacentHTML( zone_list.insertAdjacentHTML(
"beforeend", "beforeend",
`<li id="room-selector-${new_room.id}" class="room"><a href="#/room/${new_room.id}">${new_room.name}</a></li>`, `<li id="zone-selector-${new_zone.id}" class="zone"><a href="#/zone/${new_zone.id}">${new_zone.name}</a></li>`,
); );
new_room_name_input.value = ""; new_zone_name_input.value = "";
window.location.hash = `/chat/room/${new_room.id}`; window.location.hash = `/chat/zone/${new_zone.id}`;
room_create_form.style["height"] = "0"; zone_create_form.style["height"] = "0";
}; };
} }
</script> </script>
</form> </form>
</div> </div>
<div> <div>
<span class="title">chat rooms</span> <span class="title">chat zones</span>
</div> </div>
<ul id="room-list" class="room-list"></ul> <ul id="zone-list" class="zone-list"></ul>
</div> </div>
<div id="room-chat-container"> <div id="zone-chat-container">
<div id="room-chat-content"></div> <div id="zone-chat-content"></div>
<div id="room-chat-entry-container"> <div id="zone-chat-entry-container">
<form id="room-chat-entry" action="" data-smart="true" data-method="POST"> <form id="zone-chat-entry" action="" data-smart="true" data-method="POST">
<input id="parent-id" type="hidden" name="parent_id" value="" />
<input <input
id="file-upload-and-share-input" id="file-upload-and-share-input"
aria-label="Upload and share file" aria-label="Upload and share file"
@ -100,12 +101,12 @@
<div class="icon attachment"></div> <div class="icon attachment"></div>
</label> </label>
<textarea <textarea
id="room-chat-input" id="zone-chat-input"
class="room-chat-input" class="zone-chat-input"
rows="1" rows="1"
name="data.message" name="data.message"
></textarea> ></textarea>
<button id="room-chat-send" class="primary" aria-label="Send a message"> <button id="zone-chat-send" class="primary" aria-label="Send a message">
<i class="icon send"></i> <i class="icon send"></i>
</button> </button>
<script> <script>
@ -114,10 +115,11 @@
const file_input = document.querySelector( const file_input = document.querySelector(
'input[name="file-upload-and-share"]', 'input[name="file-upload-and-share"]',
); );
const chat_input = document.getElementById("room-chat-input"); const chat_input = document.getElementById("zone-chat-input");
const room_chat_container = const parent_id_input = document.getElementById("parent-id");
document.getElementById("room-chat-container"); const zone_chat_container =
const room_chat_content = document.getElementById("room-chat-content"); document.getElementById("zone-chat-container");
const zone_chat_content = document.getElementById("zone-chat-content");
let messages_in_flight = {}; let messages_in_flight = {};
@ -129,9 +131,9 @@
form.on_submit = async (event) => { form.on_submit = async (event) => {
const user = JSON.parse(document.body.dataset.user); const user = JSON.parse(document.body.dataset.user);
const room_id = room_chat_container.dataset.room_id; const zone_id = zone_chat_container.dataset.zone_id;
if (!room_id) { if (!zone_id) {
alert("Failed to get room_id!"); alert("Failed to get zone_id!");
return false; return false;
} }
@ -169,7 +171,7 @@
return false; return false;
} }
form.action = `/api/rooms/${room_id}/events`; form.action = `/api/zones/${zone_id}/events`;
}; };
form.on_parsed = (json) => { form.on_parsed = (json) => {
@ -196,7 +198,7 @@
} }
const user = JSON.parse(document.body.dataset.user); const user = JSON.parse(document.body.dataset.user);
render_text_event(room_chat_content, json, user); render_text_event(zone_chat_content, json, user);
document document
.getElementById(`chat-${temp_id}`) .getElementById(`chat-${temp_id}`)
?.classList.add("sending"); ?.classList.add("sending");
@ -213,7 +215,8 @@
.getElementById(`chat-${sent_message.meta?.temp_id ?? ""}`) .getElementById(`chat-${sent_message.meta?.temp_id ?? ""}`)
?.classList.remove("sending"); ?.classList.remove("sending");
append_room_events([sent_message]); append_zone_events([sent_message]);
parent_id_input.value = "";
chat_input.value = ""; chat_input.value = "";
chat_input.focus(); chat_input.focus();
}; };

View file

@ -257,7 +257,7 @@ let time_tick_tock_class = "time-tock";
let last_creator_id = null; let last_creator_id = null;
let user_tick_tock_class = "user-tock"; let user_tick_tock_class = "user-tock";
function render_text_event(room_chat_content, event, creator, existing_element) { function render_text_event(zone_chat_content, event, creator, existing_element) {
const event_datetime = datetime_to_local(event.timestamps.created); const event_datetime = datetime_to_local(event.timestamps.created);
if (event_datetime.value - last_event_datetime_value > time_tick_tock_timeout) { if (event_datetime.value - last_event_datetime_value > time_tick_tock_timeout) {
@ -278,12 +278,14 @@ function render_text_event(room_chat_content, event, creator, existing_element)
id="show_message_actions-${message_id}" id="show_message_actions-${message_id}"
class="message-actions-display-toggle" class="message-actions-display-toggle"
/> />
<label for="show_message_actions-${message_id}" class="message-actions-display-toggle-label" <label class="message-actions-display-toggle-label" for="show_message_actions-${message_id}">
><div class="icon more"></div> <div class="icon more-borderless"></div>
</label> </label>
<div class="message-action" data-action="reply"><i class="icon reply"></i><span class="action-name">Reply</span></div>
<div class="message-action" data-action="forward_copy"><i class="icon forward-copy"></i><span class="action-name">Forward (Copy Link)</span></div> <button class="message-action" data-action="react"><i class="icon more-circle"></i><span class="action-name">React</span></button>
<div class="message-action" data-action="delete"><i class="icon trash"></i><span class="action-name">Delete</span></div> <button class="message-action" data-action="reply" onclick="document.getElementById( 'parent-id' ).value = '${message_id}';"><i class="icon reply"></i><span class="action-name">Reply</span></button>
<button class="message-action" data-action="forward_copy"><i class="icon forward-copy"></i><span class="action-name">Copy Link</span></button>
<button class="message-action" data-action="delete"><i class="icon trash"></i><span class="action-name">Delete</span></button>
</div> </div>
<div class="info-container"> <div class="info-container">
<div class="avatar-container"> <div class="avatar-container">
@ -305,31 +307,31 @@ function render_text_event(room_chat_content, event, creator, existing_element)
template.innerHTML = html_content; template.innerHTML = html_content;
existing_element.replaceWith(template.content.firstChild); existing_element.replaceWith(template.content.firstChild);
} else { } else {
room_chat_content.insertAdjacentHTML("beforeend", html_content); zone_chat_content.insertAdjacentHTML("beforeend", html_content);
} }
} }
async function get_new_room_element() { async function get_new_zone_element() {
const existing_new_room_element = document.getElementById("new-room"); const existing_new_zone_element = document.getElementById("new-zone");
if (existing_new_room_element) { if (existing_new_zone_element) {
return existing_new_room_element; return existing_new_zone_element;
} }
const room_list = document.getElementById("room-list"); const zone_list = document.getElementById("zone-list");
room_list.insertAdjacentHTML( zone_list.insertAdjacentHTML(
"beforeend", "beforeend",
`<li id="new-room" class="room"><a href="" contenteditable="true">new room</a></li>`, `<li id="new-zone" class="zone"><a href="" contenteditable="true">new zone</a></li>`,
); );
await new Promise((resolve) => setTimeout(resolve, 1)); await new Promise((resolve) => setTimeout(resolve, 1));
const new_room_element = document.getElementById("new-room"); const new_zone_element = document.getElementById("new-zone");
return new_room_element; return new_zone_element;
} }
const users = {}; const users = {};
async function append_room_events(events) { async function append_zone_events(events) {
const room_chat_content = document.getElementById("room-chat-content"); const zone_chat_content = document.getElementById("zone-chat-content");
let last_message_id = room_chat_content.dataset.last_message_id ?? ""; let last_message_id = zone_chat_content.dataset.last_message_id ?? "";
for (const event of events) { for (const event of events) {
// if the last message is undefined, it becomes this event, otherwise, if this event's id is newer, // if the last message is undefined, it becomes this event, otherwise, if this event's id is newer,
// it becomes the latest message // it becomes the latest message
@ -339,8 +341,8 @@ async function append_room_events(events) {
: last_message_id; : last_message_id;
// if the last message has been updated, update the content's dataset to reflect that // if the last message has been updated, update the content's dataset to reflect that
if (last_message_id !== room_chat_content.dataset.last_message_id) { if (last_message_id !== zone_chat_content.dataset.last_message_id) {
room_chat_content.dataset.last_message_id = last_message_id; zone_chat_content.dataset.last_message_id = last_message_id;
} }
users[event.creator_id] = users[event.creator_id] =
@ -352,37 +354,37 @@ async function append_room_events(events) {
(event.meta?.temp_id (event.meta?.temp_id
? document.getElementById(`chat-${event.meta.temp_id}`) ? document.getElementById(`chat-${event.meta.temp_id}`)
: undefined); : undefined);
render_text_event(room_chat_content, event, users[event.creator_id], existing_element); render_text_event(zone_chat_content, event, users[event.creator_id], existing_element);
} }
room_chat_content.scrollTop = room_chat_content.scrollHeight; zone_chat_content.scrollTop = zone_chat_content.scrollHeight;
} }
// TODO: we need some abortcontroller handling here or something // TODO: we need some abortcontroller handling here or something
// similar for when we change rooms, this is the most basic // similar for when we change zones, this is the most basic
// first pass outline // first pass outline
let room_polling_request_abort_controller = null; let zone_polling_request_abort_controller = null;
async function poll_for_new_events() { async function poll_for_new_events() {
const room_chat_content = document.getElementById("room-chat-content"); const zone_chat_content = document.getElementById("zone-chat-content");
const room_id = room_chat_content.dataset.room_id; const zone_id = zone_chat_content.dataset.zone_id;
const last_message_id = room_chat_content.dataset.last_message_id; const last_message_id = zone_chat_content.dataset.last_message_id;
if (!room_id) { if (!zone_id) {
return; return;
} }
const message_polling_url = `/api/rooms/${room_id}/events?type=chat&limit=100&sort=newest&wait=true${last_message_id ? `&after_id=${last_message_id}` : ""}`; const message_polling_url = `/api/zones/${zone_id}/events?type=chat&limit=100&sort=newest&wait=true${last_message_id ? `&after_id=${last_message_id}` : ""}`;
room_polling_request_abort_controller = zone_polling_request_abort_controller =
room_polling_request_abort_controller || new AbortController(); zone_polling_request_abort_controller || new AbortController();
api.fetch(message_polling_url, { api.fetch(message_polling_url, {
signal: room_polling_request_abort_controller.signal, signal: zone_polling_request_abort_controller.signal,
}) })
.then(async (new_events_response) => { .then(async (new_events_response) => {
const new_events = ((await new_events_response.json()) ?? []).reverse(); const new_events = ((await new_events_response.json()) ?? []).reverse();
await append_room_events(new_events.toReversed()); await append_zone_events(new_events.toReversed());
poll_for_new_events(room_id); poll_for_new_events(zone_id);
}) })
.catch((error) => { .catch((error) => {
// TODO: poll again? back off? // TODO: poll again? back off?
@ -390,37 +392,37 @@ async function poll_for_new_events() {
}); });
} }
async function load_room(room_id) { async function load_zone(zone_id) {
const room_chat_content = document.getElementById("room-chat-content"); const zone_chat_content = document.getElementById("zone-chat-content");
if (room_polling_request_abort_controller) { if (zone_polling_request_abort_controller) {
room_polling_request_abort_controller.abort(); zone_polling_request_abort_controller.abort();
room_polling_request_abort_controller = null; zone_polling_request_abort_controller = null;
delete room_chat_content.dataset.last_message_id; delete zone_chat_content.dataset.last_message_id;
} }
const room_response = await api.fetch(`/api/rooms/${room_id}`); const zone_response = await api.fetch(`/api/zones/${zone_id}`);
if (!room_response.ok) { if (!zone_response.ok) {
const error = await room_response.json(); const error = await zone_response.json();
alert(error.message ?? JSON.stringify(error)); alert(error.message ?? JSON.stringify(error));
return; return;
} }
const room = await room_response.json(); const zone = await zone_response.json();
room_chat_content.dataset.room_id = room.id; zone_chat_content.dataset.zone_id = zone.id;
room_chat_content.innerHTML = ""; zone_chat_content.innerHTML = "";
const room_selectors = document.querySelectorAll("li.room"); const zone_selectors = document.querySelectorAll("li.zone");
for (const room_selector of room_selectors) { for (const zone_selector of zone_selectors) {
room_selector.classList.remove("active"); zone_selector.classList.remove("active");
if (room_selector.id === `room-selector-${room_id}`) { if (zone_selector.id === `zone-selector-${zone_id}`) {
room_selector.classList.add("active"); zone_selector.classList.add("active");
} }
} }
const events_response = await api.fetch( const events_response = await api.fetch(
`/api/rooms/${room_id}/events?type=chat&limit=100&sort=newest`, `/api/zones/${zone_id}/events?type=chat&limit=100&sort=newest`,
); );
if (!events_response.ok) { if (!events_response.ok) {
const error = await events_response.json(); const error = await events_response.json();
@ -430,36 +432,36 @@ async function load_room(room_id) {
const events = (await events_response.json()).reverse(); const events = (await events_response.json()).reverse();
await append_room_events(events); await append_zone_events(events);
poll_for_new_events(room_id); poll_for_new_events(zone_id);
} }
let last_room_update = undefined; let last_zone_update = undefined;
async function update_chat_rooms() { async function update_chat_zones() {
const now = new Date(); const now = new Date();
const time_since_last_update = now - (last_room_update ?? 0); const time_since_last_update = now - (last_zone_update ?? 0);
if (time_since_last_update < 5_000) { if (time_since_last_update < 5_000) {
return; return;
} }
const rooms_response = await api.fetch("/api/rooms"); const zones_response = await api.fetch("/api/zones");
if (rooms_response.ok) { if (zones_response.ok) {
const room_list = document.getElementById("room-list"); const zone_list = document.getElementById("zone-list");
room_list.innerHTML = ""; zone_list.innerHTML = "";
const rooms = await rooms_response.json(); const zones = await zones_response.json();
for (const room of rooms) { for (const zone of zones) {
room_list.insertAdjacentHTML( zone_list.insertAdjacentHTML(
"beforeend", "beforeend",
`<li id="room-selector-${room.id}" class="room"><a href="#/chat/room/${room.id}">${room.name}</a></li>`, `<li id="zone-selector-${zone.id}" class="zone"><a href="#/chat/zone/${zone.id}">${zone.name}</a></li>`,
); );
} }
last_room_update = now; last_zone_update = now;
} }
} }
window.addEventListener("locationchange", update_chat_rooms); window.addEventListener("locationchange", update_chat_zones);
function check_for_room_in_url() { function check_for_zone_in_url() {
const user_json = document.body.dataset.user; const user_json = document.body.dataset.user;
if (!user_json) { if (!user_json) {
return; return;
@ -471,28 +473,28 @@ function check_for_room_in_url() {
return; return;
} }
const first_room_id = document.querySelector("li.room")?.id.substring(14); const first_zone_id = document.querySelector("li.zone")?.id.substring(14);
// #/chat/room/{room_id} // #/chat/zone/{zone_id}
// ^ 12 // ^ 12
const room_id = hash.substring(12) || first_room_id; const zone_id = hash.substring(12) || first_zone_id;
if (!room_id) { if (!zone_id) {
setTimeout(check_for_room_in_url, 100); setTimeout(check_for_zone_in_url, 100);
return; return;
} }
const room_chat_container = document.getElementById("room-chat-container"); const zone_chat_container = document.getElementById("zone-chat-container");
if (room_chat_container.dataset.room_id !== room_id) { if (zone_chat_container.dataset.zone_id !== zone_id) {
window.location.hash = `/chat/room/${room_id}`; window.location.hash = `/chat/zone/${zone_id}`;
room_chat_container.dataset.room_id = room_id; zone_chat_container.dataset.zone_id = zone_id;
load_room(room_id); load_zone(zone_id);
} }
} }
window.addEventListener("locationchange", check_for_room_in_url); window.addEventListener("locationchange", check_for_zone_in_url);
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
await update_chat_rooms(); await update_chat_zones();
check_for_room_in_url(); check_for_zone_in_url();
}); });

File diff suppressed because it is too large Load diff

View file

@ -2,10 +2,10 @@ import { api, API_CLIENT } from '../../../utils/api.ts';
import * as asserts from '@std/assert'; import * as asserts from '@std/assert';
import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../helpers.ts'; import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../helpers.ts';
import { generateTotp } from '../../../utils/totp.ts'; import { generateTotp } from '../../../utils/totp.ts';
import { clear_room_events_cache } from '../../../models/event.ts'; import { clear_zone_events_cache } from '../../../models/event.ts';
Deno.test({ Deno.test({
name: 'API - ROOMS - Create', name: 'API - ZONES - Create',
permissions: { permissions: {
env: true, env: true,
read: true, read: true,
@ -25,7 +25,7 @@ Deno.test({
const user_info = await get_new_user(client); const user_info = await get_new_user(client);
try { try {
const _permission_denied_room = await client.fetch('/rooms', { const _permission_denied_zone = await client.fetch('/zones', {
method: 'POST', method: 'POST',
headers: { headers: {
'x-session_id': user_info.session.id, 'x-session_id': user_info.session.id,
@ -36,15 +36,15 @@ Deno.test({
} }
}); });
asserts.fail('allowed creation of a room without room creation permissions'); asserts.fail('allowed creation of a zone without zone creation permissions');
} catch (error) { } catch (error) {
asserts.assertEquals((error as Error).cause, 'permission_denied'); asserts.assertEquals((error as Error).cause, 'permission_denied');
} }
await set_user_permissions(client, user_info.user, user_info.session, [...user_info.user.permissions, 'rooms.create']); await set_user_permissions(client, user_info.user, user_info.session, [...user_info.user.permissions, 'zones.create']);
try { try {
const _too_long_name_room = await client.fetch('/rooms', { const _too_long_name_zone = await client.fetch('/zones', {
method: 'POST', method: 'POST',
headers: { headers: {
'x-session_id': user_info.session.id, 'x-session_id': user_info.session.id,
@ -55,25 +55,25 @@ Deno.test({
} }
}); });
asserts.fail('allowed creation of a room with an excessively long name'); asserts.fail('allowed creation of a zone with an excessively long name');
} catch (error) { } catch (error) {
asserts.assertEquals((error as Error).cause, 'invalid_room_name'); asserts.assertEquals((error as Error).cause, 'invalid_zone_name');
} }
const new_room = await client.fetch('/rooms', { const new_zone = await client.fetch('/zones', {
method: 'POST', method: 'POST',
headers: { headers: {
'x-session_id': user_info.session.id, 'x-session_id': user_info.session.id,
'x-totp': await generateTotp(user_info.session.secret) 'x-totp': await generateTotp(user_info.session.secret)
}, },
json: { json: {
name: 'test room' name: 'test zone'
} }
}); });
asserts.assert(new_room); asserts.assert(new_zone);
} finally { } finally {
clear_room_events_cache(); clear_zone_events_cache();
if (test_server_info) { if (test_server_info) {
await test_server_info?.server?.stop(); await test_server_info?.server?.stop();
} }

View file

@ -2,10 +2,10 @@ import { api, API_CLIENT } from '../../../utils/api.ts';
import * as asserts from '@std/assert'; import * as asserts from '@std/assert';
import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../helpers.ts'; import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../helpers.ts';
import { generateTotp } from '../../../utils/totp.ts'; import { generateTotp } from '../../../utils/totp.ts';
import { clear_room_events_cache } from '../../../models/event.ts'; import { clear_zone_events_cache } from '../../../models/event.ts';
Deno.test({ Deno.test({
name: 'API - ROOMS - Delete', name: 'API - ZONES - Delete',
permissions: { permissions: {
env: true, env: true,
read: true, read: true,
@ -24,22 +24,22 @@ Deno.test({
const user_info = await get_new_user(client); const user_info = await get_new_user(client);
await set_user_permissions(client, user_info.user, user_info.session, [...user_info.user.permissions, 'rooms.create']); await set_user_permissions(client, user_info.user, user_info.session, [...user_info.user.permissions, 'zones.create']);
const new_room = await client.fetch('/rooms', { const new_zone = await client.fetch('/zones', {
method: 'POST', method: 'POST',
headers: { headers: {
'x-session_id': user_info.session.id, 'x-session_id': user_info.session.id,
'x-totp': await generateTotp(user_info.session.secret) 'x-totp': await generateTotp(user_info.session.secret)
}, },
json: { json: {
name: 'test delete room' name: 'test delete zone'
} }
}); });
asserts.assert(new_room); asserts.assert(new_zone);
const deleted_room = await client.fetch(`/rooms/${new_room.id}`, { const deleted_zone = await client.fetch(`/zones/${new_zone.id}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'x-session_id': user_info.session.id, 'x-session_id': user_info.session.id,
@ -47,9 +47,9 @@ Deno.test({
} }
}); });
asserts.assert(deleted_room); asserts.assert(deleted_zone);
} finally { } finally {
clear_room_events_cache(); clear_zone_events_cache();
if (test_server_info) { if (test_server_info) {
await test_server_info?.server?.stop(); await test_server_info?.server?.stop();
} }

View file

@ -2,10 +2,10 @@ import * as asserts from '@std/assert';
import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../../helpers.ts'; import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../../helpers.ts';
import { api, API_CLIENT } from '../../../../utils/api.ts'; import { api, API_CLIENT } from '../../../../utils/api.ts';
import { generateTotp } from '../../../../utils/totp.ts'; import { generateTotp } from '../../../../utils/totp.ts';
import { clear_room_events_cache } from '../../../../models/event.ts'; import { clear_zone_events_cache } from '../../../../models/event.ts';
Deno.test({ Deno.test({
name: 'API - ROOMS - EVENTS - Create', name: 'API - ZONES - EVENTS - Create',
permissions: { permissions: {
env: true, env: true,
read: true, read: true,
@ -24,25 +24,25 @@ Deno.test({
const owner_info = await get_new_user(client); const owner_info = await get_new_user(client);
await set_user_permissions(client, owner_info.user, owner_info.session, [...owner_info.user.permissions, 'rooms.create']); await set_user_permissions(client, owner_info.user, owner_info.session, [...owner_info.user.permissions, 'zones.create']);
const room = await client.fetch('/rooms', { const zone = await client.fetch('/zones', {
method: 'POST', method: 'POST',
headers: { headers: {
'x-session_id': owner_info.session.id, 'x-session_id': owner_info.session.id,
'x-totp': await generateTotp(owner_info.session.secret) 'x-totp': await generateTotp(owner_info.session.secret)
}, },
json: { json: {
name: 'test events room', name: 'test events zone',
permissions: { permissions: {
write_events: [owner_info.user.id] write_events: [owner_info.user.id]
} }
} }
}); });
asserts.assert(room); asserts.assert(zone);
const event_from_owner = await client.fetch(`/rooms/${room.id}/events`, { const event_from_owner = await client.fetch(`/zones/${zone.id}/events`, {
method: 'POST', method: 'POST',
headers: { headers: {
'x-session_id': owner_info.session.id, 'x-session_id': owner_info.session.id,
@ -61,7 +61,7 @@ Deno.test({
const other_user_info = await get_new_user(client); const other_user_info = await get_new_user(client);
try { try {
const _permission_denied_room = await client.fetch(`/rooms/${room.id}/events`, { const _permission_denied_zone = await client.fetch(`/zones/${zone.id}/events`, {
method: 'POST', method: 'POST',
headers: { headers: {
'x-session_id': other_user_info.session.id, 'x-session_id': other_user_info.session.id,
@ -75,13 +75,13 @@ Deno.test({
} }
}); });
asserts.fail('allowed adding an event to a room without permission'); asserts.fail('allowed adding an event to a zone without permission');
} catch (error) { } catch (error) {
asserts.assertEquals((error as Error).cause, 'permission_denied'); asserts.assertEquals((error as Error).cause, 'permission_denied');
} }
// make the room public write // make the zone public write
const updated_by_owner_room = await client.fetch(`/rooms/${room.id}`, { const updated_by_owner_zone = await client.fetch(`/zones/${zone.id}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'x-session_id': owner_info.session.id, 'x-session_id': owner_info.session.id,
@ -89,16 +89,16 @@ Deno.test({
}, },
json: { json: {
permissions: { permissions: {
...room.permissions, ...zone.permissions,
write_events: [] write_events: []
} }
} }
}); });
asserts.assert(updated_by_owner_room); asserts.assert(updated_by_owner_zone);
asserts.assertEquals(updated_by_owner_room.permissions.write_events, []); asserts.assertEquals(updated_by_owner_zone.permissions.write_events, []);
const event_from_other_user = await client.fetch(`/rooms/${room.id}/events`, { const event_from_other_user = await client.fetch(`/zones/${zone.id}/events`, {
method: 'POST', method: 'POST',
headers: { headers: {
'x-session_id': other_user_info.session.id, 'x-session_id': other_user_info.session.id,
@ -114,7 +114,7 @@ Deno.test({
asserts.assert(event_from_other_user); asserts.assert(event_from_other_user);
} finally { } finally {
clear_room_events_cache(); clear_zone_events_cache();
if (test_server_info) { if (test_server_info) {
await test_server_info?.server?.stop(); await test_server_info?.server?.stop();
} }

View file

@ -2,10 +2,10 @@ import * as asserts from '@std/assert';
import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../../helpers.ts'; import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../../helpers.ts';
import { api, API_CLIENT } from '../../../../utils/api.ts'; import { api, API_CLIENT } from '../../../../utils/api.ts';
import { generateTotp } from '../../../../utils/totp.ts'; import { generateTotp } from '../../../../utils/totp.ts';
import { clear_room_events_cache } from '../../../../models/event.ts'; import { clear_zone_events_cache } from '../../../../models/event.ts';
Deno.test({ Deno.test({
name: 'API - ROOMS - EVENTS - Get', name: 'API - ZONES - EVENTS - Get',
permissions: { permissions: {
env: true, env: true,
read: true, read: true,
@ -29,25 +29,25 @@ Deno.test({
const owner_info = await get_new_user(client); const owner_info = await get_new_user(client);
await set_user_permissions(client, owner_info.user, owner_info.session, [...owner_info.user.permissions, 'rooms.create']); await set_user_permissions(client, owner_info.user, owner_info.session, [...owner_info.user.permissions, 'zones.create']);
const room = await client.fetch('/rooms', { const zone = await client.fetch('/zones', {
method: 'POST', method: 'POST',
headers: { headers: {
'x-session_id': owner_info.session.id, 'x-session_id': owner_info.session.id,
'x-totp': await generateTotp(owner_info.session.secret) 'x-totp': await generateTotp(owner_info.session.secret)
}, },
json: { json: {
name: 'test get events room' name: 'test get events zone'
} }
}); });
asserts.assert(room); asserts.assert(zone);
const NUM_INITIAL_EVENTS = 5; const NUM_INITIAL_EVENTS = 5;
const events_initial_batch: any[] = []; const events_initial_batch: any[] = [];
for (let i = 0; i < NUM_INITIAL_EVENTS; ++i) { for (let i = 0; i < NUM_INITIAL_EVENTS; ++i) {
const event = await client.fetch(`/rooms/${room.id}/events`, { const event = await client.fetch(`/zones/${zone.id}/events`, {
method: 'POST', method: 'POST',
headers: { headers: {
'x-session_id': owner_info.session.id, 'x-session_id': owner_info.session.id,
@ -69,7 +69,7 @@ Deno.test({
const other_user_info = await get_new_user(client); const other_user_info = await get_new_user(client);
const events_from_server = await client.fetch(`/rooms/${room.id}/events`, { const events_from_server = await client.fetch(`/zones/${zone.id}/events`, {
method: 'GET', method: 'GET',
headers: { headers: {
'x-session_id': other_user_info.session.id, 'x-session_id': other_user_info.session.id,
@ -82,7 +82,7 @@ Deno.test({
const newest_event = events_from_server[0]; const newest_event = events_from_server[0];
asserts.assert(newest_event); asserts.assert(newest_event);
const long_poll_request_promise = client.fetch(`/rooms/${room.id}/events?wait=true&after_id=${newest_event.id}`, { const long_poll_request_promise = client.fetch(`/zones/${zone.id}/events?wait=true&after_id=${newest_event.id}`, {
method: 'GET', method: 'GET',
headers: { headers: {
'x-session_id': other_user_info.session.id, 'x-session_id': other_user_info.session.id,
@ -92,7 +92,7 @@ Deno.test({
const wait_and_then_create_an_event = new Promise((resolve) => { const wait_and_then_create_an_event = new Promise((resolve) => {
setTimeout(async () => { setTimeout(async () => {
await client.fetch(`/rooms/${room.id}/events`, { await client.fetch(`/zones/${zone.id}/events`, {
method: 'POST', method: 'POST',
headers: { headers: {
'x-session_id': owner_info.session.id, 'x-session_id': owner_info.session.id,
@ -117,7 +117,7 @@ Deno.test({
asserts.assertEquals(long_polled_events[0].data?.i, 12345); asserts.assertEquals(long_polled_events[0].data?.i, 12345);
}); });
} finally { } finally {
clear_room_events_cache(); clear_zone_events_cache();
if (test_server_info) { if (test_server_info) {
await test_server_info.server.stop(); await test_server_info.server.stop();
} }

View file

@ -2,10 +2,10 @@ import * as asserts from '@std/assert';
import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../../helpers.ts'; import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../../helpers.ts';
import { api, API_CLIENT } from '../../../../utils/api.ts'; import { api, API_CLIENT } from '../../../../utils/api.ts';
import { generateTotp } from '../../../../utils/totp.ts'; import { generateTotp } from '../../../../utils/totp.ts';
import { clear_room_events_cache } from '../../../../models/event.ts'; import { clear_zone_events_cache } from '../../../../models/event.ts';
Deno.test({ Deno.test({
name: 'API - ROOMS - EVENTS - Update', name: 'API - ZONES - EVENTS - Update',
permissions: { permissions: {
env: true, env: true,
read: true, read: true,
@ -24,22 +24,22 @@ Deno.test({
const owner_info = await get_new_user(client); const owner_info = await get_new_user(client);
await set_user_permissions(client, owner_info.user, owner_info.session, [...owner_info.user.permissions, 'rooms.create']); await set_user_permissions(client, owner_info.user, owner_info.session, [...owner_info.user.permissions, 'zones.create']);
const room = await client.fetch('/rooms', { const zone = await client.fetch('/zones', {
method: 'POST', method: 'POST',
headers: { headers: {
'x-session_id': owner_info.session.id, 'x-session_id': owner_info.session.id,
'x-totp': await generateTotp(owner_info.session.secret) 'x-totp': await generateTotp(owner_info.session.secret)
}, },
json: { json: {
name: 'test update events room' name: 'test update events zone'
} }
}); });
asserts.assert(room); asserts.assert(zone);
const event_from_owner = await client.fetch(`/rooms/${room.id}/events`, { const event_from_owner = await client.fetch(`/zones/${zone.id}/events`, {
method: 'POST', method: 'POST',
headers: { headers: {
'x-session_id': owner_info.session.id, 'x-session_id': owner_info.session.id,
@ -55,7 +55,7 @@ Deno.test({
asserts.assert(event_from_owner); asserts.assert(event_from_owner);
const fetched_event_from_owner = await client.fetch(`/rooms/${room.id}/events/${event_from_owner.id}`, { const fetched_event_from_owner = await client.fetch(`/zones/${zone.id}/events/${event_from_owner.id}`, {
method: 'GET', method: 'GET',
headers: { headers: {
'x-session_id': owner_info.session.id, 'x-session_id': owner_info.session.id,
@ -65,7 +65,7 @@ Deno.test({
asserts.assertEquals(fetched_event_from_owner, event_from_owner); asserts.assertEquals(fetched_event_from_owner, event_from_owner);
const updated_event_from_owner = await client.fetch(`/rooms/${room.id}/events/${event_from_owner.id}`, { const updated_event_from_owner = await client.fetch(`/zones/${zone.id}/events/${event_from_owner.id}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'x-session_id': owner_info.session.id, 'x-session_id': owner_info.session.id,
@ -83,7 +83,7 @@ Deno.test({
asserts.assertEquals(updated_event_from_owner.type, 'other'); asserts.assertEquals(updated_event_from_owner.type, 'other');
asserts.assertEquals(updated_event_from_owner.data.foo, 'baz'); asserts.assertEquals(updated_event_from_owner.data.foo, 'baz');
const fetched_updated_event_from_owner = await client.fetch(`/rooms/${room.id}/events/${event_from_owner.id}`, { const fetched_updated_event_from_owner = await client.fetch(`/zones/${zone.id}/events/${event_from_owner.id}`, {
method: 'GET', method: 'GET',
headers: { headers: {
'x-session_id': owner_info.session.id, 'x-session_id': owner_info.session.id,
@ -97,7 +97,7 @@ Deno.test({
const other_user_info = await get_new_user(client); const other_user_info = await get_new_user(client);
const event_from_other_user = await client.fetch(`/rooms/${room.id}/events`, { const event_from_other_user = await client.fetch(`/zones/${zone.id}/events`, {
method: 'POST', method: 'POST',
headers: { headers: {
'x-session_id': other_user_info.session.id, 'x-session_id': other_user_info.session.id,
@ -113,7 +113,7 @@ Deno.test({
asserts.assert(event_from_other_user); asserts.assert(event_from_other_user);
const fetched_event_from_other_user = await client.fetch(`/rooms/${room.id}/events/${event_from_other_user.id}`, { const fetched_event_from_other_user = await client.fetch(`/zones/${zone.id}/events/${event_from_other_user.id}`, {
method: 'GET', method: 'GET',
headers: { headers: {
'x-session_id': other_user_info.session.id, 'x-session_id': other_user_info.session.id,
@ -123,7 +123,7 @@ Deno.test({
asserts.assertEquals(fetched_event_from_other_user, event_from_other_user); asserts.assertEquals(fetched_event_from_other_user, event_from_other_user);
const updated_event_from_other_user = await client.fetch(`/rooms/${room.id}/events/${event_from_other_user.id}`, { const updated_event_from_other_user = await client.fetch(`/zones/${zone.id}/events/${event_from_other_user.id}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'x-session_id': other_user_info.session.id, 'x-session_id': other_user_info.session.id,
@ -141,7 +141,7 @@ Deno.test({
asserts.assertEquals(updated_event_from_other_user.type, 'other'); asserts.assertEquals(updated_event_from_other_user.type, 'other');
asserts.assertEquals(updated_event_from_other_user.data.other_user, 'bloop'); asserts.assertEquals(updated_event_from_other_user.data.other_user, 'bloop');
const fetched_updated_event_from_other_user = await client.fetch(`/rooms/${room.id}/events/${event_from_other_user.id}`, { const fetched_updated_event_from_other_user = await client.fetch(`/zones/${zone.id}/events/${event_from_other_user.id}`, {
method: 'GET', method: 'GET',
headers: { headers: {
'x-session_id': other_user_info.session.id, 'x-session_id': other_user_info.session.id,
@ -153,7 +153,7 @@ Deno.test({
asserts.assertNotEquals(fetched_updated_event_from_other_user, fetched_event_from_other_user); 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); asserts.assertEquals(fetched_updated_event_from_other_user, updated_event_from_other_user);
const updated_by_owner_room = await client.fetch(`/rooms/${room.id}`, { const updated_by_owner_zone = await client.fetch(`/zones/${zone.id}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'x-session_id': owner_info.session.id, 'x-session_id': owner_info.session.id,
@ -161,16 +161,16 @@ Deno.test({
}, },
json: { json: {
permissions: { permissions: {
...room.permissions, ...zone.permissions,
write_events: [owner_info.user.id] write_events: [owner_info.user.id]
} }
} }
}); });
asserts.assertEquals(updated_by_owner_room.permissions.write_events, [owner_info.user.id]); asserts.assertEquals(updated_by_owner_zone.permissions.write_events, [owner_info.user.id]);
try { try {
await client.fetch(`/rooms/${room.id}/events/${event_from_other_user.id}`, { await client.fetch(`/zones/${zone.id}/events/${event_from_other_user.id}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'x-session_id': other_user_info.session.id, 'x-session_id': other_user_info.session.id,
@ -181,13 +181,13 @@ Deno.test({
} }
}); });
asserts.fail('allowed updating an event in a room with a write_events allowed only by owner'); asserts.fail('allowed updating an event in a zone with a write_events allowed only by owner');
} catch (error) { } catch (error) {
asserts.assertEquals((error as Error).cause, 'permission_denied'); asserts.assertEquals((error as Error).cause, 'permission_denied');
} }
try { try {
await client.fetch(`/rooms/${room.id}/events/${event_from_other_user.id}`, { await client.fetch(`/zones/${zone.id}/events/${event_from_other_user.id}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'x-session_id': other_user_info.session.id, 'x-session_id': other_user_info.session.id,
@ -195,12 +195,12 @@ Deno.test({
} }
}); });
asserts.fail('allowed deleting an event in a room with a write_events allowed only by owner'); asserts.fail('allowed deleting an event in a zone with a write_events allowed only by owner');
} catch (error) { } catch (error) {
asserts.assertEquals((error as Error).cause, 'permission_denied'); asserts.assertEquals((error as Error).cause, 'permission_denied');
} }
const publicly_writable_room = await client.fetch(`/rooms/${room.id}`, { const publicly_writable_zone = await client.fetch(`/zones/${zone.id}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'x-session_id': owner_info.session.id, 'x-session_id': owner_info.session.id,
@ -208,15 +208,15 @@ Deno.test({
}, },
json: { json: {
permissions: { permissions: {
...room.permissions, ...zone.permissions,
write_events: [] write_events: []
} }
} }
}); });
asserts.assertEquals(publicly_writable_room.permissions.write_events, []); asserts.assertEquals(publicly_writable_zone.permissions.write_events, []);
const delete_other_user_event_response = await client.fetch(`/rooms/${room.id}/events/${event_from_other_user.id}`, { const delete_other_user_event_response = await client.fetch(`/zones/${zone.id}/events/${event_from_other_user.id}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'x-session_id': other_user_info.session.id, 'x-session_id': other_user_info.session.id,
@ -226,7 +226,7 @@ Deno.test({
asserts.assertEquals(delete_other_user_event_response.deleted, true); asserts.assertEquals(delete_other_user_event_response.deleted, true);
const delete_owner_event_response = await client.fetch(`/rooms/${room.id}/events/${event_from_owner.id}`, { const delete_owner_event_response = await client.fetch(`/zones/${zone.id}/events/${event_from_owner.id}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'x-session_id': owner_info.session.id, 'x-session_id': owner_info.session.id,
@ -236,7 +236,7 @@ Deno.test({
asserts.assertEquals(delete_owner_event_response.deleted, true); asserts.assertEquals(delete_owner_event_response.deleted, true);
} finally { } finally {
clear_room_events_cache(); clear_zone_events_cache();
if (test_server_info) { if (test_server_info) {
await test_server_info?.server?.stop(); await test_server_info?.server?.stop();
} }

View file

@ -2,10 +2,10 @@ import * as asserts from '@std/assert';
import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../../helpers.ts'; import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../../helpers.ts';
import { api, API_CLIENT } from '../../../../utils/api.ts'; import { api, API_CLIENT } from '../../../../utils/api.ts';
import { generateTotp } from '../../../../utils/totp.ts'; import { generateTotp } from '../../../../utils/totp.ts';
import { clear_room_events_cache } from '../../../../models/event.ts'; import { clear_zone_events_cache } from '../../../../models/event.ts';
Deno.test({ Deno.test({
name: 'API - ROOMS - EVENTS - Update (APPEND_ONLY_EVENTS)', name: 'API - ZONES - EVENTS - Update (APPEND_ONLY_EVENTS)',
permissions: { permissions: {
env: true, env: true,
read: true, read: true,
@ -27,22 +27,22 @@ Deno.test({
const owner_info = await get_new_user(client); const owner_info = await get_new_user(client);
await set_user_permissions(client, owner_info.user, owner_info.session, [...owner_info.user.permissions, 'rooms.create']); await set_user_permissions(client, owner_info.user, owner_info.session, [...owner_info.user.permissions, 'zones.create']);
const room = await client.fetch('/rooms', { const zone = await client.fetch('/zones', {
method: 'POST', method: 'POST',
headers: { headers: {
'x-session_id': owner_info.session.id, 'x-session_id': owner_info.session.id,
'x-totp': await generateTotp(owner_info.session.secret) 'x-totp': await generateTotp(owner_info.session.secret)
}, },
json: { json: {
name: 'test update events room in append only mode' name: 'test update events zone in append only mode'
} }
}); });
asserts.assert(room); asserts.assert(zone);
const event_from_owner = await client.fetch(`/rooms/${room.id}/events`, { const event_from_owner = await client.fetch(`/zones/${zone.id}/events`, {
method: 'POST', method: 'POST',
headers: { headers: {
'x-session_id': owner_info.session.id, 'x-session_id': owner_info.session.id,
@ -58,7 +58,7 @@ Deno.test({
asserts.assert(event_from_owner); asserts.assert(event_from_owner);
const fetched_event_from_owner = await client.fetch(`/rooms/${room.id}/events/${event_from_owner.id}`, { const fetched_event_from_owner = await client.fetch(`/zones/${zone.id}/events/${event_from_owner.id}`, {
method: 'GET', method: 'GET',
headers: { headers: {
'x-session_id': owner_info.session.id, 'x-session_id': owner_info.session.id,
@ -69,7 +69,7 @@ Deno.test({
asserts.assertEquals(fetched_event_from_owner, event_from_owner); asserts.assertEquals(fetched_event_from_owner, event_from_owner);
try { try {
await client.fetch(`/rooms/${room.id}/events/${event_from_owner.id}`, { await client.fetch(`/zones/${zone.id}/events/${event_from_owner.id}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'x-session_id': owner_info.session.id, 'x-session_id': owner_info.session.id,
@ -80,13 +80,13 @@ Deno.test({
} }
}); });
asserts.fail('allowed updating an event in a room with APPEND_ONLY_EVENTS on'); asserts.fail('allowed updating an event in a zone with APPEND_ONLY_EVENTS on');
} catch (error) { } catch (error) {
asserts.assertEquals((error as Error).cause, 'append_only_events'); asserts.assertEquals((error as Error).cause, 'append_only_events');
} }
try { try {
await client.fetch(`/rooms/${room.id}/events/${event_from_owner.id}`, { await client.fetch(`/zones/${zone.id}/events/${event_from_owner.id}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'x-session_id': owner_info.session.id, 'x-session_id': owner_info.session.id,
@ -94,14 +94,14 @@ Deno.test({
} }
}); });
asserts.fail('allowed deleting an event in a room with APPEND_ONLY_EVENTS on'); asserts.fail('allowed deleting an event in a zone with APPEND_ONLY_EVENTS on');
} catch (error) { } catch (error) {
asserts.assertEquals((error as Error).cause, 'append_only_events'); asserts.assertEquals((error as Error).cause, 'append_only_events');
} }
const other_user_info = await get_new_user(client); const other_user_info = await get_new_user(client);
const event_from_other_user = await client.fetch(`/rooms/${room.id}/events`, { const event_from_other_user = await client.fetch(`/zones/${zone.id}/events`, {
method: 'POST', method: 'POST',
headers: { headers: {
'x-session_id': other_user_info.session.id, 'x-session_id': other_user_info.session.id,
@ -117,7 +117,7 @@ Deno.test({
asserts.assert(event_from_other_user); asserts.assert(event_from_other_user);
const fetched_event_from_other_user = await client.fetch(`/rooms/${room.id}/events/${event_from_other_user.id}`, { const fetched_event_from_other_user = await client.fetch(`/zones/${zone.id}/events/${event_from_other_user.id}`, {
method: 'GET', method: 'GET',
headers: { headers: {
'x-session_id': other_user_info.session.id, 'x-session_id': other_user_info.session.id,
@ -128,7 +128,7 @@ Deno.test({
asserts.assertEquals(fetched_event_from_other_user, event_from_other_user); asserts.assertEquals(fetched_event_from_other_user, event_from_other_user);
try { try {
await client.fetch(`/rooms/${room.id}/events/${event_from_other_user.id}`, { await client.fetch(`/zones/${zone.id}/events/${event_from_other_user.id}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'x-session_id': other_user_info.session.id, 'x-session_id': other_user_info.session.id,
@ -139,13 +139,13 @@ Deno.test({
} }
}); });
asserts.fail('allowed updating an event in a room with APPEND_ONLY_EVENTS on'); asserts.fail('allowed updating an event in a zone with APPEND_ONLY_EVENTS on');
} catch (error) { } catch (error) {
asserts.assertEquals((error as Error).cause, 'append_only_events'); asserts.assertEquals((error as Error).cause, 'append_only_events');
} }
try { try {
await client.fetch(`/rooms/${room.id}/events/${event_from_other_user.id}`, { await client.fetch(`/zones/${zone.id}/events/${event_from_other_user.id}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'x-session_id': other_user_info.session.id, 'x-session_id': other_user_info.session.id,
@ -153,14 +153,14 @@ Deno.test({
} }
}); });
asserts.fail('allowed deleting an event in a room with APPEND_ONLY_EVENTS on'); asserts.fail('allowed deleting an event in a zone with APPEND_ONLY_EVENTS on');
} catch (error) { } catch (error) {
asserts.assertEquals((error as Error).cause, 'append_only_events'); asserts.assertEquals((error as Error).cause, 'append_only_events');
} }
} finally { } finally {
Deno.env.delete('APPEND_ONLY_EVENTS'); Deno.env.delete('APPEND_ONLY_EVENTS');
clear_room_events_cache(); clear_zone_events_cache();
if (test_server_info) { if (test_server_info) {
await test_server_info?.server?.stop(); await test_server_info?.server?.stop();
} }

View file

@ -2,10 +2,10 @@ import { api, API_CLIENT } from '../../../utils/api.ts';
import * as asserts from '@std/assert'; import * as asserts from '@std/assert';
import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../helpers.ts'; import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../helpers.ts';
import { generateTotp } from '../../../utils/totp.ts'; import { generateTotp } from '../../../utils/totp.ts';
import { clear_room_events_cache } from '../../../models/event.ts'; import { clear_zone_events_cache } from '../../../models/event.ts';
Deno.test({ Deno.test({
name: 'API - ROOMS - Update', name: 'API - ZONES - Update',
permissions: { permissions: {
env: true, env: true,
read: true, read: true,
@ -24,25 +24,25 @@ Deno.test({
const user_info = await get_new_user(client); const user_info = await get_new_user(client);
await set_user_permissions(client, user_info.user, user_info.session, [...user_info.user.permissions, 'rooms.create']); await set_user_permissions(client, user_info.user, user_info.session, [...user_info.user.permissions, 'zones.create']);
const new_room = await client.fetch('/rooms', { const new_zone = await client.fetch('/zones', {
method: 'POST', method: 'POST',
headers: { headers: {
'x-session_id': user_info.session.id, 'x-session_id': user_info.session.id,
'x-totp': await generateTotp(user_info.session.secret) 'x-totp': await generateTotp(user_info.session.secret)
}, },
json: { json: {
name: 'test update room' name: 'test update zone'
} }
}); });
asserts.assert(new_room); asserts.assert(new_zone);
const other_user_info = await get_new_user(client); const other_user_info = await get_new_user(client);
try { try {
const _permission_denied_room = await client.fetch(`/rooms/${new_room.id}`, { const _permission_denied_zone = await client.fetch(`/zones/${new_zone.id}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'x-session_id': other_user_info.session.id, 'x-session_id': other_user_info.session.id,
@ -53,12 +53,12 @@ Deno.test({
} }
}); });
asserts.fail('allowed updating a room owned by someone else'); asserts.fail('allowed updating a zone owned by someone else');
} catch (error) { } catch (error) {
asserts.assertEquals((error as Error).cause, 'permission_denied'); asserts.assertEquals((error as Error).cause, 'permission_denied');
} }
const updated_by_owner_room = await client.fetch(`/rooms/${new_room.id}`, { const updated_by_owner_zone = await client.fetch(`/zones/${new_zone.id}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'x-session_id': user_info.session.id, 'x-session_id': user_info.session.id,
@ -67,17 +67,17 @@ Deno.test({
json: { json: {
topic: 'this is a new topic', topic: 'this is a new topic',
permissions: { permissions: {
...new_room.permissions, ...new_zone.permissions,
write: [...new_room.permissions.write, other_user_info.user.id] write: [...new_zone.permissions.write, other_user_info.user.id]
} }
} }
}); });
asserts.assert(updated_by_owner_room); asserts.assert(updated_by_owner_zone);
asserts.assertEquals(updated_by_owner_room.topic, 'this is a new topic'); asserts.assertEquals(updated_by_owner_zone.topic, 'this is a new topic');
asserts.assertEquals(updated_by_owner_room.permissions.write, [user_info.user.id, other_user_info.user.id]); asserts.assertEquals(updated_by_owner_zone.permissions.write, [user_info.user.id, other_user_info.user.id]);
const updated_by_other_user_room = await client.fetch(`/rooms/${new_room.id}`, { const updated_by_other_user_zone = await client.fetch(`/zones/${new_zone.id}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'x-session_id': other_user_info.session.id, 'x-session_id': other_user_info.session.id,
@ -88,11 +88,11 @@ Deno.test({
} }
}); });
asserts.assert(updated_by_other_user_room); asserts.assert(updated_by_other_user_zone);
asserts.assertEquals(updated_by_other_user_room.topic, 'this is a newer topic'); asserts.assertEquals(updated_by_other_user_zone.topic, 'this is a newer topic');
asserts.assertEquals(updated_by_other_user_room.permissions.write, [user_info.user.id, other_user_info.user.id]); asserts.assertEquals(updated_by_other_user_zone.permissions.write, [user_info.user.id, other_user_info.user.id]);
} finally { } finally {
clear_room_events_cache(); clear_zone_events_cache();
if (test_server_info) { if (test_server_info) {
await test_server_info?.server?.stop(); await test_server_info?.server?.stop();
} }