From aa9d0c1ca578110607f88aec81647e9e9e0619be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:58:51 +0000 Subject: [PATCH] Add botAuth query parameter support for websocket authentication Co-authored-by: SrIzan10 <66965250+SrIzan10@users.noreply.github.com> --- apps/chat/src/index.ts | 58 +++++++++++++++---------- apps/docs/src/content/docs/api/chat.mdx | 33 +++++++++----- 2 files changed, 55 insertions(+), 36 deletions(-) diff --git a/apps/chat/src/index.ts b/apps/chat/src/index.ts index 03d92bf..f808475 100644 --- a/apps/chat/src/index.ts +++ b/apps/chat/src/index.ts @@ -33,8 +33,9 @@ app.get( const token = getCookie(c, 'auth_session'); const grant = c.req.query('grant'); const authHeader = c.req.header('Authorization'); + const botAuth = c.req.query('botAuth'); - if (!token && (!grant || grant === 'null') && !authHeader) { + if (!token && (!grant || grant === 'null') && !authHeader && !botAuth) { ws.close(); return; } @@ -42,11 +43,18 @@ app.get( let chatUser: ChatUser | null = null; let personalChannel: any = null; + // Check for bot authentication via Authorization header or botAuth query parameter + let apiKey: string | null = null; if (authHeader && authHeader.startsWith('Bearer ')) { - const apiKey = authHeader.substring(7); + apiKey = authHeader.substring(7); + } else if (botAuth) { + apiKey = botAuth; + } + + if (apiKey) { const botAccount = await prisma.botApiKey.findUnique({ where: { key: apiKey }, - include: { botAccount: true } + include: { botAccount: true }, }); if (botAccount) { @@ -55,12 +63,12 @@ app.get( username: botAccount.botAccount.slug, pfpUrl: botAccount.botAccount.pfpUrl, displayName: botAccount.botAccount.displayName, - isBot: true + isBot: true, }; personalChannel = { id: botAccount.botAccount.id, - name: botAccount.botAccount.slug + name: botAccount.botAccount.slug, }; } } @@ -74,7 +82,7 @@ app.get( id: session.user.id, username: userChannel.name, pfpUrl: session.user.pfpUrl, - isBot: false + isBot: false, }; personalChannel = userChannel; } @@ -82,7 +90,7 @@ app.get( } const dbGrant = await prisma.channel.findFirst({ - where: { obsChatGrantToken: grant } + where: { obsChatGrantToken: grant }, }); if (!chatUser && !dbGrant) { @@ -100,7 +108,7 @@ app.get( ws.chatUser = chatUser; ws.personalChannel = personalChannel; ws.viewerId = randomString(10); - + if (ws.raw) { ws.raw.targetUsername = username; ws.raw.chatUser = chatUser; @@ -111,10 +119,12 @@ app.get( const messages = await redis.zrange(channelKey, 0, MESSAGE_HISTORY_SIZE - 1); if (messages.length > 0) { - ws.send(JSON.stringify({ - type: 'history', - messages: messages.map((msg) => JSON.parse(msg)), - })); + ws.send( + JSON.stringify({ + type: 'history', + messages: messages.map((msg) => JSON.parse(msg)), + }) + ); } }, async onClose(evt, ws) { @@ -137,7 +147,7 @@ app.get( }, async onMessage(evt, ws) { const msg = JSON.parse(evt.data.toString()); - + if (msg.type === 'ping') { await redis.setex(`viewer:${ws.targetUsername}:${ws.viewerId}`, 30, '1'); ws.send(JSON.stringify({ type: 'pong' })); @@ -146,7 +156,7 @@ app.get( if (msg.type === 'message') { if (!ws.chatUser || !ws.personalChannel) return; - + const message = (msg.message as string).trim(); const msgObj = { user: { @@ -154,17 +164,17 @@ app.get( username: ws.chatUser.username, pfpUrl: ws.chatUser.pfpUrl, displayName: ws.chatUser.displayName, - isBot: ws.chatUser.isBot || false + isBot: ws.chatUser.isBot || false, }, message, }; - + const redisObj = { - user: msgObj.user, + user: msgObj.user, message: msgObj.message, type: 'message', }; - + const redisStr = JSON.stringify(redisObj); const msgStr = JSON.stringify(msgObj); @@ -187,14 +197,14 @@ app.get( await Promise.all( emojis.map(async (emoji) => { let url = await redis.hget('emojis', emoji); - + if (!url) { url = await redis.hget(`emojis:${emoji}`, 'url'); } if (!url) { url = await redis.hget(`emoji:${emoji}`, 'url'); } - + emojiMap[emoji] = url ?? ''; }) ); @@ -214,10 +224,10 @@ app.get( const emojiKeys = Object.keys(emojis); const idxs = uf.filter(emojiKeys, searchTerm); console.log(`Emoji search for "${searchTerm}" found ${idxs?.length || 0} results.`); - + if (idxs && idxs.length > 0) { const results: string[] = []; - + if (idxs.length <= 150) { const info = uf.info(idxs, emojiKeys, searchTerm); const order = uf.sort(info, emojiKeys, searchTerm); @@ -229,7 +239,7 @@ app.get( results.push(emojiKeys[idxs[i]]); } } - + ws.send( JSON.stringify({ type: 'emojiSearchResponse', @@ -267,4 +277,4 @@ interface ChatUser { pfpUrl: string; displayName?: string; isBot: boolean; -} \ No newline at end of file +} diff --git a/apps/docs/src/content/docs/api/chat.mdx b/apps/docs/src/content/docs/api/chat.mdx index 96565e6..4325e66 100644 --- a/apps/docs/src/content/docs/api/chat.mdx +++ b/apps/docs/src/content/docs/api/chat.mdx @@ -11,24 +11,30 @@ The chat system is powered by a websocket server. Please read the entire page be The websocket server is located at `wss://hackclub.tv/api/chat/ws/:username`, where `:username` is the channel you want to connect to. -You'll need to provide authentication, which can be done by providing an `auth_session` cookie, just like the REST API. +You'll need to provide authentication, which can be done by providing an `auth_session` cookie, just like the REST API. + Once connected, you must implement a subroutine in your code to send ping messages every 5 seconds. This is because of Cloudflare limitations. Messages are sent and received in JSON format. The following message types are supported: -- `message`: a chat message. - - sent by client: + +- `message`: a chat message. + - sent by client: ```json { "type": "message", "content": "Hello, world!" } ``` - - received by client: + - received by client: ```json { "user": { @@ -36,24 +42,24 @@ Messages are sent and received in JSON format. The following message types are s "username": "user_who_sent_message", "avatar": "https://emoji.slack-edge.com/avatar.png" }, - "message": "Hello, world!", + "message": "Hello, world!" } ``` -- `ping`: a ping message to keep the connection alive. - - sent by client: +- `ping`: a ping message to keep the connection alive. + - sent by client: ```json { "type": "ping" } ``` - - received by client: + - received by client: ```json { "type": "ping" } ``` -- `history`: a message containing the chat history. This is sent upon connection. - - received by client: +- `history`: a message containing the chat history. This is sent upon connection. + - received by client: ```json { "type": "history", @@ -71,9 +77,11 @@ Messages are sent and received in JSON format. The following message types are s ] } ``` + ## Emoji handling -*diagram source: devin deepwiki* +_diagram source: devin deepwiki_ + ```mermaid graph TB subgraph "Emoji Processing Pipeline" @@ -111,6 +119,7 @@ The server then checks Redis for the emoji URL and returns it. When a user wants to look up an emoji (by typing `:(partial name)`), the server uses uFuzzy to find matching emojis in the Redis `emojis` hash key and returns the results. Here's what gets sent on the websocket: + - `emojiMsg`: Looks up emojis - sent by client: ```json