r2chat/botclient.py
2026-04-11 14:36:49 -07:00

205 lines
5.8 KiB
Python

# ////////////////////////////////////////////////
# //// 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