Compare commits

..
28 Commits
Author SHA1 Message Date
retoor 5d6a86f8b2 Change. 2025-05-03 17:19:13 +02:00
retoor f517add468 Change. 2025-05-03 17:13:47 +02:00
retoor a98ee86ecd Updated README. 2025-04-24 21:40:44 +02:00
retoor 4d5553468e Some quality refinement. 2025-04-24 21:38:56 +02:00
retoor a91d4e72ec Update snek core bot. 2025-04-24 21:03:12 +02:00
retoor 7c688c1c9b Upgrade. 2025-02-18 19:09:51 +01:00
retoor 9d93dadae0 Fixx. 2025-02-18 16:34:57 +01:00
retoor 11d47c3ebe Fixed logging. 2025-02-17 20:11:45 +01:00
retoor b029eb8d17 Refactored messages. 2025-02-15 21:11:34 +01:00
retoor 51cf3fb500 Added excpetions. 2025-02-15 13:23:46 +01:00
retoor 1ecb595bee Updated example. 2025-02-13 10:40:22 +01:00
retoor 736e1c6ee4 Updated example. 2025-02-13 10:36:46 +01:00
retoor 4d1b1fa974 Updated example. 2025-02-13 10:31:50 +01:00
retoor 7d6ad67f97 Progress. 2025-02-13 10:26:50 +01:00
retoor 589b0468f5 Update snekbot. 2025-02-13 10:21:35 +01:00
retoor bec48eb574 Update snekbot. 2025-02-13 10:12:52 +01:00
retoor 8aee6f360d Update. 2025-02-12 01:06:19 +01:00
retoor 328534e5c9 Updated rpc. 2025-02-12 01:03:41 +01:00
retoor 04cd9489ac Added build to repository. 2025-02-10 23:12:50 +01:00
retoor 8030955f3f Updated version. 2025-02-10 23:09:59 +01:00
retoor a2b1d3c3aa Made doc changes. 2025-02-10 22:31:02 +01:00
retoor 636ea209d1 The snek bot updated! 2025-02-10 14:16:55 +01:00
retoor 0bde0cedd4 Fixed exception message. 2025-02-01 16:20:13 +01:00
retoor 9cb2630e41 Fixes. 2025-02-01 16:16:25 +01:00
retoor 8f891f7d80 Fixed script 2025-02-01 16:14:56 +01:00
retoor 818aca5449 Changes. 2025-02-01 16:14:08 +01:00
retoor 104854be6a Added Todo. 2025-02-01 16:00:05 +01:00
retoor a60c862450 Created a nice tutorial. 2025-02-01 15:58:18 +01:00
6 changed files with 279 additions and 197 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ APP=./example.py
all: install run all: install run
install: install:
python3.12 -m venv .venv python3 -m venv .venv
$(PIP) install -e . $(PIP) install -e .
run: run:
+98 -52
View File
@@ -1,81 +1,127 @@
# SnekBot: Your Instant Chat Companion # Snekbot API
## Create Your Own Bot in Minutes This is the Snekbot API. This document describes how to create a bot responding to "hello", "bye" and "@username-of-bot".
### Overview ## 5 minute tutorial
SnekBot is designed for rapid deployment and customization, providing a fully asynchronous and production-ready chat bot solution. It is built to handle network issues effectively, ensuring a reliable user experience.
### Prerequisites Literally.
- Python 3.8 or higher
- Basic understanding of Python programming
### Installation Instructions ### Installation
#### Requirements:
Python:
- python3
- python3-venv
- python3-pip
Use apt or your package manager to install these packages. There is a big chance your system already has them.
#### 1. Prepare Your Environment For Debian (Ubuntu): `sudo apt install python3 python3-venv python3-pip -y`
```bash
# For Ubuntu/Debian users:
sudo apt install python3 python3-venv python3-pip -y
# Create a virtual environment for your bot #### Environment
python3 -m venv venv - `python3 -m venv venv`
source venv/bin/activate - `source venv/bin/activate`
``` - `pip install git+https://molodetz.nl/retoor/snekbot.git`
#### 2. Install SnekBot #### Create account
```bash Create regular user account for your bot. You need this later in your script.
pip install git+https://molodetz.nl/retoor/snekbot.git Make sure you have this information right now:
``` - bot username
- bot password
- bot url (wss://your-snek-instance.com/rpc.ws)
### Bot Development #### Create a file
To create your bot, use the following template: Open a file ending with the `.py` extension and paste this content. Replace the authentication details on the bottom lines with the one of the account you just created.
```python ```python
import asyncio import asyncio
from snekbot.bot import Bot from snekbot.bot import Bot
class CustomSnekBot(Bot):
class ExampleBot(Bot):
async def on_join(self, channel_uid): async def on_join(self, channel_uid):
await super().on_join(channel_uid)
print(f"I joined!")
await self.send_message( await self.send_message(
channel_uid, channel_uid,
"Hello! I am here to assist you." f"Hello, i'm actively part of the conversation in channel {channel_uid} now, you don't have to mention me anymore. ",
) )
async def on_message(self, sender_username, sender_nick, channel_uid, message): async def on_leave(self, channel_uid):
message = message.lower() await super().on_leave(channel_uid)
if "hello" in message: print(f"I left!!")
await self.send_message(channel_uid, f"Greetings, {sender_nick}!") await self.send_message(
elif "bye" in message: channel_uid, "I stop actively being part of the conversation now. Bye!"
await self.send_message(channel_uid, f"Goodbye, {sender_nick}!") )
# Initialize your bot async def on_ping(self, username, user_nick, channel_uid, message):
bot = CustomSnekBot( print(f"Ping from {user_nick} in channel {channel_uid}: {message}")
url="wss://your-snek-instance.com/rpc.ws", await self.send_message(channel_uid, "pong " + message)
username="your_bot_username",
password="your_secure_password" async def on_own_message(self, channel_uid, data):
print(f"Received my own message: {data.message}")
async def on_mention(self, username, user_nick, channel_uid, message):
message = message[len(self.username) + 2 :]
print(f"Mention from {user_nick}: {message}")
if "source" in message:
with open(__file__) as f:
result = f.read()
result = result.replace(f'"{self.username}"', '"example username"')
result = result.replace(self.password, "example password")
result = (
"This is the actual source code running me now. Fresh from the bakery:\n\n```python\n"
+ result
+ "\n```"
)
await self.send_message(channel_uid, result)
else:
await self.send_message(
channel_uid, f'Hey {user_nick}, Thanks for mentioning me "{message}".'
)
async def on_message(self, sender_username, sender_nick, channel_uid, message):
print(f"Message from {sender_nick}: {message}")
if not self.has_joined(channel_uid):
print(f"Probably not for me since i'm not mentioned and not joined yet")
return
message = message.lower()
result = None
if "hello" in message:
result = f"Hi @{sender_nick}"
elif "bye" in message:
result = f"Bye @{sender_nick}"
if result:
await self.send_message(channel_uid, result)
bot = ExampleBot(
url="ws://snek.molodetz.nl/rpc.ws", username="example", password="example"
) )
asyncio.run(bot.run()) asyncio.run(bot.run())
``` ```
### Running Your Bot #### Run the bot
Make sure you have (still) activated your virtual env.
```bash ```bash
python your_bot_script.py python [your-script].py
``` ```
If you get the error 'python not found' or 'aiohttp not found', run `source .venv/bin/activate` again and run `python [your script].py` again.
### Event Handlers #### Debugging
You can override the following event handlers: Add `import logging` and `logging.BasicConfig(level=logging.DEBUG)`.
- `on_join`: Triggered when the bot joins a channel
- `on_leave`: Triggered when the bot leaves a channel
- `on_ping`: Responds to ping messages
- `on_mention`: Handles direct mentions
- `on_message`: Processes incoming messages
### Additional Information #### Summary
- For detailed logging, include `logging.basicConfig(level=logging.DEBUG)` in your code. The `ExampleBot` class inherits from a base `Bot` class and implements several event handlers:
- The bot is designed to automatically reconnect in case of connection drops. - `on_join`: Sends a welcome message when the bot joins a channel.
- Feel free to customize the bot to meet your specific requirements. - `on_leave`: Sends a goodbye message when the bot leaves.
- `on_ping`: Responds with "pong" when it receives a ping message.
- `on_own_message`: Logs messages sent by the bot itself.
- `on_mention`: Handles mentions; if "source" is in the message, it replies with its own source code, with sensitive data disguised.
- `on_message`: Responds to "hello" and "bye" messages if the bot has joined the channel.
The bot will be instantiated and runs asynchronously. It will survive server deploys and network outages. If such issue occurs, it will try to reconnect within a second like nothing happened. It's production ready.
### Contribution Guidelines
Contributions are welcome. Please submit pull requests for any enhancements or bug fixes.
### License
This project is licensed under the MIT License.
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "snekbot" name = "snekbot"
version = "1.1.0" version = "1.0.0"
readme = "README.md" readme = "README.md"
description = "Bot API for Snek chat" description = "Bot API for Snek chat"
authors = [ authors = [
+100 -57
View File
@@ -1,6 +1,6 @@
Metadata-Version: 2.4 Metadata-Version: 2.2
Name: snekbot Name: snekbot
Version: 1.1.0 Version: 1.0.0
Summary: Bot API for Snek chat Summary: Bot API for Snek chat
Author-email: retoor <retoor@molodetz.nl> Author-email: retoor <retoor@molodetz.nl>
Keywords: chat,snek,molodetz,bot Keywords: chat,snek,molodetz,bot
@@ -8,86 +8,129 @@ Requires-Python: >=3
Description-Content-Type: text/markdown Description-Content-Type: text/markdown
License-File: LICENSE.txt License-File: LICENSE.txt
Requires-Dist: aiohttp Requires-Dist: aiohttp
Dynamic: license-file
# SnekBot: Your Instant Chat Companion # Snekbot API
## Create Your Own Bot in Minutes This is the Snekbot API. This document describes how to create a bot responding to "hello", "bye" and "@username-of-bot".
### Overview ## 5 minute tutorial
SnekBot is designed for rapid deployment and customization, providing a fully asynchronous and production-ready chat bot solution. It is built to handle network issues effectively, ensuring a reliable user experience.
### Prerequisites Literally.
- Python 3.8 or higher
- Basic understanding of Python programming
### Installation Instructions ### Installation
#### Requirements:
Python:
- python3
- python3-venv
- python3-pip
Use apt or your package manager to install these packages. There is a big chance your system already has them.
#### 1. Prepare Your Environment For Debian (Ubuntu): `sudo apt install python3 python3-venv python3-pip -y`
```bash
# For Ubuntu/Debian users:
sudo apt install python3 python3-venv python3-pip -y
# Create a virtual environment for your bot #### Environment
python3 -m venv venv - `python3 -m venv venv`
source venv/bin/activate - `source venv/bin/activate`
``` - `pip install git+https://molodetz.nl/retoor/snekbot.git`
#### 2. Install SnekBot #### Create account
```bash Create regular user account for your bot. You need this later in your script.
pip install git+https://molodetz.nl/retoor/snekbot.git Make sure you have this information right now:
``` - bot username
- bot password
- bot url (wss://your-snek-instance.com/rpc.ws)
### Bot Development #### Create a file
To create your bot, use the following template: Open a file ending with the `.py` extension and paste this content. Replace the authentication details on the bottom lines with the one of the account you just created.
```python ```python
import asyncio import asyncio
from snekbot.bot import Bot from snekbot.bot import Bot
class CustomSnekBot(Bot):
class ExampleBot(Bot):
async def on_join(self, channel_uid): async def on_join(self, channel_uid):
print(f"I joined!")
await self.send_message( await self.send_message(
channel_uid, channel_uid,
"Hello! I am here to assist you." f"Hello, i'm actively part of the conversation in channel {channel_uid} now, you don't have to mention me anymore. "
) )
async def on_message(self, sender_username, sender_nick, channel_uid, message): async def on_leave(self, channel_uid):
message = message.lower() print(f"I left!!")
if "hello" in message: await self.send_message(
await self.send_message(channel_uid, f"Greetings, {sender_nick}!") channel_uid,
elif "bye" in message: "I stop actively being part of the conversation now. Bye!"
await self.send_message(channel_uid, f"Goodbye, {sender_nick}!") )
# Initialize your bot async def on_ping(self,username, user_nick, channel_uid, message):
bot = CustomSnekBot( print(f"Ping from {user_nick} in channel {channel_uid}: {message}")
url="wss://your-snek-instance.com/rpc.ws", await self.send_message(
username="your_bot_username", channel_uid,
password="your_secure_password" "pong " + message
) )
async def on_own_message(self, data):
print(f"Received my own message: {data.message}")
async def on_mention(self, username, user_nick, channel_uid, message):
message = message[len(self.username) + 2 :]
print(f"Mention from {user_nick}: {message}")
if "source" in message:
with open(__file__) as f:
result = f.read()
result = result.replace(f'"{self.username}"', '"example username"')
result = result.replace(self.password, "example password")
result = (
"This is the actual source code running me now. Fresh from the bakery:\n\n```python\n"
+ result
+ "\n```"
)
await self.send_message(channel_uid, result)
else:
await self.send_message(channel_uid, f'Hey {user_nick}, Thanks for mentioning me "{message}".')
async def on_message(self, sender_username, sender_nick, channel_uid, message):
print(f"Message from {sender_nick}: {message}")
if not self.has_joined(channel_uid):
print(f"Probably not for me since i'm not mentioned and not joined yet")
return
message = message.lower()
result = None
if "hello" in message:
result = f"Hi @{sender_nick}"
elif "bye" in message:
result = f"Bye @{sender_nick}"
if result:
await self.send_message(channel_uid, result)
bot = ExampleBot(url="ws://snek.molodetz.nl/rpc.ws", username="example", password="example")
asyncio.run(bot.run()) asyncio.run(bot.run())
``` ```
### Running Your Bot #### Run the bot
Make sure you have (still) activated your virtual env.
```bash ```bash
python your_bot_script.py python [your-script].py
``` ```
If you get the error 'python not found' or 'aiohttp not found', run `source .venv/bin/activate` again and run `python [your script].py` again.
### Event Handlers #### Debugging
You can override the following event handlers: Add `import logging` and `logging.BasicConfig(level=logging.DEBUG)`.
- `on_join`: Triggered when the bot joins a channel
- `on_leave`: Triggered when the bot leaves a channel
- `on_ping`: Responds to ping messages
- `on_mention`: Handles direct mentions
- `on_message`: Processes incoming messages
### Additional Information #### Summary
- For detailed logging, include `logging.basicConfig(level=logging.DEBUG)` in your code. The `ExampleBot` class inherits from a base `Bot` class and implements several event handlers:
- The bot is designed to automatically reconnect in case of connection drops. - `on_join`: Sends a welcome message when the bot joins a channel.
- Feel free to customize the bot to meet your specific requirements. - `on_leave`: Sends a goodbye message when the bot leaves.
- `on_ping`: Responds with "pong" when it receives a ping message.
- `on_own_message`: Logs messages sent by the bot itself.
- `on_mention`: Handles mentions; if "source" is in the message, it replies with its own source code, with sensitive data disguised.
- `on_message`: Responds to "hello" and "bye" messages if the bot has joined the channel.
The bot will be instantiated and runs asynchronously. It will survive server deploys and network outages. If such issue occurs, it will try to reconnect within a second like nothing happened. It's production ready.
### Contribution Guidelines
Contributions are welcome. Please submit pull requests for any enhancements or bug fixes.
### License
This project is licensed under the MIT License.
+13 -35
View File
@@ -33,7 +33,6 @@ class Bot:
self.rpc = None self.rpc = None
self.ws = None self.ws = None
self.joined = set() self.joined = set()
self.semaphore = asyncio.Semaphore(1)
async def on_init(self): async def on_init(self):
logger.debug("Bot initialized.") logger.debug("Bot initialized.")
@@ -77,8 +76,8 @@ class Bot:
def has_joined(self, channel_uid): def has_joined(self, channel_uid):
return channel_uid in self.joined return channel_uid in self.joined
async def send_message(self, channel_uid, message,final=True): async def send_message(self, channel_uid, message):
await self.rpc.send_message(channel_uid, message,final) await self.rpc.send_message(channel_uid, message)
return True return True
async def get_channel(self, channel_uid=None, refresh=False): async def get_channel(self, channel_uid=None, refresh=False):
@@ -90,7 +89,7 @@ class Bot:
async def get_channels(self, refresh=False): async def get_channels(self, refresh=False):
if refresh or not self._channels: if refresh or not self._channels:
self._channels = await self.rpc.get_channels() self._channels = await (await self.rpc.get_channels())()
return self._channels return self._channels
async def run_once(self): async def run_once(self):
@@ -102,8 +101,8 @@ class Bot:
rpc = RPC(self.ws) rpc = RPC(self.ws)
self.rpc = rpc self.rpc = rpc
await rpc.login(self.username, self.password) await (await rpc.login(self.username, self.password))()
self.user = await rpc.get_user(None) self.user = await (await rpc.get_user(None))()
logger.debug("Logged in as: " + self.user["username"]) logger.debug("Logged in as: " + self.user["username"])
if is_initial: if is_initial:
@@ -116,38 +115,17 @@ class Bot:
await self.on_idle() await self.on_idle()
message = None message = None
data = None
while True: while True:
async with self.semaphore: data = await rpc.receive()
data = await rpc.receive()
if not data: try:
return message = data.message.strip()
except AttributeError:
event = "?" continue
try: else:
event = data.event break
except AttributeError:
pass
try:
message = data.message.strip()
event = "message"
except AttributeError:
pass
if event == "?":
continue
elif event == "message":
if not data.is_final:
continue
break
try:
await getattr(self, "on_" + data.event)(**data.data)
except AttributeError:
logger.debug("Not implemented event: " + event)
if data.username == self.user["username"]: if data.username == self.user["username"]:
await self.on_own_message(data.channel_uid, message) await self.on_own_message(data.channel_uid, message)
elif message.startswith("ping"): elif message.startswith("ping"):
+66 -51
View File
@@ -7,13 +7,13 @@
# MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import asyncio
import json import json
import logging import logging
import pathlib import pathlib
import subprocess import subprocess
import uuid import uuid
import asyncio
import aiohttp import aiohttp
logger = logging.getLogger("snekbot.rpc") logger = logging.getLogger("snekbot.rpc")
@@ -22,6 +22,8 @@ logger = logging.getLogger("snekbot.rpc")
class RPC: class RPC:
class Response: class Response:
def __init__(self, msg): def __init__(self, msg):
if isinstance(msg, list):
self.list = msg
self.__dict__.update(msg) self.__dict__.update(msg)
def __iter__(self): def __iter__(self):
@@ -51,12 +53,13 @@ class RPC:
def __init__(self, ws): def __init__(self, ws):
self.ws = ws self.ws = ws
self.current_call_id = None self.current_call_id = None
self.queue = asyncio.Queue()
self.semaphore = asyncio.Semaphore(1) async def echo(self, data):
logger.debug("Schedule for retry: " + str(data))
await self.ws.send_json({"method": "echo", "args": [data]})
def __getattr__(self, name): def __getattr__(self, name):
async def method(*args, **kwargs): async def method(*args, **kwargs):
no_response = kwargs.pop("_no_response", False)
self.current_call_id = str(uuid.uuid4()) self.current_call_id = str(uuid.uuid4())
payload = { payload = {
"method": name, "method": name,
@@ -66,61 +69,73 @@ class RPC:
} }
await self.ws.send_json(payload) await self.ws.send_json(payload)
async def poller(): async def returner():
while True: while True:
response = await self.ws.receive() response = await self.ws.receive()
data = response.json() data = response.json()
if data.get("callId") == self.current_call_id: if not data.get("callId") == self.current_call_id:
self.current_call_id = None await self.echo(data)
return self.Response(data) continue
await self.queue.put(data) return self.Response(data)
if no_response: return returner
return True
async with self.semaphore:
return await poller()
return method return method
async def receive(self): async def system(self, command):
popped = [] if isinstance(command, str):
while not self.queue.empty(): command = command.split(" ")
msg = await self.queue.get()
if self.current_call_id == msg.get("callId"): path = pathlib.Path("output.txt")
self.current_call_id = None
return self.Response(msg) with path.open("w+") as f:
popped.append(msg) try:
for m in popped: subprocess.run(command, stderr=f, stdout=f)
await self.queue.put(m) except Exception as ex:
print("Error running command:", ex)
return f"Error: {ex}"
response = None
with path.open("r") as f:
response = f.read()
try:
path.unlink()
except Exception as ex:
logger.error(ex)
return response
async def receive(self):
while True: while True:
async with self.semaphore: try:
msg = await self.ws.receive()
except Exception as ex:
logger.exception("Error while receiving:", ex)
break
if msg.type == aiohttp.WSMsgType.CLOSED:
logger.exception("WebSocket closed.")
break
elif msg.type == aiohttp.WSMsgType.ERROR:
logger.exception("WebSocket error.")
break
elif msg.type == aiohttp.WSMsgType.TEXT:
if (
self.current_call_id
and not msg.json().get("callId") != self.current_call_id
):
await self.echo(msg.json())
continue
try: try:
msg = await self.ws.receive() response = self.Response(msg.json())
self.current_call_id = None
return response
except Exception as ex: except Exception as ex:
logger.exception("Error while receiving:", ex) logger.exception(ex)
break
if msg.type == aiohttp.WSMsgType.CLOSED:
logger.exception("WebSocket closed.")
break
elif msg.type == aiohttp.WSMsgType.ERROR:
logger.exception("WebSocket error.")
break
elif msg.type == aiohttp.WSMsgType.TEXT:
if (
msg.json().get("callId") != self.current_call_id
):
await self.queue.put(msg.json())
continue
try:
response = self.Response(msg.json())
self.current_call_id = None
return response
except Exception as ex:
logger.exception(ex)
break
else:
logger.exception("Unexpected message type.")
break break
else:
logger.exception("Unexpected message type.")
break
return None return None