225 lines
7.1 KiB
Python
225 lines
7.1 KiB
Python
|
|
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
|