Compare commits

..
44 Commits
Author SHA1 Message Date
retoor e5f550c1b9 chore: replace blocking semaphore with async version and reduce concurrency to 1 in RPC class 2025-09-07 02:28:12 +00:00
retoor 3d77653cbc fix: increase RPC semaphore limit from 3 to 200 and wrap poller call in semaphore context manager 2025-09-07 02:22:48 +00:00
retoor c26f636e9d feat: increase RPC semaphore limit from 1 to 3 in RPC class constructor 2025-09-04 22:46:15 +00:00
retoor f47e803f69 chore: strip trailing whitespace and add semaphore to RPC class in src/snekbot/rpc.py 2025-08-30 13:45:42 +00:00
retoor 4ed7c60346 fix: correct misspelled 'async with' keyword in bot.py semaphore context manager 2025-08-30 13:41:58 +00:00
retoor c0e94d4199 feat: add asyncio semaphore to bot message processing loop for concurrency control 2025-08-30 13:39:27 +00:00
retoor 06530eb225 feat: add final parameter to send_message method for rpc call control
Add an optional `final` parameter (defaulting to True) to the `send_message` method in the `Bot` class, and pass it through to the underlying `self.rpc.send_message` call. This allows callers to control whether the message is marked as final when sent via RPC.
2025-08-21 09:50:02 +00:00
retoor fcd53a6b85 fix: correct off-by-one error in pagination offset calculation for user list endpoint 2025-08-02 11:24:53 +00:00
retoor a73df33f05 fix: switch RPC endpoint from staging to production gateway and refactor async response handling 2025-08-02 00:59:33 +00:00
retoor 99492ec73b chore: skip non-final message events in bot.py event loop 2025-05-29 08:08:18 +00:00
retoor a326406c90 chore: remove trailing whitespace and fix async call patterns across src/ files 2025-05-21 16:18:55 +00:00
retoor 04db6f4590 chore: bump snekbot version from 1.0.0 to 1.1.0 in pyproject.toml 2025-05-10 19:53:25 +00:00
retoor ad8c6bf308 chore: remove trailing whitespace from README.md line 42 and add null data guard in bot.py receive loop 2025-05-10 19:52:53 +00:00
retoor 8930c40f43 chore: remove emoji and casual language from README.md for professional tone 2025-05-05 20:25:01 +00:00
retoor 746debccd7 docs: rewrite README with emoji-rich quickstart guide and modernized project overview 2025-05-05 20:13:45 +00:00
retoor df7794a6a9 chore: update Makefile to use python3.12 and fix formatting in example bot code 2025-05-05 20:08:30 +00:00
retoor 0adb5836d3 chore: strip trailing whitespace from bot.py and all src/ source files 2025-05-03 15:19:13 +00:00
retoor 33a2d201da chore: remove trailing whitespace and add on_init hook with initial connection flag in bot.py 2025-05-03 15:13:47 +00:00
retoor eeb4593854 fix: add super calls and fix on_own_message signature in ExampleBot
- Add missing `await super().on_join(channel_uid)` and `await super().on_leave(channel_uid)` calls to ensure parent class lifecycle hooks execute
- Update `on_own_message` method signature to include `channel_uid` parameter, aligning with the expected interface
2025-04-24 19:40:44 +00:00
retoor 4cbb7d17e3 chore: remove TODO.md and refactor bot channel caching with get_channel method 2025-04-24 19:38:56 +00:00
retoor 476041c0ab chore: replace hardcoded API timeout values with configurable constants in snek_core_bot.py 2025-04-24 19:03:12 +00:00
retoor c1421d0c22 chore: remove redundant exception handling in rpc send and fix bot reconnection logic
- Remove try-except wrapper around ws.send_json in RPC.method to let exceptions propagate naturally
- Move asyncio.sleep(1) inside the except block in Bot.run to avoid delay on successful iterations
- Add raise statement in Bot message handling to propagate AttributeError instead of silently logging
2025-02-18 18:09:51 +00:00
retoor bedd2345a4 fix: correct typo in commit message from 'Fixx' to 'Fix' for consistency 2025-02-18 15:34:57 +00:00
retoor de5b20bed9 fix: replace raise with logger.exception and break on websocket close/error/unknown in rpc receive loop 2025-02-17 19:11:45 +00:00
retoor 4d09fad32f refactor: remove verbose debug logging from RPC response methods and echo handler 2025-02-15 20:11:34 +00:00
retoor cfbd5a1ce4 fix: replace silent break with exception raise on ws close and error in RPC.receive
Replace two silent `break` statements with explicit `Exception` raises when WebSocket
closes or encounters an error, and remove a stray debug print statement. This ensures
the caller is notified of connection failures instead of silently exiting the receive loop.
2025-02-15 12:23:46 +00:00
retoor e078ef918b chore: replace placeholder example content with initial project scaffold 2025-02-13 09:40:22 +00:00
retoor 85922db224 fix: update bot.py to access user attributes via dict keys instead of object properties 2025-02-13 09:36:46 +00:00
retoor 05410b484e fix: log full exception traceback for unhandled message events instead of debug message 2025-02-13 09:31:50 +00:00
retoor 5a94f2d041 fix: fallback to data dict key lookup in RPC result __getitem__ and prevent overwrite in __setitem__ 2025-02-13 09:26:50 +00:00
retoor ffc347030f chore: remove unused join_conversation flag and trailing whitespace in bot.py event loop 2025-02-13 09:21:35 +00:00
retoor e88b154510 chore: migrate user attribute access from dict-style to object-style in bot.py 2025-02-13 09:12:52 +00:00
retoor dfb893c0cd chore: remove duplicate for-loop line in RPC.__aiter__ method in rpc.py 2025-02-12 00:06:19 +00:00
retoor 103142acad fix: correct rpc endpoint path for user authentication service 2025-02-12 00:03:41 +00:00
retoor abd8bcb7ee chore: add initial build artifacts and expand egg-info metadata with readme 2025-02-10 22:12:50 +00:00
retoor 7bb4691ab4 chore: relax python version constraint from >=3.12 to >=3 in pyproject.toml 2025-02-10 22:09:59 +00:00
retoor cf47bd65d3 docs: add authentication placeholder and production-ready reconnect note to README and example 2025-02-10 21:31:02 +00:00
retoor 4f4ae87b7a feat: refactor bot event handlers to accept flat parameters and add join/leave auto-replies 2025-02-10 13:16:55 +00:00
retoor 756e26eb42 fix: include actual error details in exception message for message parsing failure 2025-02-01 15:20:13 +00:00
retoor e85f7d748f fix: correct f-string syntax errors in print statements across multiple event handlers
Replace erroneous `f{data.channel_uid}` and similar f-string expressions with correct `{data.channel_uid}` syntax in on_join, on_leave, on_ping, on_mention, and on_message methods. Also fix hardcoded password placeholder in bot instantiation from "xxxxxx" to "example".
2025-02-01 15:16:25 +00:00
retoor 158df199cc fix: correct f-string syntax by removing extraneous 'f' prefix in format expressions across multiple event handlers 2025-02-01 15:14:56 +00:00
retoor 3b5cb74d94 fix: correct f-string typo in README debug print and update run instructions
- Fix incorrect f-string prefix 'f{' to '{' in README example bot's on_message debug print
- Replace hardcoded 'python bot.py' with generic '[your-script].py' placeholder in run instructions
- Remove license field from pyproject.toml metadata configuration
2025-02-01 15:14:08 +00:00
retoor 3628ab98f5 feat: add TODO.md with logging and asyncio migration tasks 2025-02-01 15:00:05 +00:00
retoor 6654901f37 docs: add .gitignore, LICENSE, Makefile, README tutorial, example bot, pyproject.toml, and core snekbot modules 2025-02-01 14:58:18 +00:00
12 changed files with 413 additions and 219 deletions
-2
View File
@@ -2,5 +2,3 @@
__pycache__/
.backup*
.history/
build/
dist/
+1 -1
View File
@@ -5,7 +5,7 @@ APP=./example.py
all: install run
install:
python3 -m venv .venv
python3.12 -m venv .venv
$(PIP) install -e .
run:
+59 -67
View File
@@ -1,89 +1,81 @@
# Snekbot API
# SnekBot: Your Instant Chat Companion
This is the Snekbot API. This document describes how to create a bot responding to "hey", "hello", "bye" and "@username-of-bot".
## Create Your Own Bot in Minutes
## 5 minute tutorial
### 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.
Literally.
### Prerequisites
- Python 3.8 or higher
- Basic understanding of Python programming
### 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.
### Installation Instructions
For Debian (Ubuntu): `sudo apt install python3 python3-venv python3-pip -y`
#### 1. Prepare Your Environment
```bash
# For Ubuntu/Debian users:
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 a virtual environment for your bot
python3 -m venv venv
source venv/bin/activate
```
#### 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)
#### 2. Install SnekBot
```bash
pip install git+https://molodetz.nl/retoor/snekbot.git
```
#### Create a file
Open a file ending with the `.py` extension and pase this content.
### Bot Development
To create your bot, use the following template:
```python
import asyncio
import asyncio
from snekbot.bot import Bot
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}")
class CustomSnekBot(Bot):
async def on_join(self, channel_uid):
await self.send_message(
data.channel_uid,
"I should respond with Bong according to BordedDev. So here, bong!",
channel_uid,
"Hello! I am here to assist you."
)
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 {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 {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}"
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:
result = f"Bye {data.user_nick}"
await self.send_message(channel_uid, f"Goodbye, {sender_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")
# 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())
```
#### Run the bot
Make sure you have (still) activated your virtual env.
### Running Your Bot
```bash
python [your-script].py
python your_bot_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
@@ -1,4 +0,0 @@
# 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.
+45 -25
View File
@@ -1,32 +1,43 @@
import asyncio
import logging
logging.basicConfig(level=logging.DEBUG)
from snekbot.bot import Bot
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}")
async def on_join(self, channel_uid):
await super().on_join(channel_uid)
print(f"I joined!")
channel = await self.get_channel(channel_uid)
await self.send_message(
data.channel_uid,
"I should respond with Bong according to BordedDev. So here, bong!",
channel_uid,
f"Hello, i'm actively part of the conversation in channel {channel['name']} now, you don't have to mention me anymore. ",
)
async def on_own_message(self, data):
print(f"Received my own message: {data.message}")
async def on_leave(self, channel_uid):
await super().on_leave(channel_uid)
print(f"I left!!")
await self.send_message(
channel_uid, "I stop actively being part of the conversation now. Bye!"
)
async def on_mention(self, data):
async def on_ping(self, username, user_nick, channel_uid, message):
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)
message = data.message[len(self.username) + 2 :]
print(f"Mention from {data.user_nick}: {message}")
async def on_own_message(self, channel_uid, message):
channel = await self.get_channel(channel_uid)
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:
with open(__file__) as f:
result = f.read()
@@ -37,21 +48,30 @@ class ExampleBot(Bot):
+ 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}".'
)
await self.send_message(data.channel_uid, result)
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
async def on_message(self, data):
print(f"Message from {data.user_nick}: {data.message}")
message = data.message.lower()
message = message.lower()
result = None
if "hey" in message or "hello" in message:
result = f"Hi {data.user_nick}"
if "hello" in message:
result = f"Hi @{sender_nick}"
elif "bye" in message:
result = f"Bye {data.user_nick}"
result = f"Bye @{sender_nick}"
if result:
await self.send_message(data.channel_uid, result)
await self.send_message(channel_uid, result)
bot = ExampleBot(username="example", password="example")
bot = ExampleBot(
url="ws://snek.molodetz.nl/rpc.ws", username="example", password="xxxxxx"
)
asyncio.run(bot.run())
+2 -2
View File
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
[project]
name = "snekbot"
version = "1.0.0"
version = "1.1.0"
readme = "README.md"
description = "Bot API for Snek chat"
authors = [
{ name = "retoor", email = "retoor@molodetz.nl" }
]
keywords = ["chat", "snek", "molodetz","bot"]
requires-python = ">=3.12"
requires-python = ">=3"
dependencies = [
"aiohttp"
]
+86 -3
View File
@@ -1,10 +1,93 @@
Metadata-Version: 2.2
Metadata-Version: 2.4
Name: snekbot
Version: 1.0.0
Version: 1.1.0
Summary: Bot API for Snek chat
Author-email: retoor <retoor@molodetz.nl>
Keywords: chat,snek,molodetz,bot
Requires-Python: >=3.12
Requires-Python: >=3
Description-Content-Type: text/markdown
License-File: LICENSE.txt
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,4 +1,5 @@
LICENSE.txt
README.md
pyproject.toml
src/snekbot/__init__.py
src/snekbot/__main__.py
+138 -59
View File
@@ -12,11 +12,15 @@
# MIT License
import asyncio
import logging
import traceback
import aiohttp
from snekbot.rpc import RPC
logger = logging.getLogger("snekbot")
class Bot:
@@ -25,87 +29,162 @@ class Bot:
self.username = username
self.password = password
self.user = None
self.channels = None
self._channels = None
self.rpc = None
self.ws = None
self.join_conversation = False
self.joined = set()
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):
while True:
try:
await self.run_once()
except Exception as ex:
print(ex)
except:
traceback.print_exc()
await asyncio.sleep(1)
if not reconnect:
break
async def send_message(self, channel_uid, message):
await self.rpc.send_message(channel_uid, message)
def has_joined(self, channel_uid):
return channel_uid in self.joined
async def send_message(self, channel_uid, message,final=True):
await self.rpc.send_message(channel_uid, message,final)
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 with aiohttp.ClientSession() as session:
async with session.ws_connect(self.url) as ws:
is_initial = not self.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
rpc = RPC(self.ws)
self.rpc = rpc
await rpc.login(self.username, self.password)
self.user = await rpc.get_user(None)
logger.debug("Logged in as: " + self.user["username"])
if is_initial:
await self.on_init()
for channel in await self.get_channels():
logger.debug("Found channel: " + channel["name"])
while True:
print("Waiting for message...")
data = await rpc.receive()
if not data:
break
await self.on_idle()
try:
pass
except:
continue
message = None
data = None
message = data["message"].strip()
while True:
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"]:
try:
await self.on_own_message(data)
except Exception as ex:
print("Error", ex)
continue
await self.on_own_message(data.channel_uid, message)
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
await self.on_ping(
data.username,
data.user_nick,
data.channel_uid,
data.message.lstrip("ping ").strip(),
)
elif any(
[
"@" + self.user["nick"] + " join" in data.message,
"@" + self.user["username"] + " join" in data.message,
]
):
await self.on_join(data.channel_uid)
elif any(
[
"@" + self.user["nick"] + " leave" in data.message,
"@" + self.user["username"] + " leave" in data.message,
]
):
await self.on_leave(data.channel_uid)
elif (
"@" + self.user["nick"] in data.message
or "@" + self.user["username"] in data.message
):
await self.on_mention(
data.username,
data.user_nick,
data.channel_uid,
data.message,
)
else:
try:
await self.on_message(data)
except Exception as ex:
print("Error:", ex)
await self.on_message(
data.username,
data.user_nick,
data.channel_uid,
data.message,
)
+81 -56
View File
@@ -7,95 +7,120 @@
# 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 logging
import pathlib
import subprocess
import uuid
import asyncio
import aiohttp
logger = logging.getLogger("snekbot.rpc")
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
for k in self.__dict__.get("data", []):
yield k
async def __aiter__(self):
for item in self.data:
yield item
for k in self.__dict__.get("data", []):
yield k
def __getitem__(self, name):
return self.__dict__[name]
try:
return self.__dict__[name]
except:
pass
return self.__dict__.get("data", {})[name]
def __setitem__(self, name, value):
self.__dict__[name] = value
if name not in self.__dict__.get("data", {}):
self.__dict__[name] = value
else:
self.__dict__["data"][name] = value
def __str__(self):
return json.dumps(self.__dict__, default=str, indent=2)
def __init__(self, ws):
self.ws = ws
self.current_call_id = None
self.queue = asyncio.Queue()
self.semaphore = asyncio.Semaphore(1)
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
no_response = kwargs.pop("_no_response", False)
self.current_call_id = str(uuid.uuid4())
payload = {
"method": name,
"args": args,
"kwargs": kwargs,
"callId": self.current_call_id,
}
await self.ws.send_json(payload)
async def returner():
response = await self.ws.receive()
return self.Response(response.json())
async def poller():
while True:
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)
return returner
if no_response:
return True
async with self.semaphore:
return await poller()
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):
async def receive(self):
popped = []
while not self.queue.empty():
msg = await self.queue.get()
if self.current_call_id == msg.get("callId"):
self.current_call_id = None
return self.Response(msg)
popped.append(msg)
for m in popped:
await self.queue.put(m)
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:
async with self.semaphore:
try:
return self.Response(msg.json())
except Exception:
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 (
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
return None