diff --git a/src/index.ts b/src/index.ts index 777ada8..fa6da79 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,111 +1,168 @@ -import { z } from "zod"; +import https from "node:https"; +import { URLSearchParams } from "node:url"; -const messageSchema = z.object({ - /** - * The message to send. - */ - message: z.string(), - /** - * The title of the message. - */ - title: z.string().optional(), - /** - * The URL to open when the notification is clicked. - */ - url: z - .object({ - /** - * The URL itself. - */ - url: z.string(), - /** - * The title of the URL to be displayed. - */ - title: z.string().optional(), - }) - .optional(), - priority: z - .object({ - level: z.number().default(0), - retry: z.number().optional(), - expire: z.number().optional(), - callback: z.string().optional(), - }) - .optional(), - sound: z.string().optional(), - timestamp: z.date().optional(), - html: z.boolean().default(false), - attachment: z - .object({ - data: z.string().or(z.instanceof(File)), - type: z.string(), - }) - .optional(), - users: z.array( - z.object({ - name: z.string(), - token: z.string(), - devices: z.array(z.string()), - }), - ), - appToken: z.string(), -}); +export interface PushoverMessage { + message: string; + title?: string; + link?: { + url: string; + title?: string; + }; + priority?: -2 | -1 | 0 | 1 | 2; + sound?: string; + timestamp?: number; + html?: 0 | 1; + monospace?: 0 | 1; + ttl?: number; +} -export type PushoverMessage = z.infer; +export interface PushoverResponse { + status: number; + request: string; + errors?: string[]; +} -export type Pushover = { - // ################################# - // # Helpers # - // ################################# - - /** - * Get all available sounds. - */ - getSounds: () => { sounds: string[] }; - - /** - * Get all available devices for a user. - */ - getDevices: (userToken: string) => { devices: string[] }; - - // ################################# - // # Sending # - // ################################# - - /** - * Send the notification. - */ - send: () => void; - - // ################################# - // # Options # - // ################################# - withMessage: (message: string) => Pushover; - withTitle: (title: string) => Pushover; - withUrl: (url: { url: string; title: string }) => Pushover; - withPriority: (priority: { - level: number; - retry?: number; - expire?: number; - callback?: string; - }) => Pushover; - withSound: (sound: string) => Pushover; - withTimestamp: (timestamp: number) => Pushover; - withHtml: (enable: boolean) => Pushover; - withAttachment: (attachment: File) => Pushover; - withBase64Attachment: (attachment: { - data: string; - type: string; - }) => Pushover; - addUsers: (users: User[]) => Pushover; -}; - -export type User = { - name: string; +export interface PushoverConfig { token: string; - devices: string[]; -}; + defaultUser?: string; +} -console.log( - "this is indented obnoxiously far and has no semi at the end. (testing previous commits", -); +export interface SendOptions { + recipients?: string | string[]; + device?: string | string[]; + verbose?: boolean; +} + +export class Pushover { + private token: string; + private defaultUser?: string; + private apiUrl = "https://api.pushover.net/1/messages.json"; + + constructor(config: PushoverConfig) { + this.token = config.token; + this.defaultUser = config.defaultUser; + } + + /** + * Send a notification to one or multiple recipients + */ + public async send( + message: PushoverMessage, + options: SendOptions = {}, + ): Promise { + const recipients = this.getRecipients(options); + + if (recipients.length === 0) { + throw new Error( + "No recipients specified. Provide recipients in options or set a defaultUser.", + ); + } + + if (options.verbose) { + console.log("Verbose mode enabled. Logging message and options:"); + console.log(message); + console.log(options); + console.log("----------------------"); + console.log("Sending message..."); + } + + const promises = recipients.map((recipient) => + this.sendToSingleRecipient(message, recipient, options.device), + ); + + const results = await Promise.all(promises); + + // Combine results + const combinedResponse: PushoverResponse[] = results.map( + (result) => result, + ); + + return combinedResponse; + } + + private getRecipients(options: SendOptions): string[] { + const { recipients } = options; + + if (recipients) { + return Array.isArray(recipients) ? recipients : [recipients]; + } + + return this.defaultUser ? [this.defaultUser] : []; + } + + private async sendToSingleRecipient( + message: PushoverMessage, + user: string, + device?: string | string[], + ): Promise { + const params = new URLSearchParams(); + + // Add token and user + params.append("token", this.token); + params.append("user", user); + + // Add message properties + Object.entries(message).forEach(([key, value]) => { + if (value !== undefined) { + if (key === "link") { + if (typeof value === "string") { + params.append("url", value); + return; + } + params.append("url", value.url); + if (value.title) { + params.append("url_title", value.title); + } + return; + } + params.append(key, value.toString()); + } + }); + + // Add device if specified + if (device) { + if (Array.isArray(device)) { + params.append("device", device.join(",")); + } else { + params.append("device", device); + } + } + + return this.makeRequest(params); + } + + private makeRequest(params: URLSearchParams): Promise { + return new Promise((resolve, reject) => { + const options = { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }; + + const req = https.request(this.apiUrl, options, (res) => { + let data = ""; + + res.on("data", (chunk) => { + data += chunk; + }); + + res.on("end", () => { + try { + const response = JSON.parse(data) as PushoverResponse; + resolve(response); + } catch (error) { + reject(new Error(`Failed to parse response: ${data}`)); + } + }); + }); + + req.on("error", (error) => { + reject(error); + }); + + req.write(params.toString()); + req.end(); + }); + } +}