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