Compare commits
18
Commits
main
..
1ecb595bee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ecb595bee | ||
|
|
736e1c6ee4 | ||
|
|
4d1b1fa974 | ||
|
|
7d6ad67f97 | ||
|
|
589b0468f5 | ||
|
|
bec48eb574 | ||
|
|
8aee6f360d | ||
|
|
328534e5c9 | ||
|
|
04cd9489ac | ||
|
|
8030955f3f | ||
|
|
a2b1d3c3aa | ||
|
|
636ea209d1 | ||
|
|
0bde0cedd4 | ||
|
|
9cb2630e41 | ||
|
|
8f891f7d80 | ||
|
|
818aca5449 | ||
|
|
104854be6a | ||
|
|
a60c862450 |
@@ -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:
|
||||||
|
|||||||
@@ -1,81 +1,125 @@
|
|||||||
# 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.
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Todo's
|
||||||
|
|
||||||
|
- implement logging module instead of all print statements.
|
||||||
|
- use asyncio processes instead of subprocess module processes.
|
||||||
+14
-20
@@ -9,29 +9,28 @@ from snekbot.bot import Bot
|
|||||||
class ExampleBot(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!")
|
print(f"I joined!")
|
||||||
channel = await self.get_channel(channel_uid)
|
|
||||||
await self.send_message(
|
await self.send_message(
|
||||||
channel_uid,
|
channel_uid,
|
||||||
f"Hello, i'm actively part of the conversation in channel {channel['name']} now, you don't have to mention me anymore. ",
|
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_leave(self, channel_uid):
|
async def on_leave(self, channel_uid):
|
||||||
await super().on_leave(channel_uid)
|
|
||||||
print(f"I left!!")
|
print(f"I left!!")
|
||||||
await self.send_message(
|
await self.send_message(
|
||||||
channel_uid, "I stop actively being part of the conversation now. Bye!"
|
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_ping(self,username, user_nick, channel_uid, message):
|
||||||
channel = await self.get_channel(channel_uid)
|
print(f"Ping from {user_nick} in channel {channel_uid}: {message}")
|
||||||
print(f"Ping from {user_nick} in channel {channel['name']}: {message}")
|
await self.send_message(
|
||||||
await self.send_message(channel_uid, "pong " + message)
|
channel_uid,
|
||||||
|
"pong " + message
|
||||||
|
)
|
||||||
|
|
||||||
async def on_own_message(self, channel_uid, message):
|
async def on_own_message(self, data):
|
||||||
channel = await self.get_channel(channel_uid)
|
print(f"Received my own message: {data.message}")
|
||||||
print(f"Received my own message in channel {channel['name']}: {message}")
|
|
||||||
|
|
||||||
async def on_mention(self, username, user_nick, channel_uid, message):
|
async def on_mention(self, username, user_nick, channel_uid, message):
|
||||||
|
|
||||||
@@ -50,16 +49,13 @@ class ExampleBot(Bot):
|
|||||||
)
|
)
|
||||||
await self.send_message(channel_uid, result)
|
await self.send_message(channel_uid, result)
|
||||||
else:
|
else:
|
||||||
await self.send_message(
|
await self.send_message(channel_uid, f'Hey {user_nick}, Thanks for mentioning me "{message}".')
|
||||||
channel_uid, f'Hey {user_nick}, Thanks for mentioning me "{message}".'
|
|
||||||
)
|
|
||||||
|
|
||||||
async def on_message(self, sender_username, sender_nick, channel_uid, message):
|
async def on_message(self, sender_username, sender_nick, channel_uid, message):
|
||||||
print(f"Message from {sender_nick}: {message}")
|
print(f"Message from {sender_nick}: {message}")
|
||||||
if not self.has_joined(channel_uid):
|
if not self.has_joined(channel_uid):
|
||||||
print(f"Probably not for me since i'm not mentioned and not joined yet")
|
print(f"Probably not for me since i'm not mentioned and not joined yet")
|
||||||
return
|
return
|
||||||
|
|
||||||
message = message.lower()
|
message = message.lower()
|
||||||
result = None
|
result = None
|
||||||
if "hello" in message:
|
if "hello" in message:
|
||||||
@@ -71,7 +67,5 @@ class ExampleBot(Bot):
|
|||||||
await self.send_message(channel_uid, result)
|
await self.send_message(channel_uid, result)
|
||||||
|
|
||||||
|
|
||||||
bot = ExampleBot(
|
bot = ExampleBot(url="ws://snek.molodetz.nl/rpc.ws", username="example", password="xxxxxx")
|
||||||
url="ws://snek.molodetz.nl/rpc.ws", username="example", password="xxxxxx"
|
|
||||||
)
|
|
||||||
asyncio.run(bot.run())
|
asyncio.run(bot.run())
|
||||||
|
|||||||
+1
-1
@@ -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
@@ -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.
|
|
||||||
|
|||||||
+47
-135
@@ -12,15 +12,14 @@
|
|||||||
# 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")
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger("snekbot")
|
||||||
|
|
||||||
class Bot:
|
class Bot:
|
||||||
|
|
||||||
@@ -29,162 +28,75 @@ 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.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):
|
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):
|
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):
|
|
||||||
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
|
||||||
|
success = await (await rpc.login(self.username, self.password))()
|
||||||
await rpc.login(self.username, self.password)
|
print(success)
|
||||||
self.user = await rpc.get_user(None)
|
self.channels = await (await rpc.get_channels())()
|
||||||
logger.debug("Logged in as: " + self.user["username"])
|
for channel in self.channels:
|
||||||
|
|
||||||
if is_initial:
|
|
||||||
await self.on_init()
|
|
||||||
|
|
||||||
for channel in await self.get_channels():
|
|
||||||
logger.debug("Found channel: " + channel["name"])
|
logger.debug("Found channel: " + channel["name"])
|
||||||
|
self.user = (await (await rpc.get_user(None))())
|
||||||
|
logger.debug("Logged in as: " + self.user["username"])
|
||||||
while True:
|
while True:
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
await self.on_idle()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
await self.on_idle()
|
data = await rpc.receive()
|
||||||
|
|
||||||
message = None
|
try:
|
||||||
data = None
|
message = data.message.strip()
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
|
|
||||||
while True:
|
if data.username == self.user['username']:
|
||||||
async with self.semaphore:
|
await self.on_own_message(data.channel_uid, message)
|
||||||
data = await rpc.receive()
|
elif message.startswith("ping"):
|
||||||
if not data:
|
await self.on_ping(data.username, data.user_nick, data.channel_uid, data.message.lstrip("ping ").strip())
|
||||||
return
|
elif any([
|
||||||
|
"@" + self.user['nick'] + " join" in data.message,
|
||||||
|
"@" + self.user['username'] + " join" in data.message]):
|
||||||
|
self.joined.add(data.channel_uid)
|
||||||
|
await self.on_join(data.channel_uid)
|
||||||
|
elif any([
|
||||||
|
"@" + self.user['nick'] + " leave" in data.message,
|
||||||
|
"@" + self.user['username'] + " leave" in data.message]):
|
||||||
|
self.joined.remove(data.channel_uid)
|
||||||
|
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:
|
||||||
|
await self.on_message(data.username, data.user_nick, data.channel_uid, data.message)
|
||||||
|
except AttributeError as ex:
|
||||||
|
logger.exception(ex)
|
||||||
|
except Exception as ex:
|
||||||
|
logger.exception(ex)
|
||||||
|
|
||||||
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"]:
|
|
||||||
await self.on_own_message(data.channel_uid, message)
|
|
||||||
elif message.startswith("ping"):
|
|
||||||
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:
|
|
||||||
await self.on_message(
|
|
||||||
data.username,
|
|
||||||
data.user_nick,
|
|
||||||
data.channel_uid,
|
|
||||||
data.message,
|
|
||||||
)
|
|
||||||
|
|||||||
+78
-66
@@ -7,120 +7,132 @@
|
|||||||
|
|
||||||
# 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 subprocess
|
import subprocess
|
||||||
import uuid
|
import pathlib
|
||||||
import asyncio
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger("snekbot.rpc")
|
logger = logging.getLogger("snekbot.rpc")
|
||||||
|
|
||||||
|
|
||||||
class RPC:
|
class RPC:
|
||||||
class Response:
|
class Response:
|
||||||
def __init__(self, msg):
|
def __init__(self, msg):
|
||||||
|
logger.debug("Initializing response.")
|
||||||
|
if isinstance(msg, list):
|
||||||
|
self.list = msg
|
||||||
self.__dict__.update(msg)
|
self.__dict__.update(msg)
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
|
logger.debug("Iterating sync trough result data.")
|
||||||
for k in self.__dict__.get("data", []):
|
for k in self.__dict__.get("data", []):
|
||||||
yield k
|
yield k
|
||||||
|
|
||||||
async def __aiter__(self):
|
async def __aiter__(self):
|
||||||
|
logger.debug("Iterating async trough result data.")
|
||||||
for k in self.__dict__.get("data", []):
|
for k in self.__dict__.get("data", []):
|
||||||
yield k
|
yield k
|
||||||
|
|
||||||
def __getitem__(self, name):
|
def __getitem__(self, name):
|
||||||
|
logger.debug("Getting result data: " + name + ".")
|
||||||
try:
|
try:
|
||||||
return self.__dict__[name]
|
return self.__dict__[name]
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
return self.__dict__.get("data", {})[name]
|
return self.__dict__.get('data',{})[name]
|
||||||
|
|
||||||
def __setitem__(self, name, value):
|
def __setitem__(self, name, value):
|
||||||
if name not in self.__dict__.get("data", {}):
|
logger.debug("Setting result data: " + name + ".")
|
||||||
|
if not name in self.__dict__.get('data',{}):
|
||||||
self.__dict__[name] = value
|
self.__dict__[name] = value
|
||||||
else:
|
else:
|
||||||
self.__dict__["data"][name] = value
|
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):
|
||||||
|
logger.debug("Initializing RPC.")
|
||||||
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("Sending echo to server: " + str(data))
|
||||||
|
await self.ws.send_json(dict(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 = dict(method=name, args=args, kwargs=kwargs, callId=self.current_call_id)
|
||||||
"method": name,
|
try:
|
||||||
"args": args,
|
await self.ws.send_json(payload)
|
||||||
"kwargs": kwargs,
|
except Exception as ex:
|
||||||
"callId": self.current_call_id,
|
print(ex)
|
||||||
}
|
|
||||||
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)
|
||||||
|
return returner
|
||||||
if no_response:
|
|
||||||
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"):
|
|
||||||
self.current_call_id = None
|
|
||||||
return self.Response(msg)
|
|
||||||
popped.append(msg)
|
|
||||||
for m in popped:
|
|
||||||
await self.queue.put(m)
|
|
||||||
|
|
||||||
|
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 as ex:
|
||||||
|
print("Error deleting temporary file:", 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:
|
||||||
|
print("Error while receiving:", ex)
|
||||||
|
break
|
||||||
|
if msg.type == aiohttp.WSMsgType.CLOSED:
|
||||||
|
break
|
||||||
|
elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||||
|
print("WebSocket error:", msg)
|
||||||
|
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)
|
print("Error while parsing message:", msg, 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:
|
||||||
|
raise Exception("Unexpected message type.")
|
||||||
|
print("huh")
|
||||||
return None
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user