forked from Dogo6647/r2chat
205 lines
5.8 KiB
Python
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
|