expense-approval-bot/src/processor/app.py
2026-05-08 15:40:46 -04:00

148 lines
4.1 KiB
Python

import json
import os
import re
import urllib.parse
import urllib.request
import boto3
secrets_client = boto3.client("secretsmanager")
_cached_token: str | None = None
SLACK_BOT_TOKEN_SECRET_ARN = os.environ["SLACK_BOT_TOKEN_SECRET_ARN"]
SUBMITTED_CHANNEL = "C0AQ2AWLNEN"
STAGES = {
SUBMITTED_CHANNEL: {"next": "C0APLSGABAB", "label": "Processed"},
"C0APLSGABAB": {"next": "C0AQ09CDJH4", "label": "Authorized"},
"C0AQ09CDJH4": {"next": "C0APYUM1JFP", "label": "Matched"},
}
REACT_HINT_RE = re.compile(r"_React_ :white_check_mark: _to advance to \w+_")
def handler(event, context):
if event.get("reaction") != "white_check_mark":
print("Not white_check_mark reaction, skipping")
return
from_channel = event["item"]["channel"]
message_ts = event["item"]["ts"]
route = STAGES.get(from_channel)
if not route:
print(f"Channel {from_channel} not in STAGES, skipping")
return
to_channel = route["next"]
label = route["label"]
is_origin = from_channel == SUBMITTED_CHANNEL
token = get_bot_token()
history = slack_get(
"conversations.history",
token,
{
"channel": from_channel,
"latest": message_ts,
"limit": 1,
"inclusive": "true",
},
)
original_text = history["messages"][0]["text"]
permalink_resp = slack_get(
"chat.getPermalink",
token,
{
"channel": from_channel,
"message_ts": message_ts,
},
)
permalink = permalink_resp["permalink"]
text = REACT_HINT_RE.sub("", original_text).strip()
next_stage = STAGES.get(to_channel)
next_label = next_stage["label"] if next_stage else None
react_line = (
f"\n\n_React_ :white_check_mark: _to advance to {next_label}_"
if next_label
else ""
)
permalink_line = (
f"\n\n📎 *Original Submission:* <{permalink}|View Original Message>"
if is_origin
else ""
)
full_text = f"{text}{permalink_line}{react_line}"
slack_post(
"chat.postMessage",
token,
{
"channel": to_channel,
"text": full_text,
"mrkdwn": True,
"unfurl_links": False,
"unfurl_media": False,
},
)
if is_origin:
slack_post(
"chat.postMessage",
token,
{
"channel": from_channel,
"thread_ts": message_ts,
"text": f"➡️ Advanced to {label}",
},
)
print(f"Advanced user submission to {label} (origin preserved)")
else:
slack_post(
"chat.delete",
token,
{
"channel": from_channel,
"ts": message_ts,
},
)
print(f"Advanced to {label} and deleted previous copy")
def get_bot_token() -> str:
global _cached_token
if _cached_token:
return _cached_token
result = secrets_client.get_secret_value(SecretId=SLACK_BOT_TOKEN_SECRET_ARN)
_cached_token = result["SecretString"]
return _cached_token
def slack_get(method: str, token: str, params: dict) -> dict:
url = f"https://slack.com/api/{method}?{urllib.parse.urlencode(params)}"
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
if not data.get("ok"):
raise RuntimeError(f"{method} failed: {data.get('error')}")
return data
def slack_post(method: str, token: str, body: dict) -> dict:
req = urllib.request.Request(
f"https://slack.com/api/{method}",
data=json.dumps(body).encode("utf-8"),
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json; charset=utf-8",
},
method="POST",
)
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
if not data.get("ok"):
raise RuntimeError(f"{method} failed: {data.get('error')}")
return data