diff --git a/app/pages/character/[id]/index.client.vue b/app/pages/character/[id]/index.client.vue
index 91408ad..56c4031 100644
--- a/app/pages/character/[id]/index.client.vue
+++ b/app/pages/character/[id]/index.client.vue
@@ -28,6 +28,10 @@ onMounted(() => {
{
const character = new CharacterSheet(id, user);
container.value.appendChild(character.container);
+
+ onUnmounted(() => {
+ character.ws?.close();
+ })
}
});
});
diff --git a/app/types/campaign.d.ts b/app/types/campaign.d.ts
index 52e65ac..c8d1c8d 100644
--- a/app/types/campaign.d.ts
+++ b/app/types/campaign.d.ts
@@ -19,7 +19,7 @@ export type Campaign = {
logs: CampaignLog[];
} & CampaignVariables;
export type CampaignLog = {
- from: number;
+ target: number;
timestamp: Serialize
;
type: 'ITEM' | 'CHARACTER' | 'PLACE' | 'EVENT' | 'FIGHT' | 'TEXT';
details: string;
diff --git a/app/types/character.d.ts b/app/types/character.d.ts
index 61a34f7..bcb91d2 100644
--- a/app/types/character.d.ts
+++ b/app/types/character.d.ts
@@ -42,6 +42,8 @@ export type Character = {
owner: number;
username?: string;
visibility: "private" | "public";
+
+ campaign?: number;
};
export type CharacterVariables = {
health: number;
diff --git a/db.sqlite b/db.sqlite
index f9b39d1..1dd24cc 100644
Binary files a/db.sqlite and b/db.sqlite differ
diff --git a/drizzle.config.ts b/drizzle.config.ts
index b19c6f8..eb9f90a 100644
--- a/drizzle.config.ts
+++ b/drizzle.config.ts
@@ -3,7 +3,7 @@ import { defineConfig } from 'drizzle-kit';
export default defineConfig({
out: './drizzle',
- schema: './db/schema.ts',
+ schema: './app/db/schema.ts',
dialect: 'sqlite',
dbCredentials: {
url: process.env.DB_FILE!,
diff --git a/drizzle/0023_chunky_thunderbird.sql b/drizzle/0023_chunky_thunderbird.sql
new file mode 100644
index 0000000..41a2a3a
--- /dev/null
+++ b/drizzle/0023_chunky_thunderbird.sql
@@ -0,0 +1,19 @@
+PRAGMA foreign_keys=OFF;--> statement-breakpoint
+CREATE TABLE `__new_campaign` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `name` text NOT NULL,
+ `owner` integer NOT NULL,
+ `link` text NOT NULL,
+ `status` text DEFAULT 'PREPARING',
+ `inventory` text DEFAULT '[]',
+ `money` integer DEFAULT 0,
+ `public_notes` text DEFAULT '',
+ `dm_notes` text DEFAULT '',
+ FOREIGN KEY (`owner`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
+);
+--> statement-breakpoint
+INSERT INTO `__new_campaign`("id", "name", "owner", "link", "status", "inventory", "money", "public_notes", "dm_notes") SELECT "id", "name", "owner", "link", "status", "inventory", "money", "public_notes", "dm_notes" FROM `campaign`;--> statement-breakpoint
+DROP TABLE `campaign`;--> statement-breakpoint
+ALTER TABLE `__new_campaign` RENAME TO `campaign`;--> statement-breakpoint
+PRAGMA foreign_keys=ON;--> statement-breakpoint
+ALTER TABLE `campaign_members` DROP COLUMN `rights`;
\ No newline at end of file
diff --git a/drizzle/0024_secret_arclight.sql b/drizzle/0024_secret_arclight.sql
new file mode 100644
index 0000000..7b4c301
--- /dev/null
+++ b/drizzle/0024_secret_arclight.sql
@@ -0,0 +1,17 @@
+PRAGMA foreign_keys=OFF;--> statement-breakpoint
+CREATE TABLE `__new_campaign_logs` (
+ `id` integer,
+ `target` integer,
+ `timestamp` integer NOT NULL,
+ `type` text,
+ `details` text NOT NULL,
+ PRIMARY KEY(`id`, `target`, `timestamp`),
+ FOREIGN KEY (`id`) REFERENCES `campaign`(`id`) ON UPDATE cascade ON DELETE cascade,
+ FOREIGN KEY (`target`) REFERENCES `campaign_characters`(`character`) ON UPDATE cascade ON DELETE cascade
+);
+--> statement-breakpoint
+INSERT INTO `__new_campaign_logs`("id", "target", "timestamp", "type", "details") SELECT "id", "target", "timestamp", "type", "details" FROM `campaign_logs`;--> statement-breakpoint
+DROP TABLE `campaign_logs`;--> statement-breakpoint
+ALTER TABLE `__new_campaign_logs` RENAME TO `campaign_logs`;--> statement-breakpoint
+PRAGMA foreign_keys=ON;--> statement-breakpoint
+ALTER TABLE `campaign` ADD `settings` text DEFAULT '{}';
\ No newline at end of file
diff --git a/drizzle/meta/0023_snapshot.json b/drizzle/meta/0023_snapshot.json
new file mode 100644
index 0000000..1019587
--- /dev/null
+++ b/drizzle/meta/0023_snapshot.json
@@ -0,0 +1,1002 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "c5ebef95-5e53-49f8-a40d-23a9dc414a86",
+ "prevId": "98e63b73-701b-4ae1-8472-2ee706e064ff",
+ "tables": {
+ "campaign_characters": {
+ "name": "campaign_characters",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "character": {
+ "name": "character",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "campaign_characters_id_campaign_id_fk": {
+ "name": "campaign_characters_id_campaign_id_fk",
+ "tableFrom": "campaign_characters",
+ "tableTo": "campaign",
+ "columnsFrom": [
+ "id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "campaign_characters_character_character_id_fk": {
+ "name": "campaign_characters_character_character_id_fk",
+ "tableFrom": "campaign_characters",
+ "tableTo": "character",
+ "columnsFrom": [
+ "character"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {
+ "campaign_characters_id_character_pk": {
+ "columns": [
+ "id",
+ "character"
+ ],
+ "name": "campaign_characters_id_character_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "campaign_logs": {
+ "name": "campaign_logs",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "from": {
+ "name": "from",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "timestamp": {
+ "name": "timestamp",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "details": {
+ "name": "details",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "campaign_logs_id_campaign_id_fk": {
+ "name": "campaign_logs_id_campaign_id_fk",
+ "tableFrom": "campaign_logs",
+ "tableTo": "campaign",
+ "columnsFrom": [
+ "id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "campaign_logs_from_campaign_characters_character_fk": {
+ "name": "campaign_logs_from_campaign_characters_character_fk",
+ "tableFrom": "campaign_logs",
+ "tableTo": "campaign_characters",
+ "columnsFrom": [
+ "from"
+ ],
+ "columnsTo": [
+ "character"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {
+ "campaign_logs_id_from_timestamp_pk": {
+ "columns": [
+ "id",
+ "from",
+ "timestamp"
+ ],
+ "name": "campaign_logs_id_from_timestamp_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "campaign_members": {
+ "name": "campaign_members",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "user": {
+ "name": "user",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "campaign_members_id_campaign_id_fk": {
+ "name": "campaign_members_id_campaign_id_fk",
+ "tableFrom": "campaign_members",
+ "tableTo": "campaign",
+ "columnsFrom": [
+ "id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "campaign_members_user_users_id_fk": {
+ "name": "campaign_members_user_users_id_fk",
+ "tableFrom": "campaign_members",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {
+ "campaign_members_id_user_pk": {
+ "columns": [
+ "id",
+ "user"
+ ],
+ "name": "campaign_members_id_user_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "campaign": {
+ "name": "campaign",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner": {
+ "name": "owner",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "link": {
+ "name": "link",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'PREPARING'"
+ },
+ "inventory": {
+ "name": "inventory",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'[]'"
+ },
+ "money": {
+ "name": "money",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "public_notes": {
+ "name": "public_notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "dm_notes": {
+ "name": "dm_notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "''"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "campaign_owner_users_id_fk": {
+ "name": "campaign_owner_users_id_fk",
+ "tableFrom": "campaign",
+ "tableTo": "users",
+ "columnsFrom": [
+ "owner"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "character_abilities": {
+ "name": "character_abilities",
+ "columns": {
+ "character": {
+ "name": "character",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "ability": {
+ "name": "ability",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "max": {
+ "name": "max",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "character_abilities_character_character_id_fk": {
+ "name": "character_abilities_character_character_id_fk",
+ "tableFrom": "character_abilities",
+ "tableTo": "character",
+ "columnsFrom": [
+ "character"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {
+ "character_abilities_character_ability_pk": {
+ "columns": [
+ "character",
+ "ability"
+ ],
+ "name": "character_abilities_character_ability_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "character_choices": {
+ "name": "character_choices",
+ "columns": {
+ "character": {
+ "name": "character",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "choice": {
+ "name": "choice",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "character_choices_character_character_id_fk": {
+ "name": "character_choices_character_character_id_fk",
+ "tableFrom": "character_choices",
+ "tableTo": "character",
+ "columnsFrom": [
+ "character"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {
+ "character_choices_character_id_choice_pk": {
+ "columns": [
+ "character",
+ "id",
+ "choice"
+ ],
+ "name": "character_choices_character_id_choice_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "character_leveling": {
+ "name": "character_leveling",
+ "columns": {
+ "character": {
+ "name": "character",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "level": {
+ "name": "level",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "choice": {
+ "name": "choice",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "character_leveling_character_character_id_fk": {
+ "name": "character_leveling_character_character_id_fk",
+ "tableFrom": "character_leveling",
+ "tableTo": "character",
+ "columnsFrom": [
+ "character"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {
+ "character_leveling_character_level_pk": {
+ "columns": [
+ "character",
+ "level"
+ ],
+ "name": "character_leveling_character_level_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "character": {
+ "name": "character",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner": {
+ "name": "owner",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "people": {
+ "name": "people",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "level": {
+ "name": "level",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 1
+ },
+ "variables": {
+ "name": "variables",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'{\"health\": 0,\"mana\": 0,\"spells\": [],\"items\": [],\"exhaustion\": 0,\"sickness\": [],\"poisons\": []}'"
+ },
+ "aspect": {
+ "name": "aspect",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "public_notes": {
+ "name": "public_notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "private_notes": {
+ "name": "private_notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "visibility": {
+ "name": "visibility",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'private'"
+ },
+ "thumbnail": {
+ "name": "thumbnail",
+ "type": "blob",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "character_owner_users_id_fk": {
+ "name": "character_owner_users_id_fk",
+ "tableFrom": "character",
+ "tableTo": "users",
+ "columnsFrom": [
+ "owner"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "character_training": {
+ "name": "character_training",
+ "columns": {
+ "character": {
+ "name": "character",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "stat": {
+ "name": "stat",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "level": {
+ "name": "level",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "choice": {
+ "name": "choice",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "character_training_character_character_id_fk": {
+ "name": "character_training_character_character_id_fk",
+ "tableFrom": "character_training",
+ "tableTo": "character",
+ "columnsFrom": [
+ "character"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {
+ "character_training_character_stat_level_pk": {
+ "columns": [
+ "character",
+ "stat",
+ "level"
+ ],
+ "name": "character_training_character_stat_level_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "email_validation": {
+ "name": "email_validation",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "timestamp": {
+ "name": "timestamp",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "project_content": {
+ "name": "project_content",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "content": {
+ "name": "content",
+ "type": "blob",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "project_files": {
+ "name": "project_files",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner": {
+ "name": "owner",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "navigable": {
+ "name": "navigable",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "private": {
+ "name": "private",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "timestamp": {
+ "name": "timestamp",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "project_files_path_unique": {
+ "name": "project_files_path_unique",
+ "columns": [
+ "path"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "project_files_owner_users_id_fk": {
+ "name": "project_files_owner_users_id_fk",
+ "tableFrom": "project_files",
+ "tableTo": "users",
+ "columnsFrom": [
+ "owner"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "user_permissions": {
+ "name": "user_permissions",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "permission": {
+ "name": "permission",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_permissions_id_users_id_fk": {
+ "name": "user_permissions_id_users_id_fk",
+ "tableFrom": "user_permissions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_permissions_id_permission_pk": {
+ "columns": [
+ "id",
+ "permission"
+ ],
+ "name": "user_permissions_id_permission_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "user_sessions": {
+ "name": "user_sessions",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "timestamp": {
+ "name": "timestamp",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_sessions_user_id_users_id_fk": {
+ "name": "user_sessions_user_id_users_id_fk",
+ "tableFrom": "user_sessions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_sessions_id_user_id_pk": {
+ "columns": [
+ "id",
+ "user_id"
+ ],
+ "name": "user_sessions_id_user_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "users_data": {
+ "name": "users_data",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "signin": {
+ "name": "signin",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "lastTimestamp": {
+ "name": "lastTimestamp",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "users_data_id_users_id_fk": {
+ "name": "users_data_id_users_id_fk",
+ "tableFrom": "users_data",
+ "tableTo": "users",
+ "columnsFrom": [
+ "id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "users": {
+ "name": "users",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "hash": {
+ "name": "hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "state": {
+ "name": "state",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "users_username_unique": {
+ "name": "users_username_unique",
+ "columns": [
+ "username"
+ ],
+ "isUnique": true
+ },
+ "users_email_unique": {
+ "name": "users_email_unique",
+ "columns": [
+ "email"
+ ],
+ "isUnique": true
+ },
+ "users_hash_unique": {
+ "name": "users_hash_unique",
+ "columns": [
+ "hash"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {
+ "\"campaign\".\"description\"": "\"campaign\".\"public_notes\""
+ }
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/drizzle/meta/0024_snapshot.json b/drizzle/meta/0024_snapshot.json
new file mode 100644
index 0000000..c54a84f
--- /dev/null
+++ b/drizzle/meta/0024_snapshot.json
@@ -0,0 +1,1010 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "4ef0ea2b-0c07-438c-901f-0ef64bb5f749",
+ "prevId": "c5ebef95-5e53-49f8-a40d-23a9dc414a86",
+ "tables": {
+ "campaign_characters": {
+ "name": "campaign_characters",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "character": {
+ "name": "character",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "campaign_characters_id_campaign_id_fk": {
+ "name": "campaign_characters_id_campaign_id_fk",
+ "tableFrom": "campaign_characters",
+ "tableTo": "campaign",
+ "columnsFrom": [
+ "id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "campaign_characters_character_character_id_fk": {
+ "name": "campaign_characters_character_character_id_fk",
+ "tableFrom": "campaign_characters",
+ "tableTo": "character",
+ "columnsFrom": [
+ "character"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {
+ "campaign_characters_id_character_pk": {
+ "columns": [
+ "id",
+ "character"
+ ],
+ "name": "campaign_characters_id_character_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "campaign_logs": {
+ "name": "campaign_logs",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "target": {
+ "name": "target",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "timestamp": {
+ "name": "timestamp",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "details": {
+ "name": "details",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "campaign_logs_id_campaign_id_fk": {
+ "name": "campaign_logs_id_campaign_id_fk",
+ "tableFrom": "campaign_logs",
+ "tableTo": "campaign",
+ "columnsFrom": [
+ "id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "campaign_logs_target_campaign_characters_character_fk": {
+ "name": "campaign_logs_target_campaign_characters_character_fk",
+ "tableFrom": "campaign_logs",
+ "tableTo": "campaign_characters",
+ "columnsFrom": [
+ "target"
+ ],
+ "columnsTo": [
+ "character"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {
+ "campaign_logs_id_target_timestamp_pk": {
+ "columns": [
+ "id",
+ "target",
+ "timestamp"
+ ],
+ "name": "campaign_logs_id_target_timestamp_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "campaign_members": {
+ "name": "campaign_members",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "user": {
+ "name": "user",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "campaign_members_id_campaign_id_fk": {
+ "name": "campaign_members_id_campaign_id_fk",
+ "tableFrom": "campaign_members",
+ "tableTo": "campaign",
+ "columnsFrom": [
+ "id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "campaign_members_user_users_id_fk": {
+ "name": "campaign_members_user_users_id_fk",
+ "tableFrom": "campaign_members",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {
+ "campaign_members_id_user_pk": {
+ "columns": [
+ "id",
+ "user"
+ ],
+ "name": "campaign_members_id_user_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "campaign": {
+ "name": "campaign",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner": {
+ "name": "owner",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "link": {
+ "name": "link",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'PREPARING'"
+ },
+ "settings": {
+ "name": "settings",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'{}'"
+ },
+ "inventory": {
+ "name": "inventory",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'[]'"
+ },
+ "money": {
+ "name": "money",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "public_notes": {
+ "name": "public_notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "dm_notes": {
+ "name": "dm_notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "''"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "campaign_owner_users_id_fk": {
+ "name": "campaign_owner_users_id_fk",
+ "tableFrom": "campaign",
+ "tableTo": "users",
+ "columnsFrom": [
+ "owner"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "character_abilities": {
+ "name": "character_abilities",
+ "columns": {
+ "character": {
+ "name": "character",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "ability": {
+ "name": "ability",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "max": {
+ "name": "max",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "character_abilities_character_character_id_fk": {
+ "name": "character_abilities_character_character_id_fk",
+ "tableFrom": "character_abilities",
+ "tableTo": "character",
+ "columnsFrom": [
+ "character"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {
+ "character_abilities_character_ability_pk": {
+ "columns": [
+ "character",
+ "ability"
+ ],
+ "name": "character_abilities_character_ability_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "character_choices": {
+ "name": "character_choices",
+ "columns": {
+ "character": {
+ "name": "character",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "choice": {
+ "name": "choice",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "character_choices_character_character_id_fk": {
+ "name": "character_choices_character_character_id_fk",
+ "tableFrom": "character_choices",
+ "tableTo": "character",
+ "columnsFrom": [
+ "character"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {
+ "character_choices_character_id_choice_pk": {
+ "columns": [
+ "character",
+ "id",
+ "choice"
+ ],
+ "name": "character_choices_character_id_choice_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "character_leveling": {
+ "name": "character_leveling",
+ "columns": {
+ "character": {
+ "name": "character",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "level": {
+ "name": "level",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "choice": {
+ "name": "choice",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "character_leveling_character_character_id_fk": {
+ "name": "character_leveling_character_character_id_fk",
+ "tableFrom": "character_leveling",
+ "tableTo": "character",
+ "columnsFrom": [
+ "character"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {
+ "character_leveling_character_level_pk": {
+ "columns": [
+ "character",
+ "level"
+ ],
+ "name": "character_leveling_character_level_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "character": {
+ "name": "character",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner": {
+ "name": "owner",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "people": {
+ "name": "people",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "level": {
+ "name": "level",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 1
+ },
+ "variables": {
+ "name": "variables",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'{\"health\": 0,\"mana\": 0,\"spells\": [],\"items\": [],\"exhaustion\": 0,\"sickness\": [],\"poisons\": []}'"
+ },
+ "aspect": {
+ "name": "aspect",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "public_notes": {
+ "name": "public_notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "private_notes": {
+ "name": "private_notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "visibility": {
+ "name": "visibility",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'private'"
+ },
+ "thumbnail": {
+ "name": "thumbnail",
+ "type": "blob",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "character_owner_users_id_fk": {
+ "name": "character_owner_users_id_fk",
+ "tableFrom": "character",
+ "tableTo": "users",
+ "columnsFrom": [
+ "owner"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "character_training": {
+ "name": "character_training",
+ "columns": {
+ "character": {
+ "name": "character",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "stat": {
+ "name": "stat",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "level": {
+ "name": "level",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "choice": {
+ "name": "choice",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "character_training_character_character_id_fk": {
+ "name": "character_training_character_character_id_fk",
+ "tableFrom": "character_training",
+ "tableTo": "character",
+ "columnsFrom": [
+ "character"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {
+ "character_training_character_stat_level_pk": {
+ "columns": [
+ "character",
+ "stat",
+ "level"
+ ],
+ "name": "character_training_character_stat_level_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "email_validation": {
+ "name": "email_validation",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "timestamp": {
+ "name": "timestamp",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "project_content": {
+ "name": "project_content",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "content": {
+ "name": "content",
+ "type": "blob",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "project_files": {
+ "name": "project_files",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner": {
+ "name": "owner",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "navigable": {
+ "name": "navigable",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "private": {
+ "name": "private",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "timestamp": {
+ "name": "timestamp",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "project_files_path_unique": {
+ "name": "project_files_path_unique",
+ "columns": [
+ "path"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "project_files_owner_users_id_fk": {
+ "name": "project_files_owner_users_id_fk",
+ "tableFrom": "project_files",
+ "tableTo": "users",
+ "columnsFrom": [
+ "owner"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "user_permissions": {
+ "name": "user_permissions",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "permission": {
+ "name": "permission",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_permissions_id_users_id_fk": {
+ "name": "user_permissions_id_users_id_fk",
+ "tableFrom": "user_permissions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_permissions_id_permission_pk": {
+ "columns": [
+ "id",
+ "permission"
+ ],
+ "name": "user_permissions_id_permission_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "user_sessions": {
+ "name": "user_sessions",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "timestamp": {
+ "name": "timestamp",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_sessions_user_id_users_id_fk": {
+ "name": "user_sessions_user_id_users_id_fk",
+ "tableFrom": "user_sessions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_sessions_id_user_id_pk": {
+ "columns": [
+ "id",
+ "user_id"
+ ],
+ "name": "user_sessions_id_user_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "users_data": {
+ "name": "users_data",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "signin": {
+ "name": "signin",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "lastTimestamp": {
+ "name": "lastTimestamp",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "users_data_id_users_id_fk": {
+ "name": "users_data_id_users_id_fk",
+ "tableFrom": "users_data",
+ "tableTo": "users",
+ "columnsFrom": [
+ "id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "users": {
+ "name": "users",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "hash": {
+ "name": "hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "state": {
+ "name": "state",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "users_username_unique": {
+ "name": "users_username_unique",
+ "columns": [
+ "username"
+ ],
+ "isUnique": true
+ },
+ "users_email_unique": {
+ "name": "users_email_unique",
+ "columns": [
+ "email"
+ ],
+ "isUnique": true
+ },
+ "users_hash_unique": {
+ "name": "users_hash_unique",
+ "columns": [
+ "hash"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {
+ "\"campaign_logs\".\"from\"": "\"campaign_logs\".\"target\""
+ }
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index 7618071..cef2487 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -162,6 +162,20 @@
"when": 1762948869537,
"tag": "0022_warm_bushwacker",
"breakpoints": true
+ },
+ {
+ "idx": 23,
+ "version": "6",
+ "when": 1763462411934,
+ "tag": "0023_chunky_thunderbird",
+ "breakpoints": true
+ },
+ {
+ "idx": 24,
+ "version": "6",
+ "when": 1763479527696,
+ "tag": "0024_secret_arclight",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 87c6b51..d159b92 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -172,7 +172,6 @@ export default defineNuxtConfig({
sources: ['/api/__sitemap__/urls']
},
experimental: {
- buildCache: true,
componentIslands: {
selectiveClient: true,
},
diff --git a/server/api/campaign.post.ts b/server/api/campaign.post.ts
index a256874..b28f984 100644
--- a/server/api/campaign.post.ts
+++ b/server/api/campaign.post.ts
@@ -1,7 +1,7 @@
import { eq } from 'drizzle-orm';
import { z } from 'zod/v4';
import useDatabase from '~/composables/useDatabase';
-import { campaignMembersTable, campaignTable } from '~/db/schema';
+import { campaignTable } from '~/db/schema';
import { CampaignValidation } from '#shared/campaign.util';
import { cryptURI } from '#shared/general.util';
@@ -26,13 +26,15 @@ export default defineEventHandler(async (e) => {
const id = db.transaction((tx) => {
const id = tx.insert(campaignTable).values({
name: body.data.name,
- description: body.data.description,
+ public_notes: body.data.public_notes,
owner: session.user!.id,
+ dm_notes: body.data.dm_notes,
+ settings: body.data.settings,
link: '',
}).returning({ id: campaignTable.id }).get().id;
tx.update(campaignTable).set({ link: cryptURI('campaign', id) }).where(eq(campaignTable.id, id)).run();
-
+
return id;
});
diff --git a/server/api/campaign/[id].get.ts b/server/api/campaign/[id].get.ts
index 42674c4..26b750e 100644
--- a/server/api/campaign/[id].get.ts
+++ b/server/api/campaign/[id].get.ts
@@ -24,7 +24,7 @@ export default defineEventHandler(async (e) => {
members: { with: { member: { columns: { username: true, id: true } } }, columns: { id: false, user: false } },
characters: { with: { character: { columns: { id: true, name: true, owner: true } } }, columns: { character: false } },
owner: { columns: { username: true, id: true } },
- logs: { columns: { details: true, from: true, timestamp: true, type: true }, orderBy: ({ timestamp }) => timestamp },
+ logs: { columns: { details: true, target: true, timestamp: true, type: true }, orderBy: ({ timestamp }) => timestamp },
},
where: ({ id: _id }) => eq(_id, parseInt(id, 10)),
}).sync();
diff --git a/server/api/campaign/[id].post.ts b/server/api/campaign/[id].post.ts
index f9ae7f0..6c21ad3 100644
--- a/server/api/campaign/[id].post.ts
+++ b/server/api/campaign/[id].post.ts
@@ -39,7 +39,9 @@ export default defineEventHandler(async (e) => {
db.transaction((tx) => {
tx.update(campaignTable).set({
name: body.data.name,
- description: body.data.description,
+ public_notes: body.data.public_notes,
+ dm_notes: body.data.dm_notes,
+ settings: body.data.settings,
}).where(eq(campaignTable.id, id)).run();
});
}
diff --git a/server/api/character/[id].get.ts b/server/api/character/[id].get.ts
index a242622..031172f 100644
--- a/server/api/character/[id].get.ts
+++ b/server/api/character/[id].get.ts
@@ -33,7 +33,7 @@ export default defineEventHandler(async (e) => {
columns: { username: true }
},
campaign: {
- columns: { character: false, id: false, },
+ columns: { character: false, id: true, },
with: {
campaign: {
columns: { owner: true, },
@@ -70,6 +70,8 @@ export default defineEventHandler(async (e) => {
owner: character.owner,
username: character.user.username,
visibility: character.visibility,
+
+ campaign: character.campaign?.id,
} as Character;
}
diff --git a/server/routes/ws/campaign/[id].ts b/server/routes/ws/campaign/[id].ts
index 9ce346c..265ed45 100644
--- a/server/routes/ws/campaign/[id].ts
+++ b/server/routes/ws/campaign/[id].ts
@@ -1,30 +1,36 @@
-const TRIGGER_CHAR = {
- PING: String.fromCharCode(0x02),
- PONG: String.fromCharCode(0x03),
- STATUS: String.fromCharCode(0x04),
-};
+import type { SocketMessage } from "#shared/websocket.util";
+import type { User } from "~/types/auth";
export default defineWebSocketHandler({
message(peer, message) {
- switch(message.rawData)
+ const data = message.json();
+ switch(data.type)
{
- case TRIGGER_CHAR.PING:
- peer.send(TRIGGER_CHAR.PONG);
- return;
- default:
+ case 'PING':
+ peer.send(JSON.stringify({ type: 'PONG' }));
return;
+
+ default: return;
}
},
- open(peer) {
+ async open(peer) {
const id = new URL(peer.request.url).pathname.split('/').slice(-1)[0];
if(!id) return peer.close();
- peer.subscribe(`campaigns/${id}`);
- peer.publish(`campaigns/${id}`, `${TRIGGER_CHAR.STATUS}`);
+
+ const session = await getUserSession(peer);
+ if(!session ||!session.user) return peer.close();
+ peer.context.user = session.user;
+
+ const topic = `campaigns/${id}`;
+ peer.subscribe(topic);
+ peer.publish(topic, { type: 'user', data: [{ user: (peer.context.user as User).id, status: true }] });
+ peer.send({ type: 'user', data: peer.peers.values().filter(e => e.topics.has(topic)).map(e => ({ user: (e.context.user as User).id, status: true })).toArray() })
},
close(peer, details) {
const id = new URL(peer.request.url).pathname.split('/').slice(-1)[0];
if(!id) return peer.close();
- peer.publish(`campaigns/${id}`, false);
+
+ peer.publish(`campaigns/${id}`, { type: 'user', data: [{ user: (peer.context.user as User).id, status: false }] });
peer.unsubscribe(`campaigns/${id}`);
}
});
\ No newline at end of file
diff --git a/server/utils/session.ts b/server/utils/session.ts
index 0039ac5..e4d25a6 100644
--- a/server/utils/session.ts
+++ b/server/utils/session.ts
@@ -4,6 +4,7 @@ import { defu } from 'defu'
import { createHooks } from 'hookable'
import { useRuntimeConfig } from '#imports'
import type { UserSession, UserSessionRequired } from '~/types/auth'
+import type { CompatEvent } from '~~/shared/websocket.util'
export interface SessionHooks {
/**
@@ -11,11 +12,11 @@ export interface SessionHooks {
* - Add extra properties to the session
* - Throw an error if the session could not be verified (with a database for example)
*/
- fetch: (session: UserSessionRequired, event: H3Event) => void | Promise
+ fetch: (session: UserSessionRequired, event: H3Event | CompatEvent) => void | Promise
/**
* Called before clearing the session
*/
- clear: (session: UserSession, event: H3Event) => void | Promise
+ clear: (session: UserSession, event: H3Event | CompatEvent) => void | Promise
}
export const sessionHooks = createHooks()
@@ -25,7 +26,7 @@ export const sessionHooks = createHooks()
* @param event The Request (h3) event
* @returns The user session
*/
-export async function getUserSession(event: H3Event) {
+export async function getUserSession(event: H3Event | CompatEvent) {
const session = await _useSession(event);
if(!session.data || !session.data.id)
@@ -41,7 +42,7 @@ export async function getUserSession(event: H3Event) {
* @param data User session data, please only store public information since it can be decoded with API calls
* @see https://github.com/atinux/nuxt-auth-utils
*/
-export async function setUserSession(event: H3Event, data: UserSession) {
+export async function setUserSession(event: H3Event | CompatEvent, data: UserSession) {
const session = await _useSession(event)
await session.update(defu(data, session.data))
@@ -54,7 +55,7 @@ export async function setUserSession(event: H3Event, data: UserSession) {
* @param event The Request (h3) event
* @param data User session data, please only store public information since it can be decoded with API calls
*/
-export async function replaceUserSession(event: H3Event, data: UserSession) {
+export async function replaceUserSession(event: H3Event | CompatEvent, data: UserSession) {
const session = await _useSession(event)
await session.clear()
@@ -68,7 +69,7 @@ export async function replaceUserSession(event: H3Event, data: UserSession) {
* @param event The Request (h3) event
* @returns true if the session was cleared
*/
-export async function clearUserSession(event: H3Event) {
+export async function clearUserSession(event: H3Event | CompatEvent) {
const session = await _useSession(event)
await sessionHooks.callHookParallel('clear', session.data, event)
@@ -85,7 +86,7 @@ export async function clearUserSession(event: H3Event) {
* @param opts.message The message to use for the error (defaults to Unauthorized)
* @see https://github.com/atinux/nuxt-auth-utils
*/
-export async function requireUserSession(event: H3Event, opts: { statusCode?: number, message?: string } = {}): Promise {
+export async function requireUserSession(event: H3Event | CompatEvent, opts: { statusCode?: number, message?: string } = {}): Promise {
const userSession = await getUserSession(event)
if (!userSession.user) {
@@ -100,9 +101,9 @@ export async function requireUserSession(event: H3Event, opts: { statusCode?: nu
let sessionConfig: SessionConfig
-function _useSession(event: H3Event) {
- if (!sessionConfig) {
- const runtimeConfig = useRuntimeConfig(event)
+function _useSession(event: H3Event | CompatEvent) {
+ if (!sessionConfig && '__is_event__' in event) {
+ const runtimeConfig = useRuntimeConfig(event);
sessionConfig = runtimeConfig.session;
}
diff --git a/shared/campaign.util.ts b/shared/campaign.util.ts
index d5094d9..9b86390 100644
--- a/shared/campaign.util.ts
+++ b/shared/campaign.util.ts
@@ -8,11 +8,14 @@ import { tooltip } from "#shared/floating.util";
import markdown from "#shared/markdown.util";
import { preview } from "./proses";
import { format } from "./general.util";
+import { Socket } from "#shared/websocket.util";
export const CampaignValidation = z.object({
id: z.number(),
name: z.string().nonempty(),
- description: z.string()
+ public_notes: z.string(),
+ dm_notes: z.string(),
+ settings: z.object(),
});
class CharacterPrinter
@@ -41,29 +44,39 @@ class CharacterPrinter
}
}
type PlayerState = {
- status: boolean;
statusDOM: HTMLElement;
statusTooltip: Text;
+ dom: HTMLElement;
user: { id: number, username: string };
};
+const activity = {
+ online: { class: 'absolute -bottom-1 -right-1 rounded-full w-3 h-3 block border-2 box-content bg-light-green dark:bg-dark-green border-light-green dark:border-dark-green', text: 'En ligne' },
+ afk: { class: 'absolute -bottom-1 -right-1 rounded-full w-3 h-3 block border-2 box-content bg-light-yellow dark:bg-dark-yellow border-light-yellow dark:border-dark-yellow', text: 'Inactif' },
+ offline: { class: 'absolute -bottom-1 -right-1 rounded-full w-3 h-3 block border-2 box-content border-dashed border-light-50 dark:border-dark-50 bg-light-0 dark:bg-dark-0', text: 'Hors ligne' },
+}
function defaultPlayerState(user: { id: number, username: string }): PlayerState
{
- const statusTooltip = text('Absent');
+ const statusTooltip = text(activity.offline.text), statusDOM = span(activity.offline.class);
return {
- status: false,
- statusDOM: tooltip(span('rounded-full w-3 h-3 block border-light-50 dark:border-dark-50 border-2 border-dashed'), statusTooltip, 'right'),
+ statusDOM,
statusTooltip,
+ dom: div('w-8 h-8 relative flex items-center justify-center border border-light-40 dark:border-dark-40 box-content rounded-full', [ tooltip(icon('radix-icons:person', { width: 24, height: 24, class: 'text-light-70 dark:text-dark-70' }), user.username, 'bottom'), tooltip(statusDOM, statusTooltip, 'bottom') ]),
user
}
}
export class CampaignSheet
{
- user: ComputedRef;
- campaign?: Campaign;
- container: HTMLElement = div('flex flex-col flex-1 h-full w-full items-center justify-start');
- dm!: PlayerState;
- players!: Array;
- characters!: Array;
+ private user: ComputedRef;
+ private campaign?: Campaign;
+
+ container: HTMLElement = div('flex flex-col flex-1 h-full w-full items-center justify-start gap-6');
+
+ private dm!: PlayerState;
+ private players!: Array;
+ private characters!: Array;
+
+ ws?: Socket;
+
constructor(id: string, user: ComputedRef)
{
this.user = user;
@@ -76,6 +89,26 @@ export class CampaignSheet
this.dm = defaultPlayerState(campaign.owner);
this.players = campaign.members.map(e => defaultPlayerState(e.member));
this.characters = campaign.characters.map(e => new CharacterPrinter(e.character!.id, e.character!.name));
+ this.ws = new Socket(`/ws/campaign/${id}`, true);
+ this.ws.handleMessage<{ user: number, status: boolean }[]>('user', (users) => {
+ users.forEach(user => {
+ if(this.dm.user.id === user.user)
+ {
+ this.dm.statusTooltip.textContent = activity[user.status ? 'online' : 'offline'].text;
+ this.dm.statusDOM.className = activity[user.status ? 'online' : 'offline'].class;
+ }
+ else
+ {
+ const player = this.players.find(e => e.user.id === user.user)
+
+ if(player)
+ {
+ player.statusTooltip.textContent = activity[user.status ? 'online' : 'offline'].text;
+ player.statusDOM.className = activity[user.status ? 'online' : 'offline'].class;
+ }
+ }
+ })
+ });
document.title = `d[any] - Campagne ${campaign.name}`;
this.render();
@@ -102,11 +135,11 @@ export class CampaignSheet
if(!campaign)
return;
- this.container.replaceChildren(div('grid grid-cols-3 gap-2 py-4', [
+ this.container.replaceChildren(div('grid grid-cols-3 gap-2', [
div('flex flex-row gap-2 items-center py-2', [
- tooltip(div('w-8 h-8 border border-light-40 dark:border-dark-40 box-content rounded-full'), this.dm.user.username, "bottom"),
+ this.dm.dom,
div('border-l h-full w-0 border-light-40 dark:border-dark-40'),
- div('flex flex-row gap-1', this.players.map(e => tooltip(div('w-8 h-8 border border-light-40 dark:border-dark-40 box-content rounded-full'), e.user.username, "bottom"))),
+ div('flex flex-row gap-1', this.players.map(e => e.dom)),
]),
div('flex flex-1 flex-col items-center justify-center gap-2', [
span('text-2xl font-serif font-bold italic', campaign.name),
diff --git a/shared/character.util.ts b/shared/character.util.ts
index e40e520..4677e52 100644
--- a/shared/character.util.ts
+++ b/shared/character.util.ts
@@ -10,6 +10,7 @@ import markdown from "#shared/markdown.util";
import { getText } from "#shared/i18n";
import type { User } from "~/types/auth";
import { MarkdownEditor } from "#shared/editor.util";
+import { Socket } from "#shared/websocket.util";
const config = characterConfig as CharacterConfig;
@@ -1293,10 +1294,12 @@ const subnameFactory = (item: ItemConfig, state?: ItemState): string[] => {
}
export class CharacterSheet
{
- user: ComputedRef;
- character?: CharacterCompiler;
+ private user: ComputedRef;
+ private character?: CharacterCompiler;
container: HTMLElement = div('flex flex-1 h-full w-full items-start justify-center');
- tabs?: HTMLDivElement & { refresh: () => void };
+ private tabs?: HTMLDivElement & { refresh: () => void };
+
+ ws?: Socket;
constructor(id: string, user: ComputedRef)
{
this.user = user;
@@ -1307,6 +1310,11 @@ export class CharacterSheet
{
this.character = new CharacterCompiler(character);
+ if(character.campaign)
+ {
+ this.ws = new Socket(`/ws/campaign/${character.campaign}`, true);
+ }
+
document.title = `d[any] - ${character.name}`;
load.remove();
diff --git a/shared/general.util.ts b/shared/general.util.ts
index 26a6990..313315f 100644
--- a/shared/general.util.ts
+++ b/shared/general.util.ts
@@ -1,4 +1,5 @@
-const ID_SIZE = 32;
+const ID_SIZE = 24;
+const URLSafeCharacters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.~';
export function unifySlug(slug: string | string[]): string
{
@@ -7,9 +8,39 @@ export function unifySlug(slug: string | string[]): string
export function getID()
{
for (var id = [], i = 0; i < ID_SIZE; i++)
- id.push((36 * Math.random() | 0).toString(36));
+ id.push(URLSafeCharacters[URLSafeCharacters.length * Math.random() | 0]);
return id.join("");
}
+export function encodeBase(value: string): string
+{
+ if(value === '')
+ return '';
+
+ const buffer = Buffer.from(value, 'utf8'), base = BigInt(URLSafeCharacters.length);
+ let nb = BigInt('0x' + buffer.toHex()), result = [];
+
+ while(nb > 0)
+ {
+ const remaining = nb % base;
+ result.push(URLSafeCharacters[Number(remaining)]);
+ nb = (nb - remaining) / base;
+ }
+
+ const text = result.reverse().join('');
+ return text;
+}
+export function decodeBase(value: string): string
+{
+ if(value === '')
+ return '';
+
+ const result = '', base = BigInt(URLSafeCharacters.length);
+ let nb = BigInt(0);
+ value.split('').forEach(e => nb = nb * base + BigInt(URLSafeCharacters.indexOf(e)));
+
+ const text = Buffer.from(nb.toString(16), 'hex').toString('utf8');
+ return text;
+}
export function group<
T,
K extends keyof T,
@@ -56,7 +87,7 @@ export function format(date: Date, template: string): string
for(const key of keys)
{
- template = template.replaceAll(key, () => transforms[key]!(date));
+ template = key in transforms ? template.replaceAll(key, () => transforms[key]!(date)) : template;
}
return template;
@@ -70,33 +101,29 @@ export function lerp(x: number, a: number, b: number): number
return (1-x)*a+x*b;
}
// The value position is randomized
-// The metadata separator is randomized as a letter (to avoid collision with numbers)
-// The URI is (| == picked separator) |v_length|first part of the hash + seed + second part of the hash|v_pos|seed as hex.
-// Every number are converted to string as hexadecimal values
-export function cryptURI(key: string, value: number, seed?: number): string
+// The metadata separator is randomized from the URLSafeCharacters set
+// The URI is (| == picked separator) |v_length|first part of the hash + value + second part of the hash|v_pos as hex.
+export function cryptURI(key: string, value: number): string
{
- const _seed = seed ?? Date.now();
- const hash = Bun.hash(key + value.toString(), _seed).toString(16);
+ const hash = Bun.hash.crc32(key + value.toString()).toString(16);
const pos = Math.floor(Math.random() * hash.length);
- const separator = String.fromCharCode(Math.floor(Math.random() * 26 + 96));
+ const separator = URLSafeCharacters[URLSafeCharacters.length * Math.random() | 0]!;
- return Bun.zstdCompressSync(separator + value.toString(16).length + separator + hash.substring(0, pos) + value + hash.substring(pos) + separator + pos + separator + _seed.toString(16), { level: 1 }).toString('base64');
+ return encodeBase(separator + value.toString().length + separator + hash.substring(0, pos) + value + hash.substring(pos) + separator + pos);
}
export function decryptURI(uri: string, key: string): number | undefined
{
- const decoder = new TextDecoder();
- const _uri = decoder.decode(Bun.zstdDecompressSync(Buffer.from(uri, 'base64')));
-
+ const _uri = decodeBase(uri);
+
const separator = _uri.charAt(0);
const length = parseInt(_uri.substring(1, _uri.indexOf(separator, 1)), 10);
- const seed = parseInt(_uri.substring(_uri.lastIndexOf(separator) + 1), 16);
- const pos = parseInt(_uri.substring(_uri.lastIndexOf(separator, _uri.length - seed.toString(16).length - 2) + 1, _uri.lastIndexOf(separator)), 10);
- const _hash = _uri.substring(2 + length.toString(10).length, _uri.length - (2 + seed.toString(16).length + pos.toString(10).length));
+ const pos = parseInt(_uri.substring(_uri.lastIndexOf(separator) + 1), 16);
+ const _hash = _uri.substring(_uri.lastIndexOf(separator, _uri.length - pos.toString(16).length - 2) + 1, _uri.lastIndexOf(separator));
const value = _hash.substring(pos, pos + length);
const hash = _hash.substring(0, pos) + _hash.substring(pos + length);
- if(Bun.hash(key + value, seed).toString(16) === hash)
+ if(Bun.hash.crc32(key + value).toString(16) === hash)
return parseInt(value, 10);
else
return undefined;
diff --git a/shared/websocket.util.ts b/shared/websocket.util.ts
new file mode 100644
index 0000000..1f29973
--- /dev/null
+++ b/shared/websocket.util.ts
@@ -0,0 +1,77 @@
+export type SocketMessage = {
+ type: string;
+ data: any;
+}
+export type CompatEvent = {
+ request: {
+ headers: Headers;
+ };
+ context: any;
+} | {
+ headers: Headers;
+ context: any;
+};
+
+export class Socket
+{
+ private _frequency?: number;
+ private _timeout?: number;
+ private _heartbeat: boolean = false;
+ private _heartbeatWaiting = false;
+ private _timeoutTimer?: NodeJS.Timeout;
+
+ private _ws: WebSocket;
+ private _handlers: Map void> = new Map();
+
+ constructor(url: string, heartbeat?: true | { timeout?: number, frequency?: number })
+ {
+ this._frequency = heartbeat === true ? 10000 : heartbeat?.frequency ?? 10000;
+ this._timeout = heartbeat === true ? 100000 : heartbeat?.timeout ?? 100000;
+ this._heartbeat = heartbeat !== undefined;
+
+ this._ws = new WebSocket(`${location.protocol.endsWith('s:') ? 'wss' : 'ws'}://${location.host}${url.startsWith('/') ? url : '/' + url}`);
+
+ this._ws.addEventListener('open', (e) => {
+ console.log(`[ws] Connected to ${this._ws.url}`);
+ this._heartbeat && setTimeout(() => this.heartbeat(), this._frequency);
+ });
+ this._ws.addEventListener('close', (e) => {
+ console.log(`[ws] Disconnected from ${this._ws.url} (code: ${e.code}, reason: ${e.reason}, ${e.wasClean ? 'clean close' : 'dirty close'})`)
+ this._heartbeatWaiting = false;
+ this._timeoutTimer && clearTimeout(this._timeoutTimer);
+ });
+ this._ws.addEventListener('message', (e) => {
+ const data = JSON.parse(e.data) as SocketMessage;
+
+ switch(data.type)
+ {
+ case 'PONG':
+ if(this._heartbeatWaiting)
+ {
+ this._heartbeatWaiting = false;
+ this._timeoutTimer && clearTimeout(this._timeoutTimer);
+ this._heartbeat && setTimeout(() => this.heartbeat(), this._frequency);
+ }
+ return;
+ default: return this._handlers.has(data.type) && queueMicrotask(() => this._handlers.get(data.type)!(data.data));
+ }
+ });
+ }
+ public handleMessage(type: string, callback: (data: T) => void)
+ {
+ this._handlers.set(type, callback);
+ }
+ public close()
+ {
+ this._ws.close(1000);
+ }
+ private heartbeat()
+ {
+ if(this._heartbeat && this._ws.readyState === WebSocket.OPEN)
+ {
+ this._ws.send(JSON.stringify({ type: 'PING' }));
+ this._heartbeatWaiting = true;
+ this._timeoutTimer = setTimeout(() => this._ws.close(3000, 'Timeout'), this._timeout);
+ }
+ }
+}
\ No newline at end of file