1.0.27
This commit is contained in:
38 files changed
+1115
-274
No files matched your search
@@ -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}`;
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user