Compare commits

..
5 Commits
Author SHA1 Message Date
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
12 changed files with 220 additions and 414 deletions
+2
View File
@@ -2,3 +2,5 @@
__pycache__/ __pycache__/
.backup* .backup*
.history/ .history/
build/
dist/
+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:
+68 -60
View File
@@ -1,81 +1,89 @@
# 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 "hey", "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 pase this content.
```python ```python
import asyncio
import asyncio
from snekbot.bot import Bot from snekbot.bot import Bot
class CustomSnekBot(Bot):
async def on_join(self, channel_uid): class ExampleBot(Bot):
async def on_join(self, data):
print(f"I joined {data.channel_uid}!")
async def on_leave(self, data):
print(f"I left {data.channel_uid}!")
async def on_ping(self, data):
print(f"Ping from {data.user_nick}")
await self.send_message( await self.send_message(
channel_uid, data.channel_uid,
"Hello! I am here to assist you." "I should respond with Bong according to BordedDev. So here, bong!",
) )
async def on_message(self, sender_username, sender_nick, channel_uid, message): async def on_own_message(self, data):
message = message.lower() print(f"Received my own message: {data.message}")
if "hello" in message:
await self.send_message(channel_uid, f"Greetings, {sender_nick}!")
elif "bye" in message:
await self.send_message(channel_uid, f"Goodbye, {sender_nick}!")
# Initialize your bot async def on_mention(self, data):
bot = CustomSnekBot( message = data.message[len(self.username) + 2 :]
url="wss://your-snek-instance.com/rpc.ws", print(f"Mention from {data.user_nick}: {message}")
username="your_bot_username",
password="your_secure_password" result = f'Hey {data.user_nick}, Thanks for mentioning me "{message}".'
) await self.send_message(data.channel_uid, result)
async def on_message(self, data):
print(f"Message from {data.user_nick}: {data.message}")
message = data.message.lower()
result = None
if "hey" in message or "hello" in message:
result = f"Hi {data.user_nick}"
elif "bye" in message:
result = f"Bye {data.user_nick}"
if result:
await self.send_message(data.channel_uid, result)
bot = ExampleBot(username="Your username", password="Your password",url="wss://your-snek-instance.com/rpc.ws")
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
You can override the following event handlers:
- `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
- For detailed logging, include `logging.basicConfig(level=logging.DEBUG)` in your code.
- The bot is designed to automatically reconnect in case of connection drops.
- Feel free to customize the bot to meet your specific requirements.
### Contribution Guidelines
Contributions are welcome. Please submit pull requests for any enhancements or bug fixes.
### License
This project is licensed under the MIT License.
+4
View File
@@ -0,0 +1,4 @@
# Todo's
- implement logging module instead of all print statements.
- use asyncio processes instead of subprocess module processes.
Binary file not shown.
BIN
View File
Binary file not shown.
+25 -45
View File
@@ -1,43 +1,32 @@
import asyncio import asyncio
import logging
logging.basicConfig(level=logging.DEBUG)
from snekbot.bot import Bot from snekbot.bot import Bot
class ExampleBot(Bot): class ExampleBot(Bot):
async def on_join(self, channel_uid): async def on_join(self, data):
await super().on_join(channel_uid) print(f"I joined {data.channel_uid}!")
print(f"I joined!")
channel = await self.get_channel(channel_uid) async def on_leave(self, data):
print(f"I left {data.channel_uid}!")
async def on_ping(self, data):
print(f"Ping from {data.user_nick}")
await self.send_message( await self.send_message(
channel_uid, data.channel_uid,
f"Hello, i'm actively part of the conversation in channel {channel['name']} now, you don't have to mention me anymore. ", "I should respond with Bong according to BordedDev. So here, bong!",
) )
async def on_leave(self, channel_uid): async def on_own_message(self, data):
await super().on_leave(channel_uid) print(f"Received my own message: {data.message}")
print(f"I left!!")
await self.send_message(
channel_uid, "I stop actively being part of the conversation now. Bye!"
)
async def on_ping(self, username, user_nick, channel_uid, message): async def on_mention(self, data):
channel = await self.get_channel(channel_uid)
print(f"Ping from {user_nick} in channel {channel['name']}: {message}")
await self.send_message(channel_uid, "pong " + message)
async def on_own_message(self, channel_uid, message): message = data.message[len(self.username) + 2 :]
channel = await self.get_channel(channel_uid) print(f"Mention from {data.user_nick}: {message}")
print(f"Received my own message in channel {channel['name']}: {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}")
result = f'Hey {data.user_nick}, Thanks for mentioning me "{message}".'
if "source" in message: if "source" in message:
with open(__file__) as f: with open(__file__) as f:
result = f.read() result = f.read()
@@ -48,30 +37,21 @@ class ExampleBot(Bot):
+ result + result
+ "\n```" + "\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): await self.send_message(data.channel_uid, result)
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() async def on_message(self, data):
print(f"Message from {data.user_nick}: {data.message}")
message = data.message.lower()
result = None result = None
if "hello" in message: if "hey" in message or "hello" in message:
result = f"Hi @{sender_nick}" result = f"Hi {data.user_nick}"
elif "bye" in message: elif "bye" in message:
result = f"Bye @{sender_nick}" result = f"Bye {data.user_nick}"
if result: if result:
await self.send_message(channel_uid, result) await self.send_message(data.channel_uid, result)
bot = ExampleBot( bot = ExampleBot(username="example", password="example")
url="ws://snek.molodetz.nl/rpc.ws", username="example", password="xxxxxx"
)
asyncio.run(bot.run()) asyncio.run(bot.run())
+2 -2
View File
@@ -4,14 +4,14 @@ 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 = [
{ name = "retoor", email = "retoor@molodetz.nl" } { name = "retoor", email = "retoor@molodetz.nl" }
] ]
keywords = ["chat", "snek", "molodetz","bot"] keywords = ["chat", "snek", "molodetz","bot"]
requires-python = ">=3" requires-python = ">=3.12"
dependencies = [ dependencies = [
"aiohttp" "aiohttp"
] ]
+3 -86
View File
@@ -1,93 +1,10 @@
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
Requires-Python: >=3 Requires-Python: >=3.12
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
## Create Your Own Bot in Minutes
### Overview
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
- Python 3.8 or higher
- Basic understanding of Python programming
### Installation Instructions
#### 1. Prepare Your Environment
```bash
# For Ubuntu/Debian users:
sudo apt install python3 python3-venv python3-pip -y
# Create a virtual environment for your bot
python3 -m venv venv
source venv/bin/activate
```
#### 2. Install SnekBot
```bash
pip install git+https://molodetz.nl/retoor/snekbot.git
```
### Bot Development
To create your bot, use the following template:
```python
import asyncio
from snekbot.bot import Bot
class CustomSnekBot(Bot):
async def on_join(self, channel_uid):
await self.send_message(
channel_uid,
"Hello! I am here to assist you."
)
async def on_message(self, sender_username, sender_nick, channel_uid, message):
message = message.lower()
if "hello" in message:
await self.send_message(channel_uid, f"Greetings, {sender_nick}!")
elif "bye" in message:
await self.send_message(channel_uid, f"Goodbye, {sender_nick}!")
# Initialize your bot
bot = CustomSnekBot(
url="wss://your-snek-instance.com/rpc.ws",
username="your_bot_username",
password="your_secure_password"
)
asyncio.run(bot.run())
```
### Running Your Bot
```bash
python your_bot_script.py
```
### Event Handlers
You can override the following event handlers:
- `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
- For detailed logging, include `logging.basicConfig(level=logging.DEBUG)` in your code.
- The bot is designed to automatically reconnect in case of connection drops.
- Feel free to customize the bot to meet your specific requirements.
### 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
View File
@@ -1,5 +1,4 @@
LICENSE.txt LICENSE.txt
README.md
pyproject.toml pyproject.toml
src/snekbot/__init__.py src/snekbot/__init__.py
src/snekbot/__main__.py src/snekbot/__main__.py
+59 -138
View File
@@ -12,15 +12,11 @@
# MIT License # MIT License
import asyncio import asyncio
import logging
import traceback
import aiohttp import aiohttp
from snekbot.rpc import RPC from snekbot.rpc import RPC
logger = logging.getLogger("snekbot")
class Bot: class Bot:
@@ -29,162 +25,87 @@ class Bot:
self.username = username self.username = username
self.password = password self.password = password
self.user = None self.user = None
self._channels = None self.channels = None
self.rpc = None self.rpc = None
self.ws = None self.ws = None
self.joined = set() self.join_conversation = False
self.semaphore = asyncio.Semaphore(1)
async def on_init(self):
logger.debug("Bot initialized.")
async def on_join(self, channel_uid):
self.joined.add(channel_uid)
logger.debug("Joined channel: " + channel_uid)
async def on_leave(self, channel_uid):
self.joined.remove(channel_uid)
logger.debug("Left channel: " + channel_uid)
async def on_mention(self, username, user_nick, channel_uid, message):
logger.debug("Received mention from " + username + ": " + message)
async def on_idle(self):
logger.debug("Bot is idle.")
async def on_ping(self, username, user_nick, channel_uid, message):
logger.debug("Received ping from " + username + ": " + message)
async def on_own_message(self, channel_uid, message):
logger.debug("Received own message: " + message)
async def on_message(self, username, user_nick, channel_uid, message):
logger.debug("Received message from " + username + ": " + message)
async def run(self, reconnect=True): async def run(self, reconnect=True):
while True: while True:
try: try:
await self.run_once() await self.run_once()
except: except Exception as ex:
traceback.print_exc() print(ex)
await asyncio.sleep(1) await asyncio.sleep(1)
if not reconnect: if not reconnect:
break break
def has_joined(self, channel_uid): async def send_message(self, channel_uid, message):
return channel_uid in self.joined await self.rpc.send_message(channel_uid, message)
async def send_message(self, channel_uid, message,final=True):
await self.rpc.send_message(channel_uid, message,final)
return True return True
async def get_channel(self, channel_uid=None, refresh=False):
for channel in await self.get_channels(refresh):
if channel["uid"] == channel_uid:
return channel
if not refresh:
return await self.get_channel(channel_uid, True)
async def get_channels(self, refresh=False):
if refresh or not self._channels:
self._channels = await self.rpc.get_channels()
return self._channels
async def run_once(self): async def run_once(self):
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.ws_connect(self.url) as ws: async with session.ws_connect(self.url) as ws:
is_initial = not self.ws
self.ws = ws self.ws = ws
rpc = RPC(self.ws) self.rpc = RPC(ws)
self.rpc = rpc rpc = self.rpc
await (await rpc.login(self.username, self.password))()
await rpc.login(self.username, self.password) try:
self.user = await rpc.get_user(None) raise Exception(self.login_result.exception)
logger.debug("Logged in as: " + self.user["username"]) except:
pass
if is_initial: self.channels = await (await rpc.get_channels())()
await self.on_init() self.user = (await (await rpc.get_user(None))()).data
self.join_conversation = False
for channel in await self.get_channels():
logger.debug("Found channel: " + channel["name"])
while True: while True:
print("Waiting for message...")
data = await rpc.receive()
await self.on_idle() if not data:
break
message = None try:
data = None pass
except:
continue
while True: message = data["message"].strip()
async with self.semaphore:
data = await rpc.receive()
if not data:
return
event = "?"
try:
event = data.event
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) try:
await self.on_own_message(data)
except Exception as ex:
print("Error", ex)
continue
elif message.startswith("ping"): elif message.startswith("ping"):
await self.on_ping( try:
data.username, await self.on_ping(data)
data.user_nick, except Exception as ex:
data.channel_uid, print("Error:", ex)
data.message.lstrip("ping ").strip(), continue
) elif "@" + self.user["nick"] in data.message:
elif any( try:
[ await self.on_mention(data)
"@" + self.user["nick"] + " join" in data.message, except Exception as ex:
"@" + self.user["username"] + " join" in data.message, print("Error:", ex)
] continue
): elif "@" + self.user["nick"] + " join" in data.message:
await self.on_join(data.channel_uid) self.join_conversation = True
elif any( try:
[ await self.on_join(data)
"@" + self.user["nick"] + " leave" in data.message, except:
"@" + self.user["username"] + " leave" in data.message, print("Error:", ex)
] continue
): elif "@" + self.user["nick"] + " leave" in data.message:
await self.on_leave(data.channel_uid) self.join_conversation = False
elif ( try:
"@" + self.user["nick"] in data.message await self.on_leave(data)
or "@" + self.user["username"] in data.message except:
): print("Error:", ex)
await self.on_mention( continue
data.username,
data.user_nick,
data.channel_uid,
data.message,
)
else: else:
await self.on_message( try:
data.username, await self.on_message(data)
data.user_nick, except Exception as ex:
data.channel_uid, print("Error:", ex)
data.message,
)
+56 -81
View File
@@ -7,120 +7,95 @@
# 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 pathlib import pathlib
import subprocess import subprocess
import uuid
import asyncio
import aiohttp
logger = logging.getLogger("snekbot.rpc") import aiohttp
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):
for k in self.__dict__.get("data", []): for item in self.data:
yield k yield item
async def __aiter__(self): async def __aiter__(self):
for k in self.__dict__.get("data", []): for item in self.data:
yield k yield item
def __getitem__(self, name): def __getitem__(self, name):
try: return self.__dict__[name]
return self.__dict__[name]
except:
pass
return self.__dict__.get("data", {})[name]
def __setitem__(self, name, value): def __setitem__(self, name, value):
if name not in self.__dict__.get("data", {}): self.__dict__[name] = value
self.__dict__[name] = value
else:
self.__dict__["data"][name] = value
def __str__(self): def __str__(self):
return json.dumps(self.__dict__, default=str, indent=2) return json.dumps(self.__dict__, default=str, indent=2)
def __init__(self, ws): def __init__(self, ws):
self.ws = ws self.ws = ws
self.current_call_id = None
self.queue = asyncio.Queue()
self.semaphore = asyncio.Semaphore(1)
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) payload = {"method": name, "args": args}
self.current_call_id = str(uuid.uuid4()) try:
payload = { await self.ws.send_json(payload)
"method": name, except Exception:
"args": args, return None
"kwargs": kwargs,
"callId": self.current_call_id,
}
await self.ws.send_json(payload)
async def poller(): async def returner():
response = await self.ws.receive()
while True: return self.Response(response.json())
response = await self.ws.receive()
data = response.json()
if data.get("callId") == self.current_call_id:
self.current_call_id = None
return self.Response(data)
await self.queue.put(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:
pass
return response
async def receive(self):
while True: while True:
async with self.semaphore: try:
msg = await self.ws.receive()
except Exception:
break
if msg.type == aiohttp.WSMsgType.CLOSED:
break
elif msg.type == aiohttp.WSMsgType.ERROR:
break
elif msg.type == aiohttp.WSMsgType.TEXT:
try: try:
msg = await self.ws.receive() return self.Response(msg.json())
except Exception as ex: except Exception:
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 (
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
return None return None