Created a nice tutorial.

This commit is contained in:
retoor 2025-02-01 15:58:18 +01:00
commit a60c862450
15 changed files with 441 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.venv/
__pycache__/
.backup*
.history/
build/
dist/

21
LICENSE.txt Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 retoor
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.

13
Makefile Normal file
View File

@ -0,0 +1,13 @@
PYTHON=./.venv/bin/python
PIP=./.venv/bin/pip
APP=./example.py
all: install run
install:
python3 -m venv .venv
$(PIP) install -e .
run:
$(PYTHON) $(APP)

89
README.md Normal file
View File

@ -0,0 +1,89 @@
# Snekbot API
This is the Snekbot API. This document describes how to create a bot responding to "hey", "hello", "bye" and "@username-of-bot".
## 5 minute tutorial
Literally.
### 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.
For Debian (Ubuntu): `sudo apt install python3 python3-venv python3-pip -y`
#### Environment
- `python3 -m venv venv`
- `source venv/bin/activate`
- `pip install git+https://molodetz.nl/retoor/snekbot.git`
#### Create account
Create regular user account for your bot. You need this later in your script.
Make sure you have this information right now:
- bot username
- bot password
- bot url (wss://your-snek-instance.com/rpc.ws)
#### Create a file
Open a file ending with the `.py` extension and pase this content.
```python
import asyncio
from snekbot.bot import Bot
class ExampleBot(Bot):
async def on_join(self, data):
print(f"I joined f{data.channel_uid}!")
async def on_leave(self, data):
print(f"I left f{data.channel_uid}!")
async def on_ping(self, data):
print(f"Ping from f{data.user_nick}")
await self.send_message(
data.channel_uid,
"I should respond with Bong according to BordedDev. So here, bong!",
)
async def on_own_message(self, data):
print(f"Received my own message: {data.message}")
async def on_mention(self, data):
message = data.message[len(self.username) + 2 :]
print(f"Mention from f{data.user_nick}: {message}")
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 f{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())
```
#### Run the bot
Make sure you have (still) activated your virtual env.
```bash
python bot.py
```
If you get the error 'python not found' or 'aiohttp not found', run `source .venv/bin/activate` again and run `python bot.py` again.

57
example.py Normal file
View File

@ -0,0 +1,57 @@
import asyncio
from snekbot.bot import Bot
class ExampleBot(Bot):
async def on_join(self, data):
print(f"I joined f{data.channel_uid}!")
async def on_leave(self, data):
print(f"I left f{data.channel_uid}!")
async def on_ping(self, data):
print(f"Ping from f{data.user_nick}")
await self.send_message(
data.channel_uid,
"I should respond with Bong according to BordedDev. So here, bong!",
)
async def on_own_message(self, data):
print(f"Received my own message: {data.message}")
async def on_mention(self, data):
message = data.message[len(self.username) + 2 :]
print(f"Mention from f{data.user_nick}: {message}")
result = f'Hey {data.user_nick}, Thanks for mentioning me "{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(data.channel_uid, result)
async def on_message(self, data):
print(f"Message from f{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="example", password="xxxxxx")
asyncio.run(bot.run())

19
pyproject.toml Normal file
View File

@ -0,0 +1,19 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "snekbot"
version = "1.0.0"
readme = "README.md"
license = { file = "LICENSE", content-type="text/markdown" }
description = "Bot API for Snek chat"
authors = [
{ name = "retoor", email = "retoor@molodetz.nl" }
]
keywords = ["chat", "snek", "molodetz","bot"]
requires-python = ">=3.12"
dependencies = [
"aiohttp"
]

View File

@ -0,0 +1,10 @@
Metadata-Version: 2.2
Name: snekbot
Version: 1.0.0
Summary: Bot API for Snek chat
Author-email: retoor <retoor@molodetz.nl>
Keywords: chat,snek,molodetz,bot
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE.txt
Requires-Dist: aiohttp

View File

@ -0,0 +1,11 @@
LICENSE.txt
pyproject.toml
src/snekbot/__init__.py
src/snekbot/__main__.py
src/snekbot/bot.py
src/snekbot/rpc.py
src/snekbot.egg-info/PKG-INFO
src/snekbot.egg-info/SOURCES.txt
src/snekbot.egg-info/dependency_links.txt
src/snekbot.egg-info/requires.txt
src/snekbot.egg-info/top_level.txt

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@
aiohttp

View File

@ -0,0 +1 @@
snekbot

0
src/snekbot/__init__.py Normal file
View File

0
src/snekbot/__main__.py Normal file
View File

111
src/snekbot/bot.py Normal file
View File

@ -0,0 +1,111 @@
# Written by retoor@molodetz.nl
# This script serves as a bot service that connects to a WebSocket server using `aiohttp` and `asyncio`.
# It logs in, retrieves channel information, processes incoming messages, and responds based on specified conditions. The bot can execute commands received from a specific user and respond to certain ping messages.
# Used external imports:
# - aiohttp: A library for handling asynchronous HTTP and WebSocket connections.
# - asyncio: A library for writing single-threaded concurrent code using coroutines.
# - agent from agent: Module to get an AI agent for communication.
# - RPC from rpc: Module to interact with the server using Remote Procedure Call method.
# MIT License
import asyncio
import aiohttp
from snekbot.rpc import RPC
class Bot:
def __init__(self, username, password, url="wss://snek.molodetz.nl/rpc.ws"):
self.url = url
self.username = username
self.password = password
self.user = None
self.channels = None
self.rpc = None
self.ws = None
self.join_conversation = False
async def run(self, reconnect=True):
while True:
try:
await self.run_once()
except Exception as ex:
print(ex)
await asyncio.sleep(1)
if not reconnect:
break
async def send_message(self, channel_uid, message):
await self.rpc.send_message(channel_uid, message)
return True
async def run_once(self):
async with aiohttp.ClientSession() as session:
async with session.ws_connect(self.url) as ws:
self.ws = ws
self.rpc = RPC(ws)
rpc = self.rpc
await (await rpc.login(self.username, self.password))()
try:
raise Exception(self.login_result.exception)
except:
pass
self.channels = await (await rpc.get_channels())()
self.user = (await (await rpc.get_user(None))()).data
self.join_conversation = False
while True:
print("Waiting for message...")
data = await rpc.receive()
if not data:
break
try:
pass
except:
continue
message = data["message"].strip()
if data.username == self.user["username"]:
try:
await self.on_own_message(data)
except Exception as ex:
print("Error", ex)
continue
elif message.startswith("ping"):
try:
await self.on_ping(data)
except Exception as ex:
print("Error:", ex)
continue
elif "@" + self.user["nick"] in data.message:
try:
await self.on_mention(data)
except Exception as ex:
print("Error:", ex)
continue
elif "@" + self.user["nick"] + " join" in data.message:
self.join_conversation = True
try:
await self.on_join(data)
except:
print("Error:", ex)
continue
elif "@" + self.user["nick"] + " leave" in data.message:
self.join_conversation = False
try:
await self.on_leave(data)
except:
print("Error:", ex)
continue
else:
try:
await self.on_message(data)
except Exception as ex:
print("Error:", ex)

101
src/snekbot/rpc.py Normal file
View File

@ -0,0 +1,101 @@
# Written by retoor@molodetz.nl
# This code defines an RPC class that allows asynchronous communication over websockets,
# including command execution and handling of asynchronous responses using Python's asyncio and websockets library.
# Uses aiohttp for asynchronous HTTP network communication.
# 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 json
import pathlib
import subprocess
import aiohttp
class RPC:
class Response:
def __init__(self, msg):
if isinstance(msg, list):
self.list = msg
self.__dict__.update(msg)
def __iter__(self):
for item in self.data:
yield item
async def __aiter__(self):
for item in self.data:
yield item
def __getitem__(self, name):
return self.__dict__[name]
def __setitem__(self, name, value):
self.__dict__[name] = value
def __str__(self):
return json.dumps(self.__dict__, default=str, indent=2)
def __init__(self, ws):
self.ws = ws
def __getattr__(self, name):
async def method(*args, **kwargs):
payload = {"method": name, "args": args}
try:
await self.ws.send_json(payload)
except Exception:
return None
async def returner():
response = await self.ws.receive()
return self.Response(response.json())
return returner
return method
async def system(self, command):
if isinstance(command, str):
command = command.split(" ")
path = pathlib.Path("output.txt")
with path.open("w+") as f:
try:
subprocess.run(command, stderr=f, stdout=f)
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:
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:
return self.Response(msg.json())
except Exception:
break
return None