infoalloggi-monorepo/apps/infoalloggi/src/server/controllers/skebby.controller.ts

173 lines
5.2 KiB
TypeScript
Raw Normal View History

import { env } from "~/env";
2025-08-04 17:45:44 +02:00
import { GetFlagValueHandler } from "~/server/controllers/flags.controller";
import { db } from "~/server/db";
2025-08-28 18:27:07 +02:00
import { NewMail } from "~/server/services/mailer";
2025-08-04 17:45:44 +02:00
const BASEURL = "https://api.skebby.it/API/v1.0/REST/";
enum MessageType {
2025-08-28 18:27:07 +02:00
HIGH_QUALITY = "GP",
MEDIUM_QUALITY = "TI",
LOW_QUALITY = "SI",
2025-08-04 17:45:44 +02:00
}
interface Auth {
2025-08-28 18:27:07 +02:00
user_key: string;
session_key: string;
2025-08-04 17:45:44 +02:00
}
interface SMSData {
2025-08-28 18:27:07 +02:00
/** The type of SMS */
message_type: MessageType;
/** The body of the message. *Message max length could be 160 chars when using low-quality SMSs. New line char needs to be codified as “\n”. */
message: string;
/** A list of recipents phone numbers. The recipients numbers must be in the international format (+393471234567 or 00393471234567) */
recipient: string[];
/** The Sender name. If the message type allows a custom TPOA and the field is left empty, the users preferred TPOA is used. Must be empty if the message type does not allow a custom TPOA */
sender?: string;
/** The messages will be sent at the given scheduled time
* formats: ddMMyy, yyyyMMdd, ddMMyyHHmm, yyyyMMddHHmmss, yyyy-MM-dd HH:mm, yyyy-MM-dd HH:mm.0 */
scheduled_delivery_time?: string;
/** IANA time zone: Europe/Rome, America/New_York, etc. */
scheduled_delivery_timezone?: string;
/** Specifies a custom order ID
* optional, max 32 characters */
order_id?: string;
/** Returns the number of used sms credits. */
returnCredits?: boolean;
/** Returns the number of remaining SMS credits for the quality used in the sent SMS and for the default user nation */
returnRemaining?: boolean;
/** if true, allows sending to invalid recipients */
allowInvalidRecipients?: boolean;
/** default is "gsm" */
encoding?: "gsm" | "ucs2";
id_landing?: number;
/** optional, max 64 characters */
campaign_name?: string;
/** max number of fragments for the message, default is 7 */
max_fragments?: number;
/** if true, truncates the message to fit the max fragments , default is true */
truncate?: boolean;
/** Period after which the SMS message will be deleted from the SMS center. */
validity_period_min?: number;
richsms_url?: string;
2025-08-04 17:45:44 +02:00
}
export type MinimalSMSData = Omit<SMSData, "message_type" | "sender">;
interface SMSResponse {
2025-08-28 18:27:07 +02:00
result: unknown; // or "OK" | any if you're less strict
[key: string]: unknown; // allow additional unknown keys
2025-08-04 17:45:44 +02:00
}
/**
* Authenticates the user given their username and password.
*/
async function login(username: string, password: string): Promise<Auth> {
2025-08-28 18:27:07 +02:00
const url = `${BASEURL}login?username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`;
const response = await fetch(url);
if (response.status === 401) {
throw new Error("Authentication failed: Invalid username or password");
}
if (response.status === 400) {
throw new Error(`Error in request: ${await response.text()}`);
}
if (!response.ok || response.status !== 200) {
throw new Error(`Login failed: ${response.status} ${response.statusText}`);
}
const text = await response.text();
const [user_key, session_key] = text.split(";");
if (!user_key || !session_key) {
throw new Error("Invalid login response format");
}
2025-08-29 16:18:32 +02:00
return { session_key, user_key };
2025-08-04 17:45:44 +02:00
}
/**
* Sends an SMS message
*/
async function sendSMS(auth: Auth, smsData: SMSData) {
2025-08-28 18:27:07 +02:00
const url = `${BASEURL}sms`;
const response = await fetch(url, {
2025-08-29 16:18:32 +02:00
body: JSON.stringify(smsData),
2025-08-28 18:27:07 +02:00
headers: {
"Content-Type": "application/json",
Session_key: auth.session_key,
2025-08-29 16:18:32 +02:00
user_key: auth.user_key,
2025-08-28 18:27:07 +02:00
},
2025-08-29 16:18:32 +02:00
method: "POST",
2025-08-28 18:27:07 +02:00
});
if (response.status === 400) {
throw new Error(`Invalid input: ${await response.text()}`);
}
if (response.status === 401) {
throw new Error("Authentication failed: Invalid user key or session key");
}
if (response.status === 404) {
throw new Error("Not Found: The User_key was not found");
}
if (response.status !== 201) {
const errorText = await response.text();
throw new Error(`SMS send failed: ${response.status} ${errorText}`);
}
const data = (await response.json()) as SMSResponse;
if (data.result !== "OK") {
throw new Error("SMS send returned non-OK result");
}
2025-08-04 17:45:44 +02:00
}
// Example usage
/*const smsData: SMSData = {
recipient: ["+393407246729"],
message: "Hello world!",
message_type: MessageType.HIGH_QUALITY,
sender: "InfoAlloggi",
};*/
export const SendMessage = async (data: MinimalSMSData) => {
2025-08-28 18:27:07 +02:00
const smsFlag = await GetFlagValueHandler({
db: db,
id: "SMS_ENABLED",
2025-08-28 18:27:07 +02:00
});
if (smsFlag) {
try {
const auth = await login(env.SKEBBY_USER, env.SKEBBY_PASS);
await sendSMS(auth, {
...data,
message_type: MessageType.HIGH_QUALITY,
sender: "InfoAlloggi",
});
} catch (error) {
console.error("Error:", error);
await NewMail({
template: {
mailType: "generic",
props: {
testo: `Errore durante l'invio dell'SMS: to:${data.recipient.join(", ")} Mgs:${data.message}; ${error instanceof Error ? error.message : String(error)}`,
title: "Errore invio SMS",
},
},
mail: {
subject: "Errore invio SMS",
to: "web@infoalloggi.it",
},
});
}
} else {
2025-08-28 18:27:07 +02:00
console.warn(
`SMS sending is disabled to:${data.recipient.join(", ")} Mgs:${data.message}`,
);
return {
message: `SMS sending is disabled: to:${data.recipient.join(", ")} Mgs:${data.message}`,
2025-08-29 16:18:32 +02:00
success: true,
2025-08-28 18:27:07 +02:00
};
}
2025-08-04 17:45:44 +02:00
};