"""poll.py: runs a vote in a chat room, in Python. A member writes "!poll Pizza or tacos?" (or "!poll Lunch? | Pizza | Tacos | Sushi"), the bot opens the poll, members vote with a client event, and when the time is up it posts the result. One vote per user: the user id comes from the member data your backend signed, so nobody votes twice by opening a second tab. Python 3.10+. pip install -r requirements.txt, then python poll.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(",") SECONDS = int(env("POLL_SECONDS", "60")) bot = Bot("Poll", access=3) def parse(text): """"Pizza or tacos?" -> two options; "Lunch? | Pizza | Tacos" -> a question and its options.""" parts = [p.strip() for p in text.split("|") if p.strip()] if len(parts) >= 3: return {"question": parts[0], "options": parts[1:7]} options = [o.strip() for o in re.split(r"\s+or\s+", re.sub(r"\?\s*$", "", text), flags=re.I) if o.strip()] return {"question": text, "options": options[:6]} if len(options) >= 2 else None def run_polls(room): state = {"poll": None} # the open poll: id, question, options, votes (user -> option) def user_of(sender): # Who a member is: the user id in the member data your backend signed, or the connection. return room.member(sender).get("user_id", sender) @room.on("message") async def message(sender, data, info): text = str((data or {}).get("text", "")).strip() if not text.lower().startswith("!poll ") or room.is_bot(sender): return if state["poll"]: return await room.send("message", {"text": f"A poll is already open: {state['poll']['question']}"}) parsed = parse(text[6:].strip()) if not parsed: return await room.send("message", {"text": 'Try "!poll Pizza or tacos?" or "!poll Lunch? | Pizza | Tacos | Sushi"'}) poll = state["poll"] = {"id": format(int(time.time() * 1000), "x"), **parsed, "votes": {}} who = room.member(sender).get("name", "Someone") # The poll itself, for the pages to draw buttons, and a line for the chat. await room.send("poll", {"id": poll["id"], "question": poll["question"], "options": poll["options"], "seconds": SECONDS}) await room.send("message", {"text": f"{who} asks: {poll['question']} Vote within {SECONDS} s."}) poll["task"] = asyncio.create_task(close_after(SECONDS)) # kept, so it is not collected while it waits # A vote: room.send("vote", { poll: id, option: 0 }) on the member's page. The last one counts. @room.on("vote") def vote(sender, data, info): poll = state["poll"] if not poll or not isinstance(data, dict) or data.get("poll") != poll["id"] or room.is_bot(sender): return option = data.get("option") if type(option) is int and 0 <= option < len(poll["options"]): poll["votes"][user_of(sender)] = option async def close_after(seconds): await asyncio.sleep(seconds) poll, state["poll"] = state["poll"], None counts = [list(poll["votes"].values()).count(i) for i in range(len(poll["options"]))] top = max(counts) winners = [o for o, c in zip(poll["options"], counts) if c == top] tally = ", ".join(f"{o} {c}" for o, c in zip(poll["options"], counts)) verdict = "No votes." if top == 0 else f"A tie: {' and '.join(winners)}." if len(winners) > 1 else f"{winners[0]} wins." await room.send("poll-result", {"id": poll["id"], "question": poll["question"], "options": poll["options"], "counts": counts}) await room.send("message", {"text": f"{poll['question']} {tally}. {verdict}"}) for name in ROOMS: run_polls(bot.join(name)) print(f"Polls in {', '.join(ROOMS)}") bot.start()