OPEN-ISP v0.5 (pickle-lime) — rilis open-source perdana
Aplikasi billing & manajemen ISP (fiber optic) — PWA pelanggan, portal teknisi & kolektor, admin panel lengkap, multi-vendor OLT, RADIUS, WhatsApp/Telegram, pembayaran QRIS. Kredit & lisensi (ISC): - Base kode & repo: alijayanet/billing-rtrw oleh Alijaya Net — terima kasih banyak atas base kode dan repo yang bermanfaat. - Add-on: GoWA, LibreSpeed/OpenSpeedTest, Uptime Kuma, notification-forwarder. - Dikembangkan menjadi OPEN-ISP oleh oxpoe — https://github.com/oxpoe/open-isp
This commit is contained in:
commit
eaf05f2811
280 files changed
+123975
No files matched your search
@@ -0,0 +1,156 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const jimpMod = require('jimp');
|
||||
const Jimp = jimpMod.Jimp || jimpMod;
|
||||
const jsQR = require('jsqr');
|
||||
const QRCode = require('qrcode');
|
||||
const { BinaryBitmap, HybridBinarizer, RGBLuminanceSource, MultiFormatReader, BarcodeFormat, DecodeHintType } = require('@zxing/library');
|
||||
|
||||
function normalizeQrisPayload(raw) {
|
||||
let s = String(raw || '').replace(/[\r\n\t]+/g, '').trim();
|
||||
const idx = s.indexOf('000201');
|
||||
if (idx > 0) s = s.slice(idx);
|
||||
const lastCrc = s.lastIndexOf('6304');
|
||||
if (lastCrc >= 0 && s.length >= lastCrc + 8) {
|
||||
s = s.slice(0, lastCrc + 8);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function crc16CcittFalse(input) {
|
||||
const s = String(input || '');
|
||||
let crc = 0xffff;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
crc ^= (s.charCodeAt(i) & 0xff) << 8;
|
||||
for (let b = 0; b < 8; b++) {
|
||||
if (crc & 0x8000) crc = ((crc << 1) ^ 0x1021) & 0xffff;
|
||||
else crc = (crc << 1) & 0xffff;
|
||||
}
|
||||
}
|
||||
return crc & 0xffff;
|
||||
}
|
||||
|
||||
function parseEmvTlvString(input) {
|
||||
const raw = String(input || '').replace(/[\r\n\t]+/g, '').trim();
|
||||
if (!raw) throw new Error('QRIS payload kosong');
|
||||
if (raw.length < 8) throw new Error('QRIS payload terlalu pendek');
|
||||
|
||||
const items = [];
|
||||
let i = 0;
|
||||
while (i < raw.length) {
|
||||
if (i + 4 > raw.length) throw new Error('QRIS payload TLV tidak valid');
|
||||
const tag = raw.slice(i, i + 2);
|
||||
const lenStr = raw.slice(i + 2, i + 4);
|
||||
if (!/^\d{2}$/.test(lenStr)) throw new Error('QRIS payload TLV length tidak valid');
|
||||
const len = Number(lenStr);
|
||||
const start = i + 4;
|
||||
const end = start + len;
|
||||
if (end > raw.length) throw new Error('QRIS payload TLV length melebihi data');
|
||||
const value = raw.slice(start, end);
|
||||
items.push({ tag, value });
|
||||
i = end;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function buildEmvTlvString(items) {
|
||||
const list = Array.isArray(items) ? items : [];
|
||||
let out = '';
|
||||
for (const it of list) {
|
||||
const tag = String(it?.tag || '');
|
||||
const value = String(it?.value ?? '');
|
||||
const len = value.length;
|
||||
if (!/^\d{2}$/.test(tag)) throw new Error('Tag TLV tidak valid');
|
||||
if (len > 99) throw new Error('TLV length > 99 tidak didukung');
|
||||
out += tag + String(len).padStart(2, '0') + value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function convertStaticQrisToDynamic(staticPayload, amount) {
|
||||
const amt = Math.max(0, Math.floor(Number(amount || 0) || 0));
|
||||
if (!amt) throw new Error('Nominal QRIS dinamis tidak valid');
|
||||
|
||||
const source = parseEmvTlvString(staticPayload)
|
||||
.filter(x => x && x.tag)
|
||||
.map(x => ({ tag: String(x.tag), value: String(x.value ?? '') }));
|
||||
|
||||
const managed = new Set(['54', '55', '56', '57', '63']);
|
||||
const result = [];
|
||||
let amountInserted = false;
|
||||
|
||||
for (const el of source) {
|
||||
if (managed.has(el.tag)) continue;
|
||||
if (el.tag === '01') {
|
||||
result.push({ tag: '01', value: '12' });
|
||||
continue;
|
||||
}
|
||||
if (el.tag === '58' && !amountInserted) {
|
||||
result.push({ tag: '54', value: String(amt) });
|
||||
amountInserted = true;
|
||||
}
|
||||
result.push(el);
|
||||
}
|
||||
|
||||
if (!amountInserted) {
|
||||
result.push({ tag: '54', value: String(amt) });
|
||||
}
|
||||
|
||||
const body = buildEmvTlvString(result);
|
||||
const partial = body + '6304';
|
||||
const crc = crc16CcittFalse(partial).toString(16).toUpperCase().padStart(4, '0');
|
||||
return partial + crc;
|
||||
}
|
||||
|
||||
async function decodeQrisPayloadFromBuffer(buf) {
|
||||
if (!buf || !buf.length) return '';
|
||||
|
||||
// 1. Try jsQR (Fast & reliable for screenshots)
|
||||
try {
|
||||
const img = await Jimp.read(buf);
|
||||
const w = img.bitmap.width;
|
||||
const h = img.bitmap.height;
|
||||
const data = new Uint8ClampedArray(img.bitmap.data);
|
||||
const code = jsQR(data, w, h);
|
||||
if (code && code.data) {
|
||||
const payload = normalizeQrisPayload(code.data);
|
||||
if (payload && payload.startsWith('000201')) return payload;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// 2. Fallback to @zxing/library
|
||||
try {
|
||||
const img = await Jimp.read(buf);
|
||||
const rgba = new Uint8ClampedArray(img.bitmap.data.buffer, img.bitmap.data.byteOffset, img.bitmap.data.byteLength);
|
||||
const source = new RGBLuminanceSource(rgba, img.bitmap.width, img.bitmap.height);
|
||||
const bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
const reader = new MultiFormatReader();
|
||||
const hints = new Map();
|
||||
hints.set(DecodeHintType.POSSIBLE_FORMATS, [BarcodeFormat.QR_CODE]);
|
||||
hints.set(DecodeHintType.TRY_HARDER, true);
|
||||
reader.setHints(hints);
|
||||
const decoded = reader.decode(bitmap, hints);
|
||||
const text = typeof decoded?.getText === 'function' ? decoded.getText() : String(decoded?.text || '');
|
||||
const payload = normalizeQrisPayload(text);
|
||||
if (payload && payload.startsWith('000201')) return payload;
|
||||
} catch (e) {}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
async function buildDynamicQrisJpgBuffer(staticPayload, amount) {
|
||||
const dynamic = convertStaticQrisToDynamic(staticPayload, amount);
|
||||
const png = await QRCode.toBuffer(dynamic, { errorCorrectionLevel: 'M', margin: 1, width: 420, type: 'png' });
|
||||
const img = await Jimp.read(png);
|
||||
return await img.getBuffer('image/jpeg');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
normalizeQrisPayload,
|
||||
crc16CcittFalse,
|
||||
parseEmvTlvString,
|
||||
buildEmvTlvString,
|
||||
convertStaticQrisToDynamic,
|
||||
decodeQrisPayloadFromBuffer,
|
||||
buildDynamicQrisJpgBuffer
|
||||
};
|
||||
Reference in new issue
Block a user