diff options
| -rw-r--r-- | .gitignore | 7 | ||||
| -rw-r--r-- | .zuliprc.example | 4 | ||||
| -rw-r--r-- | README.md | 33 | ||||
| -rw-r--r-- | main.py | 67 | ||||
| -rw-r--r-- | requirements.txt | 2 |
5 files changed, 113 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8005bdc --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.zuliprc +__pycache__/ +*.pyc +*.pyo +.env +venv/ +.venv/ diff --git a/.zuliprc.example b/.zuliprc.example new file mode 100644 index 0000000..86ec090 --- /dev/null +++ b/.zuliprc.example @@ -0,0 +1,4 @@ +[api] +email=your-bot@zulip.yourserver.org +key=your_api_key_here +site=https://zulip.yourserver.org diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ffb3a1 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# slopbot + +Zulip bot for [meshheads.org](https://zulip.meshheads.org) that replies with stock prices when it sees `$TICKER` patterns in messages. + +## Usage + +Post `$NVDA` or `$AAPL is mooning` in any stream and slopbot replies with the current price. + +## Setup + +```bash +pip install -r requirements.txt +cp .zuliprc.example .zuliprc +# edit .zuliprc with your bot credentials +python main.py +``` + +Get your `.zuliprc` from Zulip: Settings → Bots → your bot → download config. + +## Architecture + +- `main.py` — single-file bot, raw `zulip` event loop (no framework) +- `fetch_price(ticker)` — isolated data layer, easy to swap yfinance for a real API +- Regex: `\$([A-Z]{1,5})` — matches 1-5 uppercase letters after `$` +- Deduplicates tickers per message, replies in-place (same stream + topic) + +## Extending + +Future ideas: +- Swap `fetch_price` for Finnhub/Polygon for real-time data +- Add LLM commentary on price moves +- Support crypto tickers +- Add `@slopbot summarize $NVDA` command style @@ -0,0 +1,67 @@ +#!/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 re +import zulip +import yfinance as yf + +# Regex to match stock tickers like $NVDA, $AAPL, $BRK +TICKER_RE = re.compile(r'\$([A-Z]{1,5})') + + +def fetch_price(ticker: str) -> str: + """Return a formatted price string for a ticker, or an error message.""" + try: + info = yf.Ticker(ticker).fast_info + price = info["lastPrice"] + return f"${ticker}: ${price:,.2f}" + except Exception as e: + return f"${ticker}: couldn't fetch price ({e})" + + +def handle_event(event: dict, client: zulip.Client) -> 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 + + lines = [fetch_price(t) for t in dict.fromkeys(tickers)] # dedup, preserve order + reply = "\n".join(lines) + + client.send_message({ + "type": msg["type"], # "stream" or "private" + "to": msg["stream_id"] if msg["type"] == "stream" else msg["sender_email"], + "topic": msg.get("subject", ""), + "content": reply, + }) + + +def main(): + client = zulip.Client(config_file=".zuliprc") + print(f"slopbot running as {client.email} ...") + client.call_on_each_event( + lambda event: handle_event(event, client), + event_types=["message"], + ) + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9184f43 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +zulip>=0.9.0 +yfinance>=0.2.0 |
