import json import logging from datetime import datetime, timezone import requests logger = logging.getLogger(__name__) _dm_channel_id = None def send_dm(user_id, blocks, text, token): global _dm_channel_id if _dm_channel_id is None: resp = requests.post( "https://slack.com/api/conversations.open", headers={"Authorization": f"Bearer {token}"}, json={"users": user_id}, ) resp.raise_for_status() data = resp.json() if not data.get("ok"): raise RuntimeError(f"conversations.open failed: {data.get('error')}") _dm_channel_id = data["channel"]["id"] resp = requests.post( "https://slack.com/api/chat.postMessage", headers={"Authorization": f"Bearer {token}"}, json={ "channel": _dm_channel_id, "blocks": blocks, "text": text, }, ) resp.raise_for_status() data = resp.json() if not data.get("ok"): raise RuntimeError(f"chat.postMessage failed: {data.get('error')}") return data.get("ts") def build_high_priority_alert(message): time_ago = _format_time_ago(message["internal_date"]) gmail_url = f"https://mail.google.com/mail/u/0/#inbox/{message['message_id']}" return [ { "type": "header", "text": {"type": "plain_text", "text": ":rotating_light: High-Priority Email"}, }, { "type": "section", "fields": [ {"type": "mrkdwn", "text": f"*From*\n{message['from_name'] or message['from_email']}"}, {"type": "mrkdwn", "text": f"*Received*\n{time_ago}"}, ], }, { "type": "section", "text": {"type": "mrkdwn", "text": f"*{message['subject']}*\n{message['snippet'][:300]}"}, }, { "type": "context", "elements": [ {"type": "mrkdwn", "text": f":brain: _{message.get('classification_reason', '')}_"}, ], }, { "type": "actions", "elements": [ { "type": "button", "text": {"type": "plain_text", "text": "Open in Gmail"}, "url": gmail_url, "action_id": "open_gmail", } ], }, ] def build_daily_digest(high_items, bypassed, unanswered, normal_count, low_count): today_str = datetime.now(timezone.utc).strftime("%A, %B %d") total = len(high_items) + normal_count + low_count blocks = [ { "type": "header", "text": {"type": "plain_text", "text": ":newspaper: Daily Inbox Digest"}, }, { "type": "context", "elements": [ {"type": "mrkdwn", "text": f"{today_str} · {total} emails processed today"}, ], }, {"type": "divider"}, ] if high_items: blocks.append({ "type": "section", "text": {"type": "mrkdwn", "text": f":rotating_light: *High Priority ({len(high_items)})*"}, }) for item in high_items: time_ago = _format_time_ago(item["internal_date"]) gmail_url = f"https://mail.google.com/mail/u/0/#inbox/{item['pk'].replace('MSG#', '')}" blocks.append({ "type": "section", "text": { "type": "mrkdwn", "text": ( f"*{item['subject']}*\n" f"From: {item.get('from_name') or item['from_email']} · {time_ago}\n" f"_{item.get('classification_reason', '')}_" ), }, "accessory": { "type": "button", "text": {"type": "plain_text", "text": "Open"}, "url": gmail_url, "action_id": f"open_{item['pk']}", }, }) blocks.append({"type": "divider"}) if bypassed: blocks.append({ "type": "section", "text": { "type": "mrkdwn", "text": ( f":warning: *Bypassed Work Orders ({len(bypassed)})*\n" "_Sent directly to you without CC'ing work-orders@_" ), }, }) for item in bypassed: blocks.append({ "type": "section", "text": { "type": "mrkdwn", "text": f" · *{item['subject']}* from {item.get('from_name') or item['from_email']}", }, }) blocks.append({"type": "divider"}) if unanswered: blocks.append({ "type": "section", "text": { "type": "mrkdwn", "text": ( f":hourglass: *Unanswered Threads ({len(unanswered)})*\n" "_Awaiting your reply for 24h+_" ), }, }) for thread in unanswered: hours = _hours_since_iso(thread.get("unanswered_since", "")) blocks.append({ "type": "section", "text": { "type": "mrkdwn", "text": ( f" · *{thread.get('subject', '(no subject)')}*" f" — last from {thread.get('last_message_from', 'unknown')}" f" · {hours}h waiting" ), }, }) blocks.append({"type": "divider"}) blocks.append({ "type": "context", "elements": [ { "type": "mrkdwn", "text": ( f":bar_chart: Today: {len(high_items)} high · " f"{normal_count} normal · {low_count} low · " f"{len(bypassed)} bypassed WO · {len(unanswered)} unanswered" ), } ], }) return blocks def send_message_to_channel(channel, text, token): resp = requests.post( "https://slack.com/api/chat.postMessage", headers={"Authorization": f"Bearer {token}"}, json={"channel": channel, "text": text}, ) resp.raise_for_status() data = resp.json() if not data.get("ok"): logger.error("chat.postMessage failed: %s", data.get("error")) def _format_time_ago(internal_date): if not internal_date: return "unknown" msg_time = datetime.fromtimestamp(int(internal_date) / 1000, tz=timezone.utc) delta = datetime.now(timezone.utc) - msg_time hours = int(delta.total_seconds() / 3600) if hours < 1: mins = int(delta.total_seconds() / 60) return f"{mins}m ago" if hours < 24: return f"{hours}h ago" return f"{hours // 24}d ago" def _hours_since_iso(iso_str): if not iso_str: return 0 try: dt = datetime.fromisoformat(iso_str) delta = datetime.now(timezone.utc) - dt return int(delta.total_seconds() / 3600) except (ValueError, TypeError): return 0