feat: sends message

Message sending is not yet feature complete
Missing:
- attachments
- priority 2 -> retry & expire

Format: text/markdown
Milestone: minor
This commit is contained in:
2025-03-13 02:46:01 +01:00
parent 09645c30e6
commit ac239fd090

View File

@@ -1,111 +1,168 @@
import { z } from "zod"; import https from "node:https";
import { URLSearchParams } from "node:url";
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 interface PushoverResponse {
status: number;
request: string;
errors?: string[];
}
export interface PushoverConfig {
token: string;
defaultUser?: string;
}
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;
}
const messageSchema = z.object({
/** /**
* The message to send. * Send a notification to one or multiple recipients
*/ */
message: z.string(), public async send(
/** message: PushoverMessage,
* The title of the message. options: SendOptions = {},
*/ ): Promise<PushoverResponse[]> {
title: z.string().optional(), const recipients = this.getRecipients(options);
/**
* The URL to open when the notification is clicked. if (recipients.length === 0) {
*/ throw new Error(
url: z "No recipients specified. Provide recipients in options or set a defaultUser.",
.object({ );
/** }
* The URL itself.
*/ if (options.verbose) {
url: z.string(), console.log("Verbose mode enabled. Logging message and options:");
/** console.log(message);
* The title of the URL to be displayed. console.log(options);
*/ console.log("----------------------");
title: z.string().optional(), console.log("Sending message...");
}) }
.optional(),
priority: z const promises = recipients.map((recipient) =>
.object({ this.sendToSingleRecipient(message, recipient, options.device),
level: z.number().default(0), );
retry: z.number().optional(),
expire: z.number().optional(), const results = await Promise.all(promises);
callback: z.string().optional(),
}) // Combine results
.optional(), const combinedResponse: PushoverResponse[] = results.map(
sound: z.string().optional(), (result) => result,
timestamp: z.date().optional(), );
html: z.boolean().default(false),
attachment: z return combinedResponse;
.object({ }
data: z.string().or(z.instanceof(File)),
type: z.string(), private getRecipients(options: SendOptions): string[] {
}) const { recipients } = options;
.optional(),
users: z.array( if (recipients) {
z.object({ return Array.isArray(recipients) ? recipients : [recipients];
name: z.string(), }
token: z.string(),
devices: z.array(z.string()), return this.defaultUser ? [this.defaultUser] : [];
}), }
),
appToken: z.string(), private async sendToSingleRecipient(
message: PushoverMessage,
user: string,
device?: string | string[],
): Promise<PushoverResponse> {
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());
}
}); });
export type PushoverMessage = z.infer<typeof messageSchema>; // Add device if specified
if (device) {
if (Array.isArray(device)) {
params.append("device", device.join(","));
} else {
params.append("device", device);
}
}
export type Pushover = { return this.makeRequest(params);
// ################################# }
// # Helpers #
// #################################
/** private makeRequest(params: URLSearchParams): Promise<PushoverResponse> {
* Get all available sounds. return new Promise((resolve, reject) => {
*/ const options = {
getSounds: () => { sounds: string[] }; method: "POST",
headers: {
/** "Content-Type": "application/x-www-form-urlencoded",
* 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 = { const req = https.request(this.apiUrl, options, (res) => {
name: string; let data = "";
token: string;
devices: string[];
};
console.log( res.on("data", (chunk) => {
"this is indented obnoxiously far and has no semi at the end. (testing previous commits", 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();
});
}
}