feat: merge #64 from hippogriff101/main
BIN
apps/docs/public/guides/obs-changing-settings-op.gif
Normal file
|
After Width: | Height: | Size: 8.4 MiB |
BIN
apps/docs/public/guides/obs-open-settings-op.gif
Normal file
|
After Width: | Height: | Size: 6.8 MiB |
BIN
apps/docs/src/assets/hctv-settings.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
apps/docs/src/assets/lock.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
apps/docs/src/assets/obs-changing-settings-op.gif
Normal file
|
After Width: | Height: | Size: 8.4 MiB |
BIN
apps/docs/src/assets/obs-custom.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
apps/docs/src/assets/obs-open-settings-op.gif
Normal file
|
After Width: | Height: | Size: 6.8 MiB |
BIN
apps/docs/src/assets/obs.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
@@ -3,20 +3,42 @@ title: Development Setup
|
||||
description: Instructions to set up a local development environment for hackclub.tv
|
||||
---
|
||||
|
||||
1. clone repo
|
||||
2. `pnpm install`
|
||||
3. `cp apps/web/.env.example apps/web/.env && cp packages/db/.env.example packages/db/.env`
|
||||
4. `pnpm dev`
|
||||
5. `pnpm db:migrate` (RUN THIS AFTER POPULATING ENV)
|
||||
Follow these steps to run hackclub.tv locally:
|
||||
|
||||
- slack notifier app manifest is as follows:
|
||||
1. Clone the repository.
|
||||
2. Install dependencies:
|
||||
|
||||
```
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. Create environment files:
|
||||
|
||||
```
|
||||
cp apps/web/.env.example apps/web/.env && cp packages/db/.env.example packages/db/.env
|
||||
```
|
||||
|
||||
4. Fill in the required values in both .env files.
|
||||
5. Start the development servers:
|
||||
|
||||
```
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
6. Run database migrations (after environment variables are set):
|
||||
|
||||
```
|
||||
pnpm db:migrate
|
||||
```
|
||||
|
||||
- Slack notifier app manifest:
|
||||
```
|
||||
display_information:
|
||||
# please change the name to something that can be linked to you if possible
|
||||
# Please change the name to something linked to you.
|
||||
name: hctv notifier dev
|
||||
features:
|
||||
bot_user:
|
||||
# same with this :pray:
|
||||
# Same here.
|
||||
display_name: hctv notifier dev
|
||||
always_online: false
|
||||
oauth_config:
|
||||
|
||||
333
apps/docs/src/content/docs/guides/python-wrapper.mdx
Normal file
@@ -0,0 +1,333 @@
|
||||
---
|
||||
title: Python wrapper
|
||||
description: How to use hctv's unofficial Python wrapper.
|
||||
---
|
||||
|
||||
A Pycord-style Python wrapper for [hackclub.tv](https://hackclub.tv), made by [Christian](https://github.com/christianwell). Build chat bots with decorators and minimal boilerplate.
|
||||
|
||||
Check it out on PyPI: https://pypi.org/project/hctvwrapper/
|
||||
You can take a look at the code [here](https://github.com/christianwell/hctvwrapper)
|
||||
|
||||
```
|
||||
pip install hctvwrapper
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
import os
|
||||
from hctvwrapper import Bot
|
||||
|
||||
bot = Bot(command_prefix="!")
|
||||
|
||||
@bot.event
|
||||
async def on_ready(session):
|
||||
print(f"Logged in as {session.viewer.username}")
|
||||
|
||||
@bot.event
|
||||
async def on_message(message):
|
||||
print(f"{message.author.username}: {message.content}")
|
||||
|
||||
@bot.command()
|
||||
async def ping(ctx):
|
||||
await ctx.reply("pong!")
|
||||
|
||||
bot.run(os.environ['BOT_TOKEN'], channel="bot-playground")
|
||||
```
|
||||
|
||||
### Getting a Bot Token
|
||||
|
||||
1. Go to [hackclub.tv](https://hackclub.tv)
|
||||
2. Create a bot account and get your API key (starts with `hctvb_`)
|
||||
3. Set it as an environment variable: `export BOT_TOKEN=hctvb_xxx`
|
||||
|
||||
## Guide
|
||||
|
||||
### Events
|
||||
|
||||
Register event handlers with `@bot.event`. The function name determines which event it handles.
|
||||
|
||||
```python
|
||||
@bot.event
|
||||
async def on_ready(session):
|
||||
"""Fired when the bot connects and receives session info."""
|
||||
print(f"Logged in as {session.viewer.username}")
|
||||
print(f"Can moderate: {session.permissions.can_moderate}")
|
||||
print(f"Max message length: {session.moderation.max_message_length}")
|
||||
|
||||
@bot.event
|
||||
async def on_message(message):
|
||||
"""Fired on every chat message."""
|
||||
print(f"[{message.channel}] {message.author.username}: {message.content}")
|
||||
# message.author.id, .pfp_url, .display_name, .is_bot,
|
||||
# .is_platform_admin, .channel_role
|
||||
# message.msg_id, .timestamp, .type
|
||||
|
||||
@bot.event
|
||||
async def on_history(messages):
|
||||
"""Fired once on connect with up to 100 recent messages."""
|
||||
print(f"Got {len(messages)} historical messages")
|
||||
|
||||
@bot.event
|
||||
async def on_system_message(message):
|
||||
"""Fired on system notifications (bans, unbans, etc.)."""
|
||||
print(f"System: {message.content}")
|
||||
|
||||
@bot.event
|
||||
async def on_message_deleted(event):
|
||||
"""Fired when a message is deleted by a moderator."""
|
||||
print(f"Message {event.msg_id} deleted in {event.channel}")
|
||||
|
||||
@bot.event
|
||||
async def on_chat_access(access, channel):
|
||||
"""Fired when chat permissions change (timeouts, bans)."""
|
||||
print(f"Can send in {channel}: {access.can_send}")
|
||||
if access.restriction:
|
||||
print(f" Restriction: {access.restriction.type}")
|
||||
|
||||
@bot.event
|
||||
async def on_moderation_error(error, channel):
|
||||
"""Fired when a moderation action or message is rejected."""
|
||||
print(f"Error in {channel}: {error.code} — {error.message}")
|
||||
# error.code is one of: FORBIDDEN, RATE_LIMIT, SLOW_MODE,
|
||||
# TIMED_OUT, BANNED, MESSAGE_TOO_LONG, BLOCKED_TERM,
|
||||
# INVALID_TARGET, INVALID_REQUEST, NOT_FOUND
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
Register commands with `@bot.command()`. The bot automatically parses messages starting with the prefix.
|
||||
|
||||
```python
|
||||
bot = Bot(command_prefix="!")
|
||||
|
||||
# Simple command, no arguments
|
||||
@bot.command()
|
||||
async def ping(ctx):
|
||||
await ctx.reply("pong!")
|
||||
|
||||
# Named command with aliases
|
||||
@bot.command(name="say", aliases=["echo", "repeat"])
|
||||
async def say_cmd(ctx, *, text):
|
||||
await ctx.send(text)
|
||||
|
||||
# Positional arguments (split by whitespace)
|
||||
@bot.command()
|
||||
async def greet(ctx, name, greeting="hello"):
|
||||
await ctx.reply(f"{greeting}, {name}!")
|
||||
|
||||
# Keyword-only (rest of message)
|
||||
# Use *, text to capture everything after the command as a single string:
|
||||
@bot.command()
|
||||
async def echo(ctx, *, text):
|
||||
await ctx.reply(text)
|
||||
# !echo hello world foo → text = "hello world foo"
|
||||
```
|
||||
|
||||
> The bot automatically ignores its own messages to prevent loops.
|
||||
|
||||
### Context
|
||||
|
||||
The `ctx` object passed to commands gives you everything you need:
|
||||
|
||||
```python
|
||||
@bot.command()
|
||||
async def info(ctx):
|
||||
ctx.message # the full Message object
|
||||
ctx.author # shortcut to ctx.message.author (Author)
|
||||
ctx.channel # channel name (str)
|
||||
ctx.bot # reference to the Bot
|
||||
|
||||
await ctx.reply("text") # sends "@username text"
|
||||
await ctx.send("text") # sends "text" without mention
|
||||
await ctx.delete() # deletes the triggering message (needs mod perms)
|
||||
```
|
||||
|
||||
### Sending Messages
|
||||
|
||||
```python
|
||||
# Inside a command
|
||||
await ctx.reply("mentioned reply")
|
||||
await ctx.send("plain message")
|
||||
|
||||
# Anywhere (if you have a reference to the bot)
|
||||
await bot.send("hello!", channel="bot-playground")
|
||||
```
|
||||
|
||||
### Multi-Channel
|
||||
|
||||
Connect to multiple channels at once:
|
||||
|
||||
```python
|
||||
bot.run("hctvb_xxx", channels=["channel1", "channel2", "bot-playground"])
|
||||
```
|
||||
|
||||
Messages and commands work across all channels. Use `ctx.channel` or `message.channel` to know which channel a message came from.
|
||||
|
||||
```python
|
||||
@bot.event
|
||||
async def on_message(message):
|
||||
print(f"[{message.channel}] {message.author.username}: {message.content}")
|
||||
|
||||
@bot.command()
|
||||
async def where(ctx):
|
||||
await ctx.reply(f"you're in {ctx.channel}")
|
||||
```
|
||||
|
||||
### Moderation
|
||||
|
||||
Bots with moderation permissions can manage users:
|
||||
|
||||
```python
|
||||
# Timeout a user for 5 minutes (default)
|
||||
await bot.timeout_user("channel", user_id="user123", duration=300, reason="spam")
|
||||
|
||||
# Ban a user
|
||||
await bot.ban_user("channel", user_id="user123", reason="repeated violations")
|
||||
|
||||
# Remove a timeout
|
||||
await bot.lift_timeout("channel", user_id="user123")
|
||||
|
||||
# Unban a user
|
||||
await bot.unban_user("channel", user_id="user123")
|
||||
|
||||
# Delete a specific message
|
||||
await bot.delete_message("channel", msg_id="msg-uuid")
|
||||
```
|
||||
|
||||
### Emojis
|
||||
|
||||
Look up or search emojis from the Slack. Results come back via events.
|
||||
|
||||
```python
|
||||
# Look up emoji URLs
|
||||
await bot.lookup_emojis(["yay", "aga"])
|
||||
|
||||
# Search emojis
|
||||
await bot.search_emojis("yay")
|
||||
|
||||
# Handle results
|
||||
@bot.event
|
||||
async def on_emoji_response(emojis):
|
||||
# emojis = {"yay": "https://...", "aga": "https://..."}
|
||||
print(emojis)
|
||||
|
||||
@bot.event
|
||||
async def on_emoji_search(results):
|
||||
# results = ["yay", "yay-bounce", "yay-spin", ...]
|
||||
print(results)
|
||||
```
|
||||
|
||||
### Async Entry Point
|
||||
|
||||
If you manage your own event loop:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
async def main():
|
||||
bot = Bot(command_prefix="!")
|
||||
|
||||
@bot.event
|
||||
async def on_ready(session):
|
||||
print("Connected!")
|
||||
|
||||
await bot.start("hctvb_xxx", channel="bot-playground")
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Echo Bot
|
||||
|
||||
```python
|
||||
from hctvwrapper import Bot
|
||||
import os
|
||||
|
||||
bot = Bot(command_prefix="!")
|
||||
|
||||
@bot.event
|
||||
async def on_ready(session):
|
||||
print(f"✅ Logged in as {session.viewer}")
|
||||
|
||||
@bot.command()
|
||||
async def ping(ctx):
|
||||
await ctx.reply("pong! 🏓")
|
||||
|
||||
@bot.command(name="echo", aliases=["say"])
|
||||
async def echo_cmd(ctx, *, text):
|
||||
await ctx.send(text)
|
||||
|
||||
bot.run(os.environ["BOT_TOKEN"], channel="bot-playground")
|
||||
```
|
||||
|
||||
### AI Bot
|
||||
|
||||
```python
|
||||
from hctvwrapper import Bot
|
||||
import aiohttp, os
|
||||
|
||||
bot = Bot(command_prefix="/")
|
||||
|
||||
@bot.command(name="ai")
|
||||
async def ai_cmd(ctx, *, prompt):
|
||||
async with aiohttp.ClientSession() as http:
|
||||
resp = await http.post(
|
||||
"https://ai.hackclub.com/proxy/v1/chat/completions",
|
||||
headers={"Authorization": f"Bearer {os.environ['AI_TOKEN']}"},
|
||||
json={
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
},
|
||||
)
|
||||
data = await resp.json()
|
||||
answer = data["choices"][0]["message"]["content"]
|
||||
await ctx.reply(answer)
|
||||
|
||||
bot.run(os.environ["BOT_TOKEN"], channel="bot-playground")
|
||||
```
|
||||
|
||||
### Moderation Bot
|
||||
|
||||
```python
|
||||
from hctvwrapper import Bot
|
||||
import os
|
||||
|
||||
bot = Bot(command_prefix="!")
|
||||
|
||||
@bot.command()
|
||||
async def timeout(ctx, user_id, seconds="300"):
|
||||
await bot.timeout_user(ctx.channel, user_id, duration=int(seconds))
|
||||
await ctx.send(f"⏰ Timed out for {seconds}s")
|
||||
|
||||
@bot.event
|
||||
async def on_moderation_error(error, channel):
|
||||
print(f"⚠️ {error.code}: {error.message}")
|
||||
|
||||
bot.run(os.environ["BOT_TOKEN"], channel="my-channel")
|
||||
```
|
||||
|
||||
## Models Reference
|
||||
|
||||
| Model | Fields |
|
||||
|---|---|
|
||||
| `Message` | `content`, `author`, `channel`, `msg_id`, `timestamp`, `type`, `is_bot` |
|
||||
| `Author` | `id`, `username`, `pfp_url`, `display_name`, `is_bot`, `is_platform_admin`, `channel_role` |
|
||||
| `Session` | `viewer` (Author), `permissions`, `moderation` |
|
||||
| `Permissions` | `can_moderate` |
|
||||
| `ModerationSettings` | `has_blocked_terms`, `slow_mode_seconds`, `max_message_length` |
|
||||
| `SystemMessage` | `type`, `channel`, `content`, `timestamp` |
|
||||
| `ChatAccess` | `can_send`, `restriction` |
|
||||
| `Restriction` | `type` (timeout/ban), `reason`, `expires_at` |
|
||||
| `ModerationError` | `code`, `message`, `restriction` |
|
||||
| `ModerationEvent` | `type`, `msg_id`, `channel` |
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.10+
|
||||
- `websockets` (only dependency)
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2026 Christian Well - [MIT License](https://github.com/christianwell/hctvwrapper/blob/main/LICENSE)
|
||||
@@ -3,13 +3,42 @@ title: How to stream
|
||||
description: Get started with OBS and streaming on hackclub.tv
|
||||
---
|
||||
|
||||
- open obs
|
||||
- open settings
|
||||
- open "Stream"
|
||||
- set service to custom
|
||||
- set url to `srt://localhost:8890?streamid=publish:CHANNEL_NAME:thisusernameislongonpurposesoyoudontaccidentallyleakyourstreamkey:STREAM_KEY&pkt_size=1316`
|
||||
- on the website, click "Regenerate key"
|
||||
- paste the key into your obs "Stream Key"
|
||||
- start streaming!
|
||||
_This guide demonstrates how to stream with hackclub.tv via OBS Studio. For other streaming software, you will need to adapt these steps for your chosen software._
|
||||
|
||||
(screenshots to be added soon)
|
||||
- Open OBS Studio.
|
||||
|
||||
(_If it is not installed, head to [this webpage](https://obsproject.com/) to download the installer._)
|
||||
|
||||

|
||||
|
||||
- In OBS, click `File > Settings` in the top navigation bar.
|
||||
|
||||
<img alt="Animated GIF of opening the settings page" src="/guides/obs-open-settings-op.gif" />
|
||||
|
||||
- In the Settings popup, open `Stream` from the sidebar.
|
||||
|
||||
- Set `Service` to `Custom...`.
|
||||
|
||||
<img
|
||||
alt="Animated GIF of opening 'Stream' settings and changing to 'Custom'"
|
||||
src="/guides/obs-changing-settings-op.gif"
|
||||
/>
|
||||
|
||||
- Go to https://hackclub.tv/settings/channel/ in your web browser.
|
||||
|
||||
(_If you are not logged in, you may have to log in again and then try again._)
|
||||
|
||||
- Under `Stream Settings`, you will find your `Stream Key` and `Stream URL`. Copy the `Stream URL` and paste it into OBS.
|
||||
|
||||

|
||||
|
||||
- Go back to the website and click this <img alt="Lock Icon" src="../../../assets/lock.png" width="35"/> icon to regenerate your stream key. Copy the new key and paste it into the `Stream Key` box in OBS.
|
||||
|
||||

|
||||
(_Your OBS settings should now look like this_)
|
||||
|
||||
- You can now safely apply the changes in OBS and start streaming.
|
||||
|
||||
Thanks for using this guide and happy streaming!
|
||||
|
||||
_A video tutorial is currently being made, so check back later if you would like to see it!_
|
||||
|
||||