diff --git a/drizzle/0002_last_sleepwalker.sql b/drizzle/0002_last_sleepwalker.sql new file mode 100644 index 0000000..def34bf --- /dev/null +++ b/drizzle/0002_last_sleepwalker.sql @@ -0,0 +1 @@ +ALTER TABLE `user` ADD `invites` text DEFAULT '[]' NOT NULL; \ No newline at end of file diff --git a/drizzle/0003_sleepy_risque.sql b/drizzle/0003_sleepy_risque.sql new file mode 100644 index 0000000..d2db04d --- /dev/null +++ b/drizzle/0003_sleepy_risque.sql @@ -0,0 +1,2 @@ +ALTER TABLE `server` ADD `invites` text DEFAULT '[]' NOT NULL;--> statement-breakpoint +ALTER TABLE `user` DROP COLUMN `invites`; \ No newline at end of file diff --git a/drizzle/0004_minor_wildside.sql b/drizzle/0004_minor_wildside.sql new file mode 100644 index 0000000..0100b86 --- /dev/null +++ b/drizzle/0004_minor_wildside.sql @@ -0,0 +1,4 @@ +ALTER TABLE `invite` ADD `creator_id` text NOT NULL REFERENCES user(id);--> statement-breakpoint +ALTER TABLE `invite` ADD `expires_at` integer NOT NULL;--> statement-breakpoint +ALTER TABLE `invite` ADD `uses` text DEFAULT '[]' NOT NULL;--> statement-breakpoint +ALTER TABLE `invite` ADD `max_uses` integer; \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..ad711f4 --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,564 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "37c8a92f-0590-4c8c-870d-ad145966dadb", + "prevId": "e21369b9-d475-4c0f-8a6d-1c5f92ab8948", + "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 + }, + "first_member": { + "name": "first_member", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "second_member": { + "name": "second_member", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "messages": { + "name": "messages", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": {}, + "foreignKeys": { + "directMessage_first_member_user_id_fk": { + "name": "directMessage_first_member_user_id_fk", + "tableFrom": "directMessage", + "tableTo": "user", + "columnsFrom": [ + "first_member" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "directMessage_second_member_user_id_fk": { + "name": "directMessage_second_member_user_id_fk", + "tableFrom": "directMessage", + "tableTo": "user", + "columnsFrom": [ + "second_member" + ], + "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 + }, + "change_title": { + "name": "change_title", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "add_members": { + "name": "add_members", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "remove_members": { + "name": "remove_members", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "members": { + "name": "members", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "messages": { + "name": "messages", + "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": "'[]'" + }, + "invites": { + "name": "invites", + "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/0003_snapshot.json b/drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000..27e8a36 --- /dev/null +++ b/drizzle/meta/0003_snapshot.json @@ -0,0 +1,564 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "6112aa0b-60eb-4835-86a9-b137b02ce152", + "prevId": "37c8a92f-0590-4c8c-870d-ad145966dadb", + "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 + }, + "first_member": { + "name": "first_member", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "second_member": { + "name": "second_member", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "messages": { + "name": "messages", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": {}, + "foreignKeys": { + "directMessage_first_member_user_id_fk": { + "name": "directMessage_first_member_user_id_fk", + "tableFrom": "directMessage", + "tableTo": "user", + "columnsFrom": [ + "first_member" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "directMessage_second_member_user_id_fk": { + "name": "directMessage_second_member_user_id_fk", + "tableFrom": "directMessage", + "tableTo": "user", + "columnsFrom": [ + "second_member" + ], + "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 + }, + "change_title": { + "name": "change_title", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "add_members": { + "name": "add_members", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "remove_members": { + "name": "remove_members", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "members": { + "name": "members", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "messages": { + "name": "messages", + "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": "'[]'" + }, + "invites": { + "name": "invites", + "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/0004_snapshot.json b/drizzle/meta/0004_snapshot.json new file mode 100644 index 0000000..f903ad5 --- /dev/null +++ b/drizzle/meta/0004_snapshot.json @@ -0,0 +1,606 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "7f91f3dc-746e-40eb-b1d5-8ca20e04dd5f", + "prevId": "6112aa0b-60eb-4835-86a9-b137b02ce152", + "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 + }, + "first_member": { + "name": "first_member", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "second_member": { + "name": "second_member", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "messages": { + "name": "messages", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": {}, + "foreignKeys": { + "directMessage_first_member_user_id_fk": { + "name": "directMessage_first_member_user_id_fk", + "tableFrom": "directMessage", + "tableTo": "user", + "columnsFrom": [ + "first_member" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "directMessage_second_member_user_id_fk": { + "name": "directMessage_second_member_user_id_fk", + "tableFrom": "directMessage", + "tableTo": "user", + "columnsFrom": [ + "second_member" + ], + "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 + }, + "change_title": { + "name": "change_title", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "add_members": { + "name": "add_members", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "remove_members": { + "name": "remove_members", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "members": { + "name": "members", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "messages": { + "name": "messages", + "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 + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uses": { + "name": "uses", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "max_uses": { + "name": "max_uses", + "type": "integer", + "primaryKey": false, + "notNull": false, + "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" + }, + "invite_creator_id_user_id_fk": { + "name": "invite_creator_id_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": [ + "creator_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": "'[]'" + }, + "invites": { + "name": "invites", + "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 986751f..b8517fe 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -15,6 +15,27 @@ "when": 1767860870240, "tag": "0001_tiresome_stature", "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1768471463663, + "tag": "0002_last_sleepwalker", + "breakpoints": true + }, + { + "idx": 3, + "version": "6", + "when": 1768471566545, + "tag": "0003_sleepy_risque", + "breakpoints": true + }, + { + "idx": 4, + "version": "6", + "when": 1768584253382, + "tag": "0004_minor_wildside", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/lib/components/ui/switch/index.ts b/src/lib/components/ui/switch/index.ts new file mode 100644 index 0000000..f5533db --- /dev/null +++ b/src/lib/components/ui/switch/index.ts @@ -0,0 +1,7 @@ +import Root from "./switch.svelte"; + +export { + Root, + // + Root as Switch, +}; diff --git a/src/lib/components/ui/switch/switch.svelte b/src/lib/components/ui/switch/switch.svelte new file mode 100644 index 0000000..80661fd --- /dev/null +++ b/src/lib/components/ui/switch/switch.svelte @@ -0,0 +1,29 @@ + + + + + diff --git a/src/lib/index.ts b/src/lib/index.ts index bdd9888..79b3e7b 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -5,6 +5,7 @@ export const UserID = definePrefix('user'); export const GroupID = definePrefix('group'); export const ServerID = definePrefix('srv'); export const ChannelID = definePrefix('ch'); +export const InviteID = definePrefix('inv'); export const FriendRequestID = definePrefix('frq'); export const DirectMessageID = definePrefix('dmid'); @@ -14,6 +15,7 @@ export type GroupId = Puuid<'group'>; export type ServerId = Puuid<'srv'>; export type DirectMessageId = Puuid<'dmid'>; export type ChannelId = Puuid<'ch'>; +export type InviteId = Puuid<'inv'>; export interface Status { statusMessage: string; diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index 4d5a8cd..6434e12 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -1,4 +1,3 @@ -import { boolean } from 'drizzle-orm/singlestore-core'; import { Status } from '../../index.ts'; import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core'; @@ -9,7 +8,6 @@ export const user = sqliteTable('user', { 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 }); @@ -29,7 +27,8 @@ export const server = sqliteTable('server', { .notNull() .references(() => user.id), members: text('members', { mode: 'json' }).default([]).notNull(), - channels: text('channels', { mode: 'json' }).default([]).notNull() // string[] of ChannelIDs + channels: text('channels', { mode: 'json' }).default([]).notNull(), // string[] of ChannelIDs + invites: text('invites', { mode: 'json' }).default([]).notNull() // string[] of InviteIDs }); export const group = sqliteTable('group', { @@ -86,7 +85,13 @@ export const invite = sqliteTable('invite', { serverId: text('server_id') .notNull() .references(() => server.id), - code: text('code').notNull() + code: text('code').notNull(), + creatorId: text('creator_id') + .notNull() + .references(() => user.id), + createdAt: integer('expires_at', { mode: 'timestamp' }).notNull(), + uses: text('uses', { mode: 'json' }).default([]).notNull(), // users who used the invite. can repeat, + maxUses: integer('max_uses') }); export type Session = typeof session.$inferSelect; diff --git a/src/routes/api/updates/+server.ts b/src/routes/api/updates/+server.ts index a76c379..26d80cf 100644 --- a/src/routes/api/updates/+server.ts +++ b/src/routes/api/updates/+server.ts @@ -57,12 +57,12 @@ export async function GET({ locals, request }) { controller.enqueue(`data: ${JSON.stringify({ type: 'connected', sessionId })}\n\n`); - if (overwrite === Status.DND) { - kvStore.set(`user-${userId}-state`, Status.DND); + if (overwrite === Status.DND || overwrite == Status.ONLINE) { + kvStore.set(`user-${userId}-state`, overwrite); _sendToSubscribers(userId, { type: 'status', id: userId, - status: Status.DND, + status: overwrite, statusMessage: kvStore.get('user-' + userId + '-state') != Status.OFFLINE ? kvStore.get('user-' + userId + '-message') diff --git a/src/routes/app/+page.server.ts b/src/routes/app/+page.server.ts index 7f84151..1cf2977 100644 --- a/src/routes/app/+page.server.ts +++ b/src/routes/app/+page.server.ts @@ -3,200 +3,20 @@ import { getRequestEvent } from '$app/server'; import type { Actions, PageServerLoad } from './$types'; import { db, kvStore } from '$lib/server/db'; import * as table from '$lib/server/db/schema'; -import { DirectMessageID, FriendRequestID, GroupID, ServerID } from '$lib'; import { eq } from 'drizzle-orm'; -import { and } from 'drizzle-orm'; -import { type User } from '$lib/server/db/schema'; -import { _sendToSubscribers, _sendToUser } from '../api/updates/+server'; +import { _sendToSubscribers } from '../api/updates/+server'; import { validateUsername } from '$lib/server/auth'; +import FriendActions from './actions/friend'; +import GroupActions from './actions/group'; +import ServerActions from './actions/server'; + export const load: PageServerLoad = async () => { 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'); - - if (username && !userId) { - if (typeof username !== 'string' || username.length < 3) { - return fail(400, { error: 'Invalid username' }); - } - } - - 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 (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' }); - - 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) - ) - ) - .limit(1); - - await db.insert(table.directMessage).values({ - id: DirectMessageID.newV4(), - firstMember: locals.user!.id, - secondMember: user[0].id, - messages: [] - }); - - // 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)); - }); - - _sendToSubscribers(locals.user!.id, { type: 'friends', status: 'accepted' }); - _sendToSubscribers(user[0].id, { type: 'friends', status: 'accepted' }); - 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' }); - - await db.insert(table.friendRequest).values({ - id: FriendRequestID.newV4(), - fromUser: locals.user!.id, - toUser: user[0].id, - toUsername: user[0].username, - fromUsername: locals.user!.username - }); - - _sendToSubscribers(locals.user!.id, { type: 'friends', status: 'sent-request' }); - _sendToSubscribers(user[0].id, { type: 'friends', status: 'new-request' }); - return { success: true }; - }, - removeFriend: async ({ request, locals }) => { - const data = await request.formData(); - const userId = data.get('userId'); - - if (typeof userId !== 'string') { - return fail(400, { error: 'Invalid user ID' }); - } - - // verify we are actually friends - if (!locals.user?.friends.find((z) => z.id === userId)) { - return fail(400, { error: 'Not in friends list' }); - } - - // fetch the target user - const user = await db.select().from(table.user).where(eq(table.user.id, userId)).limit(1); - - if (!user?.length) { - return fail(404, { error: 'User not found' }); - } - - const target = user[0]; - - // remove each other from friends lists - await db.transaction(async (tx) => { - // update current user – filter out removed friend - await tx - .update(table.user) - .set({ - friends: locals.user!.friends.map((z) => z.id).filter((id) => id !== userId) - }) - .where(eq(table.user.id, locals.user!.id)); - - // update target user – filter out us - await tx - .update(table.user) - .set({ - friends: (target.friends as string[]).filter((id) => id !== locals.user!.id) - }) - .where(eq(table.user.id, target.id)); - }); - - 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' }); - } - - // 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' }); - } - - 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' }); - } - - // delete the request - await db.delete(table.friendRequest).where(eq(table.friendRequest.id, requestId)).limit(1); - - _sendToSubscribers(fr.fromUser, { type: 'friends', status: 'request-cancelled' }); - _sendToSubscribers(fr.toUser, { type: 'friends', status: 'request-cancelled' }); - - return { success: true }; - }, updateProfile: async ({ request, locals }) => { const user = locals.user; if (!user) return fail(401); @@ -232,322 +52,9 @@ export const actions = { return { success: true }; }, - createGroup: async ({ request, locals }) => { - const data = await request.formData(); - const members = data.getAll('member').map((z) => z.toString()); - - if (!members.length) { - return fail(400, { error: 'No members selected' }); - } - if (members.includes(locals.user!.id)) { - return fail(403, { error: 'You cannot add yourself to a group.' }); - } - - if (members.length > 9) { - return fail(400, { error: 'Too many members' }); - } - if (members.length < 2) { - return fail(400, { error: 'Not enough members' }); - } - for (const member of members) { - if (!locals.user!.friends.find((z) => z.id == member)) { - return fail(403, { error: 'A member is not your friend.' }); - } - } - const nameArray = []; - - for await (const member of members) { - const dbUser = await db.select().from(table.user).where(eq(table.user.id, member)).limit(1); - nameArray.push(dbUser[0].username); - - if (!dbUser.length) { - return fail(400, { error: 'Invalid member' }); - } - } - - members.push(locals.user!.id); - nameArray.push(locals.user!.username); - - const group = await db - .insert(table.group) - .values({ - id: GroupID.newV4(), - name: nameArray.join(', '), - owner: locals.user!.id, - members: members, - messages: [] - }) - .returning(); - - await db.transaction(async (tx) => { - for await (const member of members) { - _sendToUser(member, { type: 'group', status: 'added-to-group' }); - const user = await tx.select().from(table.user).where(eq(table.user.id, member)).limit(1); - await tx - .update(table.user) - .set({ groups: (user[0].groups as string[]).concat(group[0].id) }) - .where(eq(table.user.id, member)); - } - }); - - return { success: true, group }; - }, - - joinServer: async ({ request, locals }) => { - const data = await request.formData(); - const invite = data.get('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); - - 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); - - 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' }); - - 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.server) - .set({ members: (server[0].members as string[]).concat([locals.user!.id]) }) - .where(eq(table.server.id, server[0].id)); - }); - - return { success: true }; - }, - addMembers: async ({ request, locals }) => { - const data = await request.formData(); - const groupId = data.get('groupId'); - const memberIds = data.getAll('memberIds').map(String); - - if (typeof groupId !== 'string') { - return fail(400, { error: 'Invalid group ID' }); - } - - if (!memberIds.length) { - return fail(400, { error: 'No members selected' }); - } - - const group = await db.select().from(table.group).where(eq(table.group.id, groupId)).limit(1); - if (!group.length) return fail(404, { error: 'Group not found' }); - - const g = group[0]; - - if (!(g.members as string[]).includes(locals.user!.id)) { - return fail(403, { error: 'You do not have permission to act on this group.' }); - } - - const isOwner = g.owner === locals.user!.id; - if (!isOwner && !g.addMembers) { - return fail(403, { error: 'No permission to add members' }); - } - - for (const id of memberIds) { - if (!locals.user!.friends.find((f) => f.id === id)) { - return fail(403, { error: 'Can only add friends' }); - } - } - - const newMembers = [...new Set([...(g.members as string[]), ...memberIds])]; - - await db.transaction(async (tx) => { - await tx.update(table.group).set({ members: newMembers }).where(eq(table.group.id, groupId)); - - for (const id of memberIds) { - const user = await tx.select().from(table.user).where(eq(table.user.id, id)).limit(1); - if (!user.length) continue; - if (newMembers.includes(id)) { - _sendToUser(id, { type: 'group', status: 'added-to-group' }); - } else { - _sendToUser(id, { type: 'group', status: 'member-added-to-group' }); - } - - await tx - .update(table.user) - .set({ groups: (user[0].groups as string[]).concat(groupId) }) - .where(eq(table.user.id, id)); - } - }); - - return { success: true }; - }, - removeMembers: async ({ request, locals }) => { - const data = await request.formData(); - const groupId = data.get('groupId'); - const memberIds = data.getAll('memberIds').map(String); - - if (typeof groupId !== 'string') { - return fail(400, { error: 'Invalid group ID' }); - } - - const group = await db.select().from(table.group).where(eq(table.group.id, groupId)).limit(1); - if (!group.length) return fail(404, { error: 'Group not found' }); - - const g = group[0]; - - if (!(g.members as string[]).includes(locals.user!.id)) { - return fail(403, { error: 'You do not have permission to act on this group.' }); - } - const isOwner = g.owner === locals.user!.id; - if (!isOwner && !g.removeMembers) { - return fail(403, { error: 'No permission to remove members' }); - } - - if (memberIds.includes(g.owner)) { - return fail(400, { error: 'Cannot remove group owner' }); - } - - const remaining = (g.members as string[]).filter((id) => !memberIds.includes(id)); - - await db.transaction(async (tx) => { - await tx.update(table.group).set({ members: remaining }).where(eq(table.group.id, groupId)); - - for (const id of remaining) { - _sendToUser(id, { type: 'group', status: 'someone-was-removed' }); - } - - for (const id of memberIds) { - _sendToUser(id, { type: 'group', status: 'removed-from-group' }); - - await tx - .update(table.user) - .set({ - groups: locals.user!.groups.map((z) => z.id).filter((g) => g !== groupId) - }) - .where(eq(table.user.id, id)); - } - }); - - return { success: true }; - }, - changeTitle: async ({ request, locals }) => { - const data = await request.formData(); - const groupId = data.get('groupId'); - const title = data.get('title'); - - if (typeof groupId !== 'string' || typeof title !== 'string' || title.length < 1) { - return fail(400, { error: 'Invalid input' }); - } - - const group = await db.select().from(table.group).where(eq(table.group.id, groupId)).limit(1); - if (!group.length) return fail(404, { error: 'Group not found' }); - - const g = group[0]; - if (!(g.members as string[]).includes(locals.user!.id)) { - return fail(403, { error: 'You do not have permission to act on this group.' }); - } - const isOwner = g.owner === locals.user!.id; - - if (!isOwner && !g.changeTitle) { - return fail(403, { error: 'No permission to change title' }); - } - - await db.update(table.group).set({ name: title }).where(eq(table.group.id, groupId)); - //@TODO if a user isn't in the group screen this doesnt get propogated - _sendToSubscribers(groupId, { type: 'group', status: 'name-changed' }); - return { success: true }; - }, - configureGroup: async ({ request, locals }) => { - const data = await request.formData(); - const groupId = data.get('groupId'); - - if (typeof groupId !== 'string') { - return fail(400, { error: 'Invalid group ID' }); - } - - const group = await db.select().from(table.group).where(eq(table.group.id, groupId)).limit(1); - if (!group.length) return fail(404, { error: 'Group not found' }); - - if (group[0].owner !== locals.user!.id) { - return fail(403, { error: 'Only owner can configure group' }); - } - await db - .update(table.group) - .set({ - addMembers: data.has('addMembers') ? 1 : 0, - removeMembers: data.has('removeMembers') ? 1 : 0, - changeTitle: data.has('changeTitle') ? 1 : 0 - }) - .where(eq(table.group.id, groupId)); - _sendToSubscribers(group[0].id, { type: 'group', status: 'permission-change' }); - - return { success: true }; - }, - deleteGroup: async ({ request, locals }) => { - const data = await request.formData(); - const groupId = data.get('groupId'); - - if (typeof groupId !== 'string') { - return fail(400, { error: 'Invalid group ID' }); - } - - const group = await db.select().from(table.group).where(eq(table.group.id, groupId)).limit(1); - - if (!group.length) { - return fail(404, { error: 'Group not found' }); - } - - const g = group[0]; - - if (g.owner !== locals.user!.id) { - return fail(403, { error: 'Not allowed' }); - } - - if ((g.members as string[]).length !== 1 || (g.members as string[])[0] !== locals.user!.id) { - return fail(400, { error: 'Group still has members' }); - } - - await db.transaction(async (tx) => { - await tx - .update(table.user) - .set({ - groups: locals.user!.groups.map((z) => z.id).filter((id) => id !== groupId) - }) - .where(eq(table.user.id, locals.user!.id)); - - await tx.delete(table.group).where(eq(table.group.id, groupId)); - }); - _sendToUser(locals.user!.id, { type: 'group', status: 'removed-from-group' }); - - return { success: true }; - }, - - 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' }); - } - const serverId = ServerID.newV4(); - - 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)); - - _sendToUser(locals.user!.id, { type: 'server', status: 'server-created' }); - return { success: true }; - } + ...FriendActions, + ...GroupActions, + ...ServerActions } satisfies Actions; function requireLogin() { diff --git a/src/routes/app/+page.svelte b/src/routes/app/+page.svelte index 0861297..0db4716 100644 --- a/src/routes/app/+page.svelte +++ b/src/routes/app/+page.svelte @@ -1,6 +1,5 @@ -{#if form} - - - - {form?.error ? 'Ran into an error.' : 'Success!'} - - {form?.error || 'Action completed succesfully.'} - - - - Close - - - -{/if} + type FakePermission = + | 'changeChannelName' + | 'changeServerName' + | 'createInvite' + | 'createChannels' + | 'deleteChannels' + | 'deleteInvites' + | 'addRoles' + | 'deleteRoles' + | 'kickPeople' + | 'banPeople'; + + type FakeRole = { + id: string; + name: string; + permissions: Record; + }; + let newRoleName = $state(''); + + let fakeRoles = $state([ + { + id: 'admin', + name: 'Admin', + permissions: { + changeChannelName: true, + changeServerName: true, + createInvite: true, + createChannels: true, + deleteChannels: true, + deleteInvites: true, + addRoles: true, + deleteRoles: true, + kickPeople: true, + banPeople: true + } + }, + { + id: 'moderator', + name: 'Moderator', + permissions: { + changeChannelName: true, + changeServerName: false, + createInvite: true, + createChannels: false, + deleteChannels: true, + deleteInvites: true, + addRoles: true, + deleteRoles: true, + kickPeople: true, + banPeople: false + } + }, + { + id: 'member', + name: 'Member', + permissions: { + changeChannelName: false, + changeServerName: false, + createInvite: true, + createChannels: false, + deleteChannels: false, + deleteInvites: false, + addRoles: false, + deleteRoles: false, + kickPeople: false, + banPeople: false + } + } + ]); + + let selectedRoleId = $state(null); + + let selectedRole = $derived(fakeRoles.find((r) => r.id === selectedRoleId)); + {#if currentPageID && currentPage && ServerID.is(currentPageID) && !currentSubPageID} -

add invite creation, role creation, moderation, et cetera to this page.

+ {@const server = currentPage as OverviewServer} + + + + Roles + General + + +
+
+

Roles (fake)

+
+ + +
+
+ +
+ {#each fakeRoles as role (role.id)} + + {/if} + + {/each} +
+ + {#if selectedRole} + + + {selectedRole.name} permissions + + +
+ {#each Object.entries(selectedRole.permissions) as [perm, enabled] (perm)} +
+ + { + selectedRole.permissions[perm as FakePermission] = e.detail; + }} + /> +
+ {/each} +
+

+ Changes are local only (no backend yet) +

+
+
+ {/if} +
+
+ +
+

Server Settings

+
+ { + server.name = e.currentTarget.value; + }} + /> + +
+

Changes are local only (no backend yet)

+
+ +
+

Invites

+
{ + return async ({ result }) => { + if (result.type === 'success') { + toast.success('Invite created successfully'); + inviteCode = location.origin + '/invite/' + result.data!.code; + await invalidateAll(); + await fill_overview_data(); + } else if (result.type === 'failure') { + toast.error('Failed to create invite: ' + result.data?.error); + } + }; + }} + class="flex gap-2" + > + +
+ +
+ + +
+
+ +
+ +
+

Active Invites

+
+ {#each server.invites || [] as invite (invite.code)} +
+
+
{invite.code}
+
+ {invite.uses}/{invite.maxUses || '∞'} uses • Created by {invite.creator} on {formatTimestamp( + invite.createdAt + )} +
+
+ +
+ {/each} +
+
+
+
+
{/if} {#if currentPageID && currentPage && ((ServerID.is(currentPageID) && ChannelID.is(currentSubPageID)) || UserID.is(currentPageID) || GroupID.is(currentPageID))}
@@ -425,7 +694,7 @@ user={data.user} data={overview_data} {members} - currentEntity={currentPage} + currentEntity={currentPage as OverviewGroup | OverviewServer} currentEntityId={currentPageID} /> {/if} diff --git a/src/routes/app/actions/friend.ts b/src/routes/app/actions/friend.ts new file mode 100644 index 0000000..ea38b00 --- /dev/null +++ b/src/routes/app/actions/friend.ts @@ -0,0 +1,193 @@ +import type { Actions } from '../$types'; +import { fail } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import * as table from '$lib/server/db/schema'; +import { DirectMessageID, FriendRequestID } from '$lib'; +import { eq } from 'drizzle-orm'; +import { and } from 'drizzle-orm'; +import { type User } from '$lib/server/db/schema'; +import { _sendToSubscribers } from '../../api/updates/+server'; + +export default { + 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) { + return fail(400, { error: 'Invalid username' }); + } + } + + 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 (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' }); + + 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) + ) + ) + .limit(1); + + await db.insert(table.directMessage).values({ + id: DirectMessageID.newV4(), + firstMember: locals.user!.id, + secondMember: user[0].id, + messages: [] + }); + + // 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)); + }); + + _sendToSubscribers(locals.user!.id, { type: 'friends', status: 'accepted' }); + _sendToSubscribers(user[0].id, { type: 'friends', status: 'accepted' }); + 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' }); + + await db.insert(table.friendRequest).values({ + id: FriendRequestID.newV4(), + fromUser: locals.user!.id, + toUser: user[0].id, + toUsername: user[0].username, + fromUsername: locals.user!.username + }); + + _sendToSubscribers(locals.user!.id, { type: 'friends', status: 'sent-request' }); + _sendToSubscribers(user[0].id, { type: 'friends', status: 'new-request' }); + return { success: true }; + }, + removeFriend: async ({ request, locals }) => { + const data = await request.formData(); + const userId = data.get('userId'); + + if (typeof userId !== 'string') { + return fail(400, { error: 'Invalid user ID' }); + } + + // verify we are actually friends + if (!locals.user?.friends.find((z) => z.id === userId)) { + return fail(400, { error: 'Not in friends list' }); + } + + // fetch the target user + const user = await db.select().from(table.user).where(eq(table.user.id, userId)).limit(1); + + if (!user?.length) { + return fail(404, { error: 'User not found' }); + } + + const target = user[0]; + + // remove each other from friends lists + await db.transaction(async (tx) => { + // update current user – filter out removed friend + await tx + .update(table.user) + .set({ + friends: locals.user!.friends.map((z) => z.id).filter((id) => id !== userId) + }) + .where(eq(table.user.id, locals.user!.id)); + + // update target user – filter out us + await tx + .update(table.user) + .set({ + friends: (target.friends as string[]).filter((id) => id !== locals.user!.id) + }) + .where(eq(table.user.id, target.id)); + }); + + 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' }); + } + + // 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' }); + } + + 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' }); + } + + // delete the request + await db.delete(table.friendRequest).where(eq(table.friendRequest.id, requestId)).limit(1); + + _sendToSubscribers(fr.fromUser, { type: 'friends', status: 'request-cancelled' }); + _sendToSubscribers(fr.toUser, { type: 'friends', status: 'request-cancelled' }); + + return { success: true }; + } +} satisfies Actions; diff --git a/src/routes/app/actions/group.ts b/src/routes/app/actions/group.ts new file mode 100644 index 0000000..201820a --- /dev/null +++ b/src/routes/app/actions/group.ts @@ -0,0 +1,265 @@ +import type { Actions } from '../$types'; +import { fail } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import * as table from '$lib/server/db/schema'; +import { GroupID } from '$lib'; +import { eq } from 'drizzle-orm'; +import { _sendToSubscribers, _sendToUser } from '../../api/updates/+server'; +export default { + addMembers: async ({ request, locals }) => { + const data = await request.formData(); + const groupId = data.get('groupId'); + const memberIds = data.getAll('memberIds').map(String); + + if (typeof groupId !== 'string') { + return fail(400, { error: 'Invalid group ID' }); + } + + if (!memberIds.length) { + return fail(400, { error: 'No members selected' }); + } + + const group = await db.select().from(table.group).where(eq(table.group.id, groupId)).limit(1); + if (!group.length) return fail(404, { error: 'Group not found' }); + + const g = group[0]; + + if (!(g.members as string[]).includes(locals.user!.id)) { + return fail(403, { error: 'You do not have permission to act on this group.' }); + } + + const isOwner = g.owner === locals.user!.id; + if (!isOwner && !g.addMembers) { + return fail(403, { error: 'No permission to add members' }); + } + + for (const id of memberIds) { + if (!locals.user!.friends.find((f) => f.id === id)) { + return fail(403, { error: 'Can only add friends' }); + } + } + + const newMembers = [...new Set([...(g.members as string[]), ...memberIds])]; + + await db.transaction(async (tx) => { + await tx.update(table.group).set({ members: newMembers }).where(eq(table.group.id, groupId)); + + for (const id of memberIds) { + const user = await tx.select().from(table.user).where(eq(table.user.id, id)).limit(1); + if (!user.length) continue; + if (newMembers.includes(id)) { + _sendToUser(id, { type: 'group', status: 'added-to-group' }); + } else { + _sendToUser(id, { type: 'group', status: 'member-added-to-group' }); + } + + await tx + .update(table.user) + .set({ groups: (user[0].groups as string[]).concat(groupId) }) + .where(eq(table.user.id, id)); + } + }); + + return { success: true }; + }, + removeMembers: async ({ request, locals }) => { + const data = await request.formData(); + const groupId = data.get('groupId'); + const memberIds = data.getAll('memberIds').map(String); + + if (typeof groupId !== 'string') { + return fail(400, { error: 'Invalid group ID' }); + } + + const group = await db.select().from(table.group).where(eq(table.group.id, groupId)).limit(1); + if (!group.length) return fail(404, { error: 'Group not found' }); + + const g = group[0]; + + if (!(g.members as string[]).includes(locals.user!.id)) { + return fail(403, { error: 'You do not have permission to act on this group.' }); + } + const isOwner = g.owner === locals.user!.id; + if (!isOwner && !g.removeMembers) { + return fail(403, { error: 'No permission to remove members' }); + } + + if (memberIds.includes(g.owner)) { + return fail(400, { error: 'Cannot remove group owner' }); + } + + const remaining = (g.members as string[]).filter((id) => !memberIds.includes(id)); + + await db.transaction(async (tx) => { + await tx.update(table.group).set({ members: remaining }).where(eq(table.group.id, groupId)); + + for (const id of remaining) { + _sendToUser(id, { type: 'group', status: 'someone-was-removed' }); + } + + for (const id of memberIds) { + _sendToUser(id, { type: 'group', status: 'removed-from-group' }); + + await tx + .update(table.user) + .set({ + groups: locals.user!.groups.map((z) => z.id).filter((g) => g !== groupId) + }) + .where(eq(table.user.id, id)); + } + }); + + return { success: true }; + }, + changeTitle: async ({ request, locals }) => { + const data = await request.formData(); + const groupId = data.get('groupId'); + const title = data.get('title'); + + if (typeof groupId !== 'string' || typeof title !== 'string' || title.length < 1) { + return fail(400, { error: 'Invalid input' }); + } + + const group = await db.select().from(table.group).where(eq(table.group.id, groupId)).limit(1); + if (!group.length) return fail(404, { error: 'Group not found' }); + + const g = group[0]; + if (!(g.members as string[]).includes(locals.user!.id)) { + return fail(403, { error: 'You do not have permission to act on this group.' }); + } + const isOwner = g.owner === locals.user!.id; + + if (!isOwner && !g.changeTitle) { + return fail(403, { error: 'No permission to change title' }); + } + + await db.update(table.group).set({ name: title }).where(eq(table.group.id, groupId)); + //@TODO if a user isn't in the group screen this doesnt get propogated + _sendToSubscribers(groupId, { type: 'group', status: 'name-changed' }); + return { success: true }; + }, + configureGroup: async ({ request, locals }) => { + const data = await request.formData(); + const groupId = data.get('groupId'); + + if (typeof groupId !== 'string') { + return fail(400, { error: 'Invalid group ID' }); + } + + const group = await db.select().from(table.group).where(eq(table.group.id, groupId)).limit(1); + if (!group.length) return fail(404, { error: 'Group not found' }); + + if (group[0].owner !== locals.user!.id) { + return fail(403, { error: 'Only owner can configure group' }); + } + await db + .update(table.group) + .set({ + addMembers: data.has('addMembers') ? 1 : 0, + removeMembers: data.has('removeMembers') ? 1 : 0, + changeTitle: data.has('changeTitle') ? 1 : 0 + }) + .where(eq(table.group.id, groupId)); + _sendToSubscribers(group[0].id, { type: 'group', status: 'permission-change' }); + + return { success: true }; + }, + deleteGroup: async ({ request, locals }) => { + const data = await request.formData(); + const groupId = data.get('groupId'); + + if (typeof groupId !== 'string') { + return fail(400, { error: 'Invalid group ID' }); + } + + const group = await db.select().from(table.group).where(eq(table.group.id, groupId)).limit(1); + + if (!group.length) { + return fail(404, { error: 'Group not found' }); + } + + const g = group[0]; + + if (g.owner !== locals.user!.id) { + return fail(403, { error: 'Not allowed' }); + } + + if ((g.members as string[]).length !== 1 || (g.members as string[])[0] !== locals.user!.id) { + return fail(400, { error: 'Group still has members' }); + } + + await db.transaction(async (tx) => { + await tx + .update(table.user) + .set({ + groups: locals.user!.groups.map((z) => z.id).filter((id) => id !== groupId) + }) + .where(eq(table.user.id, locals.user!.id)); + + await tx.delete(table.group).where(eq(table.group.id, groupId)); + }); + _sendToUser(locals.user!.id, { type: 'group', status: 'removed-from-group' }); + + return { success: true }; + }, + createGroup: async ({ request, locals }) => { + const data = await request.formData(); + const members = data.getAll('member').map((z) => z.toString()); + + if (!members.length) { + return fail(400, { error: 'No members selected' }); + } + if (members.includes(locals.user!.id)) { + return fail(403, { error: 'You cannot add yourself to a group.' }); + } + + if (members.length > 9) { + return fail(400, { error: 'Too many members' }); + } + if (members.length < 2) { + return fail(400, { error: 'Not enough members' }); + } + for (const member of members) { + if (!locals.user!.friends.find((z) => z.id == member)) { + return fail(403, { error: 'A member is not your friend.' }); + } + } + const nameArray = []; + + for await (const member of members) { + const dbUser = await db.select().from(table.user).where(eq(table.user.id, member)).limit(1); + nameArray.push(dbUser[0].username); + + if (!dbUser.length) { + return fail(400, { error: 'Invalid member' }); + } + } + + members.push(locals.user!.id); + nameArray.push(locals.user!.username); + + const group = await db + .insert(table.group) + .values({ + id: GroupID.newV4(), + name: nameArray.join(', '), + owner: locals.user!.id, + members: members, + messages: [] + }) + .returning(); + + await db.transaction(async (tx) => { + for await (const member of members) { + _sendToUser(member, { type: 'group', status: 'added-to-group' }); + const user = await tx.select().from(table.user).where(eq(table.user.id, member)).limit(1); + await tx + .update(table.user) + .set({ groups: (user[0].groups as string[]).concat(group[0].id) }) + .where(eq(table.user.id, member)); + } + }); + + return { success: true, group }; + } +} satisfies Actions; diff --git a/src/routes/app/actions/server.ts b/src/routes/app/actions/server.ts new file mode 100644 index 0000000..46ef054 --- /dev/null +++ b/src/routes/app/actions/server.ts @@ -0,0 +1,156 @@ +import { fail } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import * as table from '$lib/server/db/schema'; +import { InviteID, ServerID } from '$lib'; +import { eq } from 'drizzle-orm'; +import { _sendToUser } from '../../api/updates/+server'; +import type { Actions } from '../$types'; + +export default { + joinServer: async ({ request, locals }) => { + const data = await request.formData(); + const inviteOrUrl = data.get('invite'); + + if (typeof inviteOrUrl !== 'string') { + return fail(400, { error: 'Invalid invite' }); + } + + let isUrl: URL | undefined; + + try { + isUrl = new URL(inviteOrUrl); // http(s|)://[^/]*/invite/(.*) + } catch { + // IsURL stays undefined if url is not able to be parsed. + } + + const code = isUrl ? decodeURIComponent(isUrl.pathname.split('/').at(-1)!) : inviteOrUrl; + + const invRaw = await db.select().from(table.invite).where(eq(table.invite.code, code)).limit(1); + + const invite = invRaw[0]; + + if (!invite) return fail(400, { error: 'Invalid invite' }); + + const server = await db + .select() + .from(table.server) + .where(eq(table.server.id, invite.serverId)) + .limit(1); + + 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' }); + + // @TODO check if maybe i'm trippin here and this is not how you do it + if (invite.maxUses && (invite.uses as string[]).length >= invite.maxUses) { + await db.transaction(async (tx) => { + await tx + .update(table.server) + .set({ + invites: (server[0].invites as string[]).filter((id) => id !== invite.id) + }) + .where(eq(table.server.id, server[0].id)); + + await tx.delete(table.invite).where(eq(table.invite.id, invite.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)); + }); + } else { + // Normal case - just update the invite uses + await db.transaction(async (tx) => { + await tx + .update(table.invite) + .set({ uses: (invite.uses as string[]).concat([locals.user!.id]) }) + .where(eq(table.invite.id, invite.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)); + }); + } + + return { success: true }; + }, + createInvite: async ({ request, locals }) => { + const data = await request.formData(); + const serverId = data.get('serverId'); // hidden in frontend + const maxUses = data.has('maxUses') ? parseInt(data.get('maxUses')!.toString()) : 10; + + if (typeof serverId !== 'string') { + return fail(400, { error: 'Server ID incorrect' }); + } + + const server = await db.select().from(table.server).where(eq(table.server.id, serverId)); + if (!server || server.length == 0) { + return fail(400, { error: 'Server ID invalid' }); + } + + // @TODO check permissions here (if owner ignore, then check roles,if any role has createInvite then let create invite) + const inviteId = InviteID.newV4(); + const code = new Array(5) + .fill('') + .map(() => + String.fromCodePoint( + Math.random() < 0.5 + ? (0x1f600 + Math.random() * 0x80) | 0 + : (0x1f300 + Math.random() * 0x500) | 0 + ) + ) + .join(''); + + await db.transaction(async (tx) => { + await tx.insert(table.invite).values({ + id: inviteId, + serverId: serverId, + code, + creatorId: locals.user!.id, + createdAt: new Date(), + maxUses: maxUses <= 0 ? undefined : maxUses // if maxUses is not undefined, there are infnite uses + }); + + await tx + .update(table.server) + .set({ + invites: (server[0].invites as string[]).concat([inviteId]) + }) + .where(eq(table.server.id, serverId)); + }); + return { success: true, code }; + }, + 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' }); + } + const serverId = ServerID.newV4(); + + 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)); + + _sendToUser(locals.user!.id, { type: 'server', status: 'server-created' }); + return { success: true }; + } +} satisfies Actions;