This commit is contained in:
jarek committed 2026-04-26 08:01:32 +02:00
1 parent ccfda4c054
commit 7e3797cbfe
38 files changed
+1115 -274

No files matched your search

+47
View File
@@ -0,0 +1,47 @@
// Pure parsing/building functions for notification providers.
// Extracted from notifications.ts so unit tests can import without pulling in DB deps.
// --- Telegram ---
// Escape special characters for Telegram legacy Markdown (parse_mode: 'Markdown')
// Only _ * ` [ need escaping — ] and other chars are not special in legacy mode
export function escapeTelegramMarkdown(text: string): string {
return text
.replace(/_/g, '\\_') // Underscore (italic)
.replace(/\*/g, '\\*') // Asterisk (bold)
.replace(/`/g, '\\`') // Backtick (code)
.replace(/\[/g, '\\['); // Opening bracket (link)
}
export function parseTelegramUrl(url: string): { botToken: string; chatId: string; topicId?: number } | null {
const match = url.match(/^tgram:\/\/([^/]+)\/([^:\/]+)(?::(\d+))?$/);
if (!match) return null;
const [, botToken, chatId, topicIdStr] = match;
return { botToken, chatId, topicId: topicIdStr ? parseInt(topicIdStr, 10) : undefined };
}
// --- Gotify ---
export function buildGotifyUrl(appriseUrl: string): string | null {
const match = appriseUrl.match(/^gotifys?:\/\/([^/]+)\/(.+)/);
if (!match) return null;
const [, hostname, pathPart] = match;
const protocol = appriseUrl.startsWith('gotifys') ? 'https' : 'http';
const lastSlash = pathPart.lastIndexOf('/');
const subpath = lastSlash >= 0 ? pathPart.substring(0, lastSlash) : '';
const token = lastSlash >= 0 ? pathPart.substring(lastSlash + 1) : pathPart;
return `${protocol}://${hostname}${subpath ? '/' + subpath : ''}/message?token=${token}`;
}
// --- Workflows (Microsoft Power Automate) ---
export function parseWorkflowsUrl(appriseUrl: string): { hostname: string; workflow: string; signature: string } | null {
const match = appriseUrl.match(/^workflows?:\/\/([^/]+)\/(.+)\/(.+)/);
if (!match) return null;
const [, hostname, workflow, signature] = match;
return { hostname, workflow, signature };
}
export function buildWorkflowsHttpUrl(hostname: string, workflow: string, signature: string): string {
return `https://${hostname}/powerautomate/automations/direct/workflows/${workflow}/triggers/manual/paths/invoke?api-version=1&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=${signature}`;
}
+33 -16
View File
@@ -13,9 +13,10 @@ interface PortInfo {
}
/**
* Format Docker port mappings, collapsing consecutive ranges.
* Format Docker port mappings, collapsing consecutive ranges of 3+ ports.
* Accepts both Docker API format (PublicPort/PrivatePort) and camelCase (publicPort/privatePort).
* e.g. 8080:8080, 8081:8081, 8082:8082 → 8080-8082:8080-8082
* But 80:80, 81:81 stay as individual ports (only 2 consecutive).
*/
export function formatPorts(ports: PortInfo[] | undefined | null): PortMapping[] {
if (!ports || ports.length === 0) return [];
@@ -35,30 +36,46 @@ export function formatPorts(ports: PortInfo[] | undefined | null): PortMapping[]
})
.sort((a, b) => a.publicPort - b.publicPort);
// Collapse consecutive port ranges
// Collapse consecutive port ranges (3+ ports only)
if (individual.length <= 1) return individual;
const result: PortMapping[] = [];
let rangeStart = individual[0];
let rangeEnd = individual[0];
let rangeStart = 0;
let rangeEnd = 0;
for (let i = 1; i < individual.length; i++) {
const curr = individual[i];
const offset = curr.publicPort - rangeStart.publicPort;
const expectedPrivate = rangeStart.privatePort + offset;
if (curr.publicPort === rangeEnd.publicPort + 1 && curr.privatePort === expectedPrivate) {
rangeEnd = curr;
const start = individual[rangeStart];
const prev = individual[rangeEnd];
const offset = curr.publicPort - start.publicPort;
const expectedPrivate = start.privatePort + offset;
if (curr.publicPort === prev.publicPort + 1 && curr.privatePort === expectedPrivate) {
rangeEnd = i;
} else {
result.push(rangeStart.publicPort === rangeEnd.publicPort
? rangeStart
: { publicPort: rangeStart.publicPort, privatePort: rangeStart.privatePort, display: `${rangeStart.publicPort}-${rangeEnd.publicPort}:${rangeStart.privatePort}-${rangeEnd.privatePort}`, isRange: true });
rangeStart = curr;
rangeEnd = curr;
flushRange(individual, rangeStart, rangeEnd, result);
rangeStart = i;
rangeEnd = i;
}
}
result.push(rangeStart.publicPort === rangeEnd.publicPort
? rangeStart
: { publicPort: rangeStart.publicPort, privatePort: rangeStart.privatePort, display: `${rangeStart.publicPort}-${rangeEnd.publicPort}:${rangeStart.privatePort}-${rangeEnd.privatePort}`, isRange: true });
flushRange(individual, rangeStart, rangeEnd, result);
return result;
}
function flushRange(items: PortMapping[], start: number, end: number, result: PortMapping[]) {
const rangeLen = end - start + 1;
if (rangeLen >= 3) {
// Collapse into range
result.push({
publicPort: items[start].publicPort,
privatePort: items[start].privatePort,
display: `${items[start].publicPort}-${items[end].publicPort}:${items[start].privatePort}-${items[end].privatePort}`,
isRange: true
});
} else {
// Keep as individual ports
for (let i = start; i <= end; i++) {
result.push(items[i]);
}
}
}