diff --git a/.prettierrc b/.prettierrc index 819fa57..2fb2353 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,5 @@ { - "useTabs": true, + "useTabs": false, "singleQuote": true, "trailingComma": "none", "printWidth": 100, @@ -12,5 +12,5 @@ } } ], - "tailwindStylesheet": "./src/routes/layout.css" + "tailwindStylesheet": "./src/app.css" } diff --git a/bun.lock b/bun.lock index 0fcf3e3..1e384be 100644 --- a/bun.lock +++ b/bun.lock @@ -51,9 +51,9 @@ "packages": { "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], - "@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], + "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], - "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], + "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], @@ -381,20 +381,10 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - - "better-sqlite3": ["better-sqlite3@12.5.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg=="], - - "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], - - "bits-ui": ["bits-ui@2.14.4", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.35.1", "svelte-toolbelt": "^0.10.6", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-W6kenhnbd/YVvur+DKkaVJ6GldE53eLewur5AhUCqslYQ0vjZr8eWlOfwZnMiPB+PF5HMVqf61vXBvmyrAmPWg=="], - - "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + "bits-ui": ["bits-ui@2.15.2", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.35.1", "svelte-toolbelt": "^0.10.6", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-S8eDbFkZCN17kZ7J9fD3MRXziV9ozjdFt2D3vTr2bvXCl7BtrIqguYt2U/zrFgLdR2erwybvCKv0JXYn8uKLDQ=="], "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "bun-sqlite-key-value": ["bun-sqlite-key-value@1.13.1", "", { "peerDependencies": { "typescript": "^5.5.3" } }, "sha512-cb3thB8QXPeXB6B7NhObpADEYvtVNwqg/0ED7PgKt2OxVAxPSejkiTsy1+byQDC0AwLYajw3nhtr/ubKvcLcKw=="], @@ -407,8 +397,6 @@ "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], - "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -429,10 +417,6 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], - - "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], @@ -447,8 +431,6 @@ "drizzle-orm": ["drizzle-orm@0.45.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="], - "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], @@ -483,8 +465,6 @@ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], @@ -497,8 +477,6 @@ "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], - "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], - "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], @@ -507,16 +485,12 @@ "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], - "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], - "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], @@ -527,18 +501,12 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], @@ -611,14 +579,8 @@ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], - "mode-watcher": ["mode-watcher@1.1.0", "", { "dependencies": { "runed": "^0.25.0", "svelte-toolbelt": "^0.7.1" }, "peerDependencies": { "svelte": "^5.27.0" } }, "sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g=="], "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], @@ -629,18 +591,12 @@ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "node-abi": ["node-abi@3.85.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg=="], - "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], @@ -671,8 +627,6 @@ "postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="], - "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], "prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="], @@ -683,14 +637,8 @@ "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], - "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], - - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], @@ -705,8 +653,6 @@ "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], @@ -715,10 +661,6 @@ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], - - "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], - "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -727,8 +669,6 @@ "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], - "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], @@ -755,20 +695,14 @@ "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], - "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], - - "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], - "ts-api-utils": ["ts-api-utils@2.3.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg=="], + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], - "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], @@ -797,8 +731,6 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], @@ -815,9 +747,9 @@ "@rollup/plugin-commonjs/is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], @@ -837,8 +769,6 @@ "mode-watcher/svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="], - "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - "vite/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], diff --git a/drizzle/0007_outstanding_punisher.sql b/drizzle/0007_outstanding_punisher.sql new file mode 100644 index 0000000..b8beb42 --- /dev/null +++ b/drizzle/0007_outstanding_punisher.sql @@ -0,0 +1,9 @@ +CREATE TABLE `invite` ( + `id` text PRIMARY KEY NOT NULL, + `server_id` text NOT NULL, + `code` text NOT NULL, + FOREIGN KEY (`server_id`) REFERENCES `server`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +ALTER TABLE `friendRequest` ADD `from_username` text NOT NULL REFERENCES user(id);--> statement-breakpoint +ALTER TABLE `friendRequest` ADD `to_username` text NOT NULL REFERENCES user(id); \ No newline at end of file diff --git a/drizzle/0008_mute_lilandra.sql b/drizzle/0008_mute_lilandra.sql new file mode 100644 index 0000000..5ab8d4a --- /dev/null +++ b/drizzle/0008_mute_lilandra.sql @@ -0,0 +1,9 @@ +CREATE TABLE `directMessage` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `server_id` text NOT NULL, + `messages` text DEFAULT '[]' NOT NULL, + FOREIGN KEY (`server_id`) REFERENCES `server`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +ALTER TABLE `user` ADD `status_overwrite` integer DEFAULT 3 NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0007_snapshot.json b/drizzle/meta/0007_snapshot.json new file mode 100644 index 0000000..29029e3 --- /dev/null +++ b/drizzle/meta/0007_snapshot.json @@ -0,0 +1,450 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "c0ac7678-4e2c-433f-ac26-73c7b88cc38b", + "prevId": "e809c266-891b-4355-a87c-facd55a5293a", + "tables": { + "channel": { + "name": "channel", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "messages": { + "name": "messages", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": {}, + "foreignKeys": { + "channel_server_id_server_id_fk": { + "name": "channel_server_id_server_id_fk", + "tableFrom": "channel", + "tableTo": "server", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "friendRequest": { + "name": "friendRequest", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "from_user": { + "name": "from_user", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "from_username": { + "name": "from_username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "to_username": { + "name": "to_username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "to_user": { + "name": "to_user", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "friendRequest_from_user_user_id_fk": { + "name": "friendRequest_from_user_user_id_fk", + "tableFrom": "friendRequest", + "tableTo": "user", + "columnsFrom": [ + "from_user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "friendRequest_from_username_user_id_fk": { + "name": "friendRequest_from_username_user_id_fk", + "tableFrom": "friendRequest", + "tableTo": "user", + "columnsFrom": [ + "from_username" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "friendRequest_to_username_user_id_fk": { + "name": "friendRequest_to_username_user_id_fk", + "tableFrom": "friendRequest", + "tableTo": "user", + "columnsFrom": [ + "to_username" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "friendRequest_to_user_user_id_fk": { + "name": "friendRequest_to_user_user_id_fk", + "tableFrom": "friendRequest", + "tableTo": "user", + "columnsFrom": [ + "to_user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "group": { + "name": "group", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "members": { + "name": "members", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": {}, + "foreignKeys": { + "group_owner_user_id_fk": { + "name": "group_owner_user_id_fk", + "tableFrom": "group", + "tableTo": "user", + "columnsFrom": [ + "owner" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invite": { + "name": "invite", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "invite_server_id_server_id_fk": { + "name": "invite_server_id_server_id_fk", + "tableFrom": "invite", + "tableTo": "server", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "server": { + "name": "server", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "members": { + "name": "members", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "channels": { + "name": "channels", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": {}, + "foreignKeys": { + "server_owner_user_id_fk": { + "name": "server_owner_user_id_fk", + "tableFrom": "server", + "tableTo": "user", + "columnsFrom": [ + "owner" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "friends": { + "name": "friends", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "servers": { + "name": "servers", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "groups": { + "name": "groups", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": { + "user_username_unique": { + "name": "user_username_unique", + "columns": [ + "username" + ], + "isUnique": true + }, + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0008_snapshot.json b/drizzle/meta/0008_snapshot.json new file mode 100644 index 0000000..3f8dbe8 --- /dev/null +++ b/drizzle/meta/0008_snapshot.json @@ -0,0 +1,511 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "bd16f3db-5965-4d36-89b7-d259cbd21149", + "prevId": "c0ac7678-4e2c-433f-ac26-73c7b88cc38b", + "tables": { + "channel": { + "name": "channel", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "messages": { + "name": "messages", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": {}, + "foreignKeys": { + "channel_server_id_server_id_fk": { + "name": "channel_server_id_server_id_fk", + "tableFrom": "channel", + "tableTo": "server", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "directMessage": { + "name": "directMessage", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "messages": { + "name": "messages", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": {}, + "foreignKeys": { + "directMessage_server_id_server_id_fk": { + "name": "directMessage_server_id_server_id_fk", + "tableFrom": "directMessage", + "tableTo": "server", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "friendRequest": { + "name": "friendRequest", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "from_user": { + "name": "from_user", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "from_username": { + "name": "from_username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "to_username": { + "name": "to_username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "to_user": { + "name": "to_user", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "friendRequest_from_user_user_id_fk": { + "name": "friendRequest_from_user_user_id_fk", + "tableFrom": "friendRequest", + "tableTo": "user", + "columnsFrom": [ + "from_user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "friendRequest_from_username_user_id_fk": { + "name": "friendRequest_from_username_user_id_fk", + "tableFrom": "friendRequest", + "tableTo": "user", + "columnsFrom": [ + "from_username" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "friendRequest_to_username_user_id_fk": { + "name": "friendRequest_to_username_user_id_fk", + "tableFrom": "friendRequest", + "tableTo": "user", + "columnsFrom": [ + "to_username" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "friendRequest_to_user_user_id_fk": { + "name": "friendRequest_to_user_user_id_fk", + "tableFrom": "friendRequest", + "tableTo": "user", + "columnsFrom": [ + "to_user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "group": { + "name": "group", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "members": { + "name": "members", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": {}, + "foreignKeys": { + "group_owner_user_id_fk": { + "name": "group_owner_user_id_fk", + "tableFrom": "group", + "tableTo": "user", + "columnsFrom": [ + "owner" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invite": { + "name": "invite", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "invite_server_id_server_id_fk": { + "name": "invite_server_id_server_id_fk", + "tableFrom": "invite", + "tableTo": "server", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "server": { + "name": "server", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "members": { + "name": "members", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "channels": { + "name": "channels", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": {}, + "foreignKeys": { + "server_owner_user_id_fk": { + "name": "server_owner_user_id_fk", + "tableFrom": "server", + "tableTo": "user", + "columnsFrom": [ + "owner" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_overwrite": { + "name": "status_overwrite", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "friends": { + "name": "friends", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "servers": { + "name": "servers", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "groups": { + "name": "groups", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": { + "user_username_unique": { + "name": "user_username_unique", + "columns": [ + "username" + ], + "isUnique": true + }, + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index f5ee151..7217414 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -50,6 +50,20 @@ "when": 1767537153441, "tag": "0006_gifted_machine_man", "breakpoints": true + }, + { + "idx": 7, + "version": "6", + "when": 1767543898231, + "tag": "0007_outstanding_punisher", + "breakpoints": true + }, + { + "idx": 8, + "version": "6", + "when": 1767549353944, + "tag": "0008_mute_lilandra", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/hooks.server.ts b/src/hooks.server.ts index e4d0856..f861857 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,27 +1,81 @@ import type { Handle } from '@sveltejs/kit'; import * as auth from '$lib/server/auth'; +import { kvStore } from '$lib/server/db'; +import { _sendToSubscribers } from './routes/api/updates/+server'; +import { Status } from '$lib'; const handleAuth: Handle = async ({ event, resolve }) => { - const sessionToken = event.cookies.get(auth.sessionCookieName); + const sessionToken = event.cookies.get(auth.sessionCookieName); - if (!sessionToken) { - event.locals.user = null; - event.locals.session = null; + if (!sessionToken) { + event.locals.user = null; + event.locals.session = null; - return resolve(event); - } + return resolve(event); + } - const { session, user } = await auth.validateSessionToken(sessionToken); - if (session) { - auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); - } else { - auth.deleteSessionTokenCookie(event); - } + const { session, user } = await auth.validateSessionToken(sessionToken); + if (session) { + auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); + } else { + auth.deleteSessionTokenCookie(event); + } - event.locals.user = user; - event.locals.session = session; + event.locals.user = user; + event.locals.session = session; - return resolve(event); + if (event.locals.user) { + const id = event.locals.user.id; + const overwrite = event.locals.user.statusOverwrite as 1 | 2 | 3 | undefined; + const now = Date.now(); + + const lastActiveKey = `user-${id}-last-active`; + const stateKey = `user-${id}-state`; + const timerKey = `user-${id}-offline-timer`; + + const lastActive = kvStore.get(lastActiveKey) ?? 0; + const state = kvStore.get<1 | 2 | 3>(stateKey) ?? Status.OFFLINE; + + // always update activity + kvStore.set(lastActiveKey, now); + if (overwrite !== Status.OFFLINE) { + const shouldSend = state === Status.OFFLINE && now - lastActive > 5000; + + if (shouldSend) { + const outgoingStatus = overwrite === Status.DND ? Status.DND : Status.ONLINE; + + _sendToSubscribers(id, { + type: 'status', + id, + status: outgoingStatus + }); + + kvStore.set(stateKey, outgoingStatus); + } + } + const existingTimer = kvStore.get(timerKey); + if (existingTimer) { + clearTimeout(existingTimer); + } + + const offlineTimer = setTimeout(() => { + const last = kvStore.get(lastActiveKey) ?? 0; + + if (Date.now() - last >= 10_000) { + _sendToSubscribers(id, { + type: 'status', + id, + status: Status.OFFLINE + }); + + kvStore.set(stateKey, Status.OFFLINE); + kvStore.delete(timerKey); + } + }, 10_000); + + kvStore.set(timerKey, offlineTimer + ''); + } + return resolve(event); }; export const handle: Handle = handleAuth; diff --git a/src/lib/components/app-sidebar.svelte b/src/lib/components/app-sidebar.svelte index fc99ced..9e3f76e 100644 --- a/src/lib/components/app-sidebar.svelte +++ b/src/lib/components/app-sidebar.svelte @@ -1,22 +1,27 @@ @@ -25,7 +30,7 @@
@@ -33,193 +38,170 @@ chat.sad.ovh
-
+
+ + + + - - - - + +
+ + + Cancel + + +
+ + + + Outgoing + Incoming + - - - Add a friend - - Add a friend using their username or manage pending requests. - - + + + {#if user.friendRequests.filter((r) => r.fromUser === user.id).length === 0} +

No outgoing requests

+ {:else} + {#each user.friendRequests.filter((r) => r.fromUser === user.id) as request (request.id)} + + + {request.username} + Request sent + + +
+ + +
+
+
+ {/each} + {/if} +
- -
- - - - Cancel - - - -
+ + + {#if user.friendRequests.filter((r) => r.toUser === user.id).length === 0} +

No incoming requests

+ {:else} + {#each user.friendRequests.filter((r) => r.toUser === user.id) as request (request.id)} + + + {request.username} + Sent you a friend request + + + +
+ + +
+ +
+ + +
+
+
+ {/each} + {/if} +
+
+ +
- - - - Outgoing - Incoming - + + + + - - - {#if user.friendRequests.filter(r => r.fromUser === user.id).length === 0} -

No outgoing requests

- {:else} - {#each user.friendRequests.filter(r => r.fromUser === user.id) as request (request.id)} - - - {request.username} - Request sent - - -
- - -
-
-
- {/each} - {/if} -
+ +
+ + Create a group + Add friends into your group! + - - - {#if user.friendRequests.filter(r => r.toUser === user.id).length === 0} -

No incoming requests

- {:else} - {#each user.friendRequests.filter(r => r.toUser === user.id) as request (request.id)} - - - {request.username} - Sent you a friend request - - - - - - - - -
- - -
-
-
- {/each} - {/if} -
-
- + {#each data.friends as friend (friend.id)} + + {/each} + + Cancel + + + + +
- + + + + - - - - + +
+ + Join a server + Enter an invite link. + - - - - Create a group - - Add friends into your group! - - + - {#each data.friends as friend (friend.id)} - - {/each} + + Cancel + + + +
+
- - - Cancel - - - - - -
+ + + + - - - - + +
+ + Create a server + Name your new server. + - - - - Join a server - - Enter an invite link. - - - - - - - - Cancel - - - - -
-
- - - - - - - -
- - Create a server - - Name your new server. - - - - - - - - Cancel - - - -
-
-
+ + + Cancel + + + + +
@@ -230,26 +212,24 @@ - - Friends - - - + + Friends + + + {#each data.friends as friend (friend.id)} - - { - e.preventDefault(); - currentPage = friend.id; - }} user={friend}> + { + e.preventDefault(); + currentPage = friend.id; + }} + user={friend} + > {/each} @@ -263,12 +243,8 @@ Groups - - + + @@ -276,10 +252,13 @@ {#each data.groups as group (group.id)} - { - e.preventDefault(); - currentPage = group.id; - }} href="##"> + { + e.preventDefault(); + currentPage = group.id; + }} + href="##" + > {group.name} ({group.members} members) @@ -295,12 +274,8 @@ Servers - - + + @@ -308,11 +283,19 @@ {#each data.servers as server (server.id)} - { - e.preventDefault(); - currentPage = server.id; - }} href="##" class="flex items-center gap-2"> - {server.name} + { + e.preventDefault(); + currentPage = server.id; + }} + href="##" + class="flex items-center gap-2" + > + {server.name} {server.name} diff --git a/src/lib/components/hooks/is-mobile.svelte.ts b/src/lib/components/hooks/is-mobile.svelte.ts new file mode 100644 index 0000000..4829c00 --- /dev/null +++ b/src/lib/components/hooks/is-mobile.svelte.ts @@ -0,0 +1,9 @@ +import { MediaQuery } from "svelte/reactivity"; + +const DEFAULT_MOBILE_BREAKPOINT = 768; + +export class IsMobile extends MediaQuery { + constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) { + super(`max-width: ${breakpoint - 1}px`); + } +} diff --git a/src/lib/index.ts b/src/lib/index.ts index e90df46..bbaac9c 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,48 +1,48 @@ -import { definePrefix, type Puuid } from "./puuid" +import { definePrefix, type Puuid } from './puuid'; -export const UserID = definePrefix("user"); -export const GroupID = definePrefix("group"); -export const ServerID = definePrefix("srv"); -export const FriendRequestID = definePrefix("frq"); +export const UserID = definePrefix('user'); +export const GroupID = definePrefix('group'); +export const ServerID = definePrefix('srv'); +export const FriendRequestID = definePrefix('frq'); -export type UserId = Puuid<"user">; -export type GroupId = Puuid<"group">; -export type ServerId = Puuid<"srv">; -export type FriendRequestID = Puuid<"frq">; +export type UserId = Puuid<'user'>; +export type GroupId = Puuid<'group'>; +export type ServerId = Puuid<'srv'>; +export type FriendRequestID = Puuid<'frq'>; -export const Status: Record = { +export const Status: Record = { OFFLINE: 1, DND: 2, ONLINE: 3 -} +}; export type OverviewUser = { - id: string; - username: string; - image: string; + id: string; + username: string; + image: string; }; export type OverviewServer = { - id: string; - name: string; + id: string; + name: string; ownerId: string; image: string; }; export type OverviewGroup = { - id: string; - name: string; + id: string; + name: string; ownerId: string; members: number; image: string; }; -export interface OverviewData { - friends: OverviewUser[], - groups: OverviewGroup[], - servers: OverviewServer[], -}; - export interface UserWithStatus extends OverviewUser { - status: 1|2|3, - statusMessage: string + status: 1 | 2 | 3; + statusMessage: string; +} + +export interface OverviewData { + friends: UserWithStatus[]; + groups: OverviewGroup[]; + servers: OverviewServer[]; } diff --git a/src/lib/puuid.ts b/src/lib/puuid.ts index 02280b9..5504ccf 100644 --- a/src/lib/puuid.ts +++ b/src/lib/puuid.ts @@ -1,42 +1,36 @@ -import { v4 as uuidv4 } from "uuid"; -import { v7 as uuidv7 } from "uuid"; +import { v4 as uuidv4 } from 'uuid'; +import { v7 as uuidv7 } from 'uuid'; type Brand = K & { __brand: T }; -export type Puuid = Brand< - string, - { prefix: Prefix } ->; +export type Puuid = Brand; export function definePrefix(prefix: P) { - const withPrefix = (uuid: string) => - `${prefix}_${uuid}` as Puuid

; + const withPrefix = (uuid: string) => `${prefix}_${uuid}` as Puuid

; - return { - prefix, - is(value: string): value is Puuid

{ - return value.startsWith(prefix + "_"); - }, + return { + prefix, + is(value: string): value is Puuid

{ + return value.startsWith(prefix + '_'); + }, - newV7(): Puuid

{ - return withPrefix(uuidv7()); - }, + newV7(): Puuid

{ + return withPrefix(uuidv7()); + }, - newV4(): Puuid

{ - return withPrefix(uuidv4()); - }, + newV4(): Puuid

{ + return withPrefix(uuidv4()); + }, - parse(value: string): Puuid

{ - if (!value.startsWith(prefix + "_")) { - throw new Error( - `Invalid prefix, expected "${prefix}_"` - ); - } - return value as Puuid

; - }, + parse(value: string): Puuid

{ + if (!value.startsWith(prefix + '_')) { + throw new Error(`Invalid prefix, expected "${prefix}_"`); + } + return value as Puuid

; + }, - inner(id: Puuid

): string { - return id.slice(prefix.length + 1); - }, - }; + inner(id: Puuid

): string { + return id.slice(prefix.length + 1); + } + }; } diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index f739167..d599477 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -10,141 +10,151 @@ const DAY_IN_MS = 1000 * 60 * 60 * 24; export const sessionCookieName = 'auth-session'; export function generateSessionToken() { - const bytes = crypto.getRandomValues(new Uint8Array(18)); - const token = encodeBase64url(bytes); - return token; + const bytes = crypto.getRandomValues(new Uint8Array(18)); + const token = encodeBase64url(bytes); + return token; } export async function createSession(token: string, userId: string) { - const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); - const session: table.Session = { - id: sessionId, - userId, - expiresAt: new Date(Date.now() + DAY_IN_MS * 30) - }; - await db.insert(table.session).values(session); - return session; + const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); + const session: table.Session = { + id: sessionId, + userId, + expiresAt: new Date(Date.now() + DAY_IN_MS * 30) + }; + await db.insert(table.session).values(session); + return session; } export async function validateSessionToken(token: string) { - const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); - const [row] = await db - .select({ - user: { - id: table.user.id, - username: table.user.username, - friends: table.user.friends, - servers: table.user.servers, - groups: table.user.groups, - }, - session: table.session, - }) - .from(table.session) - .innerJoin(table.user, eq(table.session.userId, table.user.id)) - .where(eq(table.session.id, sessionId)); + const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); + const [row] = await db + .select({ + user: { + id: table.user.id, + username: table.user.username, + friends: table.user.friends, + servers: table.user.servers, + groups: table.user.groups, + statusOverwrite: table.user.statusOverwrite + }, + session: table.session + }) + .from(table.session) + .innerJoin(table.user, eq(table.session.userId, table.user.id)) + .where(eq(table.session.id, sessionId)); - if (!row) { - return { session: null, user: null }; - } - const { session, user } = row; + if (!row) { + return { session: null, user: null }; + } + const { session, user } = row; - const sessionExpired = Date.now() >= session.expiresAt.getTime(); - if (sessionExpired) { - await db.delete(table.session).where(eq(table.session.id, session.id)); - return { session: null, user: null }; - } + const sessionExpired = Date.now() >= session.expiresAt.getTime(); + if (sessionExpired) { + await db.delete(table.session).where(eq(table.session.id, session.id)); + return { session: null, user: null }; + } - const renewSession = Date.now() >= session.expiresAt.getTime() - DAY_IN_MS * 15; - if (renewSession) { - session.expiresAt = new Date(Date.now() + DAY_IN_MS * 30); - await db - .update(table.session) - .set({ expiresAt: session.expiresAt }) - .where(eq(table.session.id, session.id)); - } - const friends = (user.friends as string[]).length - ? await db - .select({ - id: table.user.id, - username: table.user.username, - }) - .from(table.user) - .where(inArray(table.user.id, (user.friends as string[]))) - : []; + const renewSession = Date.now() >= session.expiresAt.getTime() - DAY_IN_MS * 15; + if (renewSession) { + session.expiresAt = new Date(Date.now() + DAY_IN_MS * 30); + await db + .update(table.session) + .set({ expiresAt: session.expiresAt }) + .where(eq(table.session.id, session.id)); + } + const friends = (user.friends as string[]).length + ? await db + .select({ + id: table.user.id, + username: table.user.username + }) + .from(table.user) + .where(inArray(table.user.id, user.friends as string[])) + : []; - const servers = (user.servers as string[]).length - ? await db - .select({ - id: table.server.id, - name: table.server.name, - ownerId: table.server.owner, - }) - .from(table.server) - .where(inArray(table.server.id, (user.servers as string[]))) - : []; - const groups = (user.groups as string[]).length - ? await db - .select({ - id: table.group.id, - name: table.group.name, - ownerId: table.group.owner, - members: table.group.members - }) - .from(table.group) - .where(inArray(table.group.id, (user.groups as string[]))) - : []; + const servers = (user.servers as string[]).length + ? await db + .select({ + id: table.server.id, + name: table.server.name, + ownerId: table.server.owner + }) + .from(table.server) + .where(inArray(table.server.id, user.servers as string[])) + : []; + const groups = (user.groups as string[]).length + ? await db + .select({ + id: table.group.id, + name: table.group.name, + ownerId: table.group.owner, + members: table.group.members + }) + .from(table.group) + .where(inArray(table.group.id, user.groups as string[])) + : []; - const friendRequests = await db - .select({ - id: table.friendRequest.id, - fromUser: table.friendRequest.fromUser, - toUser: table.friendRequest.toUser, - }) - .from(table.friendRequest) - .where(or(eq(table.friendRequest.fromUser, user.id), eq(table.friendRequest.toUser, user.id))) + const friendRequests = await db + .select({ + id: table.friendRequest.id, + fromUser: table.friendRequest.fromUser, + toUser: table.friendRequest.toUser + }) + .from(table.friendRequest) + .where(or(eq(table.friendRequest.fromUser, user.id), eq(table.friendRequest.toUser, user.id))); - - return { session, user: {...user, servers, friends, groups: groups.map(z => { return { ...z, members: (z.members as string[]).length}}), friendRequests} }; + return { + session, + user: { + ...user, + servers, + friends, + groups: groups.map((z) => { + return { ...z, members: (z.members as string[]).length }; + }), + friendRequests + } + }; } export type SessionValidationResult = Awaited>; export async function invalidateSession(sessionId: string) { - await db.delete(table.session).where(eq(table.session.id, sessionId)); + await db.delete(table.session).where(eq(table.session.id, sessionId)); } export function setSessionTokenCookie(event: RequestEvent, token: string, expiresAt: Date) { - event.cookies.set(sessionCookieName, token, { - expires: expiresAt, - path: '/' - }); + event.cookies.set(sessionCookieName, token, { + expires: expiresAt, + path: '/' + }); } export function deleteSessionTokenCookie(event: RequestEvent) { - event.cookies.delete(sessionCookieName, { - path: '/' - }); + event.cookies.delete(sessionCookieName, { + path: '/' + }); } export function validateUsername(username: unknown): username is string { - return ( - typeof username === 'string' && - username.length >= 3 && - username.length <= 31 && - /^[a-z0-9_-]+$/.test(username) - ); + return ( + typeof username === 'string' && + username.length >= 3 && + username.length <= 31 && + /^[a-z0-9_-]+$/.test(username) + ); } export function validatePassword(password: unknown): password is string { - return typeof password === 'string' && password.length >= 6 && password.length <= 255; + return typeof password === 'string' && password.length >= 6 && password.length <= 255; } - export function validateEmail(email: unknown): email is string { - return ( - typeof email === 'string' && - email.length >= 5 && - email.length <= 254 && - /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i.test(email) - ); + return ( + typeof email === 'string' && + email.length >= 5 && + email.length <= 254 && + /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i.test(email) + ); } diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts index 574ee4d..fc7ebcc 100644 --- a/src/lib/server/db/index.ts +++ b/src/lib/server/db/index.ts @@ -1,8 +1,8 @@ import { drizzle } from 'drizzle-orm/bun-sqlite'; import { Database } from 'bun:sqlite'; -import { BunSqliteKeyValue } from "bun-sqlite-key-value" +import { BunSqliteKeyValue } from 'bun-sqlite-key-value'; const sqlite = new Database('database.db'); export const db = drizzle(sqlite); -export const kvStore = new BunSqliteKeyValue("./kvStore.db") +export const kvStore = new BunSqliteKeyValue('./kvStore.db'); diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index 7361790..b42bdd0 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -1,14 +1,16 @@ +import { Status } from '../../index.ts'; import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core'; export const user = sqliteTable('user', { - id: text('id').primaryKey(), - username: text('username').notNull().unique(), - email: text('email').notNull().unique(), - passwordHash: text('password_hash').notNull(), - friends: text('friends', { mode: "json"}).default([]).notNull(), + id: text('id').primaryKey(), + username: text('username').notNull().unique(), + email: text('email').notNull().unique(), + passwordHash: text('password_hash').notNull(), + statusOverwrite: integer('status_overwrite').default(Status.ONLINE).notNull(), + friends: text('friends', { mode: 'json' }).default([]).notNull(), - servers: text('servers', { mode: "json"}).default([]).notNull(), // string[] of ServerIDs - groups: text('groups', { mode: "json"}).default([]).notNull(), // string[] of GroupIDs + servers: text('servers', { mode: 'json' }).default([]).notNull(), // string[] of ServerIDs + groups: text('groups', { mode: 'json' }).default([]).notNull() // string[] of GroupIDs }); export const session = sqliteTable('session', { @@ -19,39 +21,66 @@ export const session = sqliteTable('session', { expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull() }); -export const server = sqliteTable("server", { +export const server = sqliteTable('server', { id: text('id').primaryKey(), name: text('name').notNull(), - owner: text('owner').notNull().references(() => user.id), - members: text('members', { mode: "json"}).default([]).notNull(), - channels: text('channels', { mode: "json"}).default([]).notNull(), // string[] of ChannelIDs -}) + owner: text('owner') + .notNull() + .references(() => user.id), + members: text('members', { mode: 'json' }).default([]).notNull(), + channels: text('channels', { mode: 'json' }).default([]).notNull() // string[] of ChannelIDs +}); -export const group = sqliteTable("group", { +export const group = sqliteTable('group', { id: text('id').primaryKey(), name: text('name').notNull(), - owner: text('owner').notNull().references(() => user.id), - members: text('members', { mode: "json"}).default([]).notNull(), -}) + owner: text('owner') + .notNull() + .references(() => user.id), + members: text('members', { mode: 'json' }).default([]).notNull() +}); -export const channel = sqliteTable("channel", { +export const channel = sqliteTable('channel', { id: text('id').primaryKey(), name: text('name').notNull(), - serverId: text('server_id').notNull().references(() => server.id), - messages: text('messages', { mode: "json"}).default([]).notNull(), -}) + serverId: text('server_id') + .notNull() + .references(() => server.id), + messages: text('messages', { mode: 'json' }).default([]).notNull() +}); -export const friendRequest = sqliteTable("friendRequest", { +export const directMessage = sqliteTable('directMessage', { id: text('id').primaryKey(), - fromUser: text('from_user').notNull().references(() => user.id), - toUser: text('to_user').notNull().references(() => user.id), -}) + name: text('name').notNull(), + serverId: text('server_id') + .notNull() + .references(() => server.id), + messages: text('messages', { mode: 'json' }).default([]).notNull() +}); -export const invite = sqliteTable("invite", { +export const friendRequest = sqliteTable('friendRequest', { id: text('id').primaryKey(), - serverId: text('server_id').notNull().references(() => server.id), - code: text('code').notNull(), -}) + fromUser: text('from_user') + .notNull() + .references(() => user.id), + fromUsername: text('from_username') + .notNull() + .references(() => user.id), + toUsername: text('to_username') + .notNull() + .references(() => user.id), + toUser: text('to_user') + .notNull() + .references(() => user.id) +}); + +export const invite = sqliteTable('invite', { + id: text('id').primaryKey(), + serverId: text('server_id') + .notNull() + .references(() => server.id), + code: text('code').notNull() +}); export type Session = typeof session.$inferSelect; export type User = typeof user.$inferSelect; export type Group = typeof group.$inferSelect; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 55b3a91..ee33998 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,13 +1,13 @@ -import { clsx, type ClassValue } from "clsx"; -import { twMerge } from "tailwind-merge"; +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); + return twMerge(clsx(inputs)); } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type WithoutChild = T extends { child?: any } ? Omit : T; +export type WithoutChild = T extends { child?: any } ? Omit : T; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type WithoutChildren = T extends { children?: any } ? Omit : T; +export type WithoutChildren = T extends { children?: any } ? Omit : T; export type WithoutChildrenOrChild = WithoutChildren>; export type WithElementRef = T & { ref?: U | null }; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index effe048..870476c 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,7 +1,7 @@ diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 4902fad..1678083 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -3,7 +3,7 @@ import { fail, redirect } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async (event) => { - if (event.request.url.endsWith("/?logout")) { + if (event.request.url.endsWith('/?logout')) { if (!event.locals.session) { return fail(401); } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 988b539..9e0d907 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,43 +1,53 @@

- -
A extremely simple chatting application. Supports mobile devices (as a PWA), and also works on almost every single screen size (responsive design!)
+ +
+ A extremely simple chatting application. Supports mobile devices (as a PWA), and also works on + almost every single screen size (responsive design!) +
-
- Featurelist: -
    -
  • Lorem Ipsum is simply dummy text of the printing and typesetting industry.
  • -
  • Lorem Ipsum has been the industry's standard dummy text ever since the 1500s,
  • -
  • when an unknown printer took a galley of type and scrambled it to make a type specimen book.
  • -
  • It has survived not only five centuries, but also the leap into electronic typesetting,
  • -
  • remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages,
  • -
+
+ Featurelist: +
    +
  • Lorem Ipsum is simply dummy text of the printing and typesetting industry.
  • +
  • Lorem Ipsum has been the industry's standard dummy text ever since the 1500s,
  • +
  • + when an unknown printer took a galley of type and scrambled it to make a type specimen book. +
  • +
  • + It has survived not only five centuries, but also the leap into electronic typesetting, +
  • +
  • + remaining essentially unchanged. It was popularised in the 1960s with the release of + Letraset sheets containing Lorem Ipsum passages, +
  • +
+
+ +
+ Screenshots + +
+ temporary placeholder image + temporary placeholder image + temporary placeholder image + temporary placeholder image
+
-
- Screenshots - -
- temporary placeholder image - temporary placeholder image - temporary placeholder image - temporary placeholder image -
-
- -
- {#if data.user?.id} - - - {:else} - - {/if} -
+
+ {#if data.user?.id} + + + {:else} + + {/if} +
diff --git a/src/routes/api/health/+server.ts b/src/routes/api/health/+server.ts new file mode 100644 index 0000000..1a41b26 --- /dev/null +++ b/src/routes/api/health/+server.ts @@ -0,0 +1,3 @@ +export async function GET() { + return new Response('OK!'); +} diff --git a/src/routes/api/messages/[[groupOrServerId]]/[[channelId]]/+server.ts b/src/routes/api/messages/[[groupOrServerId]]/[[channelId]]/+server.ts index 7a78692..bb8d1a2 100644 --- a/src/routes/api/messages/[[groupOrServerId]]/[[channelId]]/+server.ts +++ b/src/routes/api/messages/[[groupOrServerId]]/[[channelId]]/+server.ts @@ -2,25 +2,23 @@ import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; export const GET: RequestHandler = async ({ params }) => { - const { groupOrServerId, channelId } = params; + const { groupOrServerId, channelId } = params; - const isGroup = !channelId; + const isGroup = !channelId; - // fake messages - const messages = Array.from({ length: 5 }, (_, i) => ({ - id: crypto.randomUUID(), - authorId: `user_${Math.floor(Math.random() * 10)}`, - content: isGroup - ? `Group message #${i + 1}` - : `Server message #${i + 1}`, - timestamp: Date.now() - Math.floor(Math.random() * 100000), - })); + // fake messages + const messages = Array.from({ length: 5 }, (_, i) => ({ + id: crypto.randomUUID(), + authorId: `user_${Math.floor(Math.random() * 10)}`, + content: isGroup ? `Group message #${i + 1}` : `Server message #${i + 1}`, + timestamp: Date.now() - Math.floor(Math.random() * 100000) + })); - return json({ - type: isGroup ? 'group' : 'server', - groupId: isGroup ? groupOrServerId : null, - serverId: isGroup ? null : groupOrServerId, - channelId: channelId ?? null, - messages, - }); + return json({ + type: isGroup ? 'group' : 'server', + groupId: isGroup ? groupOrServerId : null, + serverId: isGroup ? null : groupOrServerId, + channelId: channelId ?? null, + messages + }); }; diff --git a/src/routes/api/status/[userId]/+server.ts b/src/routes/api/status/[userId]/+server.ts index 488c13d..e04baf1 100644 --- a/src/routes/api/status/[userId]/+server.ts +++ b/src/routes/api/status/[userId]/+server.ts @@ -1,17 +1,14 @@ import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import { Status } from '$lib'; - +import { kvStore } from '$lib/server/db'; export const GET: RequestHandler = async ({ params }) => { - const { userId } = params; + const { userId } = params; - const status = Object.values(Status)[Math.floor(Math.random() * Object.values(Status).length)]; - - return json({ - userId, - status, - lastActive: Date.now() - Math.floor(Math.random() * 600000), - customStatus: Math.random() > 0.5 ? 'vibing 🟢' : null, - }); + return json({ + userId, + status: kvStore.get('user-' + userId + '-state'), + //@TODO Implement statusmessage + statusMessage: Math.random() > 0.5 ? 'vibing 🟢' : null + }); }; diff --git a/src/routes/api/updates/+server.ts b/src/routes/api/updates/+server.ts new file mode 100644 index 0000000..a5c3a8e --- /dev/null +++ b/src/routes/api/updates/+server.ts @@ -0,0 +1,48 @@ +interface SubscribedTo { + subscribed: string[]; + userId: string; + controller: ReadableStreamDefaultController; +} + +export const _clients = new Map(); +export function _sendToSubscribers(userId: string, payload: unknown) { + for (const client of _clients) { + if (client[1].subscribed.includes(userId)) { + try { + client[1].controller.enqueue(`data: ${JSON.stringify(payload)}\n\n`); + } catch { + _clients.delete(client[0]); + } + } + } +} +export async function GET({ locals, request }) { + if (!locals.user) { + return new Response('No authentication', { status: 401 }); + } + + const subscribed = locals.user.friends.map((z) => z.id); + const reqId = crypto.randomUUID(); + + const stream = new ReadableStream({ + start(controller) { + _clients.set(reqId, { subscribed, userId: locals.user!.id, controller }); + console.log(`SSE Client opened. ${_clients.size}`); + + controller.enqueue(`data: ${JSON.stringify({ type: 'connected' })}\n\n`); + + request.signal.addEventListener('abort', () => { + _clients.delete(reqId); + console.log(`SSE Client aborted. ${_clients.size}`); + }); + } + }); + + return new Response(stream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive' + } + }); +} diff --git a/src/routes/app/+page.server.ts b/src/routes/app/+page.server.ts index a045606..abdf106 100644 --- a/src/routes/app/+page.server.ts +++ b/src/routes/app/+page.server.ts @@ -6,18 +6,17 @@ import * as table from '$lib/server/db/schema'; import { FriendRequestID, ServerID } from '$lib'; import { eq } from 'drizzle-orm'; import { and } from 'drizzle-orm'; -import { User } from '$lib/server/db/schema'; +import { type User } from '$lib/server/db/schema'; export const load: PageServerLoad = async () => { - const user = requireLogin(); - return { user }; + const user = requireLogin(); + return { user }; }; - export const actions = { - addFriend: async ({ request, locals }) => { - const data = await request.formData(); - const username = data.get('username'); - const userId = data.get('userId'); + addFriend: async ({ request, locals }) => { + const data = await request.formData(); + const username = data.get('username'); + const userId = data.get('userId'); if (username && !userId) { if (typeof username !== 'string' || username.length < 3) { @@ -26,162 +25,193 @@ export const actions = { } let user: User[]; - if(username) { - user = await db.select().from(table.user).where(eq(table.user.username, username.toString())).limit(1); - } else if(userId) { - user = await db.select().from(table.user).where(eq(table.user.id, userId.toString())).limit(1); - } else { - return fail(400, { error: 'Missing username or userId' }); + if (username) { + user = await db + .select() + .from(table.user) + .where(eq(table.user.username, username.toString())) + .limit(1); + } else if (userId) { + user = await db + .select() + .from(table.user) + .where(eq(table.user.id, userId.toString())) + .limit(1); + } else { + return fail(400, { error: 'Missing username or userId' }); } + if (locals.user?.friends.find((z) => z.id == user[0].id)) { + return fail(400, { error: 'Already friends' }); + } - if(user?.length == 0) - return fail(400, { error: 'User not found' }); + if (user?.length == 0) return fail(400, { error: 'User not found' }); - const friendRequest = await db.select() - .from(table.friendRequest) - .where(and(eq(table.friendRequest.fromUser, user[0].id), eq(table.friendRequest.toUser, locals.user!.id))) + const friendRequest = await db + .select() + .from(table.friendRequest) + .where( + and( + eq(table.friendRequest.fromUser, user[0].id), + eq(table.friendRequest.toUser, locals.user!.id) + ) + ) .limit(1); - // user has already sent a request to us - // means we want to accept it - // - if(friendRequest?.length != 0) { - await db.delete(table.friendRequest) - .where(and(eq(table.friendRequest.fromUser, user[0].id), eq(table.friendRequest.toUser, locals.user!.id))) + // user has already sent a request to us + // means we want to accept it + // + if (friendRequest?.length != 0) { + await db + .delete(table.friendRequest) + .where( + and( + eq(table.friendRequest.fromUser, user[0].id), + eq(table.friendRequest.toUser, locals.user!.id) + ) + ) .limit(1); - // add other guy to us - await db.transaction(async (tx) => { - await tx.update(table.user) - .set({ friends: locals.user?.friends.map(z => z.id).concat(user[0].id) }) - .where(eq(table.user.id, locals.user!.id)); + // add other guy to us + await db.transaction(async (tx) => { + await tx + .update(table.user) + .set({ friends: locals.user?.friends.map((z) => z.id).concat(user[0].id) }) + .where(eq(table.user.id, locals.user!.id)); - await tx.update(table.user) - .set({ friends: (user[0].friends as string[]).concat(locals.user!.id) }) - .where(eq(table.user.id, user[0].id)); - }); + await tx + .update(table.user) + .set({ friends: (user[0].friends as string[]).concat(locals.user!.id) }) + .where(eq(table.user.id, user[0].id)); + }); - return {success: true} - } + return { success: true }; + } - // a request from us has already been sent to user - if(locals.user?.friendRequests.find(z => z.toUser == user[0].id && z.fromUser == locals.user!.id)) - return fail(400, { error: 'Already sent request' }); + // a request from us has already been sent to user + if ( + locals.user?.friendRequests.find( + (z) => z.toUser == user[0].id && z.fromUser == locals.user!.id + ) + ) + return fail(400, { error: 'Already sent request' }); - await db.insert(table.friendRequest).values({ - id: FriendRequestID.newV4(), - fromUser: locals.user!.id, - toUser: user[0].id, - }); + await db.insert(table.friendRequest).values({ + id: FriendRequestID.newV4(), + fromUser: locals.user!.id, + toUser: user[0].id + }); - return { success: true }; - }, - cancelFriendRequest: async ({ request, locals }) => { - const data = await request.formData(); - const requestId = data.get('requestId'); + return { success: true }; + }, + cancelFriendRequest: async ({ request, locals }) => { + const data = await request.formData(); + const requestId = data.get('requestId'); - if (typeof requestId !== 'string') { - return fail(400, { error: 'Invalid request ID' }); - } + if (typeof requestId !== 'string') { + return fail(400, { error: 'Invalid request ID' }); + } - // fetch the friend request - const friendRequest = await db.select() - .from(table.friendRequest) - .where(eq(table.friendRequest.id, requestId)) - .limit(1); + // fetch the friend request + const friendRequest = await db + .select() + .from(table.friendRequest) + .where(eq(table.friendRequest.id, requestId)) + .limit(1); - if (!friendRequest?.length) { - return fail(404, { error: 'Friend request not found' }); - } + if (!friendRequest?.length) { + return fail(404, { error: 'Friend request not found' }); + } - const fr = friendRequest[0]; + const fr = friendRequest[0]; - // only allow cancelling if it's related to current user - if (fr.fromUser !== locals.user!.id && fr.toUser !== locals.user!.id) { - return fail(403, { error: 'Not allowed' }); - } + // only allow cancelling if it's related to current user + if (fr.fromUser !== locals.user!.id && fr.toUser !== locals.user!.id) { + return fail(403, { error: 'Not allowed' }); + } - // delete the request - await db.delete(table.friendRequest) - .where(eq(table.friendRequest.id, requestId)) - .limit(1); + // delete the request + await db.delete(table.friendRequest).where(eq(table.friendRequest.id, requestId)).limit(1); - return { success: true }; -}, - createGroup: async ({ request, locals }) => { - const data = await request.formData(); - const members = data.getAll('member'); + return { success: true }; + }, + createGroup: async ({ request, locals }) => { + const data = await request.formData(); + const members = data.getAll('member'); - if (!members.length) { - return fail(400, { error: 'No members selected' }); - } + if (!members.length) { + return fail(400, { error: 'No members selected' }); + } - console.log(data, members, locals) + return { success: true }; + }, - return { success: true }; - }, + joinServer: async ({ request, locals }) => { + const data = await request.formData(); + const invite = data.get('invite'); - joinServer: async ({ request, locals }) => { - const data = await request.formData(); - const invite = data.get('invite'); + if (typeof invite !== 'string') { + return fail(400, { error: 'Invalid invite' }); + } - if (typeof invite !== 'string') { - return fail(400, { error: 'Invalid invite' }); - } + const inv = await db.select().from(table.invite).where(eq(table.invite.code, invite)).limit(1); - const inv = await db.select().from(table.invite).where(eq(table.invite.code, invite)).limit(1); + if (inv?.length == 0) return fail(400, { error: 'Invalid invite' }); - if(inv?.length == 0) - return fail(400, { error: 'Invalid invite' }); + const server = await db + .select() + .from(table.server) + .where(eq(table.server.id, inv[0].serverId)) + .limit(1); - const server = await db.select().from(table.server).where(eq(table.server.id, inv[0].serverId)).limit(1); + if (server?.length == 0) return fail(400, { error: 'Invalid server' }); - if(server?.length == 0) - return fail(400, { error: 'Invalid server' }); - - if(locals.user!.servers.some(z => z.id == server[0].id)) - return fail(400, { error: 'Already in server' }); + if (locals.user!.servers.some((z) => z.id == server[0].id)) + return fail(400, { error: 'Already in server' }); await db.transaction(async (tx) => { - await tx.update(table.user) - .set({servers: locals.user!.servers.map(z => z.id).concat([server[0].id])}) - .where(eq(table.user.id, locals.user!.id)); + await tx + .update(table.user) + .set({ servers: locals.user!.servers.map((z) => z.id).concat([server[0].id]) }) + .where(eq(table.user.id, locals.user!.id)); - await tx.update(table.server) - .set({members: (server[0].members as string[]).concat([locals.user!.id])}) - .where(eq(table.server.id, server[0].id)); - }) + await tx + .update(table.server) + .set({ members: (server[0].members as string[]).concat([locals.user!.id]) }) + .where(eq(table.server.id, server[0].id)); + }); - return { success: true }; - }, + return { success: true }; + }, - createServer: async ({ request, locals }) => { - const data = await request.formData(); - const name = data.get('name'); + createServer: async ({ request, locals }) => { + const data = await request.formData(); + const name = data.get('name'); - if (typeof name !== 'string' || name.length < 3) { - return fail(400, { error: 'Server name too short' }); - } + if (typeof name !== 'string' || name.length < 3) { + return fail(400, { error: 'Server name too short' }); + } const serverId = ServerID.newV4(); - await db.insert(table.server) - .values({id: serverId, name, owner: locals.user!.id, members: [ locals.user!.id ]}); + await db + .insert(table.server) + .values({ id: serverId, name, owner: locals.user!.id, members: [locals.user!.id] }); - await db.update(table.user) - .set({servers: locals.user!.servers.map(z => z.id).concat([serverId])}) - .where(eq(table.user.id, locals.user!.id)); + await db + .update(table.user) + .set({ servers: locals.user!.servers.map((z) => z.id).concat([serverId]) }) + .where(eq(table.user.id, locals.user!.id)); - redirect(303, `/app`); - } + redirect(303, `/app`); + } } satisfies Actions; function requireLogin() { - const { locals } = getRequestEvent(); + const { locals } = getRequestEvent(); - if (!locals.user) { - return redirect(302, '/login'); - } + if (!locals.user) { + return redirect(302, '/login'); + } - return locals.user; + return locals.user; } diff --git a/src/routes/app/+page.svelte b/src/routes/app/+page.svelte index b32ce5b..e6ea282 100644 --- a/src/routes/app/+page.svelte +++ b/src/routes/app/+page.svelte @@ -1,18 +1,27 @@ {#if form} - - - - {form?.error ? "Ran into an error." : "Success!"} - - {form?.error || "Action completed succesfully."} - - - - Close - - - + + + + {form?.error ? 'Ran into an error.' : 'Success!'} + + {form?.error || 'Action completed succesfully.'} + + + + Close + + + {/if} - @@ -107,24 +133,32 @@ {#if currentPageID && currentPage} {#if ServerID.is(currentPageID)} - {@const server = (currentPage as OverviewServer)} + {@const server = currentPage as OverviewServer} - {server!.name} + {server!.name} -

{server!.name}

+

{server!.name}

{:else if UserID.is(currentPageID)} - {@const friend = (currentPage as UserWithStatus)} + {@const friend = currentPage as UserWithStatus} - {friend!.username} + {friend!.username} -

{friend!.username} [{friend.status == Status.ONLINE ? "Online!" : friend.status == Status.DND ? "DND" : friend.status == Status.OFFLINE ? "Offline" : "Unknown"}]

+

+ {friend!.username} [{friend.status == Status.ONLINE + ? 'Online!' + : friend.status == Status.DND + ? 'DND' + : friend.status == Status.OFFLINE + ? 'Offline' + : 'Unknown'}] +

{:else if GroupID.is(currentPageID)} - {@const group = (currentPage as OverviewGroup)} + {@const group = currentPage as OverviewGroup} -

{group!.name} ({group.members} member{group.members > 1 ? "s" : ""})

+

{group!.name} ({group.members} member{group.members > 1 ? 's' : ''})

{/if} {/if} -

this is like lowkirkounely the content, i should put messages and shi here

+

this is like lowkirkounely the content, i should put messages and shi here

diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts index fd5d0ff..2fe79a5 100644 --- a/src/routes/login/+page.server.ts +++ b/src/routes/login/+page.server.ts @@ -7,56 +7,59 @@ import * as table from '$lib/server/db/schema'; import type { Actions, PageServerLoad } from './$types'; export const load: PageServerLoad = async (event) => { - if (event.locals.user) { - return redirect(302, '/demo/lucia'); - } - return {}; + if (event.locals.user) { + return redirect(302, '/demo/lucia'); + } + return {}; }; export const actions: Actions = { - login: async (event) => { - const formData = await event.request.formData(); - const username = formData.get('username'); - const password = formData.get('password'); + login: async (event) => { + const formData = await event.request.formData(); + const username = formData.get('username'); + const password = formData.get('password'); let username_is_email = false; - if (!auth.validateUsername(username)) { + if (!auth.validateUsername(username)) { if (auth.validateEmail(username)) { username_is_email = true; } else { - return fail(400, { - message: 'Invalid username (min 3, max 31 characters, alphanumeric only)' - }); + return fail(400, { + message: 'Invalid username (min 3, max 31 characters, alphanumeric only)' + }); } - } - if (!auth.validatePassword(password)) { - return fail(400, { message: 'Invalid password (min 6, max 255 characters)' }); - } + } + if (!auth.validatePassword(password)) { + return fail(400, { message: 'Invalid password (min 6, max 255 characters)' }); + } - const results = await db.select() - .from(table.user) - .where(username_is_email ? eq(table.user.email, username) : eq(table.user.username, username)); + const results = await db + .select() + .from(table.user) + .where( + username_is_email ? eq(table.user.email, username) : eq(table.user.username, username) + ); - const existingUser = results.at(0); - if (!existingUser) { - return fail(400, { message: 'Incorrect username or password' }); - } + const existingUser = results.at(0); + if (!existingUser) { + return fail(400, { message: 'Incorrect username or password' }); + } - const validPassword = await verify(existingUser.passwordHash, password, { - memoryCost: 19456, - timeCost: 2, - outputLen: 32, - parallelism: 1 - }); - if (!validPassword) { - return fail(400, { message: 'Incorrect username or password' }); - } + const validPassword = await verify(existingUser.passwordHash, password, { + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1 + }); + if (!validPassword) { + return fail(400, { message: 'Incorrect username or password' }); + } - const sessionToken = auth.generateSessionToken(); - const session = await auth.createSession(sessionToken, existingUser.id); - auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); + const sessionToken = auth.generateSessionToken(); + const session = await auth.createSession(sessionToken, existingUser.id); + auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); - return redirect(302, '/app'); - } + return redirect(302, '/app'); + } }; diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte index 262a2ec..a009d53 100644 --- a/src/routes/login/+page.svelte +++ b/src/routes/login/+page.svelte @@ -1,26 +1,24 @@
-
-
- -
-
- - - - -
- -

{form?.message ?? ''}

- - +
+
+
+
+ + + + +
+ +

{form?.message ?? ''}

+
diff --git a/src/routes/register/+page.server.ts b/src/routes/register/+page.server.ts index 277da90..aaa6dae 100644 --- a/src/routes/register/+page.server.ts +++ b/src/routes/register/+page.server.ts @@ -9,59 +9,58 @@ import { eq } from 'drizzle-orm'; import { UserID } from '$lib'; export const load: PageServerLoad = async (event) => { - if (event.locals.user) { - return redirect(302, '/demo/lucia'); - } - return {}; + if (event.locals.user) { + return redirect(302, '/demo/lucia'); + } + return {}; }; export const actions: Actions = { - register: async (event) => { - const formData = await event.request.formData(); - const username = formData.get('username'); - const email = formData.get('email'); - const password = formData.get('password'); + register: async (event) => { + const formData = await event.request.formData(); + const username = formData.get('username'); + const email = formData.get('email'); + const password = formData.get('password'); - if (!auth.validateUsername(username)) { - return fail(400, { message: 'Invalid username' }); - } + if (!auth.validateUsername(username)) { + return fail(400, { message: 'Invalid username' }); + } - if(!auth.validateEmail(email)) { - return fail(400, { message: 'Invalid email' }); - } + if (!auth.validateEmail(email)) { + return fail(400, { message: 'Invalid email' }); + } - if (!auth.validatePassword(password)) { - return fail(400, { message: 'Invalid password' }); - } + if (!auth.validatePassword(password)) { + return fail(400, { message: 'Invalid password' }); + } const userId = UserID.newV4(); - const passwordHash = await hash(password, { - // recommended minimum parameters - memoryCost: 19456, - timeCost: 2, - outputLen: 32, - parallelism: 1 - }); - const results = await db.select() - .from(table.user) - .where(or(eq(table.user.email, email), - eq(table.user.username, username))); + const passwordHash = await hash(password, { + // recommended minimum parameters + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1 + }); + const results = await db + .select() + .from(table.user) + .where(or(eq(table.user.email, email), eq(table.user.username, username))); - const existingUser = results.at(0); - if (existingUser) { - return fail(400, { message: 'Username or email already registered!' }); - } + const existingUser = results.at(0); + if (existingUser) { + return fail(400, { message: 'Username or email already registered!' }); + } - try { - await db.insert(table.user) - .values({ id: userId, email, username, passwordHash }); + try { + await db.insert(table.user).values({ id: userId, email, username, passwordHash }); - const sessionToken = auth.generateSessionToken(); - const session = await auth.createSession(sessionToken, userId); - auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); - } catch { - return fail(500, { message: 'An error has occurred' }); - } - return redirect(302, '/app'); - } + const sessionToken = auth.generateSessionToken(); + const session = await auth.createSession(sessionToken, userId); + auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); + } catch { + return fail(500, { message: 'An error has occurred' }); + } + return redirect(302, '/app'); + } }; diff --git a/src/routes/register/+page.svelte b/src/routes/register/+page.svelte index 24ce55d..9835146 100644 --- a/src/routes/register/+page.svelte +++ b/src/routes/register/+page.svelte @@ -1,25 +1,25 @@
-
-
- -
-
- - - - - -
- -

{form?.message ?? ''}

+
+
+
+
+ + + + + +
+ +

{form?.message ?? ''}

+