Files
jerapah-flow/plugins/send-sms/script.js
T
nsrbandCursor bd0c11c20c feat(plugins): add joplin-api and send-sms under repo-root plugins/
Load user plugins from /plugins instead of data/plugins so site-specific scripts stay in git and workflows can reference plugin/joplin-api and plugin/send-sms.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 06:34:33 +07:00

180 lines
5.1 KiB
JavaScript

function passContext(ctx) {
if (ctx?.context != null && typeof ctx.context === "object" && !Array.isArray(ctx.context)) {
return { ...ctx.context };
}
return {};
}
function requiredString(value, label) {
if (typeof value !== "string" || value.length === 0) {
throw new Error(`${label} is required`);
}
return value;
}
function pad2(n) {
return String(n).padStart(2, "0");
}
function formatSmsTime(date = new Date()) {
const offsetMinutes = -date.getTimezoneOffset();
const sign = offsetMinutes >= 0 ? "+" : "-";
const absHours = Math.abs(offsetMinutes) / 60;
const tz = Number.isInteger(absHours) ? String(absHours) : String(absHours);
return [
pad2(date.getFullYear() % 100),
pad2(date.getMonth() + 1),
pad2(date.getDate()),
pad2(date.getHours()),
pad2(date.getMinutes()),
pad2(date.getSeconds()),
`${sign}${tz}`,
].join(";");
}
function encodeUtf16BeHex(text) {
let out = "";
for (let i = 0; i < text.length; i++) {
out += text.charCodeAt(i).toString(16).toUpperCase().padStart(4, "0");
}
return out;
}
function isGsm7(text) {
const basic =
"@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>?" +
"¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà";
const ext = new Set(["\f", "^", "{", "}", "\\", "[", "~", "]", "|", "€"]);
for (const ch of text) {
if (!basic.includes(ch) && !ext.has(ch)) return false;
}
return true;
}
function refererFromUrl(url) {
return `${new URL(url).origin}/index.html`;
}
function parseResult(text) {
const match = String(text).match(/\{[^{}]*"result"\s*:\s*"[^"]*"[^{}]*\}/);
if (!match) return { raw: String(text) };
try {
return JSON.parse(match[0]);
} catch {
return { raw: String(text) };
}
}
function leftoverFromError(err) {
let current = err;
for (let i = 0; i < 6 && current; i++) {
if (typeof current.data === "string") return current.data;
current = current.cause;
}
return null;
}
async function ztePost(url, fields) {
const body = new URLSearchParams(fields).toString();
try {
const response = await fetch(url, {
method: "POST",
headers: {
accept: "application/json, text/javascript, */*; q=0.01",
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"x-requested-with": "XMLHttpRequest",
Referer: refererFromUrl(url),
},
body,
});
return parseResult(await response.text());
} catch (err) {
const leftover = leftoverFromError(err);
if (leftover != null) {
const parsed = parseResult(leftover);
log.info({ result: parsed.result ?? parsed }, "send-sms: recovered malformed response");
return parsed;
}
throw err;
}
}
async function sendZteSms(ctx) {
const url = requiredString(ctx.config?.url, "config.url");
const password = requiredString(ctx.config?.password, "config.password");
const to = requiredString(ctx.data?.to ?? ctx.data?.number, "data.to");
const message = requiredString(ctx.data?.message, "data.message");
log.info({ url }, "send-sms: logging in");
const login = await ztePost(url, {
goformId: "LOGIN",
password: btoa(password),
});
const loginResult = login.result;
if (loginResult !== "0" && loginResult !== 0 && loginResult !== "3" && loginResult !== 3 && loginResult !== "success") {
throw new Error(`ZTE login failed: ${JSON.stringify(login.result ?? login)}`);
}
const encodeType = isGsm7(message) ? "GSM7_default" : "UNICODE";
const smsTime = formatSmsTime();
log.info({ url, to, encodeType, messageLength: message.length }, "send-sms: sending");
const sent = await ztePost(url, {
goformId: "SEND_SMS",
notCallback: "true",
Number: to,
sms_time: smsTime,
MessageBody: encodeUtf16BeHex(message),
ID: "-1",
encode_type: encodeType,
});
if (sent.result !== "success" && sent.result !== "0" && sent.result !== 0) {
throw new Error(`ZTE SEND_SMS failed: ${JSON.stringify(sent.result ?? sent)}`);
}
log.info({ to, result: sent.result }, "send-sms: sent");
return {
output: { sent: true, to, result: sent.result },
context: passContext(ctx),
};
}
sendZteSms.meta = {
description: "Log in to a ZTE modem web UI and send an SMS",
previewConfigKey: "url",
tags: ["channel"],
config: {
url: {
type: "string",
required: true,
default: "http://192.168.5.1/reqproc/proc_post",
description: "Modem POST URL (LOGIN and SEND_SMS)",
},
password: {
type: "string",
required: true,
description: "Web UI password (sent as base64)",
},
},
input: {
to: { type: "string", required: true, description: "Recipient phone number" },
message: { type: "string", required: true, description: "SMS text" },
},
output: {
sent: { type: "boolean" },
to: { type: "string" },
result: { type: "string" },
},
example: {
data: {
to: "082134336193",
message: "Test nomor kirim aja dari XL prioritas",
},
config: {
url: "http://192.168.5.1/reqproc/proc_post",
password: "error403",
},
},
};
export default sendZteSms;