Building Slack integration
This commit is contained in:
@@ -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 } : {}),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user