Building Slack integration

This commit is contained in:
KS Jannette
2026-08-02 06:13:01 -04:00
parent dae256f6c6
commit b607ee9121
16 changed files with 2351 additions and 8 deletions

View File

@@ -111,3 +111,82 @@ export const normalizeLinear = (payload: unknown): NormalizedDelivery => {
],
};
};
/** Stands in for the message body until the enrichment hook backfills it. */
export const SLACK_PENDING_TEXT = '(pending Slack message text)';
const DEFAULT_CAPTURE_REACTION = 'pushpin';
/** Slack suffixes skin-tone modifiers onto emoji names; only the base name identifies the reaction. */
const baseReaction = (name: string): string => name.split('::')[0];
const nested = (value: unknown): Record<string, unknown> =>
typeof value === 'object' && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
/**
* Slack Events API. Only one configured reaction is interesting; every other
* delivery is ordinary traffic and yields no notes rather than an error.
*
* A reaction_added event names a message but never carries its text, and this
* runs ahead of the HTTP ack inside Slack's three-second budget, so it cannot
* go fetch it. The note therefore carries placeholder text and needsMessageText,
* which is the enrichment hook's cue to backfill the real body out of band.
*/
export const normalizeSlack = (payload: unknown): NormalizedDelivery => {
const body = asRecord(payload);
if (body.type !== 'event_callback') {
throw new NormalizationError('Slack payload is not an event_callback');
}
const externalId = asString(body.event_id);
if (!externalId) {
throw new NormalizationError('Slack payload is missing event_id');
}
const event = nested(body.event);
if (event.type !== 'reaction_added') {
return { externalId, notes: [] };
}
// Read per call: the captured reaction is operator configuration, not a build-time constant.
const configured = asString(process.env.SLACK_CAPTURE_REACTION) ?? DEFAULT_CAPTURE_REACTION;
const reaction = asString(event.reaction);
if (!reaction || baseReaction(reaction) !== baseReaction(configured)) {
return { externalId, notes: [] };
}
const item = nested(event.item);
const channelId = asString(item.channel);
const messageTs = asString(item.ts);
if (item.type !== 'message' || !channelId || !messageTs) {
return { externalId, notes: [] };
}
const teamId = asString(body.team_id);
const reactedBy = asString(event.user);
const authorUserId = asString(event.item_user);
return {
externalId,
notes: [
{
id: noteId('slack', externalId),
text: SLACK_PENDING_TEXT,
author: authorUserId ?? reactedBy ?? 'unknown',
sourceMeta: {
...provenance({ provider: 'slack', externalId }),
needsMessageText: true,
channelId,
messageTs,
reaction,
...(teamId !== undefined ? { teamId } : {}),
...(reactedBy !== undefined ? { reactedBy } : {}),
...(authorUserId !== undefined ? { authorUserId } : {}),
},
},
],
};
};