From e219229b63423964b3ce1fd63d217b4c183e9235 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Sun, 1 Mar 2026 21:26:57 +0100 Subject: [PATCH] feat: add Backspace design prototype and rebrand from Opencord - Add Backspace-design-prototype.html: finalized "Aether Drift" design language with warm matte surfaces and subtle frosted glass accents - Update CLAUDE.md with DESIGN SYSTEM section and remove Discord clone references - Rename all Opencord references to Backspace across the full codebase - Archive outdated design experiments and Discord-specific assets - Add science-backed accessibility fallback (prefers-reduced-transparency) --- .env.example | 4 +- CLAUDE.md | 535 ++++++++++++++---- Makefile | 30 - README.md | 30 +- package.json | 16 +- packages/desktop/electron-builder.yml | 4 +- packages/desktop/package.json | 2 +- packages/desktop/src/main.ts | 10 +- packages/desktop/src/preload.ts | 2 +- packages/server/package.json | 4 +- packages/server/src/config.ts | 2 +- packages/server/src/db/migrate.ts | 11 +- packages/server/src/db/seed.ts | 8 +- packages/server/src/index.ts | 2 +- packages/server/src/routes/auth.ts | 2 +- packages/server/src/routes/channels.ts | 4 +- packages/server/src/routes/dm.ts | 2 +- packages/server/src/routes/livekit.ts | 2 +- packages/server/src/routes/messages.ts | 2 +- packages/server/src/routes/servers.ts | 4 +- packages/server/src/routes/settings.ts | 2 +- packages/server/src/routes/social.ts | 2 +- packages/server/src/routes/uploads.ts | 2 +- packages/server/src/routes/users.ts | 2 +- packages/server/src/routes/utils.ts | 2 +- packages/server/src/utils/permissions.ts | 2 +- packages/server/src/ws/events.ts | 2 +- packages/server/src/ws/handler.ts | 2 +- packages/shared/package.json | 2 +- packages/web/index.html | 2 +- packages/web/package.json | 4 +- packages/web/src/api/client.ts | 4 +- packages/web/src/components/chat/Embed.tsx | 2 +- .../src/components/chat/FriendsPage.test.tsx | 2 +- .../web/src/components/chat/FriendsPage.tsx | 4 +- .../src/components/chat/MentionPopover.tsx | 2 +- packages/web/src/components/chat/Message.tsx | 2 +- .../web/src/components/chat/MessageInput.tsx | 2 +- .../web/src/components/chat/MessageList.tsx | 2 +- .../src/components/layout/ActivityPanel.tsx | 2 +- .../web/src/components/layout/AppLayout.tsx | 6 +- .../src/components/layout/MemberSidebar.tsx | 2 +- .../components/modals/AddDmMemberModal.tsx | 2 +- .../web/src/components/modals/NewDmModal.tsx | 2 +- .../src/components/modals/ServerSettings.tsx | 2 +- packages/web/src/components/ui/Avatar.tsx | 2 +- .../src/components/ui/UserProfilePopout.tsx | 4 +- .../components/voice/GlobalAudioRenderer.tsx | 2 +- packages/web/src/hooks/useWebSocket.ts | 2 +- packages/web/src/stores/authStore.ts | 12 +- packages/web/src/stores/chatStore.ts | 2 +- packages/web/src/stores/serverStore.ts | 2 +- packages/web/src/stores/settingsStore.ts | 2 +- packages/web/src/stores/socialStore.ts | 2 +- packages/web/src/stores/uiStore.ts | 4 +- packages/web/src/stores/voiceStore.ts | 2 +- packages/web/src/utils/permissions.ts | 2 +- pnpm-lock.yaml | 12 +- 58 files changed, 547 insertions(+), 239 deletions(-) delete mode 100644 Makefile diff --git a/.env.example b/.env.example index 403dab9e..5f7433ff 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -# Opencord Configuration +# Backspace Configuration # Copy this file to .env and fill in the values # Server @@ -15,7 +15,7 @@ LIVEKIT_API_SECRET= # Storage UPLOAD_DIR=./data/uploads -DB_PATH=./data/opencord.db +DB_PATH=./data/backspace.db MAX_UPLOAD_SIZE=104857600 # Registration — set to false to disable new user registration diff --git a/CLAUDE.md b/CLAUDE.md index 9381a8de..8787ab56 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,55 +1,76 @@ -# CLAUDE.md — Opencord Brain File +# CLAUDE.md — Backspace Brain File ## IDENTITY -You are the sole developer of Opencord, an open-source, self-hosted Discord alternative. You are an expert full-stack TypeScript architect. You write production-quality code. You never cut corners. You never use placeholders. You finish what you start. +You are the sole developer of Backspace, an open-source, self-hosted Discord alternative. You are an expert full-stack TypeScript architect. You write production-quality code. You never cut corners. You never use placeholders. You finish what you start. + +## DESIGN SYSTEM + +Backspace has its own visual identity — it is NOT a Discord clone. The design prototype is the single source of truth: + +- **Prototype file:** `Backspace-design-prototype.html` (open in browser to view) +- **Design language:** "Aether Drift" — warm matte surfaces with subtle frosted glass accents +- **Two-material system:** Solid matte panels for content (75%), frosted glass bubbles for persistent controls (25%) +- **Color palette:** Warm dark surfaces (#13131a chat, #1a1a23 sidebars), pastel accents (mint, peach, lavender, sky, amber, rose, coral) +- **Glass elements:** Server strip (left column), voice+user bubble (bottom-left, crosses over server strip), input bubble (bottom of chat) +- **Glass material:** `backdrop-filter: blur(20px) saturate(120%)`, warm-tinted `rgba(20,20,26,0.52)`, subtle 0.07 opacity borders +- **Key principles:** Calm over flashy. Warm over cool. Quiet glass (felt, not seen). No decorative gradients. Minimal shadows. +- **Accessibility:** `prefers-reduced-transparency` media query falls back to solid surfaces + +When making UI changes, consult the prototype for colors, spacing, materials, and hierarchy. The frontend should converge toward this design. ## MISSION -Build Opencord from scratch as a complete, working application. All 8 phases. Every file fully implemented. No stubs, no TODOs, no "add later" comments. When you are done, a user must be able to docker compose up and have a fully working Discord clone. +Maintain and extend Backspace as a complete, production-quality application. The core application is fully built and deployed. Every change must uphold the same standard: no stubs, no TODOs, no shortcuts. A user must always be able to `docker compose up` and have a fully working chat platform. ## CRITICAL RULES -- NEVER use placeholder code, TODO comments, or // ...rest of code shortcuts. Every function you write must be FULLY implemented with real logic. -- NEVER use // ...rest of code or // similar to above shortcuts. Write out every single line. +- NEVER use placeholder code, TODO comments, or `// ...rest of code` shortcuts. Every function you write must be FULLY implemented with real logic. +- NEVER use `// ...rest of code` or `// similar to above` shortcuts. Write out every single line. - NEVER skip files or say "you can add this later". Build everything NOW. - NEVER generate partial components. Every React component must be complete with all state, handlers, styling, and edge cases. - If you hit the output limit, STOP mid-sentence and continue EXACTLY where you left off in your next message. Do NOT summarize or skip ahead. -- Write production-quality code from the start. Proper error handling, input validation, TypeScript strict mode, no any types. -- After completing each phase, UPDATE the ## PROGRESS section in CLAUDE.md marking it ✅ and noting what files were created. -- After completing each phase, RUN the test commands listed to verify it works before moving on. +- Write production-quality code from the start. Proper error handling, input validation, TypeScript strict mode, no `any` types. - If something fails, FIX IT before moving on. Never leave broken code behind. +- Test changes with `pnpm dev` before considering them done. Both server and frontend must start without errors. ## TECH STACK (DO NOT DEVIATE) | Layer | Technology | Package | |-------|-----------|---------| | Runtime | Node.js 20+ with TypeScript (strict mode) | typescript, tsx | -| Backend Framework | Fastify | fastify, @fastify/cors, @fastify/multipart, @fastify/static, @fastify/websocket | +| Backend Framework | Fastify | fastify, @fastify/cors, @fastify/multipart, @fastify/static, @fastify/websocket, @fastify/rate-limit | | Database | SQLite | better-sqlite3 | | ORM | Drizzle ORM | drizzle-orm, drizzle-kit | | Auth | JWT + bcrypt | jsonwebtoken, bcryptjs | | WebSocket | ws (via @fastify/websocket) | @fastify/websocket | | Frontend | React 18 + TypeScript | react, react-dom, react-router-dom | | Styling | Tailwind CSS 3 | tailwindcss, postcss, autoprefixer | -| Build Tool | Vite | vite, @vitejs/plugin-react | -| State Management | Zustand | zustand | +| Build Tool | Vite 6 | vite, @vitejs/plugin-react | +| State Management | Zustand 5 | zustand | | Voice/Video | LiveKit Client SDK | livekit-client, @livekit/components-react | | LiveKit Token | livekit-server-sdk | livekit-server-sdk | -| Desktop | Electron | electron, electron-builder | +| Audio Processing | Web Noise Suppressor (RNNoise) | @sapphi-red/web-noise-suppressor | +| Markdown | react-markdown + remark-gfm | react-markdown, remark-gfm | +| Syntax Highlighting | prism-react-renderer | prism-react-renderer | +| HTML Parsing | Cheerio (server-side URL metadata) | cheerio | +| Desktop | Electron 33 | electron, electron-builder | +| Testing | Vitest + Testing Library | vitest, @testing-library/react, jsdom | | Monorepo | pnpm workspaces | pnpm | -| Markdown | react-markdown | react-markdown | ## PROJECT STRUCTURE ``` -Opencord/ +Backspace/ ├── CLAUDE.md +├── Backspace-design-prototype.html ├── package.json ├── pnpm-workspace.yaml ├── tsconfig.base.json ├── Dockerfile ├── docker-compose.yml +├── deploy.sh +├── Makefile ├── .env.example ├── .gitignore ├── README.md @@ -58,7 +79,8 @@ Opencord/ │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── src/ -│ │ └── types.ts +│ │ ├── types.ts +│ │ └── permissions.ts │ ├── server/ │ │ ├── package.json │ │ ├── tsconfig.json @@ -68,7 +90,8 @@ Opencord/ │ │ ├── db/ │ │ │ ├── schema.ts │ │ │ ├── index.ts -│ │ │ └── seed.ts +│ │ │ ├── seed.ts +│ │ │ └── migrate.ts │ │ ├── routes/ │ │ │ ├── auth.ts │ │ │ ├── users.ts @@ -77,7 +100,10 @@ Opencord/ │ │ │ ├── messages.ts │ │ │ ├── uploads.ts │ │ │ ├── dm.ts -│ │ │ └── livekit.ts +│ │ │ ├── livekit.ts +│ │ │ ├── social.ts +│ │ │ ├── settings.ts +│ │ │ └── utils.ts │ │ ├── ws/ │ │ │ ├── handler.ts │ │ │ └── events.ts @@ -97,35 +123,63 @@ Opencord/ │ │ ├── App.tsx │ │ ├── api/ │ │ │ └── client.ts +│ │ ├── audio/ +│ │ │ ├── AudioManager.ts +│ │ │ └── SpeakingDetector.ts │ │ ├── stores/ │ │ │ ├── authStore.ts │ │ │ ├── serverStore.ts │ │ │ ├── chatStore.ts │ │ │ ├── voiceStore.ts +│ │ │ ├── socialStore.ts +│ │ │ ├── settingsStore.ts │ │ │ └── uiStore.ts │ │ ├── hooks/ │ │ │ ├── useWebSocket.ts │ │ │ ├── useLiveKit.ts -│ │ │ └── useAuth.ts +│ │ │ ├── useAuth.ts +│ │ │ ├── useTrackStats.ts +│ │ │ └── useAudioTrackPlayer.ts +│ │ ├── utils/ +│ │ │ ├── permissions.ts +│ │ │ ├── livekitInternals.ts +│ │ │ └── screenShare.ts │ │ ├── components/ │ │ │ ├── layout/ │ │ │ │ ├── AppLayout.tsx │ │ │ │ ├── ServerSidebar.tsx │ │ │ │ ├── ChannelSidebar.tsx │ │ │ │ ├── MainContent.tsx +│ │ │ │ ├── RightPanel.tsx │ │ │ │ ├── MemberSidebar.tsx +│ │ │ │ ├── ActivityPanel.tsx +│ │ │ │ ├── MemberListToggleButton.tsx │ │ │ │ └── MobileNav.tsx │ │ │ ├── chat/ │ │ │ │ ├── MessageList.tsx │ │ │ │ ├── Message.tsx │ │ │ │ ├── MessageInput.tsx │ │ │ │ ├── TypingIndicator.tsx -│ │ │ │ └── ImagePreview.tsx +│ │ │ │ ├── ImagePreview.tsx +│ │ │ │ ├── MarkdownRenderer.tsx +│ │ │ │ ├── MentionPopover.tsx +│ │ │ │ ├── MentionBadge.tsx +│ │ │ │ ├── Embed.tsx +│ │ │ │ └── FriendsPage.tsx │ │ │ ├── voice/ │ │ │ │ ├── VoiceChannel.tsx +│ │ │ │ ├── VoiceChatPanel.tsx +│ │ │ │ ├── VoiceControlBar.tsx │ │ │ │ ├── VoiceControls.tsx │ │ │ │ ├── VoiceGrid.tsx -│ │ │ │ └── VoiceUser.tsx +│ │ │ │ ├── VoiceUser.tsx +│ │ │ │ ├── StreamTile.tsx +│ │ │ │ ├── PictureInPicture.tsx +│ │ │ │ ├── IncomingCallModal.tsx +│ │ │ │ ├── GlobalAudioRenderer.tsx +│ │ │ │ ├── SoundController.tsx +│ │ │ │ ├── ConnectionInfoPopover.tsx +│ │ │ │ └── ScreenShareSettingsPopover.tsx │ │ │ ├── auth/ │ │ │ │ ├── LoginPage.tsx │ │ │ │ └── RegisterPage.tsx @@ -135,13 +189,19 @@ Opencord/ │ │ │ │ ├── CreateChannel.tsx │ │ │ │ ├── JoinServer.tsx │ │ │ │ ├── UserSettings.tsx -│ │ │ │ └── ServerSettings.tsx +│ │ │ │ ├── ServerSettings.tsx +│ │ │ │ ├── ChannelSettingsModal.tsx +│ │ │ │ ├── NewDmModal.tsx +│ │ │ │ └── AddDmMemberModal.tsx │ │ │ └── ui/ │ │ │ ├── Avatar.tsx │ │ │ ├── Modal.tsx │ │ │ ├── Tooltip.tsx │ │ │ ├── ContextMenu.tsx -│ │ │ └── LoadingSpinner.tsx +│ │ │ ├── LoadingSpinner.tsx +│ │ │ └── UserProfilePopout.tsx +│ │ ├── test/ +│ │ │ └── setup.ts │ │ └── styles/ │ │ └── globals.css │ └── desktop/ @@ -152,7 +212,7 @@ Opencord/ │ │ └── preload.ts │ └── electron-builder.yml └── data/ - ├── opencord.db + ├── backspace.db └── uploads/ ``` @@ -168,6 +228,7 @@ CREATE TABLE users ( avatar TEXT, status TEXT DEFAULT 'offline', custom_status TEXT, + is_admin INTEGER DEFAULT 0, created_at INTEGER NOT NULL ); @@ -183,30 +244,59 @@ CREATE TABLE servers ( -- Server Members CREATE TABLE server_members ( - server_id TEXT REFERENCES servers(id) ON DELETE CASCADE, - user_id TEXT REFERENCES users(id) ON DELETE CASCADE, - role TEXT DEFAULT 'member', + server_id TEXT NOT NULL REFERENCES servers(id) ON DELETE CASCADE, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, nickname TEXT, joined_at INTEGER NOT NULL, PRIMARY KEY (server_id, user_id) ); +-- Roles +CREATE TABLE roles ( + id TEXT PRIMARY KEY, + server_id TEXT NOT NULL REFERENCES servers(id) ON DELETE CASCADE, + name TEXT NOT NULL, + color TEXT DEFAULT '#b9bbbe', + position INTEGER DEFAULT 0, + permissions TEXT, -- decimal string of bigint permission bits + created_at INTEGER NOT NULL +); + +-- Member Roles (many-to-many) +CREATE TABLE member_roles ( + server_id TEXT NOT NULL REFERENCES servers(id) ON DELETE CASCADE, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + role_id TEXT NOT NULL REFERENCES roles(id) ON DELETE CASCADE, + PRIMARY KEY (server_id, user_id, role_id) +); + -- Channels CREATE TABLE channels ( id TEXT PRIMARY KEY, - server_id TEXT REFERENCES servers(id) ON DELETE CASCADE, + server_id TEXT NOT NULL REFERENCES servers(id) ON DELETE CASCADE, name TEXT NOT NULL, - type TEXT NOT NULL, + type TEXT NOT NULL, -- 'text' | 'voice' | 'video' topic TEXT, position INTEGER DEFAULT 0, created_at INTEGER NOT NULL ); +-- Channel Permission Overrides +CREATE TABLE channel_overrides ( + channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE, + target_type TEXT NOT NULL, -- 'role' | 'member' + target_id TEXT NOT NULL, -- role ID or user ID + allow TEXT NOT NULL DEFAULT '0', -- bigint decimal string + deny TEXT NOT NULL DEFAULT '0', -- bigint decimal string + PRIMARY KEY (channel_id, target_type, target_id) +); + -- Messages CREATE TABLE messages ( id TEXT PRIMARY KEY, channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(id), + reply_to_id TEXT REFERENCES messages(id) ON DELETE SET NULL, content TEXT, edited_at INTEGER, created_at INTEGER NOT NULL @@ -216,6 +306,7 @@ CREATE TABLE messages ( CREATE TABLE attachments ( id TEXT PRIMARY KEY, message_id TEXT REFERENCES messages(id) ON DELETE CASCADE, + dm_message_id TEXT, filename TEXT NOT NULL, original_name TEXT NOT NULL, mimetype TEXT NOT NULL, @@ -223,15 +314,26 @@ CREATE TABLE attachments ( created_at INTEGER NOT NULL ); +-- Reactions +CREATE TABLE reactions ( + id TEXT PRIMARY KEY, + message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + emoji TEXT NOT NULL, + created_at INTEGER NOT NULL +); + -- DM Channels CREATE TABLE dm_channels ( id TEXT PRIMARY KEY, + owner_id TEXT, -- NULL for 1-on-1, set for group DMs created_at INTEGER NOT NULL ); CREATE TABLE dm_members ( - dm_channel_id TEXT REFERENCES dm_channels(id) ON DELETE CASCADE, - user_id TEXT REFERENCES users(id) ON DELETE CASCADE, + dm_channel_id TEXT NOT NULL REFERENCES dm_channels(id) ON DELETE CASCADE, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + closed INTEGER DEFAULT 0, -- soft-close flag PRIMARY KEY (dm_channel_id, user_id) ); @@ -240,119 +342,348 @@ CREATE TABLE dm_messages ( id TEXT PRIMARY KEY, dm_channel_id TEXT NOT NULL REFERENCES dm_channels(id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(id), + reply_to_id TEXT, content TEXT, + edited_at INTEGER, created_at INTEGER NOT NULL ); + +-- DM Reactions +CREATE TABLE dm_reactions ( + id TEXT PRIMARY KEY, + dm_message_id TEXT NOT NULL, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + emoji TEXT NOT NULL, + created_at INTEGER NOT NULL +); + +-- Friends +CREATE TABLE friends ( + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + friend_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + created_at INTEGER NOT NULL, + PRIMARY KEY (user_id, friend_id) +); + +-- Friend Requests +CREATE TABLE friend_requests ( + id TEXT PRIMARY KEY, + from_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + to_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + status TEXT DEFAULT 'pending', -- 'pending' | 'accepted' | 'declined' + created_at INTEGER NOT NULL +); + +-- Read States +CREATE TABLE read_states ( + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + channel_id TEXT NOT NULL, + last_read_message_id TEXT NOT NULL, + updated_at INTEGER NOT NULL, + PRIMARY KEY (user_id, channel_id) +); + +-- Server Folders +CREATE TABLE server_folders ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name TEXT, + color TEXT, + position INTEGER DEFAULT 0, + created_at INTEGER NOT NULL +); + +CREATE TABLE server_folder_members ( + folder_id TEXT NOT NULL REFERENCES server_folders(id) ON DELETE CASCADE, + server_id TEXT NOT NULL REFERENCES servers(id) ON DELETE CASCADE, + PRIMARY KEY (folder_id, server_id) +); + +-- Instance Settings (singleton row, id=1) +CREATE TABLE instance_settings ( + id INTEGER PRIMARY KEY DEFAULT 1, + max_bitrate_kbps INTEGER NOT NULL DEFAULT 20000, + min_bitrate_kbps INTEGER NOT NULL DEFAULT 500, + bitrate_step_kbps INTEGER NOT NULL DEFAULT 500, + allowed_resolutions TEXT NOT NULL DEFAULT '540,720,1080', + allowed_framerates TEXT NOT NULL DEFAULT '30,45,60', + max_resolution INTEGER NOT NULL DEFAULT 1080, + max_framerate INTEGER NOT NULL DEFAULT 60, + updated_at INTEGER NOT NULL +); ``` ## REST API ENDPOINTS ``` -POST /api/auth/register { username, password, displayName? } → { token, user } -POST /api/auth/login { username, password } → { token, user } +# Auth +POST /api/auth/register { username, password, displayName? } → { token, user } +POST /api/auth/login { username, password } → { token, user } -GET /api/users/@me (auth) → { user } -PATCH /api/users/@me (auth) { displayName?, avatar?, customStatus? } → { user } -GET /api/users/:id (auth) → { user } +# Users +GET /api/users/@me (auth) → { user } +PATCH /api/users/@me (auth) { displayName?, avatar?, customStatus?, status? } → { user } +GET /api/users/:id (auth) → { user } -POST /api/servers (auth) { name, icon? } → { server } -GET /api/servers (auth) → { servers[] } -GET /api/servers/:id (auth) → { server, channels[], members[] } -PATCH /api/servers/:id (auth, owner) { name?, icon? } → { server } -DELETE /api/servers/:id (auth, owner) → { success } -POST /api/servers/:id/join (auth) { inviteCode } → { server } -POST /api/servers/:id/invite (auth, admin+) → { inviteCode } +# Servers +POST /api/servers (auth) { name, icon? } → { server } +GET /api/servers (auth) → { servers[] } +GET /api/servers/:id (auth) → { server, channels[], members[], roles[] } +PATCH /api/servers/:id (auth, MANAGE_SERVER) { name?, icon? } → { server } +DELETE /api/servers/:id (auth, owner) → { success } +POST /api/servers/:id/invite (auth, CREATE_INVITE) → { inviteCode } +POST /api/servers/:id/join (auth) { inviteCode } → { server } +POST /api/servers/join (auth) { inviteCode } → { server } -GET /api/servers/:id/channels (auth, member) → { channels[] } -POST /api/servers/:id/channels (auth, admin+) { name, type, topic? } → { channel } -PATCH /api/channels/:id (auth, admin+) { name?, topic?, position? } → { channel } -DELETE /api/channels/:id (auth, admin+) → { success } +# Members +GET /api/servers/:id/members (auth, member) → { members[] } +PATCH /api/servers/:id/members/:uid (auth, MANAGE_ROLES) { roles } → { member } +DELETE /api/servers/:id/members/:uid (auth, KICK_MEMBERS|self) → { success } -GET /api/channels/:id/messages (auth, member) ?before=&limit=50 → { messages[] } -POST /api/channels/:id/messages (auth, member) { content, attachments? } → { message } -PATCH /api/messages/:id (auth, author) { content } → { message } -DELETE /api/messages/:id (auth, author|admin) → { success } +# Roles +POST /api/servers/:id/roles (auth, MANAGE_ROLES) { name, color?, permissions? } → { role } +PATCH /api/servers/:id/roles/:rid (auth, MANAGE_ROLES) { name?, color?, permissions?, position? } → { role } +DELETE /api/servers/:id/roles/:rid (auth, MANAGE_ROLES) → { success } +POST /api/servers/:id/members/:uid/roles (auth, MANAGE_ROLES) { roleId } → { success } +DELETE /api/servers/:id/members/:uid/roles/:rid (auth, MANAGE_ROLES) → { success } -GET /api/servers/:id/members (auth, member) → { members[] } -PATCH /api/servers/:id/members/:uid (auth, owner) { role } → { member } -DELETE /api/servers/:id/members/:uid (auth, owner|self) → { success } +# Channels +GET /api/servers/:id/channels (auth, member, VIEW_CHANNEL) → { channels[] } +POST /api/servers/:id/channels (auth, MANAGE_CHANNELS) { name, type, topic? } → { channel } +PATCH /api/channels/:id (auth, MANAGE_CHANNELS) { name?, topic?, position? } → { channel } +DELETE /api/channels/:id (auth, MANAGE_CHANNELS) → { success } -POST /api/livekit/token (auth) { channelId } → { token } +# Channel Permission Overrides +GET /api/channels/:id/overrides (auth, MANAGE_CHANNELS) → { overrides[] } +PUT /api/channels/:id/overrides (auth, MANAGE_CHANNELS) { targetType, targetId, allow, deny } → { override } +DELETE /api/channels/:id/overrides/:targetType/:targetId (auth, MANAGE_CHANNELS) → { success } -POST /api/uploads (auth) multipart file → { attachment } -GET /api/uploads/:filename (public) → file stream +# Messages +GET /api/channels/:id/messages (auth, member) ?before=&limit=50 → { messages[] } +POST /api/channels/:id/messages (auth, SEND_MESSAGES) { content, attachments?, replyToId? } → { message } +PATCH /api/messages/:id (auth, author) { content } → { message } +DELETE /api/messages/:id (auth, author|MANAGE_MESSAGES) → { success } -GET /api/dm (auth) → { dmChannels[] } -POST /api/dm (auth) { userId } → { dmChannel } -GET /api/dm/:id/messages (auth, member) ?before=&limit=50 → { messages[] } -POST /api/dm/:id/messages (auth, member) { content } → { message } +# File Uploads +POST /api/uploads (auth) multipart file → { attachment } +GET /api/uploads/:filename (public) → file stream + +# Direct Messages +GET /api/dm (auth) → { dmChannels[] } +POST /api/dm (auth) { userId } → { dmChannel } +DELETE /api/dm/:id (auth, member) → { success } (soft-close) +GET /api/dm/:id/messages (auth, member) ?before=&limit=50 → { messages[] } +POST /api/dm/:id/messages (auth, member) { content } → { message } +PATCH /api/dm/messages/:id (auth, author) { content } → { message } +DELETE /api/dm/messages/:id (auth, author) → { success } +POST /api/dm/:id/members (auth, owner) { userId } → { dmChannel } (group DM, max 10) +DELETE /api/dm/:id/members (auth, member) → { success } (leave group DM) + +# Social / Friends +GET /api/social/friends (auth) → { friends[] } +GET /api/social/requests (auth) → { requests[] } +POST /api/social/requests (auth) { username } → { request } +PATCH /api/social/requests/:id (auth) { action: 'accept'|'decline' } → { request } +DELETE /api/social/requests/:id (auth) → { success } (cancel) +DELETE /api/social/friends/:id (auth) → { success } +GET /api/social/search (auth) ?q= → { users[] } + +# Voice/Video +POST /api/livekit/token (auth) { channelId } → { token } + +# Instance Settings (admin) +GET /api/settings/streaming (auth) → { streamingLimits } +PATCH /api/settings/streaming (auth, admin) { maxBitrateKbps?, ... } → { streamingLimits } + +# Utilities +GET /api/utils/metadata (auth) ?url= → { title?, description?, image?, siteName? } +GET /api/health (public) → { status: 'ok', timestamp } ``` ## WEBSOCKET PROTOCOL -All WebSocket messages are JSON. Client authenticates by sending `{ type: 'auth', token: 'jwt...' }` as first message. Server responds with `{ type: 'ready', user, servers, dmChannels }`. +All WebSocket messages are JSON over `/ws`. Client authenticates by sending `{ type: 'auth', token: 'jwt...' }` as the first message. Server responds with `{ type: 'ready', ... }` containing all initial state. ### Client → Server ``` { type: 'auth', token: string } -{ type: 'message_create', channelId: string, content: string } -{ type: 'message_edit', messageId: string, content: string } -{ type: 'message_delete', messageId: string } -{ type: 'typing_start', channelId: string } +{ type: 'ping' } + +# Server Messages +{ type: 'message_create', channelId, content, replyToId?, attachmentIds? } +{ type: 'message_edit', messageId, content } +{ type: 'message_delete', messageId } +{ type: 'typing_start', channelId } +{ type: 'reaction_add', messageId, emoji } +{ type: 'reaction_remove', messageId, emoji } +{ type: 'channel_ack', channelId, messageId } + +# DM Messages +{ type: 'dm_message_create', dmChannelId, content, replyToId?, attachmentIds? } +{ type: 'dm_message_edit', messageId, content } +{ type: 'dm_message_delete', messageId } +{ type: 'dm_typing_start', dmChannelId } + +# Presence { type: 'presence_update', status: 'online' | 'idle' | 'dnd' } -{ type: 'voice_join', channelId: string } + +# Voice (Server Channels) +{ type: 'voice_join', channelId } { type: 'voice_leave' } -{ type: 'dm_message_create', dmChannelId: string, content: string } +{ type: 'voice_status', isMuted?, isDeafened?, isCameraOn?, isScreenSharing? } + +# DM Calls +{ type: 'dm_call_start', dmChannelId } +{ type: 'dm_call_accept', dmChannelId } +{ type: 'dm_call_reject', dmChannelId } +{ type: 'dm_call_end', dmChannelId } ``` ### Server → Client ``` -{ type: 'ready', user: User, servers: ServerWithChannelsAndMembers[], dmChannels: DmChannel[] } +{ type: 'ready', user, servers, dmChannels, folders, voiceStates, readStates, activeCalls } +{ type: 'pong' } + +# Server Messages { type: 'message_created', message: MessageWithUser } { type: 'message_updated', message: MessageWithUser } -{ type: 'message_deleted', messageId: string, channelId: string } -{ type: 'typing', channelId: string, userId: string, username: string } -{ type: 'presence_update', userId: string, status: string } -{ type: 'voice_state_update', channelId: string, userId: string, action: 'join' | 'leave' } -{ type: 'member_joined', serverId: string, member: MemberWithUser } -{ type: 'member_left', serverId: string, userId: string } +{ type: 'message_deleted', messageId, channelId } +{ type: 'typing', channelId, userId, username } +{ type: 'reaction_added', messageId, channelId, reaction } +{ type: 'reaction_removed', messageId, channelId, userId, emoji } + +# DM Messages { type: 'dm_message_created', message: DmMessageWithUser } +{ type: 'dm_message_updated', message: DmMessageWithUser } +{ type: 'dm_message_deleted', messageId, dmChannelId } +{ type: 'dm_channel_created', dmChannel } +{ type: 'dm_channel_closed', dmChannelId } +{ type: 'dm_member_added', dmChannelId, user } +{ type: 'dm_member_removed', dmChannelId, userId } + +# Channel/Server Updates +{ type: 'channel_created', channel } +{ type: 'channel_updated', channel } +{ type: 'channel_deleted', channelId, serverId } +{ type: 'server_updated', server } +{ type: 'channel_ack', channelId, messageId, userId } + +# Members & Presence +{ type: 'member_joined', serverId, member: MemberWithUser } +{ type: 'member_left', serverId, userId } +{ type: 'presence_update', userId, status } + +# Voice +{ type: 'voice_state_update', channelId, userId, action: 'join' | 'leave' } +{ type: 'voice_status_update', userId, isMuted, isDeafened, isCameraOn, isScreenSharing } + +# DM Calls +{ type: 'dm_call_incoming', dmChannelId, callerId, callerName } +{ type: 'dm_call_accepted', dmChannelId } +{ type: 'dm_call_rejected', dmChannelId } +{ type: 'dm_call_ended', dmChannelId } + +# Social +{ type: 'friend_request_received', request } +{ type: 'friend_request_accepted', friend } +{ type: 'friend_removed', userId } ``` -## LIVEKIT CONFIGURATION +## PERMISSION SYSTEM -``` -LiveKit URL: wss://nova.ddns.net/livekit -API Key: REDACTED_LIVEKIT_KEY -API Secret: REDACTED_LIVEKIT_SECRET -``` +Bitwise permission engine defined in `packages/shared/src/permissions.ts`. Stored as decimal strings in the database (bigint is not JSON-safe). + +| Bit | Permission | Description | +|-----|-----------|-------------| +| 0 | ADMINISTRATOR | Full access, bypasses all checks | +| 1 | VIEW_CHANNEL | See channel in list and read messages | +| 2 | MANAGE_CHANNELS | Create, edit, delete channels | +| 3 | MANAGE_ROLES | Create, edit, delete roles | +| 4 | MANAGE_SERVER | Edit server name, icon | +| 5 | CREATE_INVITE | Generate invite codes | +| 6 | KICK_MEMBERS | Remove members from server | +| 7 | BAN_MEMBERS | Ban members from server | +| 10 | SEND_MESSAGES | Post messages in text channels | +| 11 | MANAGE_MESSAGES | Delete other users' messages | +| 12 | ATTACH_FILES | Upload files to messages | +| 13 | READ_MESSAGE_HISTORY | View message history | +| 14 | ADD_REACTIONS | Add emoji reactions | +| 20 | CONNECT | Join voice channels | +| 21 | SPEAK | Transmit audio in voice | +| 22 | MUTE_MEMBERS | Server-mute other members | +| 23 | DEAFEN_MEMBERS | Server-deafen other members | +| 24 | MOVE_MEMBERS | Move members between voice channels | +| 25 | USE_VOICE_ACTIVITY | Use voice activity detection | +| 26 | STREAM | Share screen in voice channels | + +**Resolution order:** Owner → @everyone role → Assigned roles (OR'd) → ADMINISTRATOR shortcut → Channel overrides (@everyone → role overrides → member override). ## ENVIRONMENT VARIABLES -``` -PORT=3000 -HOST=0.0.0.0 -JWT_SECRET= -LIVEKIT_URL=wss://nova.ddns.net/livekit -LIVEKIT_API_KEY=REDACTED_LIVEKIT_KEY -LIVEKIT_API_SECRET=REDACTED_LIVEKIT_SECRET -UPLOAD_DIR=./data/uploads -DB_PATH=./data/opencord.db -MAX_UPLOAD_SIZE=104857600 -REGISTRATION_OPEN=true +```env +# Server +PORT=3000 # HTTP/WS listen port +HOST=0.0.0.0 # Bind address + +# Auth +JWT_SECRET= # Required — generate with: openssl rand -hex 32 + +# LiveKit (optional — leave empty to disable voice features) +LIVEKIT_URL= # WebSocket URL to LiveKit server +LIVEKIT_API_KEY= # LiveKit API key +LIVEKIT_API_SECRET= # LiveKit API secret + +# Storage +UPLOAD_DIR=./data/uploads # File upload directory +DB_PATH=./data/backspace.db # SQLite database path +MAX_UPLOAD_SIZE=104857600 # Max upload size in bytes (100MB) + +# Registration +REGISTRATION_OPEN=true # Set to false to disable new user signup ``` -## PROGRESS +## DEPLOYMENT -- Phase 1: Foundation — ✅ Complete (package.json, pnpm-workspace.yaml, tsconfig.base.json, packages/shared/src/types.ts, packages/server/src/index.ts, config.ts, db/schema.ts, db/index.ts, db/seed.ts, routes/auth.ts, routes/users.ts, utils/auth.ts, utils/snowflake.ts, utils/permissions.ts, .env) -- Phase 2: Servers & Channels — ✅ Complete (routes/servers.ts, routes/channels.ts, updated index.ts) -- Phase 3: Real-time Messaging — ✅ Complete (ws/handler.ts, ws/events.ts, routes/messages.ts, updated index.ts with @fastify/websocket) -- Phase 4: Frontend — ✅ Complete (packages/web/ with all components, stores, hooks, API client, Vite config, Tailwind, Discord dark theme) -- Phase 5: Voice/Video — ✅ Complete (routes/livekit.ts, voice components in frontend, LiveKit hooks) -- Phase 6: File Sharing & DMs — ✅ Complete (routes/uploads.ts, routes/dm.ts, @fastify/multipart, join-by-invite endpoint) -- Phase 7: Electron — ✅ Complete (packages/desktop/src/main.ts with IPC handlers, preload.ts, electron-builder.yml, tsconfig.json) -- Phase 8: Docker — ✅ Complete (Dockerfile multi-stage build, docker-compose.yml, .env.example, .gitignore, .dockerignore, README.md) +**Live instance:** `https://nova.ddns.net` — Raspberry Pi behind OpenResty reverse proxy with HSTS. -## CURRENT PHASE +**Infrastructure:** +- Docker container (`backspace`) on port 3000 +- External Docker volume: `backspace-data` (mounted at `/app/data`) +- External Docker network: `backspace-net` (shared with reverse proxy) +- LiveKit server at `wss://nova.ddns.net/livekit` -All 8 phases complete. +**Deploy commands:** +- `./deploy.sh` — Syncs code via rsync to Pi, triggers `docker compose up -d --build` +- `./deploy.sh --local` — Use local IP (192.168.1.10) +- `./deploy.sh --remote` — Use remote DNS (nova.ddns.net) +- `make deploy` — Same as deploy.sh +- `make logs` — Watch Docker logs on Pi +- `make shell` — SSH into running container +- `make status` — Check container status + +**Development:** +```bash +pnpm install +pnpm dev # Starts server (:3005) + Vite (:5173) with proxy +``` + +## FEATURE STATUS + +All core features are implemented and live: + +- **Auth:** Registration (first user = admin), login, JWT sessions +- **Servers:** Create, join by invite, server settings, delete +- **Channels:** Text, voice, video types with position ordering +- **Messaging:** Send, edit, delete, replies, attachments, reactions, typing indicators, read states +- **Permissions:** Full RBAC with roles, per-channel overrides, computed permissions +- **Voice/Video:** LiveKit integration, mute/deafen, camera, screen share with VP9 +- **Screen Share:** Configurable resolution/FPS/bitrate, gaming vs text mode, instance-level limits +- **DMs:** 1-on-1 and group DMs (up to 10), soft-close, message edit/delete +- **DM Calls:** Ringing state machine (ring → active → ended), auto-reject timeout +- **Friends:** Send/accept/decline requests, friend list, user search +- **Audio Processing:** RNNoise noise suppression, echo cancellation, auto gain control, per-user volume +- **File Uploads:** Multipart upload, immutable cache headers, directory traversal protection +- **Admin Panel:** Instance-level streaming limits (bitrate, resolution, framerate bounds) +- **URL Previews:** Server-side metadata extraction with Cheerio +- **Desktop:** Electron wrapper with tray, notifications, badge count +- **Docker:** Multi-stage build, health checks, persistent volume diff --git a/Makefile b/Makefile deleted file mode 100644 index db87e29f..00000000 --- a/Makefile +++ /dev/null @@ -1,30 +0,0 @@ -# Raspberry Pi Deployment Makefile (Direct SSH) - -PI_IP = 192.168.1.10 -PI_USER = youruser -REMOTE_PATH = ~/opencord - -.PHONY: deploy logs shell env status - -# Full deployment: sync code and rebuild/restart containers ON the Pi -deploy: - @chmod +x deploy.sh - ./deploy.sh - -# Watch real-time logs FROM the Pi server -logs: - ssh $(PI_USER)@$(PI_IP) "cd $(REMOTE_PATH) && docker compose logs -f" - -# Drop into a shell inside the running container ON the Pi -shell: - ssh -t $(PI_USER)@$(PI_IP) "docker exec -it opencord /bin/bash" - -# Push local .env to the Pi (do this once) -env: - @echo "📤 Pushing .env to Pi..." - ssh $(PI_USER)@$(PI_IP) "mkdir -p $(REMOTE_PATH)" - scp .env $(PI_USER)@$(PI_IP):$(REMOTE_PATH)/.env - -# Check container status ON the Pi -status: - ssh $(PI_USER)@$(PI_IP) "cd $(REMOTE_PATH) && docker compose ps" diff --git a/README.md b/README.md index ccc22351..cd608f11 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,22 @@ -# Opencord +# Backspace Open-source, self-hosted Discord alternative built with TypeScript. ## Features - Real-time text messaging with WebSocket -- Servers, channels, and member management +- Servers, channels, and role-based permission management - Voice and video chat via LiveKit -- Direct messages +- Screen sharing with configurable quality (VP9) +- Direct messages (1-on-1 and group DMs up to 10) +- DM voice/video calls with ringing +- Friend system with requests - File uploads and image sharing -- Markdown message formatting -- Typing indicators and presence status -- Role-based permissions (owner, admin, member) +- Markdown message formatting with syntax highlighting +- Message reactions, replies, and editing +- Typing indicators, presence status, and read states - Invite system with shareable codes +- Instance-level admin panel (streaming limits) - Desktop app (Electron) - Mobile-responsive web UI - Docker deployment @@ -34,8 +38,8 @@ Open-source, self-hosted Discord alternative built with TypeScript. ```bash # Clone the repository -git clone https://github.com/your-username/opencord.git -cd opencord +git clone https://github.com/your-username/backspace.git +cd backspace # Create environment file cp .env.example .env @@ -43,11 +47,11 @@ cp .env.example .env # Generate a JWT secret echo "JWT_SECRET=$(openssl rand -hex 32)" >> .env -# Start Opencord +# Start Backspace docker compose up -d ``` -Open `http://localhost:3000` in your browser. A default server "Opencord" is created automatically. +Open `http://localhost:3000` in your browser. A default server "Backspace" is created automatically. **Default admin account:** `admin` / `admin123` (change this after first login). @@ -78,7 +82,7 @@ cp .env.example .env pnpm dev # Or start individually -pnpm dev:server # API server on :3000 +pnpm dev:server # API server on :3005 pnpm dev:web # Vite dev server on :5173 ``` @@ -93,9 +97,9 @@ This builds the shared types, server, and web frontend. The server serves the bu ## Project Structure ``` -Opencord/ +Backspace/ ├── packages/ -│ ├── shared/ # Shared TypeScript types +│ ├── shared/ # Shared TypeScript types & permissions │ ├── server/ # Fastify API + WebSocket server │ ├── web/ # React frontend (Vite + Tailwind) │ └── desktop/ # Electron desktop app diff --git a/package.json b/package.json index 3eb7e0f9..1a356257 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,16 @@ { - "name": "opencord", + "name": "backspace", "version": "1.0.0", "private": true, "description": "Open-source, self-hosted Discord alternative", "scripts": { - "dev:server": "pnpm --filter @opencord/server dev", - "dev:web": "pnpm --filter @opencord/web dev", - "dev": "pnpm --filter @opencord/server dev & pnpm --filter @opencord/web dev", - "build:shared": "pnpm --filter @opencord/shared build", - "build:server": "pnpm --filter @opencord/server build", - "build:web": "pnpm --filter @opencord/web build", - "build": "pnpm --filter @opencord/shared build && pnpm --filter @opencord/server build & pnpm --filter @opencord/web build" + "dev:server": "pnpm --filter @backspace/server dev", + "dev:web": "pnpm --filter @backspace/web dev", + "dev": "pnpm --filter @backspace/server dev & pnpm --filter @backspace/web dev", + "build:shared": "pnpm --filter @backspace/shared build", + "build:server": "pnpm --filter @backspace/server build", + "build:web": "pnpm --filter @backspace/web build", + "build": "pnpm --filter @backspace/shared build && pnpm --filter @backspace/server build & pnpm --filter @backspace/web build" }, "pnpm": { "onlyBuiltDependencies": ["better-sqlite3", "esbuild"] diff --git a/packages/desktop/electron-builder.yml b/packages/desktop/electron-builder.yml index 14d2fc3b..aafa5181 100644 --- a/packages/desktop/electron-builder.yml +++ b/packages/desktop/electron-builder.yml @@ -1,5 +1,5 @@ -appId: com.opencord.desktop -productName: Opencord +appId: com.backspace.desktop +productName: Backspace directories: output: dist-electron files: diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 25e41c40..527ee36e 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,5 +1,5 @@ { - "name": "@opencord/desktop", + "name": "@backspace/desktop", "version": "1.0.0", "private": true, "main": "dist/main.js", diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts index 0c00dfbc..80608f49 100644 --- a/packages/desktop/src/main.ts +++ b/packages/desktop/src/main.ts @@ -14,7 +14,7 @@ let mainWindow: BrowserWindow | null = null; let tray: Tray | null = null; let isQuitting = false; -const SERVER_URL = process.env.OPENCORD_URL || 'http://localhost:3000'; +const SERVER_URL = process.env.BACKSPACE_URL || 'http://localhost:3000'; function createWindow(): void { mainWindow = new BrowserWindow({ @@ -22,7 +22,7 @@ function createWindow(): void { height: 800, minWidth: 940, minHeight: 500, - title: 'Opencord', + title: 'Backspace', titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default', backgroundColor: '#313338', show: false, @@ -61,7 +61,7 @@ function createWindow(): void { } function createTray(): void { - // Create a 16x16 tray icon (blue circle for Opencord) + // Create a 16x16 tray icon (blue circle for Backspace) const size = 16; const canvas = Buffer.alloc(size * size * 4); const cx = size / 2; @@ -89,7 +89,7 @@ function createTray(): void { const contextMenu = Menu.buildFromTemplate([ { - label: 'Show Opencord', + label: 'Show Backspace', click: () => { mainWindow?.show(); mainWindow?.focus(); @@ -111,7 +111,7 @@ function createTray(): void { }, ]); - tray.setToolTip('Opencord'); + tray.setToolTip('Backspace'); tray.setContextMenu(contextMenu); tray.on('click', () => { diff --git a/packages/desktop/src/preload.ts b/packages/desktop/src/preload.ts index 9e06a9d9..754cf12c 100644 --- a/packages/desktop/src/preload.ts +++ b/packages/desktop/src/preload.ts @@ -1,6 +1,6 @@ import { contextBridge, ipcRenderer } from 'electron'; -contextBridge.exposeInMainWorld('opencord', { +contextBridge.exposeInMainWorld('backspace', { platform: process.platform, showNotification: (title: string, body: string) => { ipcRenderer.send('show-notification', { title, body }); diff --git a/packages/server/package.json b/packages/server/package.json index e2a2897c..26873e2d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,5 +1,5 @@ { - "name": "@opencord/server", + "name": "@backspace/server", "version": "1.0.0", "private": true, "type": "module", @@ -15,7 +15,7 @@ "@fastify/rate-limit": "^9.1.0", "@fastify/static": "^7.0.4", "@fastify/websocket": "^10.0.1", - "@opencord/shared": "workspace:*", + "@backspace/shared": "workspace:*", "bcryptjs": "^2.4.3", "better-sqlite3": "^11.3.0", "cheerio": "^1.0.0", diff --git a/packages/server/src/config.ts b/packages/server/src/config.ts index 5137e05f..6379864a 100644 --- a/packages/server/src/config.ts +++ b/packages/server/src/config.ts @@ -47,7 +47,7 @@ export const config = { }, uploadDir: env('UPLOAD_DIR', resolve(__dirname, '../../../data/uploads')), - dbPath: env('DB_PATH', resolve(__dirname, '../../../data/opencord.db')), + dbPath: env('DB_PATH', resolve(__dirname, '../../../data/backspace.db')), maxUploadSize: envInt('MAX_UPLOAD_SIZE', 104857600), registrationOpen: envBool('REGISTRATION_OPEN', true), } as const; diff --git a/packages/server/src/db/migrate.ts b/packages/server/src/db/migrate.ts index 205b5323..29060fd8 100644 --- a/packages/server/src/db/migrate.ts +++ b/packages/server/src/db/migrate.ts @@ -1,5 +1,5 @@ import Database from 'better-sqlite3'; -import { DEFAULT_EVERYONE_PERMISSIONS, PermissionBits, ALL_PERMISSIONS, permissionsToString } from '@opencord/shared/src/permissions.js'; +import { DEFAULT_EVERYONE_PERMISSIONS, PermissionBits, ALL_PERMISSIONS, permissionsToString } from '@backspace/shared/src/permissions.js'; export function runMigrations(db: Database.Database): void { console.log('Checking for database migrations...'); @@ -137,9 +137,12 @@ function migrateEveryoneRoles(db: Database.Database): void { } // Migrate existing admin members: ensure an Admin role exists and assign it - const adminMembers = db.prepare( - "SELECT server_id, user_id FROM server_members WHERE role = 'admin'" - ).all() as { server_id: string; user_id: string }[]; + // Only run if the old `role` column still exists on server_members + const smColumns = db.pragma('table_info(server_members)') as { name: string }[]; + const hasRoleColumn = smColumns.some(c => c.name === 'role'); + const adminMembers = hasRoleColumn + ? db.prepare("SELECT server_id, user_id FROM server_members WHERE role = 'admin'").all() as { server_id: string; user_id: string }[] + : []; if (adminMembers.length > 0) { // Group by server diff --git a/packages/server/src/db/seed.ts b/packages/server/src/db/seed.ts index 56292500..10d5771d 100644 --- a/packages/server/src/db/seed.ts +++ b/packages/server/src/db/seed.ts @@ -2,7 +2,7 @@ import { getDb, schema } from './index.js'; import { generateSnowflake } from '../utils/snowflake.js'; import { hashPassword } from '../utils/auth.js'; import { eq } from 'drizzle-orm'; -import { DEFAULT_EVERYONE_PERMISSIONS, permissionsToString } from '@opencord/shared/src/permissions.js'; +import { DEFAULT_EVERYONE_PERMISSIONS, permissionsToString } from '@backspace/shared/src/permissions.js'; export async function seedDatabase(): Promise { const db = getDb(); @@ -31,9 +31,9 @@ export async function seedDatabase(): Promise { const serverId = generateSnowflake(); db.insert(schema.servers).values({ id: serverId, - name: 'Opencord', + name: 'Backspace', ownerId: adminId, - inviteCode: 'opencord', + inviteCode: 'backspace', createdAt: Date.now(), }).run(); @@ -76,6 +76,6 @@ export async function seedDatabase(): Promise { }).run(); console.log('Database seeded successfully'); - console.log(` Default server: Opencord (invite code: opencord)`); + console.log(` Default server: Backspace (invite code: backspace)`); console.log(` Admin user: admin / admin123`); } diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 655e6ec6..59187f71 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -93,7 +93,7 @@ async function main(): Promise { try { await app.listen({ port: config.port, host: config.host }); - console.log(`Opencord server running at http://${config.host}:${config.port}`); + console.log(`Backspace server running at http://${config.host}:${config.port}`); } catch (err) { app.log.error(err); process.exit(1); diff --git a/packages/server/src/routes/auth.ts b/packages/server/src/routes/auth.ts index 2263b955..6cd5e4d7 100644 --- a/packages/server/src/routes/auth.ts +++ b/packages/server/src/routes/auth.ts @@ -4,7 +4,7 @@ import { getDb, schema } from '../db/index.js'; import { hashPassword, verifyPassword, signJwt } from '../utils/auth.js'; import { generateSnowflake } from '../utils/snowflake.js'; import { config } from '../config.js'; -import type { RegisterRequest, LoginRequest, AuthResponse, User } from '@opencord/shared'; +import type { RegisterRequest, LoginRequest, AuthResponse, User } from '@backspace/shared'; function sanitizeUser(row: typeof schema.users.$inferSelect): User { return { diff --git a/packages/server/src/routes/channels.ts b/packages/server/src/routes/channels.ts index 7175fe95..9a35aa49 100644 --- a/packages/server/src/routes/channels.ts +++ b/packages/server/src/routes/channels.ts @@ -4,13 +4,13 @@ import { getDb, schema } from '../db/index.js'; import { authenticate } from '../utils/auth.js'; import { generateSnowflake } from '../utils/snowflake.js'; import { isMember, hasPermission, getChannelServerId, PermissionBits, computePermissions } from '../utils/permissions.js'; -import { permissionsToString } from '@opencord/shared/src/permissions.js'; +import { permissionsToString } from '@backspace/shared/src/permissions.js'; import { connectionManager } from '../ws/handler.js'; import type { CreateChannelRequest, UpdateChannelRequest, Channel, -} from '@opencord/shared'; +} from '@backspace/shared'; function rowToChannel(row: typeof schema.channels.$inferSelect): Channel { return { diff --git a/packages/server/src/routes/dm.ts b/packages/server/src/routes/dm.ts index 6729677e..24c57411 100644 --- a/packages/server/src/routes/dm.ts +++ b/packages/server/src/routes/dm.ts @@ -16,7 +16,7 @@ import type { PaginatedQuery, Attachment, Reaction, -} from '@opencord/shared'; +} from '@backspace/shared'; function sanitizeUser(row: typeof schema.users.$inferSelect): User { return { diff --git a/packages/server/src/routes/livekit.ts b/packages/server/src/routes/livekit.ts index ebd3dd22..83755617 100644 --- a/packages/server/src/routes/livekit.ts +++ b/packages/server/src/routes/livekit.ts @@ -3,7 +3,7 @@ import { AccessToken } from 'livekit-server-sdk'; import { authenticate } from '../utils/auth.js'; import { config } from '../config.js'; import { getChannelServerId, hasPermission, isDmMember, PermissionBits } from '../utils/permissions.js'; -import type { LiveKitTokenRequest, LiveKitTokenResponse } from '@opencord/shared'; +import type { LiveKitTokenRequest, LiveKitTokenResponse } from '@backspace/shared'; export async function livekitRoutes(app: FastifyInstance): Promise { app.post<{ Body: LiveKitTokenRequest & { dmChannelId?: string } }>('/api/livekit/token', { diff --git a/packages/server/src/routes/messages.ts b/packages/server/src/routes/messages.ts index abe92362..43ac8ab4 100644 --- a/packages/server/src/routes/messages.ts +++ b/packages/server/src/routes/messages.ts @@ -12,7 +12,7 @@ import type { User, MessageWithUser, Reaction, -} from '@opencord/shared'; +} from '@backspace/shared'; function sanitizeUser(row: typeof schema.users.$inferSelect): User { return { diff --git a/packages/server/src/routes/servers.ts b/packages/server/src/routes/servers.ts index ec5b65be..c5bf43a9 100644 --- a/packages/server/src/routes/servers.ts +++ b/packages/server/src/routes/servers.ts @@ -4,7 +4,7 @@ import { getDb, schema } from '../db/index.js'; import { authenticate } from '../utils/auth.js'; import { generateSnowflake } from '../utils/snowflake.js'; import { isMember, isServerOwner, hasPermission, computePermissions, PermissionBits } from '../utils/permissions.js'; -import { DEFAULT_EVERYONE_PERMISSIONS, ALL_PERMISSIONS, permissionsToString } from '@opencord/shared/src/permissions.js'; +import { DEFAULT_EVERYONE_PERMISSIONS, ALL_PERMISSIONS, permissionsToString } from '@backspace/shared/src/permissions.js'; import crypto from 'crypto'; import { connectionManager } from '../ws/handler.js'; import type { @@ -18,7 +18,7 @@ import type { MemberWithUser, ServerWithChannelsAndMembers, Role, -} from '@opencord/shared'; +} from '@backspace/shared'; function sanitizeUser(row: typeof schema.users.$inferSelect): User { return { diff --git a/packages/server/src/routes/settings.ts b/packages/server/src/routes/settings.ts index 3a9852e4..19ac1494 100644 --- a/packages/server/src/routes/settings.ts +++ b/packages/server/src/routes/settings.ts @@ -2,7 +2,7 @@ import type { FastifyInstance } from 'fastify'; import { eq } from 'drizzle-orm'; import { getDb, schema } from '../db/index.js'; import { authenticate } from '../utils/auth.js'; -import type { InstanceStreamingLimits } from '@opencord/shared'; +import type { InstanceStreamingLimits } from '@backspace/shared'; const VALID_RESOLUTIONS = [540, 720, 1080]; const VALID_FRAMERATES = [30, 45, 60]; diff --git a/packages/server/src/routes/social.ts b/packages/server/src/routes/social.ts index 0818464f..0a1cf8d4 100644 --- a/packages/server/src/routes/social.ts +++ b/packages/server/src/routes/social.ts @@ -10,7 +10,7 @@ import type { FriendRequest, SendFriendRequest, UpdateFriendRequest, -} from '@opencord/shared'; +} from '@backspace/shared'; function sanitizeUser(row: typeof schema.users.$inferSelect): User { return { diff --git a/packages/server/src/routes/uploads.ts b/packages/server/src/routes/uploads.ts index cfe71ce6..fb1894e8 100644 --- a/packages/server/src/routes/uploads.ts +++ b/packages/server/src/routes/uploads.ts @@ -7,7 +7,7 @@ import { eq } from 'drizzle-orm'; import fs from 'fs'; import path from 'path'; import { pipeline } from 'stream/promises'; -import type { Attachment } from '@opencord/shared'; +import type { Attachment } from '@backspace/shared'; export async function uploadRoutes(app: FastifyInstance): Promise { // Ensure upload directory exists diff --git a/packages/server/src/routes/users.ts b/packages/server/src/routes/users.ts index a313e0d5..aa015c14 100644 --- a/packages/server/src/routes/users.ts +++ b/packages/server/src/routes/users.ts @@ -3,7 +3,7 @@ import { eq } from 'drizzle-orm'; import { getDb, schema } from '../db/index.js'; import { authenticate } from '../utils/auth.js'; import { connectionManager } from '../ws/handler.js'; -import type { User, UpdateUserRequest } from '@opencord/shared'; +import type { User, UpdateUserRequest } from '@backspace/shared'; function sanitizeUser(row: typeof schema.users.$inferSelect): User { return { diff --git a/packages/server/src/routes/utils.ts b/packages/server/src/routes/utils.ts index 39f96657..469e4fd6 100644 --- a/packages/server/src/routes/utils.ts +++ b/packages/server/src/routes/utils.ts @@ -15,7 +15,7 @@ export async function utilRoutes(app: FastifyInstance): Promise { try { const response = await fetch(url, { headers: { - 'User-Agent': 'OpencordBot/1.0', + 'User-Agent': 'BackspaceBot/1.0', }, }); diff --git a/packages/server/src/utils/permissions.ts b/packages/server/src/utils/permissions.ts index 62230866..fb4366bb 100644 --- a/packages/server/src/utils/permissions.ts +++ b/packages/server/src/utils/permissions.ts @@ -5,7 +5,7 @@ import { ALL_PERMISSIONS, stringToPermissions, permissionsToString, -} from '@opencord/shared/src/permissions.js'; +} from '@backspace/shared/src/permissions.js'; // Re-export for convenience export { PermissionBits, ALL_PERMISSIONS, permissionsToString, stringToPermissions }; diff --git a/packages/server/src/ws/events.ts b/packages/server/src/ws/events.ts index 11a20f0a..421f5d1a 100644 --- a/packages/server/src/ws/events.ts +++ b/packages/server/src/ws/events.ts @@ -5,7 +5,7 @@ import { connectionManager } from './handler.js'; import type { VoiceRoom, DmRoomMeta, ServerRoomMeta } from './handler.js'; import { isMember, getChannelServerId, isDmMember, hasPermission, PermissionBits } from '../utils/permissions.js'; import { broadcastDmMessage, getDmMessageWithUser } from '../routes/dm.js'; -import type { User, MessageWithUser, Attachment, DmMessageWithUser } from '@opencord/shared'; +import type { User, MessageWithUser, Attachment, DmMessageWithUser } from '@backspace/shared'; function sanitizeUser(row: typeof schema.users.$inferSelect): User { return { diff --git a/packages/server/src/ws/handler.ts b/packages/server/src/ws/handler.ts index 0295120c..6ed239d6 100644 --- a/packages/server/src/ws/handler.ts +++ b/packages/server/src/ws/handler.ts @@ -15,7 +15,7 @@ import type { ServerFolder, ReadState, ActiveCallInfo, -} from '@opencord/shared'; +} from '@backspace/shared'; // SQLite's SQLITE_MAX_VARIABLE_NUMBER default is 999. // Chunk inArray() calls to stay safely under this limit. diff --git a/packages/shared/package.json b/packages/shared/package.json index adf0d279..ec32f943 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,5 +1,5 @@ { - "name": "@opencord/shared", + "name": "@backspace/shared", "version": "1.0.0", "private": true, "type": "module", diff --git a/packages/web/index.html b/packages/web/index.html index b8cc1e31..9cbd5738 100644 --- a/packages/web/index.html +++ b/packages/web/index.html @@ -4,7 +4,7 @@ - Opencord + Backspace
diff --git a/packages/web/package.json b/packages/web/package.json index 9603a415..aac0b5d7 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,5 +1,5 @@ { - "name": "@opencord/web", + "name": "@backspace/web", "version": "1.0.0", "private": true, "type": "module", @@ -10,7 +10,7 @@ }, "dependencies": { "@livekit/components-react": "^2.7.4", - "@opencord/shared": "workspace:*", + "@backspace/shared": "workspace:*", "@sapphi-red/web-noise-suppressor": "^0.3.5", "livekit-client": "^2.9.0", "prism-react-renderer": "^2.4.1", diff --git a/packages/web/src/api/client.ts b/packages/web/src/api/client.ts index ae52192b..7636c389 100644 --- a/packages/web/src/api/client.ts +++ b/packages/web/src/api/client.ts @@ -26,12 +26,12 @@ import type { Friend, FriendRequest, InstanceStreamingLimits, -} from '@opencord/shared'; +} from '@backspace/shared'; const BASE_URL = '/api'; function getToken(): string | null { - return localStorage.getItem('opencord_token'); + return localStorage.getItem('backspace_token'); } async function request( diff --git a/packages/web/src/components/chat/Embed.tsx b/packages/web/src/components/chat/Embed.tsx index 350198ea..150627e4 100644 --- a/packages/web/src/components/chat/Embed.tsx +++ b/packages/web/src/components/chat/Embed.tsx @@ -22,7 +22,7 @@ export function Embed({ url }: EmbedProps) { // Simple fetch from our new API fetch(`/api/utils/metadata?url=${encodeURIComponent(url)}`, { headers: { - 'Authorization': `Bearer ${localStorage.getItem('opencord_token')}` + 'Authorization': `Bearer ${localStorage.getItem('backspace_token')}` } }) .then(res => res.json()) diff --git a/packages/web/src/components/chat/FriendsPage.test.tsx b/packages/web/src/components/chat/FriendsPage.test.tsx index ea859355..26c022f8 100644 --- a/packages/web/src/components/chat/FriendsPage.test.tsx +++ b/packages/web/src/components/chat/FriendsPage.test.tsx @@ -5,7 +5,7 @@ import { MemoryRouter } from 'react-router-dom'; import { FriendsPage } from './FriendsPage'; import { useSocialStore } from '../../stores/socialStore'; import { useServerStore } from '../../stores/serverStore'; -import type { Friend, FriendRequest } from '@opencord/shared'; +import type { Friend, FriendRequest } from '@backspace/shared'; // Mock the api module vi.mock('../../api/client', () => ({ diff --git a/packages/web/src/components/chat/FriendsPage.tsx b/packages/web/src/components/chat/FriendsPage.tsx index 55d7e1bf..078d0d3c 100644 --- a/packages/web/src/components/chat/FriendsPage.tsx +++ b/packages/web/src/components/chat/FriendsPage.tsx @@ -6,7 +6,7 @@ import { Avatar } from '../ui/Avatar'; import { MemberListToggleButton } from '../layout/MemberListToggleButton'; import { LoadingSpinner } from '../ui/LoadingSpinner'; import { api } from '../../api/client'; -import type { Friend, FriendRequest } from '@opencord/shared'; +import type { Friend, FriendRequest } from '@backspace/shared'; type Tab = 'online' | 'all' | 'pending' | 'add'; @@ -143,7 +143,7 @@ export function FriendsPage() { return (

Add Friend

-

You can add friends with their Opencord username.

+

You can add friends with their Backspace username.

s.friends); diff --git a/packages/web/src/components/layout/AppLayout.tsx b/packages/web/src/components/layout/AppLayout.tsx index 8ef0cbab..7b5d4711 100644 --- a/packages/web/src/components/layout/AppLayout.tsx +++ b/packages/web/src/components/layout/AppLayout.tsx @@ -53,12 +53,12 @@ export function AppLayout() { // MutationObserver: neutralize rogue LiveKit
); diff --git a/packages/web/src/components/layout/MemberSidebar.tsx b/packages/web/src/components/layout/MemberSidebar.tsx index 622141e6..0da96126 100644 --- a/packages/web/src/components/layout/MemberSidebar.tsx +++ b/packages/web/src/components/layout/MemberSidebar.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react'; -import type { MemberWithUser } from '@opencord/shared'; +import type { MemberWithUser } from '@backspace/shared'; import { useServerStore } from '../../stores/serverStore'; import { useUIStore } from '../../stores/uiStore'; import { Avatar } from '../ui/Avatar'; diff --git a/packages/web/src/components/modals/AddDmMemberModal.tsx b/packages/web/src/components/modals/AddDmMemberModal.tsx index 19bfbc26..d58d7bdb 100644 --- a/packages/web/src/components/modals/AddDmMemberModal.tsx +++ b/packages/web/src/components/modals/AddDmMemberModal.tsx @@ -4,7 +4,7 @@ import { Avatar } from '../ui/Avatar'; import { useUIStore } from '../../stores/uiStore'; import { useServerStore } from '../../stores/serverStore'; import { api } from '../../api/client'; -import type { User } from '@opencord/shared'; +import type { User } from '@backspace/shared'; export function AddDmMemberModal() { const [query, setQuery] = useState(''); diff --git a/packages/web/src/components/modals/NewDmModal.tsx b/packages/web/src/components/modals/NewDmModal.tsx index 77db1cf8..19fab892 100644 --- a/packages/web/src/components/modals/NewDmModal.tsx +++ b/packages/web/src/components/modals/NewDmModal.tsx @@ -5,7 +5,7 @@ import { Avatar } from '../ui/Avatar'; import { useUIStore } from '../../stores/uiStore'; import { useServerStore } from '../../stores/serverStore'; import { api } from '../../api/client'; -import type { User } from '@opencord/shared'; +import type { User } from '@backspace/shared'; export function NewDmModal() { const [query, setQuery] = useState(''); diff --git a/packages/web/src/components/modals/ServerSettings.tsx b/packages/web/src/components/modals/ServerSettings.tsx index 8a928365..c20ee8cf 100644 --- a/packages/web/src/components/modals/ServerSettings.tsx +++ b/packages/web/src/components/modals/ServerSettings.tsx @@ -8,7 +8,7 @@ import { Avatar } from '../ui/Avatar'; import { api } from '../../api/client'; import { useNavigate } from 'react-router-dom'; import { hasPermissionBit, PermissionBits } from '../../utils/permissions'; -import type { InstanceStreamingLimits } from '@opencord/shared'; +import type { InstanceStreamingLimits } from '@backspace/shared'; const VALID_RESOLUTIONS = [540, 720, 1080] as const; const VALID_FRAMERATES = [30, 45, 60] as const; diff --git a/packages/web/src/components/ui/Avatar.tsx b/packages/web/src/components/ui/Avatar.tsx index c31bd1f4..6931eab5 100644 --- a/packages/web/src/components/ui/Avatar.tsx +++ b/packages/web/src/components/ui/Avatar.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import type { User } from '@opencord/shared'; +import type { User } from '@backspace/shared'; import { useUIStore } from '../../stores/uiStore'; interface AvatarProps { diff --git a/packages/web/src/components/ui/UserProfilePopout.tsx b/packages/web/src/components/ui/UserProfilePopout.tsx index e7f7bf32..12ff3049 100644 --- a/packages/web/src/components/ui/UserProfilePopout.tsx +++ b/packages/web/src/components/ui/UserProfilePopout.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { useNavigate } from 'react-router-dom'; -import type { User } from '@opencord/shared'; +import type { User } from '@backspace/shared'; import { Avatar } from '../ui/Avatar'; import { api } from '../../api/client'; import { useServerStore } from '../../stores/serverStore'; @@ -60,7 +60,7 @@ export function UserProfilePopout({ user, onClose, position }: UserProfilePopout
-
Opencord Member Since
+
Backspace Member Since
{new Date(user.createdAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
diff --git a/packages/web/src/components/voice/GlobalAudioRenderer.tsx b/packages/web/src/components/voice/GlobalAudioRenderer.tsx index 82ef5371..c48b100a 100644 --- a/packages/web/src/components/voice/GlobalAudioRenderer.tsx +++ b/packages/web/src/components/voice/GlobalAudioRenderer.tsx @@ -49,7 +49,7 @@ function AudioTrackElement({ // The