mirror of
https://github.com/Sea-Haven-Industries/payments-dashboard.git
synced 2026-09-30 10:03:12 +00:00
Some checks failed
Deploy / deploy (push) Has been cancelled
* Merge expense-approval-bot into payments-dashboard Port the Slack reaction-driven expense routing workflow (receiver + processor) from expense-approval-bot into this stack as JavaScript ESM. Secrets copied to payments-dashboard/ prefix in Secrets Manager. * Fix review findings from PR #28 - Add length check before timingSafeEqual to prevent RangeError on malformed signatures (returns 401 instead of 500) - Check event.type === reaction_added to prevent reaction_removed from advancing expenses - Move getPermalink call behind isOrigin check to skip unnecessary API call on non-origin stage transitions
120 lines
3.5 KiB
JavaScript
120 lines
3.5 KiB
JavaScript
import {
|
|
SecretsManagerClient,
|
|
GetSecretValueCommand,
|
|
} from "@aws-sdk/client-secrets-manager";
|
|
|
|
const secrets = new SecretsManagerClient();
|
|
const EXPENSE_BOT_TOKEN_SECRET_NAME = process.env.EXPENSE_BOT_TOKEN_SECRET_NAME;
|
|
|
|
let cachedToken;
|
|
async function getBotToken() {
|
|
if (cachedToken) return cachedToken;
|
|
const { SecretString } = await secrets.send(
|
|
new GetSecretValueCommand({ SecretId: EXPENSE_BOT_TOKEN_SECRET_NAME })
|
|
);
|
|
cachedToken = SecretString;
|
|
return cachedToken;
|
|
}
|
|
|
|
const SUBMITTED_CHANNEL = "C0AQ2AWLNEN";
|
|
|
|
const STAGES = {
|
|
[SUBMITTED_CHANNEL]: { next: "C0APLSGABAB", label: "Processed" },
|
|
C0APLSGABAB: { next: "C0AQ09CDJH4", label: "Authorized" },
|
|
C0AQ09CDJH4: { next: "C0APYUM1JFP", label: "Matched" },
|
|
};
|
|
|
|
const REACT_HINT_RE = /_React_ :white_check_mark: _to advance to \w+_/;
|
|
|
|
async function slackGet(method, token, params) {
|
|
const qs = new URLSearchParams(params).toString();
|
|
const res = await fetch(`https://slack.com/api/${method}?${qs}`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
const data = await res.json();
|
|
if (!data.ok) throw new Error(`${method} failed: ${data.error}`);
|
|
return data;
|
|
}
|
|
|
|
async function slackPost(method, token, body) {
|
|
const res = await fetch(`https://slack.com/api/${method}`, {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
"Content-Type": "application/json; charset=utf-8",
|
|
},
|
|
body: JSON.stringify(body),
|
|
});
|
|
const data = await res.json();
|
|
if (!data.ok) throw new Error(`${method} failed: ${data.error}`);
|
|
return data;
|
|
}
|
|
|
|
export const handler = async (event) => {
|
|
if (event.type !== "reaction_added" || event.reaction !== "white_check_mark") {
|
|
console.log(`Skipping event type=${event.type} reaction=${event.reaction}`);
|
|
return;
|
|
}
|
|
|
|
const fromChannel = event.item.channel;
|
|
const messageTs = event.item.ts;
|
|
|
|
const route = STAGES[fromChannel];
|
|
if (!route) {
|
|
console.log(`Channel ${fromChannel} not in STAGES, skipping`);
|
|
return;
|
|
}
|
|
|
|
const toChannel = route.next;
|
|
const label = route.label;
|
|
const isOrigin = fromChannel === SUBMITTED_CHANNEL;
|
|
const token = await getBotToken();
|
|
|
|
const history = await slackGet("conversations.history", token, {
|
|
channel: fromChannel,
|
|
latest: messageTs,
|
|
limit: "1",
|
|
inclusive: "true",
|
|
});
|
|
const originalText = history.messages[0].text;
|
|
|
|
const text = originalText.replace(REACT_HINT_RE, "").trim();
|
|
const nextStage = STAGES[toChannel];
|
|
const nextLabel = nextStage ? nextStage.label : null;
|
|
const reactLine = nextLabel
|
|
? `\n\n_React_ :white_check_mark: _to advance to ${nextLabel}_`
|
|
: "";
|
|
|
|
let permalinkLine = "";
|
|
if (isOrigin) {
|
|
const permalinkResp = await slackGet("chat.getPermalink", token, {
|
|
channel: fromChannel,
|
|
message_ts: messageTs,
|
|
});
|
|
permalinkLine = `\n\n:paperclip: *Original Submission:* <${permalinkResp.permalink}|View Original Message>`;
|
|
}
|
|
const fullText = `${text}${permalinkLine}${reactLine}`;
|
|
|
|
await slackPost("chat.postMessage", token, {
|
|
channel: toChannel,
|
|
text: fullText,
|
|
mrkdwn: true,
|
|
unfurl_links: false,
|
|
unfurl_media: false,
|
|
});
|
|
|
|
if (isOrigin) {
|
|
await slackPost("chat.postMessage", token, {
|
|
channel: fromChannel,
|
|
thread_ts: messageTs,
|
|
text: `➡️ Advanced to ${label}`,
|
|
});
|
|
console.log(`Advanced user submission to ${label} (origin preserved)`);
|
|
} else {
|
|
await slackPost("chat.delete", token, {
|
|
channel: fromChannel,
|
|
ts: messageTs,
|
|
});
|
|
console.log(`Advanced to ${label} and deleted previous copy`);
|
|
}
|
|
};
|