Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/lib/alerts/__tests__/spoken-durations.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
import { describe, expect, it } from "vitest";
import { alertSpeech, spokenDurations } from "../call";
import { alertSpeech, spokenDurations, spokenLinks } from "../call";

describe("spokenLinks", () => {
it("reads links as domain and path, no scheme", () => {
expect(spokenLinks("see https://threatcrush.com/incidents/42")).toBe("see threatcrush.com/incidents/42");
expect(spokenLinks("HTTP://dev2.profullstack.com/ down")).toBe("dev2.profullstack.com down");
expect(spokenLinks("open https://threatcrush.com/i/42?token=abc#top.")).toBe("open threatcrush.com/i/42.");
expect(spokenLinks("(http://a.com/x), and https://b.io")).toBe("(a.com/x), and b.io");
});

it("leaves bare domains and other text alone", () => {
for (const t of ["threatcrush.com/incidents/42", "From 23.95.228.174", "httpd restarted"]) {
expect(spokenLinks(t)).toBe(t);
}
});

it("is what the call speaks", () => {
const said = alertSpeech("CRITICAL ssh login. Details: https://threatcrush.com/incidents/42");
expect(said).not.toMatch(/https?:/);
expect(said).toContain("Details: threatcrush.com/incidents/42.");
});
});

describe("spokenDurations", () => {
it("reads duration shorthand as words, singular and plural", () => {
Expand Down
17 changes: 16 additions & 1 deletion src/lib/alerts/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,24 @@ export function spokenDurations(text: string): string {
});
}

/**
* Links read as domain + path: nobody wants "h t t p colon slash slash" on a
* phone call. "https://threatcrush.com/incidents/42?x=1#a" ->
* "threatcrush.com/incidents/42". Query strings and fragments are dropped
* (tokens read aloud are noise), as is a trailing slash. Texts keep the full
* link; only the spoken copy changes.
*/
export function spokenLinks(text: string): string {
return text.replace(/\bhttps?:\/\/(\S+)/gi, (_all, rest: string) => {
const trailing = rest.match(/[.,;:!?)]+$/)?.[0] ?? "";
const link = rest.slice(0, rest.length - trailing.length).replace(/[?#].*$/, "").replace(/\/+$/, "");
return link + trailing;
});
}

/** What the call says: the alert, then once more, then goodbye. */
export function alertSpeech(message: string): string {
const text = spokenDurations(message).replace(/[^\p{L}\p{N}\s'.,:;!?%$@#&()+=\/-]/gu, " ").replace(/\s+/g, " ").trim();
const text = spokenDurations(spokenLinks(message)).replace(/[^\p{L}\p{N}\s'.,:;!?%$@#&()+=\/-]/gu, " ").replace(/\s+/g, " ").trim();
const said = /[.!?]$/.test(text) ? text : `${text}.`;
return `This is an alert from SMS Hub. ${said} Again: ${said} Goodbye.`;
}
Expand Down
Loading