work heavily on all server stuffs

This commit is contained in:
Soph :3 2026-01-16 19:35:28 +02:00
parent d9f5919b60
commit 365c43c501
17 changed files with 2726 additions and 531 deletions

View file

@ -0,0 +1 @@
ALTER TABLE `user` ADD `invites` text DEFAULT '[]' NOT NULL;

View file

@ -0,0 +1,2 @@
ALTER TABLE `server` ADD `invites` text DEFAULT '[]' NOT NULL;--> statement-breakpoint
ALTER TABLE `user` DROP COLUMN `invites`;

View file

@ -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;

View file

@ -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": {}
}
}

View file

@ -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": {}
}
}

View file

@ -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": {}
}
}

View file

@ -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
}
]
}

View file

@ -0,0 +1,7 @@
import Root from "./switch.svelte";
export {
Root,
//
Root as Switch,
};

View file

@ -0,0 +1,29 @@
<script lang="ts">
import { Switch as SwitchPrimitive } from "bits-ui";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
checked = $bindable(false),
...restProps
}: WithoutChildrenOrChild<SwitchPrimitive.RootProps> = $props();
</script>
<SwitchPrimitive.Root
bind:ref
bind:checked
data-slot="switch"
class={cn(
"data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 peer inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...restProps}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
class={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>

View file

@ -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;

View file

@ -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;

View file

@ -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')

View file

@ -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() {

View file

@ -1,6 +1,5 @@
<script lang="ts">
import {
Status,
type OverviewData,
GroupID,
UserID,
@ -20,19 +19,22 @@
import type { PageServerData } from './$types';
import AppSidebar from '$lib/components/app-sidebar.svelte';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as AlertDialog from '$lib/components/ui/alert-dialog/index.js';
import { enhance } from '$app/forms';
import { onMount } from 'svelte';
import type { ActionData } from './$types';
import { formatTimestamp } from '$lib/utils';
import Input from '$lib/components/ui/input/input.svelte';
import { Button } from '$lib/components/ui/button';
import * as Tabs from '$lib/components/ui/tabs/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import { Switch } from '$lib/components/ui/switch/index.js';
import SendHorizontal from '@lucide/svelte/icons/send-horizontal';
import PersonStanding from '@lucide/svelte/icons/person-standing';
import MemberSidebar from '$lib/components/member-sidebar.svelte';
import { invalidateAll } from '$app/navigation';
import { toast } from 'svelte-sonner';
let errorOpen = $state(true);
let { form, data }: { form: ActionData; data: PageServerData } = $props();
let currentPageID: (UserId | GroupId | ServerId) | null = $state(null);
@ -40,6 +42,8 @@
let currentPage: OverviewUser | OverviewGroup | OverviewServer | undefined = $state();
let currentSubPage: Channel | null = $state(null);
let inviteCode = $state();
let sse: EventSource | undefined;
let messagesElement: HTMLDivElement | undefined = $state();
let isMembersTabOpen = $state(true);
@ -146,7 +150,7 @@
async function getMessages() {
let path = '';
let subscribe = '';
let subscribe: string | null | undefined = '';
if (ServerID.is(currentPageID) && ChannelID.is(currentSubPageID)) {
path = '/api/messages/' + currentPageID + '/' + currentSubPageID;
@ -156,6 +160,7 @@
path = '/api/messages/' + subscribe;
}
if (!subscribe) return;
const req = await fetch(path);
const data = await req.json();
@ -184,6 +189,14 @@
onMount(() => {
async function run() {
if (form) {
if (form.success) {
toast.success('Action succeeded.');
}
if (form.error) {
toast.error('Action had a error: ' + form.error);
}
await invalidateAll();
}
await fill_overview_data();
@ -261,23 +274,81 @@
}
});
});
</script>
{#if form}
<AlertDialog.Root bind:open={errorOpen}>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>{form?.error ? 'Ran into an error.' : 'Success!'}</AlertDialog.Title>
<AlertDialog.Description>
{form?.error || 'Action completed succesfully.'}
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Cancel>Close</AlertDialog.Cancel>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
{/if}
type FakePermission =
| 'changeChannelName'
| 'changeServerName'
| 'createInvite'
| 'createChannels'
| 'deleteChannels'
| 'deleteInvites'
| 'addRoles'
| 'deleteRoles'
| 'kickPeople'
| 'banPeople';
type FakeRole = {
id: string;
name: string;
permissions: Record<FakePermission, boolean>;
};
let newRoleName = $state('');
let fakeRoles = $state<FakeRole[]>([
{
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<string | null>(null);
let selectedRole = $derived(fakeRoles.find((r) => r.id === selectedRoleId));
</script>
<Sidebar.Provider>
<AppSidebar
@ -331,7 +402,205 @@
</header>
{#if currentPageID && currentPage && ServerID.is(currentPageID) && !currentSubPageID}
<h1>add invite creation, role creation, moderation, et cetera to this page.</h1>
{@const server = currentPage as OverviewServer}
<Tabs.Root value="roles" class="max-w-[800px] p-2">
<Tabs.List>
<Tabs.Trigger value="roles">Roles</Tabs.Trigger>
<Tabs.Trigger value="general">General</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="roles" class="space-y-6 p-4">
<div class="space-y-6">
<div class="flex items-center justify-between">
<h2 class="text-lg font-medium">Roles (fake)</h2>
<div class="flex gap-2">
<Input placeholder="New role name" class="h-8 w-40" bind:value={newRoleName} />
<Button
size="sm"
variant="outline"
onclick={() => {
if (!newRoleName) return;
const newRole: FakeRole = {
id: crypto.randomUUID(),
name: newRoleName,
permissions: {
changeChannelName: false,
changeServerName: false,
createInvite: false,
createChannels: false,
deleteChannels: false,
deleteInvites: false,
addRoles: false,
deleteRoles: false,
kickPeople: false,
banPeople: false
}
};
fakeRoles = [...fakeRoles, newRole];
selectedRoleId = newRole.id;
newRoleName = '';
}}
>
Create Role
</Button>
</div>
</div>
<div class="flex flex-wrap gap-2">
{#each fakeRoles as role (role.id)}
<Button
type="button"
variant={selectedRoleId === role.id ? 'secondary' : 'ghost'}
size="sm"
class="gap-1"
onclick={() => (selectedRoleId = role.id)}
>
{role.name}
{#if role.id !== 'member'}
<button
type="button"
class="text-destructive hover:text-destructive/80"
onclick={(e) => {
e.stopPropagation();
fakeRoles = fakeRoles.filter((r) => r.id !== role.id);
if (selectedRoleId === role.id) selectedRoleId = null;
}}
>
×
</button>
{/if}
</Button>
{/each}
</div>
{#if selectedRole}
<Card.Root>
<Card.Header>
<Card.Title>{selectedRole.name} permissions</Card.Title>
</Card.Header>
<Card.Content class="space-y-4">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
{#each Object.entries(selectedRole.permissions) as [perm, enabled] (perm)}
<div class="flex items-center justify-between">
<Label>
{#if perm === 'changeChannelName'}Change channel name{/if}
{#if perm === 'changeServerName'}Change server name{/if}
{#if perm === 'createInvite'}Create invite{/if}
{#if perm === 'createChannels'}Create channels{/if}
{#if perm === 'deleteChannels'}Delete channels{/if}
{#if perm === 'deleteInvites'}Delete invites{/if}
{#if perm === 'addRoles'}Add roles{/if}
{#if perm === 'deleteRoles'}Delete roles{/if}
{#if perm === 'kickPeople'}Kick members{/if}
{#if perm === 'banPeople'}Ban members{/if}
</Label>
<Switch
checked={enabled}
onchange={(e) => {
selectedRole.permissions[perm as FakePermission] = e.detail;
}}
/>
</div>
{/each}
</div>
<p class="text-sm text-muted-foreground">
Changes are local only (no backend yet)
</p>
</Card.Content>
</Card.Root>
{/if}
</div>
</Tabs.Content>
<Tabs.Content value="general" class="space-y-6 p-4">
<div class="space-y-2">
<h2 class="text-lg font-medium">Server Settings</h2>
<div class="flex items-center gap-4">
<Input
placeholder="New server name"
class="max-w-xs"
value={server?.name}
oninput={(e) => {
server.name = e.currentTarget.value;
}}
/>
<Button
variant="outline"
onclick={() => toast.info('Server name change would be saved (no backend yet)')}
>
Save Name
</Button>
</div>
<p class="text-xs text-muted-foreground">Changes are local only (no backend yet)</p>
</div>
<div class="space-y-2">
<h2 class="text-lg font-medium">Invites</h2>
<form
method="POST"
action="?/createInvite"
use:enhance={() => {
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"
>
<input type="hidden" name="serverId" value={currentPageID} />
<div class="flex flex-1 gap-2">
<Input placeholder="Create new invite" class="flex-1" readonly value={inviteCode} />
<div class="flex items-center gap-2">
<Label class="text-sm">Max uses:</Label>
<Input type="number" name="maxUses" min="0" class="w-20" placeholder="10" />
</div>
</div>
<Button type="submit">Create Invite</Button>
</form>
<div class="space-y-2 pt-4">
<h3 class="font-medium">Active Invites</h3>
<div class="space-y-2 rounded border p-3">
{#each server.invites || [] as invite (invite.code)}
<div class="flex items-center justify-between rounded p-2 hover:bg-muted/50">
<div class="flex items-center gap-2">
<div class="font-mono">{invite.code}</div>
<div class="text-sm text-muted-foreground">
{invite.uses}/{invite.maxUses || '∞'} uses • Created by {invite.creator} on {formatTimestamp(
invite.createdAt
)}
</div>
</div>
<Button
variant="destructive"
size="sm"
onclick={async () => {
const response = await fetch(`/api/invites/${invite.code}`, {
method: 'DELETE'
});
if (response.ok) {
toast.success('Invite deleted successfully');
await invalidateAll();
await fill_overview_data();
} else {
toast.error('Failed to delete invite');
}
}}
>
Delete
</Button>
</div>
{/each}
</div>
</div>
</div>
</Tabs.Content>
</Tabs.Root>
{/if}
{#if currentPageID && currentPage && ((ServerID.is(currentPageID) && ChannelID.is(currentSubPageID)) || UserID.is(currentPageID) || GroupID.is(currentPageID))}
<div class="h-min shrink overflow-y-scroll" bind:this={messagesElement}>
@ -425,7 +694,7 @@
user={data.user}
data={overview_data}
{members}
currentEntity={currentPage}
currentEntity={currentPage as OverviewGroup | OverviewServer}
currentEntityId={currentPageID}
/>
{/if}

View file

@ -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;

View file

@ -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;

View file

@ -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;