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

@@ -22,12 +22,27 @@ const backoffDelay = (attempt: number): number => {
return window + Math.random() * window;
};
/**
* A job signals that retrying cannot change the outcome by throwing an error
* carrying `permanent: true`. A revoked credential or a deleted channel is a
* settled fact; spending the remaining attempts on it only delays every job
* behind it and buries the real reason under retry noise.
*/
const isPermanent = (err: unknown): boolean =>
typeof err === 'object' &&
err !== null &&
(err as { permanent?: unknown }).permanent === true;
const runEntry = async (entry: QueueEntry): Promise<void> => {
for (let attempt = 1; ; attempt += 1) {
try {
await entry.job();
return;
} catch (err) {
if (isPermanent(err)) {
console.error(`[queue] job "${entry.name}" abandoned as permanent`, err);
return;
}
if (attempt >= settings.maxAttempts) {
console.error(
`[queue] job "${entry.name}" abandoned after ${attempt} attempt(s)`,

53
backend/lib/slackText.ts Normal file
View File

@@ -0,0 +1,53 @@
/**
* Slack delivers mrkdwn, not plain text. Link syntax and HTML entities that
* survive into a note degrade embedding quality and read as noise on a sticky,
* so they are unwrapped here. Pure and dependency-free so both the normalizer
* and the API client can use it.
*/
const ENTITIES: Record<string, string> = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
};
const unescapeEntities = (text: string): string =>
text.replace(/&(?:amp|lt|gt);/g, (match) => ENTITIES[match] ?? match);
const splitOnce = (value: string, separator: string): [string, string | undefined] => {
const index = value.indexOf(separator);
if (index === -1) return [value, undefined];
return [value.slice(0, index), value.slice(index + separator.length)];
};
const BROADCASTS: readonly string[] = ['here', 'channel', 'everyone'];
const MARKUP = /<([^<>]*)>/g;
const unwrap = (inner: string): string => {
const [target, rawLabel] = splitOnce(inner, '|');
const label = rawLabel !== undefined && rawLabel.length > 0 ? rawLabel : undefined;
if (target.startsWith('@')) return `@${label ?? target.slice(1)}`;
if (target.startsWith('#')) return `#${label ?? target.slice(1)}`;
if (target.startsWith('!')) {
const special = target.slice(1);
if (BROADCASTS.includes(special)) return `@${special}`;
return label ?? `@${special}`;
}
if (target.startsWith('mailto:')) return label ?? target.slice('mailto:'.length);
return label ?? target;
};
/**
* Entities are unescaped only after markup is unwrapped. Slack escapes a
* literal `<` as `&lt;` precisely so it is not read as markup, and reversing
* that order would turn user text into parsed link syntax.
*/
export const cleanSlackText = (input: string): string =>
unescapeEntities(input.replace(MARKUP, (_match, inner: string) => unwrap(inner)))
.replace(/[ \t]+$/gm, '')
.trim();