#!/usr/bin/env python3 """ slopbot - Zulip bot that replies with stock prices when it sees $TICKER patterns. Usage: python main.py Requires a .zuliprc file in the same directory (see .zuliprc.example). """ import logging import re import zulip import yfinance as yf logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) log = logging.getLogger("slopbot") # Regex to match stock tickers like $NVDA, $AAPL, $BRK TICKER_RE = re.compile(r'\$([A-Z]{1,5})') log.info("Connecting to Zulip...") client = zulip.Client(config_file='.zuliprc') log.info("Connected as %s", client.email) def fetch_price(ticker: str) -> str: """Return a formatted price string for a ticker, or an error message.""" log.info("Fetching price for %s", ticker) try: price = yf.Ticker(ticker).fast_info['lastPrice'] result = f"${ticker}: ${price:,.2f}" log.info(" -> %s", result) return result except Exception as e: log.warning(" -> %s unavailable: %s", ticker, e) return f"${ticker}: unavailable ({e})" def handle_event(event: dict) -> None: """Process a Zulip event and reply if tickers are found.""" if event.get("type") != "message": return msg = event["message"] # Don't reply to ourselves if msg["sender_email"] == client.email: return content = msg.get("content", "") tickers = TICKER_RE.findall(content.upper()) if not tickers: return log.info( "Message from %s in %s/%s: found tickers %s", msg["sender_email"], msg.get("display_recipient", "?"), msg.get("subject", "?"), tickers, ) lines = [fetch_price(t) for t in dict.fromkeys(tickers)] # dedup, preserve order reply = "\n".join(lines) client.send_message({ "type": msg["type"], "to": msg["stream_id"] if msg["type"] == "stream" else msg["sender_email"], "topic": msg.get("subject", ""), "content": reply, }) log.info("Reply sent.") log.info("Listening for messages...") client.call_on_each_event(handle_event, event_types=["message"])