|
| 1 | +""" |
| 2 | +Core listen operations using Slack Web API. |
| 3 | +""" |
| 4 | + |
| 5 | +import time |
| 6 | +from collections.abc import Iterator |
| 7 | +from datetime import datetime, timezone |
| 8 | +from typing import Any |
| 9 | + |
| 10 | +from slack_sdk import WebClient |
| 11 | + |
| 12 | + |
| 13 | +def listen_channel( |
| 14 | + client: WebClient, |
| 15 | + channel_id: str, |
| 16 | + thread_ts: str | None = None, |
| 17 | + interval: float = 2.0, |
| 18 | + timeout: float | None = None, |
| 19 | + include_history: int = 0, |
| 20 | +) -> Iterator[dict]: |
| 21 | + """ |
| 22 | + Yield new messages as they appear in channel or thread. |
| 23 | +
|
| 24 | + Args: |
| 25 | + client: Slack WebClient instance |
| 26 | + channel_id: Channel ID to listen to |
| 27 | + thread_ts: If provided, listen to thread replies instead of channel |
| 28 | + interval: Poll interval in seconds (default: 2.0) |
| 29 | + timeout: Exit after this many seconds (default: None = infinite) |
| 30 | + include_history: Include last N messages on start (default: 0) |
| 31 | +
|
| 32 | + Yields: |
| 33 | + Message dicts with 'received_at' ISO timestamp added |
| 34 | + """ |
| 35 | + start_time = time.monotonic() |
| 36 | + latest_ts: str | None = None |
| 37 | + |
| 38 | + # Fetch history if requested |
| 39 | + if include_history > 0: |
| 40 | + messages: list[Any] |
| 41 | + if thread_ts: |
| 42 | + response = client.conversations_replies( |
| 43 | + channel=channel_id, ts=thread_ts, limit=include_history |
| 44 | + ) |
| 45 | + messages = response.get("messages", []) |
| 46 | + # First message is parent, rest are replies |
| 47 | + if len(messages) > 1: |
| 48 | + messages = messages[1:] # Skip parent |
| 49 | + else: |
| 50 | + messages = [] |
| 51 | + else: |
| 52 | + response = client.conversations_history( |
| 53 | + channel=channel_id, limit=include_history |
| 54 | + ) |
| 55 | + messages = response.get("messages", []) |
| 56 | + |
| 57 | + # Messages come in reverse chronological order, reverse to chronological |
| 58 | + messages = list(reversed(messages)) |
| 59 | + |
| 60 | + for msg in messages: |
| 61 | + msg["received_at"] = datetime.now(timezone.utc).isoformat() |
| 62 | + yield msg |
| 63 | + # Track latest timestamp seen |
| 64 | + msg_ts = msg.get("ts") |
| 65 | + if msg_ts and (latest_ts is None or msg_ts > latest_ts): |
| 66 | + latest_ts = msg_ts |
| 67 | + |
| 68 | + # If no history fetched, start from now |
| 69 | + if latest_ts is None: |
| 70 | + latest_ts = str(time.time()) |
| 71 | + |
| 72 | + # Poll for new messages |
| 73 | + while True: |
| 74 | + if timeout is not None: |
| 75 | + elapsed = time.monotonic() - start_time |
| 76 | + if elapsed >= timeout: |
| 77 | + break |
| 78 | + |
| 79 | + time.sleep(interval) |
| 80 | + |
| 81 | + # Add epsilon to make oldest exclusive (Slack's oldest is inclusive) |
| 82 | + exclusive_oldest = str(float(latest_ts) + 0.000001) |
| 83 | + |
| 84 | + if thread_ts: |
| 85 | + response = client.conversations_replies( |
| 86 | + channel=channel_id, ts=thread_ts, oldest=exclusive_oldest |
| 87 | + ) |
| 88 | + messages = response.get("messages", []) |
| 89 | + # Filter out parent and already-seen messages |
| 90 | + messages = [ |
| 91 | + m |
| 92 | + for m in messages |
| 93 | + if m.get("ts") != thread_ts and m.get("ts", "") > latest_ts |
| 94 | + ] |
| 95 | + else: |
| 96 | + response = client.conversations_history( |
| 97 | + channel=channel_id, oldest=exclusive_oldest |
| 98 | + ) |
| 99 | + messages = response.get("messages", []) |
| 100 | + # Filter already-seen messages (defensive deduplication) |
| 101 | + messages = [m for m in messages if m.get("ts", "") > latest_ts] |
| 102 | + |
| 103 | + # Messages come in reverse chronological order, reverse to chronological |
| 104 | + messages = list(reversed(messages)) |
| 105 | + |
| 106 | + for msg in messages: |
| 107 | + msg["received_at"] = datetime.now(timezone.utc).isoformat() |
| 108 | + yield msg |
| 109 | + # Track latest timestamp seen |
| 110 | + msg_ts = msg.get("ts") |
| 111 | + if msg_ts and (latest_ts is None or msg_ts > latest_ts): |
| 112 | + latest_ts = msg_ts |
0 commit comments