"""timer.py: a countdown and reminder bot, in Python. A member writes "!timer 5m Coffee" and the bot says so in the room when the time is up; "!timers" lists the ones running. Seconds (s), minutes (m) and hours (h), up to 24 hours. Python 3.10+. pip install -r requirements.txt, then python timer.py (reads .env). Its key (dashboard, New API key): rights subscribe and client_events, kind presence, channels: prefix presence-chat-. """ import asyncio import re import time from pikoman_bot import Bot, env ROOMS = env("ROOMS", "presence-chat-lobby").split(",") PER_USER = 5 # timers one user may have running at once UNITS = {"s": 1, "m": 60, "h": 3600} bot = Bot("Timer", access=3) def seconds(text): """"90s", "5m", "1.5h" -> seconds, or None.""" match = re.fullmatch(r"(\d+(?:\.\d+)?)([smh])", text, re.I) if not match: return None total = round(float(match[1]) * UNITS[match[2].lower()]) return total if 1 <= total <= 24 * 3600 else None def spoken(total): """300 -> "5 min", 90 -> "1 min 30 s".""" h, m, s = total // 3600, total % 3600 // 60, total % 60 return " ".join(part for part in (h and f"{h} h", m and f"{m} min", s and f"{s} s") if part) def keep_timers(room): running = [] # {user, who, what, ends} @room.on("message") async def message(sender, data, info): text = str((data or {}).get("text", "")).strip() if room.is_bot(sender) or not text.startswith("!timer"): return # The user id your backend signed into the member's data, or the connection without one. member = room.member(sender) user, who = member.get("user_id", sender), member.get("name", "Someone") if text == "!timers": items = [f"{t['what']} ({t['who']}, {spoken(max(1, round(t['ends'] - time.time())))} left)" for t in running] return await room.send("message", {"text": f"Running: {'; '.join(items)}" if items else "No timers running."}) words = text.split() total = seconds(words[1]) if len(words) > 1 and words[0] == "!timer" else None if total is None: return await room.send("message", {"text": 'Try "!timer 5m Coffee": s, m or h, up to 24 h.'}) if sum(1 for t in running if t["user"] == user) >= PER_USER: return await room.send("message", {"text": f"{who}, you have {PER_USER} timers running already."}) what = " ".join(words[2:])[:100] or "Time is up" timer = {"user": user, "who": who, "what": what, "ends": time.time() + total} running.append(timer) await room.send("message", {"text": f"{who}: {what}, in {spoken(total)}."}) async def ring(): await asyncio.sleep(total) running.remove(timer) await room.send("message", {"text": f"⏰ {who}: {what} ({spoken(total)})", "timer": True}) timer["task"] = asyncio.create_task(ring()) # kept, so it is not collected while it waits for name in ROOMS: keep_timers(bot.join(name)) print(f"Timers in {', '.join(ROOMS)}") bot.start()