mirror of
https://github.com/SrIzan10/hctv.git
synced 2026-06-06 00:56:56 +00:00
fix: add bot auth query parameter (#61)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user