refactor: zones => topics

This commit is contained in:
Andy Burke 2025-09-10 12:51:27 -07:00
parent 20a5d1bc88
commit fac8f409f4
26 changed files with 470 additions and 469 deletions

View file

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

View file

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

View file

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

View file

@ -1,40 +1,40 @@
import lurid from '@andyburke/lurid';
import { get_session, get_user, PRECHECK_TABLE, require_user } from '../../../../../utils/prechecks.ts';
import { ZONE, ZONES } from '../../../../../models/zone.ts';
import { TOPIC, TOPICS } from '../../../../../models/topic.ts';
import * as CANNED_RESPONSES from '../../../../../utils/canned_responses.ts';
import { EVENT, get_events_collection_for_zone } from '../../../../../models/event.ts';
import { EVENT, get_events_collection_for_topic } from '../../../../../models/event.ts';
import parse_body from '../../../../../utils/bodyparser.ts';
import { FSDB_COLLECTION, FSDB_SEARCH_OPTIONS, WALK_ENTRY } from '@andyburke/fsdb';
import * as path from '@std/path';
export const PRECHECKS: PRECHECK_TABLE = {};
// GET /api/zones/:zone_id/events - get zone events
// GET /api/topics/:topic_id/events - get topic events
// query parameters:
// partial_id: the partial id subset you would like to match (remember, lurids are lexigraphically sorted)
PRECHECKS.GET = [get_session, get_user, require_user, async (_req: Request, meta: Record<string, any>): Promise<Response | undefined> => {
const zone_id: string = meta.params?.zone_id?.toLowerCase().trim() ?? '';
const topic_id: string = meta.params?.topic_id?.toLowerCase().trim() ?? '';
// lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same"
const zone: ZONE | null = zone_id.length === 49 ? await ZONES.get(zone_id) : null;
const topic: TOPIC | null = topic_id.length === 49 ? await TOPICS.get(topic_id) : null;
if (!zone) {
if (!topic) {
return CANNED_RESPONSES.not_found();
}
meta.zone = zone;
const zone_is_public: boolean = meta.zone.permissions.read.length === 0;
const user_has_read_for_zone = zone_is_public || meta.zone.permissions.read.includes(meta.user.id);
const zone_events_are_public = meta.zone.permissions.read_events.length === 0;
const user_has_read_events_for_zone = user_has_read_for_zone &&
(zone_events_are_public || meta.zone.permissions.read_events.includes(meta.user.id));
meta.topic = topic;
const topic_is_public: boolean = meta.topic.permissions.read.length === 0;
const user_has_read_for_topic = topic_is_public || meta.topic.permissions.read.includes(meta.user.id);
const topic_events_are_public = meta.topic.permissions.read_events.length === 0;
const user_has_read_events_for_topic = user_has_read_for_topic &&
(topic_events_are_public || meta.topic.permissions.read_events.includes(meta.user.id));
if (!user_has_read_events_for_zone) {
if (!user_has_read_events_for_topic) {
return CANNED_RESPONSES.permission_denied();
}
}];
export async function GET(request: Request, meta: Record<string, any>): Promise<Response> {
const events: FSDB_COLLECTION<EVENT> = get_events_collection_for_zone(meta.zone.id);
const events: FSDB_COLLECTION<EVENT> = get_events_collection_for_topic(meta.topic.id);
const sorts = events.sorts;
const sort_name: string = meta.query.sort ?? 'newest';
@ -126,31 +126,31 @@ export async function GET(request: Request, meta: Record<string, any>): Promise<
});
}
// POST /api/zones/:zone_id/events - Create an event
// POST /api/topics/:topic_id/events - Create an event
PRECHECKS.POST = [get_session, get_user, require_user, async (_req: Request, meta: Record<string, any>): Promise<Response | undefined> => {
const zone_id: string = meta.params?.zone_id?.toLowerCase().trim() ?? '';
const topic_id: string = meta.params?.topic_id?.toLowerCase().trim() ?? '';
// lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same"
const zone: ZONE | null = zone_id.length === 49 ? await ZONES.get(zone_id) : null;
const topic: TOPIC | null = topic_id.length === 49 ? await TOPICS.get(topic_id) : null;
if (!zone) {
if (!topic) {
return CANNED_RESPONSES.not_found();
}
meta.zone = zone;
const zone_is_public: boolean = meta.zone.permissions.read.length === 0;
const user_has_read_for_zone = zone_is_public || meta.zone.permissions.read.includes(meta.user.id);
const zone_events_are_publicly_writable = meta.zone.permissions.write_events.length === 0;
const user_has_write_events_for_zone = user_has_read_for_zone &&
(zone_events_are_publicly_writable || meta.zone.permissions.write_events.includes(meta.user.id));
meta.topic = topic;
const topic_is_public: boolean = meta.topic.permissions.read.length === 0;
const user_has_read_for_topic = topic_is_public || meta.topic.permissions.read.includes(meta.user.id);
const topic_events_are_publicly_writable = meta.topic.permissions.write_events.length === 0;
const user_has_write_events_for_topic = user_has_read_for_topic &&
(topic_events_are_publicly_writable || meta.topic.permissions.write_events.includes(meta.user.id));
if (!user_has_write_events_for_zone) {
if (!user_has_write_events_for_topic) {
return CANNED_RESPONSES.permission_denied();
}
}];
export async function POST(req: Request, meta: Record<string, any>): Promise<Response> {
try {
const events: FSDB_COLLECTION<EVENT> = get_events_collection_for_zone(meta.zone.id);
const events: FSDB_COLLECTION<EVENT> = get_events_collection_for_topic(meta.topic.id);
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 parse_body from '../../../../utils/bodyparser.ts';
import * as CANNED_RESPONSES from '../../../../utils/canned_responses.ts';
import { ZONE, ZONES } from '../../../../models/zone.ts';
import { TOPIC, TOPICS } from '../../../../models/topic.ts';
export const PRECHECKS: PRECHECK_TABLE = {};
// GET /api/zones/:id - Get a zone
// GET /api/topics/:id - Get a topic
PRECHECKS.GET = [get_session, get_user, require_user, async (_req: Request, meta: Record<string, any>): Promise<Response | undefined> => {
const zone_id: string = meta.params?.zone_id?.toLowerCase().trim() ?? '';
const topic_id: string = meta.params?.topic_id?.toLowerCase().trim() ?? '';
// lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same"
const zone: ZONE | null = zone_id.length === 49 ? await ZONES.get(zone_id) : null;
const topic: TOPIC | null = topic_id.length === 49 ? await TOPICS.get(topic_id) : null;
if (!zone) {
if (!topic) {
return CANNED_RESPONSES.not_found();
}
meta.zone = zone;
const zone_is_public = zone.permissions.read.length === 0;
const user_has_read_for_zone = zone_is_public || zone.permissions.read.includes(meta.user.id);
meta.topic = topic;
const topic_is_public = topic.permissions.read.length === 0;
const user_has_read_for_topic = topic_is_public || topic.permissions.read.includes(meta.user.id);
if (!user_has_read_for_zone) {
if (!user_has_read_for_topic) {
return CANNED_RESPONSES.permission_denied();
}
}];
export function GET(_req: Request, meta: Record<string, any>): Response {
return Response.json(meta.zone, {
return Response.json(meta.topic, {
status: 200
});
}
// PUT /api/zones/:id - Update zone
// PUT /api/topics/:id - Update topic
PRECHECKS.PUT = [get_session, get_user, require_user, async (_req: Request, meta: Record<string, any>): Promise<Response | undefined> => {
const zone_id: string = meta.params?.zone_id?.toLowerCase().trim() ?? '';
const topic_id: string = meta.params?.topic_id?.toLowerCase().trim() ?? '';
// lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same"
const zone: ZONE | null = zone_id.length === 49 ? await ZONES.get(zone_id) : null;
const topic: TOPIC | null = topic_id.length === 49 ? await TOPICS.get(topic_id) : null;
if (!zone) {
if (!topic) {
return CANNED_RESPONSES.not_found();
}
meta.zone = zone;
const user_has_write_for_zone = zone.permissions.write.includes(meta.user.id);
meta.topic = topic;
const user_has_write_for_topic = topic.permissions.write.includes(meta.user.id);
if (!user_has_write_for_zone) {
if (!user_has_write_for_topic) {
return CANNED_RESPONSES.permission_denied();
}
}];
@ -54,16 +54,16 @@ export async function PUT(req: Request, meta: Record<string, any>): Promise<Resp
try {
const body = await parse_body(req);
const updated = {
...meta.zone,
...meta.topic,
...body,
id: meta.zone.id,
id: meta.topic.id,
timestamps: {
created: meta.zone.timestamps.created,
created: meta.topic.timestamps.created,
updated: now
}
};
await ZONES.update(updated);
await TOPICS.update(updated);
return Response.json(updated, {
status: 200
});
@ -79,31 +79,31 @@ export async function PUT(req: Request, meta: Record<string, any>): Promise<Resp
}
}
// DELETE /api/zones/:id - Delete zone
// DELETE /api/topics/:id - Delete topic
PRECHECKS.DELETE = [
get_session,
get_user,
require_user,
async (_req: Request, meta: Record<string, any>): Promise<Response | undefined> => {
const zone_id: string = meta.params?.zone_id?.toLowerCase().trim() ?? '';
const topic_id: string = meta.params?.topic_id?.toLowerCase().trim() ?? '';
// lurid is 49 chars as we use them, eg: "also-play-flow-want-form-wide-thus-work-burn-same"
const zone: ZONE | null = zone_id.length === 49 ? await ZONES.get(zone_id) : null;
const topic: TOPIC | null = topic_id.length === 49 ? await TOPICS.get(topic_id) : null;
if (!zone) {
if (!topic) {
return CANNED_RESPONSES.not_found();
}
meta.zone = zone;
const user_has_write_for_zone = zone.permissions.write.includes(meta.user.id);
meta.topic = topic;
const user_has_write_for_topic = topic.permissions.write.includes(meta.user.id);
if (!user_has_write_for_zone) {
if (!user_has_write_for_topic) {
return CANNED_RESPONSES.permission_denied();
}
}
];
export async function DELETE(_req: Request, meta: Record<string, any>): Promise<Response> {
await ZONES.delete(meta.zone);
await TOPICS.delete(meta.topic);
return Response.json({
deleted: true

View file

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

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

View file

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

View file

@ -1,23 +0,0 @@
# /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,15 +0,0 @@
# /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,28 +0,0 @@
# /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.