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>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": "send-sms",
|
||||
"name": "Send SMS",
|
||||
"version": "0.1.0",
|
||||
"jerapah": ">=0.1.0 <1.0.0",
|
||||
"main": "script.js",
|
||||
"description": "Log in to a ZTE modem web UI and send an SMS"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "jflow-plugin-send-sms",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "JerapahFlow plugin: send SMS via a ZTE modem web UI"
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user