Skip to content

Commit fde9203

Browse files
committed
init bot
1 parent 504e2dd commit fde9203

13 files changed

+522
-2
lines changed

Discovery.md

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
app_name: "chDBot"
3+
title: "Serverless chDB/ClickHouse Query Discord Bot"
4+
tagline: "Your personal SQL query bot."
5+
theme_color: "#74aa9c"
6+
git: "https://github.com/lmangani/chdb-discord-bot"
7+
homepage: "https://deta.space"
8+
---
9+
10+
With this, you can run your own instance of a chDB/ClickHouse Discord bot.
11+
12+
### Installation steps:
13+
1. First install the app.
14+
2. Enter the required environment variables:
15+
- `DISCORD_APPLICATION_ID` - Your discord app's ID.
16+
- `DISCORD_PUBLIC_KEY` - Your discord app's public key.
17+
- `DISCORD_BOT_TOKEN` - Your bot's token.
18+
- `CHDB_API` - Custom chDB/ClickHouse HTTP/S API URL.
19+
3. Set the `Interactions Endpoint URL` in your discord app's general information to `<micro_url>/bot/interactions`.
20+
4. Visit `<micro_url>/bot/api/dash/<token>` to register the slash commands for the first time.
21+
22+
Run `/ping` to make sure it's working! Enjoy!

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2023 imp
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+119-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,119 @@
1-
# chdb-discord-bot
2-
Discord bot running chdb/ClickHouse queries, powered by Deta.space
1+
# Information
2+
This Discord bot is **SERVERLESS** which means it can run for **FREE** and be **ALWAYS online** on [Deta Space](https://deta.space)!
3+
You can also treat this repository as a template for making serverless bots with the [discohook](https://github.com/jnsougata/discohook) library.
4+
5+
### Table of Contents
6+
- [Information](#information)
7+
- [Table of Contents](#table-of-contents)
8+
- [Features](#features)
9+
- [File Structure](#file-structure)
10+
- [Requirements](#requirements)
11+
- [Running Online](#running-online)
12+
- [Running Locally](#running-locally)
13+
- [Links and Resources](#links-and-resources)
14+
15+
## Features
16+
- `/ping` - a simple command that tells you the bot's latency.
17+
- `/query <query> [format]` - a command that queries [chDB API](https://chdb.io) (ClickHouse).
18+
- A status message `Listening to /query! | chDB` via [scheduled actions](https://deta.space/docs/en/basics/micros#scheduled-actions).
19+
- And you can easily create and add more commands yourself!
20+
21+
## File Structure
22+
```
23+
.
24+
├─ src/ # Source code
25+
│ ├─ actions/ # Files used for scheduled actions
26+
│ │ └─ presence.py # Presence updater (bot status)
27+
│ ├─ assets/ # All asset files
28+
│ │ └─ logo.png # Logo used for space app
29+
│ ├─ commands/ # All command files
30+
│ │ ├─ query.py # Query command
31+
│ │ └─ ping.py # Ping command
32+
│ ├─ micros/ # Micro files; each file is a micro
33+
│ │ ├─ bot.py # /bot route, contains the discohook bot
34+
│ │ └─ main.py # / route, contains the actions handler
35+
│ └─ utils/ # Contains any extra utility files
36+
│ │ └─ helpers.py # Useful functions
37+
├─ .gitignore # Hides certain files
38+
├─ Discovery.md # Defines app's space discovery page
39+
├─ LICENSE # License
40+
├─ README.md # Defines this README page
41+
├─ Spacefile # Space app configuration
42+
├─ example.env # Example of an .env file
43+
└─ requirements.txt # Library dependencies
44+
```
45+
46+
## Requirements
47+
- **Discord Application:** Create an app for **FREE** at [Discord Developer Portal](https://discord.com/developers/applications).
48+
- **Deta Space account:** Create an account for **FREE** at [Deta Space](https://deta.space/), username + password.
49+
- **chDB API URL:** Use the public service, or point at your HTTP/S chDB/ClickHouse compatible API..
50+
- [**discohook**](https://github.com/jnsougata/discohook): A github library used to make async serverless Discord bots.
51+
- [**deta**](https://github.com/jnsougata/discohook): A github library used to make async [Deta Space's Base HTTP API](https://deta.space/docs/en/reference/base/HTTP) requests.
52+
- The database is only used to store the websocket resume data for the status message.
53+
- [**uvicorn**](https://pypi.org/project/uvicorn/): A PyPI library used to run an ASGI webserver.
54+
- [**python-dotenv**](https://pypi.org/project/python-dotenv/): A PyPI library used to help load variables from an `.env` file.
55+
56+
## Running Online
57+
1. Install the space app from the [app's discovery page](https://deta.space/discovery/@imp1/chatgpt).
58+
Alternatively you could build the space app yourself:
59+
1. [Clone](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) this repository.
60+
2. Install the [Space CLI](https://deta.space/docs/en/basics/cli).
61+
3. Make sure you're in the project folder: `$cd <folder>`
62+
4. Create a space app: `$space new`
63+
5. Push the space app: `$space push`
64+
2. Enter the environment variables (Space App Settings ➔ Configuration).
65+
- `DISCORD_APPLICATION_ID` - Your discord app's ID.
66+
- `DISCORD_PUBLIC_KEY` - Your discord app's public key.
67+
- `DISCORD_BOT_TOKEN` - Your bot's token.
68+
- Other environment variables are optional.
69+
3. Set `Interactions Endpoint URL` to `<micro_url>/bot/interactions`.
70+
- This is located in: `https://discord.com/developers/applications/{application_id}/information`
71+
- A Micro URL looks like this: `https://chatgpt-1-a1234567.deta.app`
72+
4. Visit `<micro_url>/bot/api/dash/<discord_bot_token>` to register the slash commands for the first time.
73+
5. Run `/ping` to make sure it's working! Enjoy!
74+
75+
## Running Locally
76+
You only need to run the bot locally if you plan to **develop new commands** for the bot.
77+
This is because `$space push`-ing each time would make development take forever.
78+
1. [Clone](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) this repository.
79+
2. Install the [Space CLI](https://deta.space/docs/en/basics/cli).
80+
3. Make sure you're in the project folder: `$cd <folder>`
81+
4. Install the library dependencies.
82+
1. Make a virtual environment: `$python -m venv venv`
83+
2. Enter the virtual environment: `$source venv/bin/activate`
84+
3. Install requirements: `$pip install -r requirements.txt`
85+
4. To leave the virtual environment later run `$deactivate`.
86+
5. Rename `example.env` to `.env` file and update its contents.
87+
- Environment variables in comments are optional.
88+
6. Run `$space dev` to start both `main` and `bot` micros.
89+
7. In another terminal, start a reverse proxy/tunnel because your `https://localhost` can't be accessed by Discord.
90+
- **Via [Deta Space](https://deta.space)**:
91+
- Run `space reverse proxy` and the URL is your micro's URL: `https://<name>-1-a1234567.deta.app`
92+
- If you have a main bot and a test bot, use `space link` to switch to a test space app.
93+
- **Via [Ngrok](https://ngrok.com)**:
94+
- [Setup Ngrok](https://ngrok.com/docs/getting-started). Create an account, install CLI, set auth-token.
95+
- Run `ngrok http 4200` to get a URL like `https://a1b2-34-567-890-123.ngrok-free.app`.
96+
- Note the Free Tier has a ratelimit of 60 requests per minute and URLs are ephemeral.
97+
- **Via [Cloudflare](https://cloudflare.com)**:
98+
- [Setup Cloudflare](https://developers.cloudflare.com/pages/how-to/preview-with-cloudflare-tunnel). Install CLI and optionally link an account.
99+
- Run `cloudflared tunnel --url https://localhost:4200` to get a URL like `https://ab-quick-brown-fox.trycloudflare.com`.
100+
- No request ratelimit (AFAIK) but URLs are still ephemeral.
101+
- Note it's technically against their ToS to host anything other than basic HTML pages on the free plan.
102+
- Use this for development only if you need a higher request ratelimit.
103+
- **List of [other solutions](https://github.com/anderspitman/awesome-tunneling)**.
104+
- For all the above, you can do `CTRL+C` to stop them.
105+
8. Set the `Interactions Endpoint URL` to `<url>/bot/interactions`.
106+
- This is located in: `https://discord.com/developers/applications/{application_id}/information`
107+
- The URL is from the previous step, for space it's this: `https://<name>-1-a1234567.deta.app`
108+
9. Finally you can now start live editing.
109+
Uvicorn is set to `--reload` so any edits you make automatically restarts the webserver.
110+
10. To stop running do `CTRL+C`.
111+
112+
When you're ready, you can run `$space push` to update the space app.
113+
114+
## Links and Resources
115+
- **Deta Space Documentation:** https://deta.space/docs
116+
- **Deta Discord:** https://discord.gg/deta
117+
- **Discohook Discord:** https://discord.gg/xEEpJvE9py
118+
- **Discord API Documentation:** https://discord.com/developers/docs
119+
- **Space App Discovery Page:** https://deta.space/discovery/@imp1/chatgpt

Spacefile

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Spacefile Docs: https://go.deta.dev/docs/spacefile/v0
2+
v: 0
3+
icon: src/assets/logo.png
4+
micros:
5+
- name: main
6+
src: .
7+
engine: python3.9
8+
primary: true
9+
include:
10+
- src/micros/main.py
11+
- src/actions
12+
- src/utils
13+
run: uvicorn src.micros.main:app --log-level warning
14+
dev: uvicorn src.micros.main:app --reload
15+
presets:
16+
env:
17+
- name: PRESENCE_LOG_WEBHOOK_URL
18+
description: Discord Webhook URL (for presence logs)
19+
default: ''
20+
- name: ERROR_LOG_WEBHOOK_URL
21+
description: Discord Webhook URL (for presence error logs)
22+
default: ''
23+
- name: ALT_DETA_PROJECT_KEY
24+
description: Alternative Deta Project Key (or uses default)
25+
default: ''
26+
- name: DISCORD_BOT_TOKEN
27+
description: Discord Application/Bot Token
28+
actions:
29+
- id: 'presence'
30+
name: 'Presence'
31+
description: 'Gives the bot a status'
32+
trigger: 'schedule'
33+
default_interval: '* * * * *'
34+
35+
- name: bot
36+
src: .
37+
engine: python3.9
38+
include:
39+
- src/micros/bot.py
40+
- src/commands
41+
- src/utils
42+
run: uvicorn src.micros.bot:app --log-level warning
43+
dev: uvicorn src.micros.bot:app --reload
44+
public_routes:
45+
- '/interactions'
46+
presets:
47+
env:
48+
- name: DISCORD_APPLICATION_ID
49+
description: Discord Application's ID
50+
- name: DISCORD_PUBLIC_KEY
51+
description: Discord Application's Public Key
52+
- name: DISCORD_BOT_TOKEN
53+
description: Discord Application/Bot Token (duplicate)
54+
- name: CHDB_API
55+
description: chDB/ClickHouse HTTP/S API URL
56+
default: "https://chdb.fly.dev"
57+
- name: ERROR_LOG_WEBHOOK_URL
58+
description: Discord Webhook URL (for bot error logs)
59+
default: ''

example.env

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
DISCORD_BOT_TOKEN=
2+
DISCORD_APPLICATION_ID=
3+
DISCORD_PUBLIC_KEY=
4+
DISCORD_BOT_TOKEN=
5+
# ERROR_LOG_WEBHOOK_URL=
6+
# PRESENCE_LOG_WEBHOOK_URL=
7+
# ALT_DETA_PROJECT_KEY=

requirements.txt

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
git+https://github.com/jnsougata/discohook
2+
git+https://github.com/jnsougata/deta
3+
uvicorn
4+
python-dotenv

src/actions/presence.py

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import os
2+
import datetime
3+
import logging
4+
import asyncio
5+
import deta # https://github.com/jnsougata/deta
6+
import discohook # https://github.com/jnsougata/discohook
7+
8+
activity_name = '/query! | chDB'
9+
activity_type = 2 # listening to, https://discord.com/developers/docs/topics/gateway-events#activity-object
10+
status = 'online'
11+
12+
token = os.getenv('DISCORD_BOT_TOKEN')
13+
webhook_url = os.getenv('PRESENCE_LOG_WEBHOOK_URL')
14+
deta_project_key = os.getenv('ALT_DETA_PROJECT_KEY', os.getenv('DETA_PROJECT_KEY'))
15+
deta_base_name = 'presence_base'
16+
deta_base_key = 'resume'
17+
deta_base_value = 'value'
18+
gateway_url = 'wss://gateway.discord.gg'
19+
loop_timeout = 20 # max time for scheduled action
20+
21+
logger = logging.getLogger(__name__)
22+
handler = logging.StreamHandler()
23+
handler.setFormatter(logging.Formatter(
24+
'[%(asctime)s.%(msecs)03d] %(message)s',
25+
'%Y-%m-%d %H:%M:%S'
26+
))
27+
logger.addHandler(handler)
28+
logger.setLevel(logging.INFO) # set to DEBUG to debug
29+
30+
presence_payload = {
31+
'activities' : [{
32+
'name': activity_name,
33+
'type': activity_type
34+
}],
35+
'status' : status,
36+
'since' : 0,
37+
'afk' : False
38+
}
39+
40+
async def identify(ws):
41+
await ws.send_json({
42+
'op' : 2,
43+
'd' : {
44+
'token' : token,
45+
'intents' : 0, # recieve no events
46+
'properties' : {
47+
'os' : 'linux',
48+
'browser' : 'disco',
49+
'device' : 'disco'
50+
},
51+
'presence' : presence_payload
52+
}
53+
})
54+
55+
async def presence_update(ws):
56+
await ws.send_json({
57+
'op' : 3,
58+
'd' : presence_payload
59+
})
60+
61+
async def resume(ws, session_id, s):
62+
await ws.send_json({
63+
'op' : 6,
64+
'd' : {
65+
'token' : token,
66+
'session_id' : session_id,
67+
'seq' : s
68+
}
69+
})
70+
71+
# Resume data stored in 1 record in deta base, looks like this
72+
# key = resume | value = {'url' : 'ws://etc.', 'session_id' : 'asdasd', 's' : s} or record does not exist
73+
74+
async def run(session):
75+
logger.debug('Enter function')
76+
task = asyncio.create_task(loop(session))
77+
try:
78+
await asyncio.wait_for(task, timeout = loop_timeout + 1)
79+
except asyncio.TimeoutError: # for local testing, prevents accidental infinite loop
80+
logger.info('Timed out!')
81+
82+
async def loop(session):
83+
assert deta_project_key, 'Deta project key not found, give the variable a value if you\'re running this locally.'
84+
base = deta.Deta(deta_project_key, session = session).base(deta_base_name)
85+
try:
86+
data = (await base.get(deta_base_key))[deta_base_value]
87+
url = data['url']
88+
except deta.NotFound: # will not exist for first time
89+
data = None
90+
url = gateway_url
91+
while True:
92+
logger.debug('Connecting to URL: {}'.format(url))
93+
ws = await session.ws_connect(url)
94+
async for msg in ws:
95+
msg = msg.json()
96+
97+
if msg['op'] == 0: # Dispatch
98+
99+
if msg['t'] == 'READY': # contains guild count, save it somewhere to use
100+
logger.debug('Ready')
101+
data = {
102+
'url' : msg['d']['resume_gateway_url'],
103+
'session_id' : msg['d']['session_id']
104+
}
105+
# count = len(msg['d']['guilds'])
106+
107+
elif msg['t'] == 'RESUMED':
108+
logger.debug('Resumed + send PRESENCE UPDATE')
109+
await presence_update(ws)
110+
111+
data['s'] = msg['s']
112+
record = deta.Record({deta_base_value : data}, key = deta_base_key)
113+
await base.put(record) # updates resume data
114+
continue
115+
116+
elif msg['op'] == 9: # Invalid session
117+
logger.debug('Invalid session, reconnect: {}'.format(msg['d']))
118+
if not msg['d']: # cant reconnect bool
119+
await base.delete(deta_base_key) # deletes resume data
120+
data = None
121+
url = gateway_url
122+
await ws.close()
123+
continue
124+
125+
elif msg['op'] == 10: # Hello
126+
if data:
127+
logger.debug('Send RESUME')
128+
await resume(ws, data['session_id'], data['s'])
129+
content = 'Resume'
130+
else:
131+
logger.debug('Send IDENTIFY')
132+
await identify(ws)
133+
content = 'Identify <------ !!'
134+
content = '[`{}`] {}'.format(datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3], content)
135+
if webhook_url:
136+
await session.post(webhook_url, json = {'content' : content})
137+
continue
138+
139+
logger.warning('Unhandled message: {}'.format(msg))
140+
await asyncio.sleep(1)

src/assets/logo.png

31.5 KB
Loading

src/commands/ping.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import time
2+
import discohook
3+
from ..utils.helpers import snowflake_time
4+
5+
@discohook.command('ping', 'Ping test the bot.')
6+
async def ping_command(interaction):
7+
created_at = snowflake_time(int(interaction.id))
8+
now = time.time()
9+
since = now - created_at
10+
content = 'Pong! Latency: `{:.2f}ms`'.format(since * 1000)
11+
await interaction.response(content)

0 commit comments

Comments
 (0)