bot client
This commit is contained in:
parent
f734201242
commit
8eaa8e3adf
4 changed files with 307 additions and 5 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
|||
chat.txt
|
||||
__pycache__/
|
||||
|
|
|
|||
80
README.md
80
README.md
|
|
@ -9,12 +9,82 @@ Make sure you're running the script from within the folder it's in.
|
|||
python r2chat.py
|
||||
```
|
||||
|
||||
Chat history is saved as `chat.txt` inside either "C:\Users\Public\r2chat\" on Windows
|
||||
- Chat history is saved as `chat_CHANNEL.txt` inside either "C:\Users\Public\r2chat\" on Windows
|
||||
or "/var/tmp/r2chat/" on Unix-like.
|
||||
|
||||
Preferences are stored as a json in your home directory and can be changed
|
||||
- Preferences are stored as a json in your home directory and can be changed
|
||||
directly from the GUI by clicking "Preferences" on the menu bar.
|
||||
- You can insert color text into the chat by using the following syntax: `((#XXXXXX | yourtext))`
|
||||
- The "insert" menu is not functional yet.
|
||||
|
||||
You can insert color text into the chat by using the following syntax: `((#XXXXXX | yourtext))`
|
||||
## Bot client usage
|
||||
You can look at `examples/botexample.py` for an example working setup you can use as a template.
|
||||
|
||||
The "insert" menu is not functional yet.
|
||||
Unlike regular r2chat, the bot client doesn't require any dependencies and its config file
|
||||
is stored as `r2bot.config.json` where you're running the script from, so it's important
|
||||
to separate bots between directories to prevent them from overwriting each other's files.
|
||||
|
||||
### Bot client API
|
||||
#### Init:
|
||||
```
|
||||
bot = r2bot(channel, username)
|
||||
```
|
||||
|
||||
#### Sending a message:
|
||||
```
|
||||
bot.send(msg)
|
||||
```
|
||||
|
||||
#### Sending a system message:
|
||||
```
|
||||
bot.system_message(msg)
|
||||
```
|
||||
|
||||
#### Receiving messages
|
||||
```
|
||||
messages = bot.poll()
|
||||
```
|
||||
Returns a list of new lines and triggers event callbacks,
|
||||
which can be used like this:
|
||||
|
||||
```
|
||||
def handlemsg(msg):
|
||||
print("got message:", msg)
|
||||
|
||||
# Register event
|
||||
bot.on("on_message", handlemsg)
|
||||
```
|
||||
|
||||
List of possible on-event types:
|
||||
- "on_message"
|
||||
- "on_mention"
|
||||
- "on_system"
|
||||
|
||||
#### Statuses
|
||||
```
|
||||
bot.set_status(status)
|
||||
```
|
||||
|
||||
List of standard statuses:
|
||||
- "online"
|
||||
- "offline"
|
||||
- "inactive"
|
||||
- "busy"
|
||||
|
||||
#### Leave signal
|
||||
```
|
||||
bot.leave_channel()
|
||||
```
|
||||
|
||||
#### Parse text blocks
|
||||
The bot client can split normal text and colored text
|
||||
into an array;
|
||||
```
|
||||
segments = bot.parse_colors(text)
|
||||
```
|
||||
Would output something like:
|
||||
```
|
||||
[
|
||||
("text", "normal text"),
|
||||
("color", "#ff0000", "This is red text")
|
||||
]
|
||||
```
|
||||
|
|
|
|||
205
botclient.py
Normal file
205
botclient.py
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
# ////////////////////////////////////////////////
|
||||
# //// R2BOT - Bot client library for r2chat ////
|
||||
# //////////////////////////////////////////////
|
||||
|
||||
import os, sys, json, time, re, getpass
|
||||
from pathlib import Path
|
||||
|
||||
# --- Setup/default config ---
|
||||
if sys.platform == "win32":
|
||||
ROOT = Path("C:/Users/Public")
|
||||
else:
|
||||
ROOT = Path("/var/tmp")
|
||||
|
||||
CHANNELS_DIR = ROOT / "r2chat"
|
||||
INSTANCE_DIR = CHANNELS_DIR / "instance"
|
||||
CONFIG_FILE = Path("r2bot.config.json")
|
||||
|
||||
os.makedirs(INSTANCE_DIR, exist_ok=True)
|
||||
os.makedirs(CHANNELS_DIR, exist_ok=True)
|
||||
|
||||
COLOR_PATTERN = re.compile(r"\(\((#[0-9a-fA-F]{6})\s*\|\s*([^)]+)\)\)")
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"user_color": "#0000ff",
|
||||
"status_join": "{username} is now online.",
|
||||
"status_leave": "{username} is now offline.",
|
||||
"status_inactive": "{username} will be right back.",
|
||||
"status_busy": "{username} is busy."
|
||||
}
|
||||
|
||||
|
||||
# --- Engine ---
|
||||
class r2bot:
|
||||
def __init__(self, channel="chat", username=None):
|
||||
self.username = username or f"{getpass.getuser()}'s-bot"
|
||||
self.botname = f"{self.username}#BOT"
|
||||
self.channel = self._normalize_channel(channel)
|
||||
print(f"[r2bot] Logged in as {self.username} in {self.channel}.")
|
||||
self.chat_file = CHANNELS_DIR / f"{self.channel}.txt"
|
||||
self.user_file = INSTANCE_DIR / f"{self.username}.json"
|
||||
|
||||
self.cfg = self.load_config()
|
||||
self.last_size = os.path.getsize(self.chat_file)
|
||||
self.status = "online"
|
||||
|
||||
self.callbacks = {
|
||||
"on_message": None,
|
||||
"on_mention": None,
|
||||
"on_system": None
|
||||
}
|
||||
|
||||
self._ensure_files()
|
||||
self.user_data = self.load_userdata()
|
||||
self._join_channel()
|
||||
|
||||
def _normalize_channel(self, name):
|
||||
if name == "chat":
|
||||
return name
|
||||
name = name.replace(" ", "_").replace(".", "-")
|
||||
return f"chat_{name}"
|
||||
|
||||
def _ensure_files(self):
|
||||
if not self.chat_file.exists():
|
||||
self.chat_file.touch()
|
||||
|
||||
def load_config(self):
|
||||
if CONFIG_FILE.exists():
|
||||
try:
|
||||
return {**DEFAULT_CONFIG, **json.load(open(CONFIG_FILE))}
|
||||
except:
|
||||
pass
|
||||
return DEFAULT_CONFIG.copy()
|
||||
|
||||
def load_userdata(self):
|
||||
if self.user_file.exists():
|
||||
try:
|
||||
return json.load(open(self.user_file))
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
"username": self.username,
|
||||
"status": "online",
|
||||
"channels": [],
|
||||
"last_seen": time.time()
|
||||
}
|
||||
|
||||
def save_userdata(self):
|
||||
self.user_data["last_seen"] = time.time()
|
||||
json.dump(self.user_data, open(self.user_file, "w"), indent=2)
|
||||
|
||||
def _join_channel(self):
|
||||
if self.channel not in self.user_data["channels"]:
|
||||
self.user_data["channels"].append(self.channel)
|
||||
|
||||
self.user_data["status"] = self.status
|
||||
self.save_userdata()
|
||||
|
||||
self.system_message(self.cfg["status_join"])
|
||||
|
||||
def leave_channel(self):
|
||||
self.system_message(self.cfg["status_leave"])
|
||||
|
||||
if self.channel in self.user_data["channels"]:
|
||||
self.user_data["channels"].remove(self.channel)
|
||||
|
||||
if not self.user_data["channels"]:
|
||||
self.status = "offline"
|
||||
|
||||
self.user_data["status"] = self.status
|
||||
self.save_userdata()
|
||||
|
||||
def send(self, msg):
|
||||
if not msg.strip():
|
||||
return
|
||||
|
||||
timestamp = time.strftime("%y/%m/%d %H:%M")
|
||||
line = f"[{timestamp}] (({self.cfg['user_color']} | {self.botname})): {msg}\n"
|
||||
|
||||
with open(self.chat_file, "a", encoding="utf-8") as f:
|
||||
f.write(line)
|
||||
|
||||
def system_message(self, msg):
|
||||
line = f"//// {msg.format(username=self.botname)} ////\n"
|
||||
with open(self.chat_file, "a", encoding="utf-8") as f:
|
||||
f.write(line)
|
||||
|
||||
if self.callbacks["on_system"]:
|
||||
self.callbacks["on_system"](line)
|
||||
|
||||
def set_status(self, new_status):
|
||||
if new_status == self.status:
|
||||
return
|
||||
|
||||
self.status = new_status
|
||||
|
||||
mapping = {
|
||||
"online": self.cfg["status_join"],
|
||||
"offline": self.cfg["status_leave"],
|
||||
"inactive": self.cfg["status_inactive"],
|
||||
"busy": self.cfg["status_busy"]
|
||||
}
|
||||
|
||||
if new_status in mapping:
|
||||
self.system_message(mapping[new_status])
|
||||
|
||||
self.user_data["status"] = self.status
|
||||
self.save_userdata()
|
||||
|
||||
def poll(self):
|
||||
# (reads new messages)
|
||||
current_size = os.path.getsize(self.chat_file)
|
||||
|
||||
if current_size <= self.last_size:
|
||||
return []
|
||||
|
||||
with open(self.chat_file, "r", encoding="utf-8") as f:
|
||||
f.seek(self.last_size)
|
||||
new_data = f.read()
|
||||
|
||||
self.last_size = current_size
|
||||
lines = new_data.splitlines()
|
||||
|
||||
for line in lines:
|
||||
self._handle_line(line)
|
||||
|
||||
return lines
|
||||
|
||||
def _handle_line(self, line):
|
||||
if line.startswith("----"):
|
||||
if self.callbacks["on_system"]:
|
||||
self.callbacks["on_system"](line)
|
||||
return
|
||||
|
||||
if f"@{self.username}" in line:
|
||||
if self.callbacks["on_mention"]:
|
||||
self.callbacks["on_mention"](line)
|
||||
|
||||
if self.callbacks["on_message"]:
|
||||
self.callbacks["on_message"](line)
|
||||
|
||||
def on(self, event, func):
|
||||
if event in self.callbacks:
|
||||
self.callbacks[event] = func
|
||||
|
||||
def parse_colors(self, text):
|
||||
# (return text segments with color info)
|
||||
segments = []
|
||||
pos = 0
|
||||
|
||||
for match in COLOR_PATTERN.finditer(text):
|
||||
start, end = match.span()
|
||||
color = match.group(1)
|
||||
content = match.group(2)
|
||||
|
||||
if start > pos:
|
||||
segments.append(("text", text[pos:start]))
|
||||
|
||||
segments.append(("color", color, content))
|
||||
pos = end
|
||||
|
||||
if pos < len(text):
|
||||
segments.append(("text", text[pos:]))
|
||||
|
||||
return segments
|
||||
26
examples/botexample.py
Normal file
26
examples/botexample.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import os, sys
|
||||
sys.path.append(os.path.abspath("../"))
|
||||
from botclient import r2bot
|
||||
import time
|
||||
|
||||
bot = r2bot(channel="chat", username="genericbot")
|
||||
|
||||
def mainhandler(msg):
|
||||
if "/test" in msg:
|
||||
bot.send("hello world")
|
||||
|
||||
def mentionhandler(msg):
|
||||
bot.send("mentioned")
|
||||
|
||||
bot.on("on_message", mainhandler)
|
||||
bot.on("on_mention", mentionhandler)
|
||||
|
||||
try:
|
||||
print("Bot is running.")
|
||||
while True:
|
||||
bot.poll()
|
||||
time.sleep(1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("Stopping bot.")
|
||||
bot.leave_channel()
|
||||
Loading…
Add table
Add a link
Reference in a new issue