- Add DM edit/delete endpoints (REST + WebSocket) - Add DM typing indicators with real-time broadcast - Fix volume sliders to control LiveKit mic/speaker - Fix Send Message button on user profile popout - Add New DM modal with user search - Fix message pagination (SQL cursor instead of in-memory) - Guard optional attachments on DM messages - Dynamic LiveKit URL from request Host header - DM sidebar auto-sorts by most recent message
156 lines
6.2 KiB
TypeScript
156 lines
6.2 KiB
TypeScript
import { sqliteTable, text, integer, primaryKey, foreignKey } from 'drizzle-orm/sqlite-core';
|
|
|
|
export const users = sqliteTable('users', {
|
|
id: text('id').primaryKey(),
|
|
username: text('username').unique().notNull(),
|
|
displayName: text('display_name'),
|
|
passwordHash: text('password_hash').notNull(),
|
|
avatar: text('avatar'),
|
|
status: text('status').default('offline'),
|
|
customStatus: text('custom_status'),
|
|
createdAt: integer('created_at').notNull(),
|
|
});
|
|
|
|
export const servers = sqliteTable('servers', {
|
|
id: text('id').primaryKey(),
|
|
name: text('name').notNull(),
|
|
icon: text('icon'),
|
|
ownerId: text('owner_id').notNull().references(() => users.id),
|
|
inviteCode: text('invite_code').unique(),
|
|
createdAt: integer('created_at').notNull(),
|
|
});
|
|
|
|
export const serverMembers = sqliteTable('server_members', {
|
|
serverId: text('server_id').notNull().references(() => servers.id, { onDelete: 'cascade' }),
|
|
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
role: text('role').default('member'),
|
|
nickname: text('nickname'),
|
|
joinedAt: integer('joined_at').notNull(),
|
|
}, (table) => ({
|
|
pk: primaryKey({ columns: [table.serverId, table.userId] }),
|
|
}));
|
|
|
|
export const channels = sqliteTable('channels', {
|
|
id: text('id').primaryKey(),
|
|
serverId: text('server_id').notNull().references(() => servers.id, { onDelete: 'cascade' }),
|
|
name: text('name').notNull(),
|
|
type: text('type').notNull(),
|
|
topic: text('topic'),
|
|
position: integer('position').default(0),
|
|
createdAt: integer('created_at').notNull(),
|
|
});
|
|
|
|
export const messages = sqliteTable('messages', {
|
|
id: text('id').primaryKey(),
|
|
channelId: text('channel_id').notNull().references(() => channels.id, { onDelete: 'cascade' }),
|
|
userId: text('user_id').notNull().references(() => users.id),
|
|
replyToId: text('reply_to_id'),
|
|
content: text('content'),
|
|
editedAt: integer('edited_at'),
|
|
createdAt: integer('created_at').notNull(),
|
|
}, (table) => ({
|
|
replyToFk: foreignKey({
|
|
columns: [table.replyToId],
|
|
foreignColumns: [table.id],
|
|
}).onDelete('set null'),
|
|
}));
|
|
|
|
export const attachments = sqliteTable('attachments', {
|
|
id: text('id').primaryKey(),
|
|
messageId: text('message_id').references(() => messages.id, { onDelete: 'cascade' }),
|
|
dmMessageId: text('dm_message_id'),
|
|
filename: text('filename').notNull(),
|
|
originalName: text('original_name').notNull(),
|
|
mimetype: text('mimetype').notNull(),
|
|
size: integer('size').notNull(),
|
|
createdAt: integer('created_at').notNull(),
|
|
});
|
|
|
|
export const dmChannels = sqliteTable('dm_channels', {
|
|
id: text('id').primaryKey(),
|
|
createdAt: integer('created_at').notNull(),
|
|
});
|
|
|
|
export const dmMembers = sqliteTable('dm_members', {
|
|
dmChannelId: text('dm_channel_id').notNull().references(() => dmChannels.id, { onDelete: 'cascade' }),
|
|
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
}, (table) => ({
|
|
pk: primaryKey({ columns: [table.dmChannelId, table.userId] }),
|
|
}));
|
|
|
|
export const dmMessages = sqliteTable('dm_messages', {
|
|
id: text('id').primaryKey(),
|
|
dmChannelId: text('dm_channel_id').notNull().references(() => dmChannels.id, { onDelete: 'cascade' }),
|
|
userId: text('user_id').notNull().references(() => users.id),
|
|
replyToId: text('reply_to_id'),
|
|
content: text('content'),
|
|
editedAt: integer('edited_at'),
|
|
createdAt: integer('created_at').notNull(),
|
|
});
|
|
|
|
export const friends = sqliteTable('friends', {
|
|
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
friendId: text('friend_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
createdAt: integer('created_at').notNull(),
|
|
}, (table) => ({
|
|
pk: primaryKey({ columns: [table.userId, table.friendId] }),
|
|
}));
|
|
|
|
export const friendRequests = sqliteTable('friend_requests', {
|
|
id: text('id').primaryKey(),
|
|
fromId: text('from_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
toId: text('to_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
status: text('status').default('pending'), // 'pending', 'accepted', 'declined'
|
|
createdAt: integer('created_at').notNull(),
|
|
});
|
|
|
|
export const reactions = sqliteTable('reactions', {
|
|
id: text('id').primaryKey(),
|
|
messageId: text('message_id').notNull().references(() => messages.id, { onDelete: 'cascade' }),
|
|
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
emoji: text('emoji').notNull(),
|
|
createdAt: integer('created_at').notNull(),
|
|
});
|
|
|
|
export const dmReactions = sqliteTable('dm_reactions', {
|
|
id: text('id').primaryKey(),
|
|
dmMessageId: text('dm_message_id').notNull(),
|
|
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
emoji: text('emoji').notNull(),
|
|
createdAt: integer('created_at').notNull(),
|
|
});
|
|
|
|
export const roles = sqliteTable('roles', {
|
|
id: text('id').primaryKey(),
|
|
serverId: text('server_id').notNull().references(() => servers.id, { onDelete: 'cascade' }),
|
|
name: text('name').notNull(),
|
|
color: text('color').default('#b9bbbe'),
|
|
position: integer('position').default(0),
|
|
permissions: text('permissions'), // JSON string of permission keys
|
|
createdAt: integer('created_at').notNull(),
|
|
});
|
|
|
|
export const memberRoles = sqliteTable('member_roles', {
|
|
serverId: text('server_id').notNull().references(() => servers.id, { onDelete: 'cascade' }),
|
|
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
roleId: text('role_id').notNull().references(() => roles.id, { onDelete: 'cascade' }),
|
|
}, (table) => ({
|
|
pk: primaryKey({ columns: [table.serverId, table.userId, table.roleId] }),
|
|
}));
|
|
|
|
export const serverFolders = sqliteTable('server_folders', {
|
|
id: text('id').primaryKey(),
|
|
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
name: text('name'),
|
|
color: text('color'),
|
|
position: integer('position').default(0),
|
|
createdAt: integer('created_at').notNull(),
|
|
});
|
|
|
|
export const serverFolderMembers = sqliteTable('server_folder_members', {
|
|
folderId: text('folder_id').notNull().references(() => serverFolders.id, { onDelete: 'cascade' }),
|
|
serverId: text('server_id').notNull().references(() => servers.id, { onDelete: 'cascade' }),
|
|
}, (table) => ({
|
|
pk: primaryKey({ columns: [table.folderId, table.serverId] }),
|
|
}));
|