fix: add bot auth query parameter (#61)

This commit is contained in:
2026-01-30 17:13:41 +01:00
committed by GitHub
2 changed files with 61 additions and 37 deletions

View File

@@ -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,22 @@ app.get(
let chatUser: ChatUser | null = null;
let personalChannel: any = null;
let apiKey: string | null = null;
if (authHeader && authHeader.startsWith('Bearer ')) {
const apiKey = authHeader.substring(7);
const extractedKey = authHeader.substring(7);
if (extractedKey.startsWith('hctvb_')) {
apiKey = extractedKey;
}
} else if (botAuth && typeof botAuth === 'string' && botAuth.trim().length > 0) {
if (botAuth.startsWith('hctvb_')) {
apiKey = botAuth;
}
}
if (apiKey) {
const botAccount = await prisma.botApiKey.findUnique({
where: { key: apiKey },
include: { botAccount: true }
include: { botAccount: true },
});
if (botAccount) {
@@ -55,12 +67,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 +86,7 @@ app.get(
id: session.user.id,
username: userChannel.name,
pfpUrl: session.user.pfpUrl,
isBot: false
isBot: false,
};
personalChannel = userChannel;
}
@@ -82,7 +94,7 @@ app.get(
}
const dbGrant = await prisma.channel.findFirst({
where: { obsChatGrantToken: grant }
where: { obsChatGrantToken: grant },
});
if (!chatUser && !dbGrant) {
@@ -100,7 +112,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 +123,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 +151,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 +160,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 +168,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 +201,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 +228,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 +243,7 @@ app.get(
results.push(emojiKeys[idxs[i]]);
}
}
ws.send(
JSON.stringify({
type: 'emojiSearchResponse',
@@ -267,4 +281,4 @@ interface ChatUser {
pfpUrl: string;
displayName?: string;
isBot: boolean;
}
}

View File

@@ -11,24 +11,31 @@ 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.
<Aside type="tip">
Bot accounts are now supported. You can choose to connect as a bot by providing a bot account's API key on the Authentication header: `Bearer hctvb_xxxxxxx`
Bot accounts are now supported. You can choose to connect as a bot by providing a bot account's API key in one of two ways:
- Using the `Authorization` header: `Bearer hctvb_xxxxxxx`
- Using the `?botAuth=hctvb_xxxxxxx` query parameter
**Security Note:** When using the `?botAuth=` query parameter, be aware that query parameters may be logged in server logs, and/or proxy logs. Use the `Authorization` header method whenever possible. The query parameter method should only be used when connecting from an environment where headers cannot be set.
It is highly advised to use a bot account for any automated task, and to implement anything pointed out in this page.
</Aside>
Once connected, you must implement a subroutine in your code to send ping messages every 5 seconds. This is because of Cloudflare limitations.
Once connected, you must implement a subroutine in your code to send ping messages every about 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 +43,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 +78,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 +120,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