diff --git a/src/bios.ts b/src/bios.ts index c010ab2..7dd437a 100644 --- a/src/bios.ts +++ b/src/bios.ts @@ -1,50 +1,66 @@ import { HTMLElement, parse } from "node-html-parser"; -class Bios { +/** + * Represents a BIOS entry with version, date, changelog, and optional download URL. + */ +export class Bios { + /** BIOS release date (YYYY/MM/DD) */ readonly date: string; + /** BIOS version string */ readonly version: string; + /** BIOS changelog/description */ readonly changelog: string; - readonly url: string | null; + /** Optional download URL for the BIOS file */ + readonly url?: string; + /** + * @param date - BIOS release date + * @param version - BIOS version string + * @param changelog - BIOS changelog/description + * @param url - Optional download URL + * @throws Error if required fields are missing + */ constructor( date: string, version: string, changelog: string, - url: string | null, + url?: string, ) { - if (!date || !version || !changelog) { - throw new Error("Required BIOS information missing"); + if (!date?.trim() || !version?.trim() || !changelog?.trim()) { + throw new Error( + "Required BIOS information missing: date, version, or changelog", + ); } - this.date = date.trim(); this.version = version.trim(); this.changelog = changelog.trim(); - this.url = url?.trim() || null; + this.url = url?.trim(); } + /** + * Returns a formatted string representation of the BIOS entry. + */ toString(): string { - return `${this.version} (${this.date}):\n${this.changelog}`; + return `${this.version} (${this.date}):\n ${this.changelog}\n ${this.url}`; } } /** - * Fetches and parses BIOS information from a motherboard support page + * Fetches and parses BIOS information from a motherboard support page. * @param mobo - URL of the motherboard support page * @returns Promise containing array of Bios objects * @throws Error if the URL is invalid or parsing fails */ -async function getBios(mobo: string): Promise { - if (!mobo || !mobo.startsWith("http")) { - throw new Error("Invalid motherboard URL"); +export async function getBios(mobo: string): Promise { + if (!mobo || !/^https?:\/\//.test(mobo)) { + throw new Error("Invalid motherboard URL: must start with http or https"); } try { const response = await fetch(mobo); - if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } - const body = await response.text(); const root = parse(body); const biosList: Bios[] = []; @@ -57,15 +73,12 @@ async function getBios(mobo: string): Promise { for (const tr of rows) { try { - const bios = parseBiosRow(tr); - if (bios) { - biosList.push(bios); - } + biosList.push(parseBiosRow(tr)); } catch (error) { - console.error("Failed to parse BIOS row:", error); + const message = error instanceof Error ? error.message : String(error); + console.error("Failed to parse BIOS row:", message); } } - return biosList; } catch (error) { const message = error instanceof Error ? error.message : String(error); @@ -74,22 +87,29 @@ async function getBios(mobo: string): Promise { } /** - * Parses a table row into a Bios object + * Parses a table row into a Bios object. * @param tr - HTML table row element - * @returns Bios object or null if parsing fails + * @returns Bios object + * @throws Error if the row format is invalid */ -function parseBiosRow(tr: HTMLElement): Bios | null { +function parseBiosRow(tr: HTMLElement): Bios { try { const tds = tr.querySelectorAll("td"); if (tds.length < 6) { - throw new Error("Invalid table row format"); + throw new Error( + `Invalid table row format: expected >=6 columns, got ${tds.length}`, + ); } - + // Try to resolve the link to an absolute URL if possible const link = tds[5].querySelector("a")?.getAttribute("href"); - const url = link - ? encodeURI(link).replace(/[(]/g, "%28").replace(/[)]/g, "%29") - : null; - + let url: string | undefined = undefined; + if (link) { + try { + url = encodeURI(link).replace(/[(]/g, "%28").replace(/[)]/g, "%29"); + } catch { + url = link; + } + } return new Bios( tds[1].text, // date tds[0].text, // version @@ -97,9 +117,7 @@ function parseBiosRow(tr: HTMLElement): Bios | null { url, ); } catch (error) { - console.error("Error parsing row:", error); - return null; + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Error parsing BIOS row: ${message}`); } } - -export { Bios, getBios }; diff --git a/src/index.ts b/src/index.ts index c58ab41..2f174df 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,18 +34,17 @@ async function main() { ); if (!uploaded) { - throw new Error("Failed to upload updated BIOS list"); + console.error("Failed to upload updated BIOS list"); } // Send notification const message = formatNotificationMessage(newBios); + console.log("Sending notification:", message); const notified = await notify(message); if (!notified.success) { - throw new Error(`Failed to send notification: ${notified.error}`); + console.error(`Failed to send notification: ${notified.error}`); } - - console.log("Successfully updated BIOS list and sent notification"); } else { console.log("No new BIOS versions found"); } diff --git a/src/notify.ts b/src/notify.ts index ba226c1..6f2a28d 100644 --- a/src/notify.ts +++ b/src/notify.ts @@ -6,17 +6,17 @@ const requiredEnvVars = ["SMS_WEBHOOK_URL", "SMS_USER", "SMS_PASS"] as const; // Validate environment variables on module load for (const envVar of requiredEnvVars) { if (!process.env[envVar]) { - throw new Error(`Missing required environment variable: ${envVar}`); + console.error(`Missing required environment variable: ${envVar}`); } } -interface NotificationPayload { +export interface NotificationPayload { msg: string; user: string; pass: string; } -interface NotificationResult { +export interface NotificationResult { success: boolean; error?: string; } @@ -27,7 +27,7 @@ interface NotificationResult { * @returns Promise with the notification result * @throws Error if the message is empty or if environment variables are missing */ -async function notify(msg: string): Promise { +export async function notify(msg: string): Promise { // Input validation if (!msg || msg.trim().length === 0) { return { @@ -37,6 +37,14 @@ async function notify(msg: string): Promise { } const webhookUrl = process.env.SMS_WEBHOOK_URL!; + + if (!webhookUrl) { + return { + success: false, + error: "Missing webhook url", + }; + } + const payload: NotificationPayload = { msg: msg.trim().substring(0, 160), user: process.env.SMS_USER!, @@ -72,5 +80,3 @@ async function notify(msg: string): Promise { }; } } - -export { type NotificationPayload, type NotificationResult, notify }; diff --git a/src/s3.ts b/src/s3.ts index 3a5b6c9..21c1786 100644 --- a/src/s3.ts +++ b/src/s3.ts @@ -8,18 +8,23 @@ const requiredEnvVars = [ "AWS_SECRET_ACCESS_KEY", ] as const; -for (const envVar of requiredEnvVars) { - if (!process.env[envVar]) { - throw new Error(`Missing required environment variable: ${envVar}`); +function initS3(): S3Client | undefined { + for (const envVar of requiredEnvVars) { + if (!process.env[envVar]) { + console.error(`Missing required environment variable: ${envVar}`); + return undefined; + } + + return new S3Client({ + region: process.env.AWS_DEFAULT_REGION!, + endPoint: process.env.AWS_ENDPOINT_URL!, + accessKey: process.env.AWS_ACCESS_KEY_ID!, + secretKey: process.env.AWS_SECRET_ACCESS_KEY!, + }); } } -const s3client = new S3Client({ - region: process.env.AWS_DEFAULT_REGION!, - endPoint: process.env.AWS_ENDPOINT_URL!, - accessKey: process.env.AWS_ACCESS_KEY_ID!, - secretKey: process.env.AWS_SECRET_ACCESS_KEY!, -}); +const s3client: S3Client | undefined = initS3(); /** * get object from S3 @@ -28,14 +33,18 @@ const s3client = new S3Client({ * @returns Promise containing the content * @throws Error if key or bucket is empty */ -async function getObject( +export async function getObject( key: string, bucket: string, -): Promise { +): Promise { if (!key || !bucket) { throw new Error("Key and bucket must not be empty"); } + if (!s3client) { + return undefined; + } + try { const response = await s3client.getObject(key, { bucketName: bucket }); if (!response.ok) { @@ -44,7 +53,7 @@ async function getObject( return response.json(); } catch (error) { console.error(`Unexpected error downloading ${key}:`, error); - return null; + return undefined; } } @@ -57,7 +66,7 @@ async function getObject( * @returns Promise indicating success or failure * @throws Error if key or bucket is empty */ -async function putObject( +export async function putObject( content: Type, key: string, bucket: string, @@ -67,6 +76,10 @@ async function putObject( throw new Error("Key and bucket must not be empty"); } + if (!s3client) { + return false; + } + try { const response = await s3client.putObject( key, @@ -86,5 +99,3 @@ async function putObject( return false; } } - -export { getObject, putObject };