fixed racecard one
This commit is contained in:
parent
a1db4b228b
commit
ff1ab34c85
547
btinfo.js
547
btinfo.js
@ -1,5 +1,3 @@
|
||||
// server.js — robust HTTP→TCP bridge for ABS (ABS_POLL, LOG, BTI, RPI)
|
||||
// Node 18+ (CommonJS)
|
||||
|
||||
const express = require("express");
|
||||
const net = require("net");
|
||||
@ -21,8 +19,52 @@ const DEFAULT_TIMEOUT_MS = Number(process.env.BRIDGE_TIMEOUT_MS || 20000);
|
||||
const RETRIES = Number(process.env.BRIDGE_RETRIES || 2);
|
||||
const RETRY_DELAY_MS = Number(process.env.BRIDGE_RETRY_DELAY_MS || 500);
|
||||
|
||||
const MAX_VENUES = 10;
|
||||
const MAX_RACES = 14;
|
||||
const MAX_POOLS = 8;
|
||||
const MAX_HORSES = 24;
|
||||
|
||||
const app = express();
|
||||
|
||||
// Venue and pool mappings
|
||||
const venueMapping = {
|
||||
1: "BLR", 2: "MUM", 3: "MAA", 4: "CAL", 5: "HYD",
|
||||
6: "MYS", 7: "OTY", 8: "PUN", 9: "GYM", 10: "OVS"
|
||||
};
|
||||
|
||||
const poolMapping = {
|
||||
1: "WNP", 2: "PLP", 3: "SHP", 4: "THP", 5: "QNP",
|
||||
6: "FRP", 7: "TNP", 8: "EXP", 9: "TBP", 10: "MJP", 11: "JPP"
|
||||
};
|
||||
|
||||
const multiLegPoolMapping = {
|
||||
"T1": "TBP", "T2": "TBP", "T3": "TBP",
|
||||
"M1": "MJP", "M2": "MJP", "M3": "MJP",
|
||||
"J1": "JPP", "J2": "JPP", "J3": "JPP"
|
||||
};
|
||||
|
||||
function getDateStr(nDate) {
|
||||
const s = nDate.toString().padStart(8, "0");
|
||||
return `${s.slice(0, 4)}/${s.slice(4, 6)}/${s.slice(6, 8)}`;
|
||||
}
|
||||
|
||||
function getTimeStr(nTime) {
|
||||
const s = nTime.toString().padStart(6, "0");
|
||||
return `${s.slice(0, 2)}:${s.slice(2, 4)}:${s.slice(4, 6)}`;
|
||||
}
|
||||
|
||||
function getVenueIdStr(nVenue) {
|
||||
return venueMapping[nVenue] || "";
|
||||
}
|
||||
|
||||
function getPoolIdStr(nPoolId) {
|
||||
return poolMapping[nPoolId] || "";
|
||||
}
|
||||
|
||||
function getMultiLegPoolIdStr(szMultiLegRace) {
|
||||
return multiLegPoolMapping[szMultiLegRace] || "";
|
||||
}
|
||||
|
||||
function readJsonBody(req) {
|
||||
return new Promise((resolve) => {
|
||||
const chunks = [];
|
||||
@ -36,12 +78,12 @@ function readJsonBody(req) {
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
function writeFixedAscii(buf, offset, s, len) {
|
||||
const str = (s ?? "").toString();
|
||||
for (let i = 0; i < len; i++) buf[offset + i] = i < str.length ? (str.charCodeAt(i) & 0xff) : 0x00;
|
||||
return offset + len;
|
||||
}
|
||||
|
||||
function readFixedAscii(buf, offset, len) {
|
||||
let end = offset;
|
||||
const max = offset + len;
|
||||
@ -49,14 +91,65 @@ function readFixedAscii(buf, offset, len) {
|
||||
const raw = buf.subarray(offset, end).toString("ascii");
|
||||
return { value: raw.trimEnd(), next: offset + len };
|
||||
}
|
||||
|
||||
function hexSlice(buf, off, len) {
|
||||
const s = Math.max(0, off);
|
||||
const e = Math.min(buf.length, off + len);
|
||||
return buf.subarray(s, e).toString("hex");
|
||||
}
|
||||
function remaining(buf, off){ return Math.max(0, buf.length - off); }
|
||||
|
||||
// ---------- packetization ----------
|
||||
function remaining(buf, off) {
|
||||
return Math.max(0, buf.length - off);
|
||||
}
|
||||
|
||||
function safeAdvanceOrBreak(buf, off, want, outObj) {
|
||||
if (remaining(buf, off) < want) {
|
||||
outObj._truncated = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function looksLikeHeaderAt(buf, off, expected_nTxnCd = null) {
|
||||
if (remaining(buf, off) < 18) return false;
|
||||
try {
|
||||
const nTxnCd = buf.readInt32LE(off);
|
||||
const nRetCd = buf.readInt32LE(off + 4);
|
||||
const nNumRecs = buf.readInt32LE(off + 8);
|
||||
// Allow nTxnCd: 100 as a fallback for any section, since the server may send it
|
||||
const validTxnCd = expected_nTxnCd ? (nTxnCd === expected_nTxnCd || nTxnCd === 100) : (nTxnCd >= 100 && nTxnCd <= 105);
|
||||
return validTxnCd && nRetCd >= 0 && nRetCd < 10000 && nNumRecs >= 0 && nNumRecs < 100;
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
function alignToPossibleHeader(reply, off, expected_nTxnCd = null) {
|
||||
// Try current offset and additional offsets for alignment
|
||||
const tryOffsets = [0, 2, 4, -4, 8, 12];
|
||||
for (const d of tryOffsets) {
|
||||
const o = off + d;
|
||||
if (o >= 0 && looksLikeHeaderAt(reply, o, expected_nTxnCd)) return o;
|
||||
}
|
||||
// Fallback: align to valid nRaceDt (20000101–21001231)
|
||||
for (const d of tryOffsets) {
|
||||
const o = off + d;
|
||||
if (o >= 0 && remaining(reply, o) >= 4) {
|
||||
const nRaceDt = reply.readInt32LE(o);
|
||||
if (nRaceDt >= 20000101 && nRaceDt <= 21001231) return o;
|
||||
}
|
||||
}
|
||||
return off;
|
||||
}
|
||||
|
||||
function adjustForOptionalOp(replyBuf, bodyOffset) {
|
||||
if (replyBuf.length >= bodyOffset + 4) {
|
||||
const maybeOp = replyBuf.readInt32LE(bodyOffset);
|
||||
if (maybeOp === LOGBT || maybeOp === LOG || maybeOp === 0 || (maybeOp >= 100 && maybeOp <= 10000)) {
|
||||
return { offset: bodyOffset + 4, extraOp: maybeOp };
|
||||
}
|
||||
}
|
||||
return { offset: bodyOffset, extraOp: null };
|
||||
}
|
||||
|
||||
function absSend(sock, payload) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let off = 0;
|
||||
@ -82,6 +175,7 @@ function absSend(sock, payload) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function absRecv(sock, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
@ -110,6 +204,7 @@ function absRecv(sock, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
||||
sock.on("end", () => done(new Error("Socket ended before final '0' packet")));
|
||||
});
|
||||
}
|
||||
|
||||
async function absRoundtrip(payload, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
||||
const sock = new net.Socket();
|
||||
await new Promise((res, rej) => sock.connect(ABS_PORT, ABS_HOST, res).once("error", rej));
|
||||
@ -123,16 +218,16 @@ async function absRoundtrip(payload, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function withRetry(fn, tries = RETRIES, delayMs = RETRY_DELAY_MS) {
|
||||
let lastErr;
|
||||
for (let i=0;i<tries;i++){
|
||||
for (let i = 0; i < tries; i++) {
|
||||
try { return await fn(); }
|
||||
catch (e){ lastErr = e; if (i+1<tries) await new Promise(r=>setTimeout(r, delayMs)); }
|
||||
catch (e) { lastErr = e; if (i + 1 < tries) await new Promise(r => setTimeout(r, delayMs)); }
|
||||
}
|
||||
throw lastErr;
|
||||
}
|
||||
|
||||
// ---------- MFC-style password "encryption" ----------
|
||||
function encryptPasswordLikeMFC(plain) {
|
||||
const sec = new Date().getUTCSeconds();
|
||||
const key = Number(String(sec)[0] || "0");
|
||||
@ -146,7 +241,6 @@ function encryptPasswordLikeMFC(plain) {
|
||||
return { enc: out + String(key), key, utcSeconds: sec };
|
||||
}
|
||||
|
||||
// ---------- headers ----------
|
||||
function packSndHeader({ nTxnCd, nOpCd = 0, nNumRecsSent = 0, nNumRecsRqrd = 0, nTxnId = 0, cBtMake = 0 }) {
|
||||
const b = Buffer.alloc(24, 0);
|
||||
let o = 0;
|
||||
@ -159,7 +253,6 @@ function packSndHeader({ nTxnCd, nOpCd = 0, nNumRecsSent = 0, nNumRecsRqrd = 0,
|
||||
return b;
|
||||
}
|
||||
|
||||
/** Smart header parser: test 18 & 24, choose plausible. */
|
||||
function parseRcvHeaderFlexible(buf) {
|
||||
if (buf.length < 18) throw new Error(`Reply too short for RcvHeader: ${buf.length} bytes`);
|
||||
function readAs(len) {
|
||||
@ -172,17 +265,18 @@ function parseRcvHeaderFlexible(buf) {
|
||||
return { nTxnCd, nRetCd, nNumRecs, nTxnId, cBtMake, size: len };
|
||||
}
|
||||
const h18 = readAs(18);
|
||||
const ok18 = (h18.nTxnCd >= 50 && h18.nTxnCd < 10000) && (h18.nRetCd >= 0 && h18.nRetCd < 10000);
|
||||
let h24=null, ok24=false;
|
||||
const ok18 = (h18.nTxnCd >= 50 && h18.nTxnCd < 10000) && (h18.nRetCd >= 0 && h18.nRetCd < 10000) && (h18.nNumRecs >= 0 && h18.nNumRecs < 100);
|
||||
let h24 = null, ok24 = false;
|
||||
if (buf.length >= 24) {
|
||||
h24 = readAs(24);
|
||||
ok24 = (h24.nTxnCd >= 50 && h24.nTxnCd < 10000) && (h24.nRetCd >= 0 && h24.nRetCd < 10000);
|
||||
ok24 = (h24.nTxnCd >= 50 && h24.nTxnCd < 10000) && (h24.nRetCd >= 0 && h24.nRetCd < 10000) && (h24.nNumRecs >= 0 && h24.nNumRecs < 100);
|
||||
}
|
||||
if (ok18 && !ok24) return h18;
|
||||
if (!ok18 && ok24) return h24;
|
||||
if (ok18 && ok24) return h18; // prefer 18 to avoid over-advancing
|
||||
if (ok18 && ok24) return h18;
|
||||
return h18;
|
||||
}
|
||||
|
||||
function buildRcvHeader18({ nTxnCd, nRetCd, nNumRecs = 0, nTxnId = 0, cBtMake = 0 }) {
|
||||
const b = Buffer.alloc(18, 0);
|
||||
let o = 0;
|
||||
@ -194,61 +288,19 @@ function buildRcvHeader18({ nTxnCd, nRetCd, nNumRecs = 0, nTxnId = 0, cBtMake =
|
||||
return b;
|
||||
}
|
||||
|
||||
// ---------- aligners / detectors ----------
|
||||
function looksLikeHeaderAt(buf, off){
|
||||
if (remaining(buf, off) < 18) return false;
|
||||
try {
|
||||
const nTxnCd = buf.readInt32LE(off);
|
||||
return (nTxnCd >= 50 && nTxnCd < 10000);
|
||||
} catch { return false; }
|
||||
}
|
||||
/** align to plausible header; try +0 / +4 */
|
||||
function alignToPossibleHeader(reply, off) {
|
||||
if (looksLikeHeaderAt(reply, off)) return off;
|
||||
if (remaining(reply, off) >= 22 && looksLikeHeaderAt(reply, off + 4)) return off + 4;
|
||||
return off;
|
||||
}
|
||||
/** align to where ASCII year "20" begins for login body */
|
||||
function alignToAsciiYear(reply, off) {
|
||||
const tryOffsets = [0, 2, 4, -4];
|
||||
const tryOffsets = [0, 2, 4, -4, 8, 12];
|
||||
for (const d of tryOffsets) {
|
||||
const o = off + d;
|
||||
if (o >= 0 && o + 1 < reply.length) {
|
||||
const b0 = reply[o], b1 = reply[o+1];
|
||||
if (b0 === 0x32 && b1 === 0x30) return o; // '2''0'
|
||||
if (reply[o] === 0x2f && o >= 4 && reply[o-4] === 0x32 && reply[o-3] === 0x30) return o-4;
|
||||
const b0 = reply[o], b1 = reply[o + 1];
|
||||
if (b0 === 0x32 && b1 === 0x30) return o;
|
||||
if (reply[o] === 0x2f && o >= 4 && reply[o - 4] === 0x32 && reply[o - 3] === 0x30) return o - 4;
|
||||
}
|
||||
}
|
||||
return off;
|
||||
}
|
||||
|
||||
function adjustForOptionalOp(replyBuf, bodyOffset) {
|
||||
if (replyBuf.length >= bodyOffset + 4) {
|
||||
const maybeOp = replyBuf.readInt32LE(bodyOffset);
|
||||
if (maybeOp === LOGBT || maybeOp === LOG || maybeOp === 0 || (maybeOp >= 100 && maybeOp <= 10000)) {
|
||||
return { offset: bodyOffset + 4, extraOp: maybeOp };
|
||||
} else {
|
||||
const looksLikeYear = replyBuf[bodyOffset] === 0x32 && replyBuf[bodyOffset + 1] === 0x30;
|
||||
const looksLikeYearFwd = replyBuf[bodyOffset + 4] === 0x32 && replyBuf[bodyOffset + 5] === 0x30;
|
||||
if (!looksLikeYear && looksLikeYearFwd) return { offset: bodyOffset + 4, extraOp: null };
|
||||
}
|
||||
}
|
||||
if (bodyOffset >= 4 && replyBuf[bodyOffset] === 0x2f) {
|
||||
const couldBeYear = replyBuf[bodyOffset - 4] === 0x32 && replyBuf[bodyOffset - 3] === 0x30;
|
||||
if (couldBeYear) return { offset: bodyOffset - 4, extraOp: null };
|
||||
}
|
||||
return { offset: bodyOffset, extraOp: null };
|
||||
}
|
||||
|
||||
function safeAdvanceOrBreak(buf, off, want, outObj){
|
||||
if (remaining(buf, off) < want){
|
||||
outObj._truncated = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------- parsers ----------
|
||||
function packSndLog({ usrId = "", opCard, passwd, btId }) {
|
||||
const b = Buffer.alloc(38, 0);
|
||||
let o = 0;
|
||||
@ -258,14 +310,15 @@ function packSndLog({ usrId = "", opCard, passwd, btId }) {
|
||||
o = writeFixedAscii(b, o, btId, 5);
|
||||
return b;
|
||||
}
|
||||
|
||||
function parseRcvLog(buf, offset = 0) {
|
||||
let o = offset;
|
||||
const t1 = readFixedAscii(buf, o, 20); const cDateTime = t1.value; o = t1.next;
|
||||
const t2 = readFixedAscii(buf, o, 31); const cUsrNm = t2.value; o = t2.next;
|
||||
const t3 = readFixedAscii(buf, o, 5 ); const cUsrId = t3.value; o = t3.next;
|
||||
const t3 = readFixedAscii(buf, o, 5); const cUsrId = t3.value; o = t3.next;
|
||||
const t4 = readFixedAscii(buf, o, 31); const cSupNm = t4.value; o = t4.next;
|
||||
const t5 = readFixedAscii(buf, o, 5 ); const cSupId = t5.value; o = t5.next;
|
||||
const t6 = readFixedAscii(buf, o, 5 ); const cUsrTyp = t6.value; o = t6.next;
|
||||
const t5 = readFixedAscii(buf, o, 5); const cSupId = t5.value; o = t5.next;
|
||||
const t6 = readFixedAscii(buf, o, 5); const cUsrTyp = t6.value; o = t6.next;
|
||||
|
||||
function rf() { const v = buf.readFloatLE(o); o += 4; return v; }
|
||||
function ri() { const v = buf.readInt32LE(o); o += 4; return v; }
|
||||
@ -310,77 +363,24 @@ function parseRcvLog(buf, offset = 0) {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
// function parseRcvBtiMst(buf, offset) {
|
||||
// let o = offset;
|
||||
|
||||
// const a1 = readFixedAscii(buf, o, 5); const cBtId = a1.value; o = a1.next;
|
||||
// const a2 = readFixedAscii(buf, o, 31); const cBtName = a2.value; o = a2.next;
|
||||
// const a3 = readFixedAscii(buf, o, 5); const cEnclCd = a3.value; o = a3.next;
|
||||
// const a4 = readFixedAscii(buf, o, 4); const cGrpCd = a4.value; o = a4.next;
|
||||
|
||||
// const cBtMode = String.fromCharCode(buf[o]); o += 1;
|
||||
|
||||
// const a5 = readFixedAscii(buf, o, 4); const cMoneyTyp = a5.value; o = a5.next;
|
||||
|
||||
// // Debug: raw hex of these fields
|
||||
// console.error("fMinSaleBet raw:", buf.subarray(o, o+4).toString("hex"));
|
||||
// const fMinSaleBet = buf.readInt32LE(o); o += 4;
|
||||
|
||||
// console.error("fMaxSaleBet raw:", buf.subarray(o, o+4).toString("hex"));
|
||||
// const fMaxSaleBet = buf.readInt32LE(o); o += 4;
|
||||
|
||||
// console.error("fPayMaxAmt raw:", buf.subarray(o, o+4).toString("hex"));
|
||||
// const fPayMaxAmt = buf.readInt32LE(o); o += 4;
|
||||
|
||||
// return {
|
||||
// cBtId, cBtName, cEnclCd, cGrpCd,
|
||||
// cBtMode, cMoneyTyp,
|
||||
// fMinSaleBet, fMaxSaleBet, fPayMaxAmt,
|
||||
// size: o - offset
|
||||
// };
|
||||
// }
|
||||
|
||||
|
||||
function parseRcvBtiMst(buf, offset) {
|
||||
let o = offset;
|
||||
|
||||
const a1 = readFixedAscii(buf, o, 5); const cBtId = a1.value; o = a1.next;
|
||||
const a2 = readFixedAscii(buf, o, 31); const cBtName = a2.value; o = a2.next;
|
||||
const a3 = readFixedAscii(buf, o, 5); const cEnclCd = a3.value; o = a3.next;
|
||||
const a4 = readFixedAscii(buf, o, 4); const cGrpCd = a4.value; o = a4.next;
|
||||
|
||||
const cBtMode = String.fromCharCode(buf[o]); o += 1;
|
||||
|
||||
const a5 = readFixedAscii(buf, o, 4); const cMoneyTyp = a5.value; o = a5.next;
|
||||
|
||||
// align to 4-byte boundary (for float fields)
|
||||
o = (o + 3) & ~3;
|
||||
|
||||
console.error("fMinSaleBet raw:", buf.subarray(o, o+4).toString("hex"));
|
||||
const fMinSaleBet = buf.readFloatLE(o); o += 4;
|
||||
|
||||
console.error("fMaxSaleBet raw:", buf.subarray(o, o+4).toString("hex"));
|
||||
const fMaxSaleBet = buf.readFloatLE(o); o += 4;
|
||||
|
||||
console.error("fPayMaxAmt raw:", buf.subarray(o, o+4).toString("hex"));
|
||||
const fPayMaxAmt = buf.readFloatLE(o); o += 4;
|
||||
|
||||
return {
|
||||
cBtId, cBtName, cEnclCd, cGrpCd,
|
||||
cBtMode, cMoneyTyp,
|
||||
fMinSaleBet, fMaxSaleBet, fPayMaxAmt,
|
||||
size: o - offset
|
||||
};
|
||||
return { cBtId, cBtName, cEnclCd, cGrpCd, cBtMode, cMoneyTyp, fMinSaleBet, fMaxSaleBet, fPayMaxAmt, size: o - offset };
|
||||
}
|
||||
|
||||
|
||||
function parseRcvBtiDtl(buf, offset) {
|
||||
let o = offset;
|
||||
const cDtlTyp = String.fromCharCode(buf[o]); o += 1;
|
||||
|
||||
// If not enough bytes to try the happy path, take what's available
|
||||
if (remaining(buf, o) < 4) {
|
||||
const avail = Math.max(0, remaining(buf, o));
|
||||
const aRem = readFixedAscii(buf, o, avail);
|
||||
@ -390,23 +390,16 @@ function parseRcvBtiDtl(buf, offset) {
|
||||
if (o < buf.length) o += 1;
|
||||
return { cDtlTyp, cPoolOrVenue, cDtlSts, size: o - offset };
|
||||
}
|
||||
|
||||
// Try 3-byte pool/venue first (most common)
|
||||
const a3 = readFixedAscii(buf, o, 3);
|
||||
const cPoolOrVenue3 = a3.value;
|
||||
const after3 = a3.next;
|
||||
|
||||
// Candidate status byte (must be printable ASCII — typical statuses are letters/digits)
|
||||
const statusCandidate = buf[after3];
|
||||
const isPrintable = (statusCandidate >= 0x20 && statusCandidate <= 0x7E);
|
||||
|
||||
if (isPrintable) {
|
||||
o = after3;
|
||||
const cDtlSts = String.fromCharCode(buf[o]); o += 1;
|
||||
return { cDtlTyp, cPoolOrVenue: cPoolOrVenue3, cDtlSts, size: o - offset };
|
||||
}
|
||||
|
||||
// Fallback: read 4-byte pool/venue (older/odd layouts)
|
||||
const a4 = readFixedAscii(buf, o, 4);
|
||||
const cPoolOrVenue4 = a4.value;
|
||||
o = a4.next;
|
||||
@ -415,10 +408,6 @@ function parseRcvBtiDtl(buf, offset) {
|
||||
return { cDtlTyp, cPoolOrVenue: cPoolOrVenue4, cDtlSts, size: o - offset };
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function parseRcvRpi1(buf, offset) {
|
||||
let o = offset;
|
||||
const nRaceDt = buf.readInt32LE(o); o += 4;
|
||||
@ -428,6 +417,7 @@ function parseRcvRpi1(buf, offset) {
|
||||
const aAd = readFixedAscii(buf, o, 41); const cAdvertisement = aAd.value; o = aAd.next;
|
||||
return { nRaceDt, cVenueNum, cVenueSts, cRaces, cAdvertisement, size: o - offset };
|
||||
}
|
||||
|
||||
function parseRcvRpi2(buf, offset) {
|
||||
let o = offset;
|
||||
const nRaceDt = buf.readInt32LE(o); o += 4;
|
||||
@ -437,6 +427,7 @@ function parseRcvRpi2(buf, offset) {
|
||||
const nRaceStartTm = buf.readInt32LE(o); o += 4;
|
||||
return { nRaceDt, cVenueNum, cRaceNum, cHorses, nRaceStartTm, size: o - offset };
|
||||
}
|
||||
|
||||
function parseRcvRpi3(buf, offset) {
|
||||
let o = offset;
|
||||
const nRaceDt = buf.readInt32LE(o); o += 4;
|
||||
@ -445,6 +436,7 @@ function parseRcvRpi3(buf, offset) {
|
||||
const aP = readFixedAscii(buf, o, 9); const cPools = aP.value; o = aP.next;
|
||||
return { nRaceDt, cVenueNum, cRaceNum, cPools, size: o - offset };
|
||||
}
|
||||
|
||||
function parseRcvRpi4(buf, offset) {
|
||||
let o = offset;
|
||||
const nRaceDt = buf.readInt32LE(o); o += 4;
|
||||
@ -454,6 +446,7 @@ function parseRcvRpi4(buf, offset) {
|
||||
const cPoolSts = String.fromCharCode(buf[o]); o += 1;
|
||||
return { nRaceDt, cVenueNum, cRaceNum, cSelectRaceNum, cPoolSts, size: o - offset };
|
||||
}
|
||||
|
||||
function parseRcvPvm(buf, offset) {
|
||||
let o = offset;
|
||||
const a1 = readFixedAscii(buf, o, 4); const cRaceVenue = a1.value; o = a1.next;
|
||||
@ -465,8 +458,8 @@ function parseRcvPvm(buf, offset) {
|
||||
return { cRaceVenue, cPoolId, fUnitPrice, fTaxRate, fDeductRate, cRemarks, size: o - offset };
|
||||
}
|
||||
|
||||
// ---------- routes ----------
|
||||
app.get("/", (_req, res) => res.send("ABS bridge is up"));
|
||||
|
||||
app.get("/health", (_req, res) => {
|
||||
const s = new net.Socket();
|
||||
s.setTimeout(1500);
|
||||
@ -475,13 +468,12 @@ app.get("/health", (_req, res) => {
|
||||
s.on("error", (e) => { s.destroy(); res.status(502).json({ ok: false, error: String(e) }); });
|
||||
});
|
||||
|
||||
// ---------- ABS_POLL ----------
|
||||
app.post("/abs/poll", async (req, res) => {
|
||||
try {
|
||||
const body = await readJsonBody(req);
|
||||
const btMake = (typeof body.btMake === "string")
|
||||
? body.btMake.charCodeAt(0)
|
||||
: (Number.isFinite(body.btMake) ? (body.btMake|0) : 0x00);
|
||||
: (Number.isFinite(body.btMake) ? (body.btMake | 0) : 0x00);
|
||||
|
||||
const header = packSndHeader({ nTxnCd: ABS_POLL, nOpCd: 0, nNumRecsSent: 0, nNumRecsRqrd: 0, nTxnId: 0, cBtMake: btMake });
|
||||
|
||||
@ -504,7 +496,6 @@ app.post("/abs/poll", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ---------- LOGIN ----------
|
||||
app.post("/login", async (req, res) => {
|
||||
try {
|
||||
const body = await readJsonBody(req);
|
||||
@ -523,7 +514,7 @@ app.post("/login", async (req, res) => {
|
||||
|
||||
const btMake = (typeof body.btMake === "string")
|
||||
? body.btMake.charCodeAt(0)
|
||||
: (Number.isFinite(body.btMake) ? (body.btMake|0) : 0x00);
|
||||
: (Number.isFinite(body.btMake) ? (body.btMake | 0) : 0x00);
|
||||
|
||||
const sndHeader = packSndHeader({ nTxnCd: LOG, nOpCd: LOGBT, nNumRecsSent: 1, nNumRecsRqrd: 1, nTxnId: 0, cBtMake: btMake });
|
||||
const sndLog = packSndLog({ usrId, opCard, passwd, btId });
|
||||
@ -550,7 +541,6 @@ app.post("/login", async (req, res) => {
|
||||
json.encryption = { used: false, providedPasswordEnc: true };
|
||||
}
|
||||
|
||||
// locate login body start
|
||||
let bodyOffset = rcvHeader.size;
|
||||
bodyOffset = adjustForOptionalOp(reply, bodyOffset).offset;
|
||||
bodyOffset = alignToAsciiYear(reply, bodyOffset);
|
||||
@ -570,7 +560,6 @@ app.post("/login", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ---------- DOWNLOAD BT INFO (LOG/BTI) ----------
|
||||
app.post("/download/btinfo", async (req, res) => {
|
||||
try {
|
||||
const body = await readJsonBody(req);
|
||||
@ -590,34 +579,27 @@ app.post("/download/btinfo", async (req, res) => {
|
||||
|
||||
const reply = await withRetry(() => absRoundtrip(sendBuf, DEFAULT_TIMEOUT_MS), RETRIES, RETRY_DELAY_MS);
|
||||
|
||||
const result = { ok: true, replyBytes: reply.length, replyHexFirst64: reply.subarray(0,64).toString('hex') };
|
||||
const result = { ok: true, replyBytes: reply.length, replyHexFirst64: reply.subarray(0, 64).toString("hex") };
|
||||
|
||||
let offset = 0;
|
||||
if (remaining(reply, offset) < 18) return res.json({ ok:false, error:"truncated: no header" });
|
||||
// const hdr = parseRcvHeaderFlexible(reply.subarray(offset, Math.min(reply.length, offset + 24)));
|
||||
// result.primaryHeader = hdr;
|
||||
// offset += hdr.size;
|
||||
if (remaining(reply, offset) < 18) return res.json({ ok: false, error: "truncated: no header" });
|
||||
|
||||
const hdr = parseRcvHeaderFlexible(reply.subarray(offset, Math.min(reply.length, offset + 24)));
|
||||
result.primaryHeader = hdr;
|
||||
result.primaryHeader = hdr;
|
||||
|
||||
// Advance to end of header *and* align to the natural 4-byte boundary
|
||||
// (C structs are commonly padded so sizeof(sRcvHeader) will often be 20, not 18)
|
||||
const hdrLenAligned = (hdr.size + 3) & ~3; // e.g. 18 -> 20, 24 -> 24
|
||||
// safety: ensure we have at least hdrLenAligned bytes in reply
|
||||
if (!safeAdvanceOrBreak(reply, 0, hdrLenAligned, result)) {
|
||||
const hdrLenAligned = (hdr.size + 3) & ~3;
|
||||
if (!safeAdvanceOrBreak(reply, 0, hdrLenAligned, result)) {
|
||||
result.error = "truncated: incomplete header (after alignment)";
|
||||
return res.json(result);
|
||||
}
|
||||
offset = hdrLenAligned;
|
||||
|
||||
}
|
||||
offset = hdrLenAligned;
|
||||
|
||||
if (hdr.nRetCd !== SUCCESS) {
|
||||
result.error = `server returned nRetCd=${hdr.nRetCd}`;
|
||||
return res.json(result);
|
||||
}
|
||||
|
||||
if (!safeAdvanceOrBreak(reply, offset, 5+31+5+4+1+4+4+4+4, result)) {
|
||||
if (!safeAdvanceOrBreak(reply, offset, 5 + 31 + 5 + 4 + 1 + 4 + 4 + 4 + 4, result)) {
|
||||
result.error = "truncated before BTI master";
|
||||
return res.json(result);
|
||||
}
|
||||
@ -629,9 +611,7 @@ offset = hdrLenAligned;
|
||||
let readCount = 0;
|
||||
while (readCount < hdr.nNumRecs) {
|
||||
if (looksLikeHeaderAt(reply, offset)) break;
|
||||
// if (!safeAdvanceOrBreak(reply, offset, 1+4+1, result)) break;
|
||||
if (!safeAdvanceOrBreak(reply, offset, 1+3+1, result)) break;
|
||||
|
||||
if (!safeAdvanceOrBreak(reply, offset, 1 + 3 + 1, result)) break;
|
||||
const dt = parseRcvBtiDtl(reply, offset);
|
||||
offset += dt.size;
|
||||
details.push(dt);
|
||||
@ -647,7 +627,8 @@ offset = hdrLenAligned;
|
||||
}
|
||||
});
|
||||
|
||||
// ---------- DOWNLOAD RP INFO (LOG/RPI) ----------
|
||||
|
||||
//----------------------------RPINFO DOWNLOAD ----------------------------------------------------
|
||||
app.post("/download/rpinfo", async (req, res) => {
|
||||
try {
|
||||
const body = await readJsonBody(req);
|
||||
@ -657,98 +638,256 @@ app.post("/download/rpinfo", async (req, res) => {
|
||||
const usrId = (body.usrId ?? "").toString();
|
||||
const plain = (body.password ?? "");
|
||||
const encPass = (body.passwordEnc ?? "");
|
||||
|
||||
if (!opCard || !btId || (!plain && !encPass)) {
|
||||
return res.status(400).json({ ok: false, error: "Missing opCard, btId, and either password or passwordEnc" });
|
||||
}
|
||||
const passwd = encPass ? String(encPass) : encryptPasswordLikeMFC(String(plain)).enc;
|
||||
|
||||
const sndHeader = packSndHeader({ nTxnCd: LOG, nOpCd: RPI, nNumRecsSent: 1, nNumRecsRqrd: 0, nTxnId: 0, cBtMake: 0 });
|
||||
const passwd = encPass ? String(encPass) : encryptPasswordLikeMFC(String(plain)).enc;
|
||||
const btAdvSalesFlg = body.btAdvSalesFlg || "N";
|
||||
const nBtAdvSaleEnb = btAdvSalesFlg === "A" ? 1 : 0;
|
||||
|
||||
const sndHeader = packSndHeader({ nTxnCd: LOG, nOpCd: RPI, nNumRecsSent: 1, nNumRecsRqrd: nBtAdvSaleEnb, nTxnId: 0, cBtMake: 0 });
|
||||
const sndLog = packSndLog({ usrId, opCard, passwd, btId });
|
||||
const sendBuf = Buffer.concat([sndHeader, sndLog]);
|
||||
|
||||
const reply = await withRetry(() => absRoundtrip(sendBuf, DEFAULT_TIMEOUT_MS), RETRIES, RETRY_DELAY_MS);
|
||||
|
||||
const result = { ok: true, replyBytes: reply.length, replyHexFirst64: reply.subarray(0,64).toString('hex') };
|
||||
|
||||
const result = { ok: true, replyBytes: reply.length, replyHexFirst64: reply.subarray(0, 64).toString("hex") };
|
||||
let offset = 0;
|
||||
if (remaining(reply, offset) < 18) return res.json({ ok:false, error:"truncated: no top header" });
|
||||
const topHdr = parseRcvHeaderFlexible(reply.subarray(offset, Math.min(reply.length, offset+24)));
|
||||
|
||||
if (remaining(reply, offset) < 18) return res.json({ ok: false, error: "truncated: no top header" });
|
||||
const topHdr = parseRcvHeaderFlexible(reply.subarray(offset, Math.min(reply.length, offset + 24)));
|
||||
offset += topHdr.size;
|
||||
result.topHeader = topHdr;
|
||||
|
||||
if (topHdr.nRetCd !== SUCCESS) {
|
||||
result.error = `server returned nRetCd=${topHdr.nRetCd}`;
|
||||
return res.json(result);
|
||||
}
|
||||
if (debug) result.debug_afterTopHdr = hexSlice(reply, 0, 128);
|
||||
|
||||
// Helper to parse a section safely
|
||||
function parseSection(minRecBytes, recParser, outArrName, outHeaderName) {
|
||||
if (remaining(reply, offset) < 18) { result._truncated = true; return { header:null, items:[], read:0 }; }
|
||||
offset = alignToPossibleHeader(reply, offset);
|
||||
const hdr = parseRcvHeaderFlexible(reply.subarray(offset, Math.min(reply.length, offset+24)));
|
||||
if (debug) result.debug_afterTopHdr = { offset, hex: hexSlice(reply, offset, 128) };
|
||||
|
||||
function parseSection(minRecBytes, recParser, outArrName, outHeaderName, expected_nTxnCd) {
|
||||
const items = [];
|
||||
let read = 0;
|
||||
let hdr = null;
|
||||
|
||||
if (remaining(reply, offset) < 18) {
|
||||
result[outArrName] = items;
|
||||
result[outHeaderName] = null;
|
||||
result[outArrName.replace(/s?$/, "ReadCount")] = read;
|
||||
if (debug) result[`debug_${outArrName}_no_data`] = `No data for ${outArrName}, offset=${offset}, remaining=${remaining(reply, offset)}, hex=${hexSlice(reply, offset, 64)}`;
|
||||
return items;
|
||||
}
|
||||
|
||||
// Try parsing header with more aggressive alignment
|
||||
offset = alignToPossibleHeader(reply, offset, expected_nTxnCd);
|
||||
if (looksLikeHeaderAt(reply, offset, expected_nTxnCd)) {
|
||||
hdr = parseRcvHeaderFlexible(reply.subarray(offset, Math.min(reply.length, offset + 24)));
|
||||
offset += hdr.size;
|
||||
const adj = adjustForOptionalOp(reply, offset);
|
||||
offset = adj.offset;
|
||||
|
||||
const items = [];
|
||||
let read = 0;
|
||||
while (read < hdr.nNumRecs) {
|
||||
if (looksLikeHeaderAt(reply, offset)) break;
|
||||
if (!safeAdvanceOrBreak(reply, offset, minRecBytes, result)) break;
|
||||
if (debug) result[`debug_${outArrName}_header`] = { offset, hdr, hex: hexSlice(reply, offset - hdr.size, 64), expected_nTxnCd };
|
||||
|
||||
while (read < hdr.nNumRecs && remaining(reply, offset) >= minRecBytes) {
|
||||
try {
|
||||
const rec = recParser(reply, offset);
|
||||
offset += rec.size;
|
||||
if (rec && rec.nRaceDt >= 20000101 && rec.nRaceDt <= 21001231 && rec.cVenueNum >= 1 && rec.cVenueNum <= MAX_VENUES) {
|
||||
items.push(rec);
|
||||
offset += rec.size;
|
||||
read++;
|
||||
} else {
|
||||
offset += 1;
|
||||
if (debug) result[`debug_${outArrName}_invalid`] = `Invalid record at offset ${offset}: ${JSON.stringify(rec)}`;
|
||||
}
|
||||
} catch (e) {
|
||||
if (debug) result[`debug_${outArrName}_error`] = `Error at offset ${offset}: ${e.message}, hex=${hexSlice(reply, offset, 64)}`;
|
||||
offset += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback: try parsing data without header for up to 100 records
|
||||
let fallbackAttempts = 0;
|
||||
const maxFallbackAttempts = 100;
|
||||
while (remaining(reply, offset) >= minRecBytes && fallbackAttempts < maxFallbackAttempts) {
|
||||
try {
|
||||
const rec = recParser(reply, offset);
|
||||
if (rec && rec.nRaceDt >= 20000101 && rec.nRaceDt <= 21001231 && rec.cVenueNum >= 1 && rec.cVenueNum <= MAX_VENUES) {
|
||||
items.push(rec);
|
||||
offset += rec.size;
|
||||
read++;
|
||||
if (debug) result[`debug_${outArrName}_fallback`] = `Parsed ${outArrName} data without valid header at offset ${offset - rec.size}`;
|
||||
} else {
|
||||
offset += 1;
|
||||
fallbackAttempts++;
|
||||
if (debug) result[`debug_${outArrName}_fallback_invalid`] = `Fallback invalid record at offset ${offset}: ${JSON.stringify(rec)}`;
|
||||
}
|
||||
} catch (e) {
|
||||
offset += 1;
|
||||
fallbackAttempts++;
|
||||
if (debug) result[`debug_${outArrName}_fallback_error`] = `Fallback parse error at offset ${offset}: ${e.message}, hex=${hexSlice(reply, offset, 64)}`;
|
||||
}
|
||||
}
|
||||
if (debug && fallbackAttempts > 0) result[`debug_${outArrName}_fallback_attempts`] = `Attempted ${fallbackAttempts} fallback parses for ${outArrName}`;
|
||||
}
|
||||
|
||||
result[outHeaderName] = hdr;
|
||||
result[outArrName] = items;
|
||||
result[outArrName.replace(/s?$/, "ReadCount")] = read;
|
||||
return { header: hdr, items, read };
|
||||
if (debug) result[`debug_${outArrName}_after`] = { offset, hex: hexSlice(reply, offset, 128) };
|
||||
return items;
|
||||
}
|
||||
|
||||
// RPI1 .. RPI4 .. PVM
|
||||
parseSection(4+1+1+15+41, parseRcvRpi1, "rpi1", "rpi1Header");
|
||||
if (debug) result.debug_afterRpi1 = hexSlice(reply, Math.max(0, offset-64), 128);
|
||||
// Parse sections with expected nTxnCd
|
||||
const sections = [
|
||||
{ minRecBytes: 4 + 1 + 1 + 15 + 41, parser: parseRcvRpi1, arrName: "rpi1", headerName: "rpi1Header", nTxnCd: 101 },
|
||||
{ minRecBytes: 4 + 1 + 1 + 25 + 4, parser: parseRcvRpi2, arrName: "rpi2", headerName: "rpi2Header", nTxnCd: 102 },
|
||||
{ minRecBytes: 4 + 1 + 1 + 9, parser: parseRcvRpi3, arrName: "rpi3", headerName: "rpi3Header", nTxnCd: 103 },
|
||||
{ minRecBytes: 4 + 1 + 3 + 11 + 1, parser: parseRcvRpi4, arrName: "rpi4", headerName: "rpi4Header", nTxnCd: 104 },
|
||||
{ minRecBytes: 4 + 4 + 4 + 4 + 4 + 31, parser: parseRcvPvm, arrName: "pvms", headerName: "pvmHeader", nTxnCd: 105 }
|
||||
];
|
||||
|
||||
parseSection(4+1+1+25+4, parseRcvRpi2, "rpi2", "rpi2Header");
|
||||
if (debug) result.debug_afterRpi2 = hexSlice(reply, Math.max(0, offset-64), 128);
|
||||
for (const section of sections) {
|
||||
parseSection(section.minRecBytes, section.parser, section.arrName, section.headerName, section.nTxnCd);
|
||||
}
|
||||
|
||||
parseSection(4+1+1+9, parseRcvRpi3, "rpi3", "rpi3Header");
|
||||
if (debug) result.debug_afterRpi3 = hexSlice(reply, Math.max(0, offset-64), 128);
|
||||
// Log remaining unparsed data
|
||||
if (debug && offset < reply.length) {
|
||||
result.debug_unparsed = {
|
||||
remainingBytes: reply.length - offset,
|
||||
hex: hexSlice(reply, offset, Math.min(128, reply.length - offset))
|
||||
};
|
||||
}
|
||||
|
||||
parseSection(4+1+3+11+1, parseRcvRpi4, "rpi4", "rpi4Header");
|
||||
if (debug) result.debug_afterRpi4 = hexSlice(reply, Math.max(0, offset-64), 128);
|
||||
|
||||
parseSection(4+4+4+4+4+31, parseRcvPvm, "pvms", "pvmHeader");
|
||||
|
||||
result.parsedBytes = offset;
|
||||
|
||||
// Build summary
|
||||
const raceCard = {};
|
||||
// Build raceCard (unchanged)
|
||||
const raceCard = [];
|
||||
for (const v of result.rpi1 || []) {
|
||||
const key = `${v.nRaceDt}_${v.cVenueNum}`;
|
||||
raceCard[key] = { date: v.nRaceDt, venueNum: v.cVenueNum, status: v.cVenueSts, races: {}, advertisement: v.cAdvertisement };
|
||||
const raceDate = getDateStr(v.nRaceDt);
|
||||
const venueId = getVenueIdStr(v.cVenueNum);
|
||||
const venueSts = v.cVenueSts;
|
||||
|
||||
for (let nRace = 0; nRace < MAX_RACES; nRace++) {
|
||||
if (v.cRaces[nRace] === "0" || !v.cRaces[nRace]) continue;
|
||||
const raceNum = (nRace + 1).toString().padStart(2, "0");
|
||||
raceCard.push({
|
||||
RACE_DATE: raceDate,
|
||||
RACE_VENUE_ID: venueId,
|
||||
RACE_NO: raceNum,
|
||||
STOPBET_STATUS: v.cRaces[nRace] === "C" ? "S" : v.cRaces[nRace],
|
||||
RACE_STATUS: v.cRaces[nRace] === "C" ? "C" : "V",
|
||||
VENUE_STATUS: venueSts,
|
||||
START_TIME: "",
|
||||
ADVT: v.cAdvertisement,
|
||||
horses: [],
|
||||
pools: []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const r of result.rpi2 || []) {
|
||||
const key = `${r.nRaceDt}_${r.cVenueNum}`;
|
||||
const raceKey = `${r.cRaceNum}`;
|
||||
if (!raceCard[key]) raceCard[key] = { date: r.nRaceDt, venueNum: r.cVenueNum, races: {} };
|
||||
raceCard[key].races[raceKey] = { raceNum: r.cRaceNum, horses: (r.cHorses || "").split(/\s+/).filter(Boolean), startTime: r.nRaceStartTm, pools: [] };
|
||||
const raceDate = getDateStr(r.nRaceDt);
|
||||
const venueId = getVenueIdStr(r.cVenueNum);
|
||||
const raceNum = r.cRaceNum.toString().padStart(2, "0");
|
||||
|
||||
for (let nHorse = 0; nHorse < MAX_HORSES; nHorse++) {
|
||||
if (r.cHorses[nHorse] === "0" || !r.cHorses[nHorse]) continue;
|
||||
const horseNum = (nHorse + 1).toString().padStart(2, "0");
|
||||
const entry = raceCard.find(e => e.RACE_DATE === raceDate && e.RACE_VENUE_ID === venueId && e.RACE_NO === raceNum);
|
||||
if (entry) {
|
||||
entry.horses.push({
|
||||
RACE_HORSE_NO: horseNum,
|
||||
HORSE_STATUS: r.cHorses[nHorse],
|
||||
REFUND_FILE_STATUS: ""
|
||||
});
|
||||
if (!entry.START_TIME) {
|
||||
entry.START_TIME = getTimeStr(r.nRaceStartTm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const p of result.rpi3 || []) {
|
||||
const key = `${p.nRaceDt}_${p.cVenueNum}`;
|
||||
const raceKey = `${p.cRaceNum}`;
|
||||
if (raceCard[key] && raceCard[key].races[raceKey]) raceCard[key].races[raceKey].pools = (p.cPools || "").split("").filter(Boolean);
|
||||
const raceDate = getDateStr(p.nRaceDt);
|
||||
const venueId = getVenueIdStr(p.cVenueNum);
|
||||
const raceNum = p.cRaceNum.toString().padStart(2, "0");
|
||||
|
||||
for (let nPoolId = 0; nPoolId < MAX_POOLS; nPoolId++) {
|
||||
if (p.cPools[nPoolId] === "0" || !p.cPools[nPoolId]) continue;
|
||||
const entry = raceCard.find(e => e.RACE_DATE === raceDate && e.RACE_VENUE_ID === venueId && e.RACE_NO === raceNum);
|
||||
if (entry) {
|
||||
entry.pools.push({
|
||||
POOL_ID: getPoolIdStr(nPoolId + 1),
|
||||
POOL_STATUS: p.cPools[nPoolId],
|
||||
BETTING_CENTRE_ID: "",
|
||||
SELECT_RACE_NO: ""
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const s of result.rpi4 || []) {
|
||||
const key = `${s.nRaceDt}_${s.cVenueNum}`;
|
||||
const raceKey = `${s.cRaceNum}`;
|
||||
if (raceCard[key] && raceCard[key].races[raceKey]) {
|
||||
raceCard[key].races[raceKey].poolStatus = s.cPoolSts;
|
||||
raceCard[key].races[raceKey].selectRaceNum = s.cSelectRaceNum;
|
||||
const raceDate = getDateStr(s.nRaceDt);
|
||||
const venueId = getVenueIdStr(s.cVenueNum);
|
||||
const raceNum = s.cRaceNum;
|
||||
const venueSts = raceCard.find(e => e.RACE_DATE === raceDate && e.RACE_VENUE_ID === venueId)?.VENUE_STATUS || "V";
|
||||
|
||||
let entry = raceCard.find(e => e.RACE_DATE === raceDate && e.RACE_VENUE_ID === venueId && e.RACE_NO === raceNum);
|
||||
if (!entry) {
|
||||
entry = {
|
||||
RACE_DATE: raceDate,
|
||||
RACE_VENUE_ID: venueId,
|
||||
RACE_NO: raceNum,
|
||||
STOPBET_STATUS: "",
|
||||
RACE_STATUS: s.cPoolSts,
|
||||
VENUE_STATUS: venueSts,
|
||||
START_TIME: "",
|
||||
ADVT: "",
|
||||
horses: [],
|
||||
pools: []
|
||||
};
|
||||
raceCard.push(entry);
|
||||
}
|
||||
|
||||
entry.pools.push({
|
||||
POOL_ID: getMultiLegPoolIdStr(s.cRaceNum),
|
||||
POOL_STATUS: s.cPoolSts,
|
||||
BETTING_CENTRE_ID: "",
|
||||
SELECT_RACE_NO: s.cSelectRaceNum
|
||||
});
|
||||
}
|
||||
|
||||
for (const p of result.rpi3 || []) {
|
||||
if (!p.cPools.includes("TBP") && !p.cPools.includes("MJP") && !p.cPools.includes("JPP")) continue;
|
||||
const raceDate = getDateStr(p.nRaceDt);
|
||||
const venueId = getVenueIdStr(p.cVenueNum);
|
||||
const raceNum = p.cRaceNum.toString().padStart(2, "0");
|
||||
|
||||
for (const s of result.rpi4 || []) {
|
||||
if (s.cSelectRaceNum && s.nRaceDt === p.nRaceDt && s.cVenueNum === p.cVenueNum) {
|
||||
const selectRaceNum = s.cSelectRaceNum.slice(0, 2);
|
||||
const selectEntry = raceCard.find(e => e.RACE_DATE === raceDate && e.RACE_VENUE_ID === venueId && e.RACE_NO === selectRaceNum);
|
||||
const targetEntry = raceCard.find(e => e.RACE_DATE === raceDate && e.RACE_VENUE_ID === venueId && e.RACE_NO === raceNum);
|
||||
if (selectEntry && targetEntry) {
|
||||
targetEntry.STOPBET_STATUS = selectEntry.STOPBET_STATUS;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pvmData = (result.pvms || []).map(p => ({
|
||||
RACE_VENUE: p.cRaceVenue,
|
||||
POOL_ID: p.cPoolId,
|
||||
UNIT_PRICE: p.fUnitPrice,
|
||||
TAX_RATE: p.fTaxRate,
|
||||
DEDUCT_RATE: p.fDeductRate,
|
||||
REMARKS: p.cRemarks
|
||||
}));
|
||||
|
||||
result.raceCard = raceCard;
|
||||
result.pvm = pvmData;
|
||||
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
@ -756,7 +895,7 @@ app.post("/download/rpinfo", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ---- start HTTP server ----
|
||||
|
||||
const HTTP_PORT = Number(process.env.HTTP_BRIDGE_PORT || 8080);
|
||||
app.listen(HTTP_PORT, () => {
|
||||
console.log(`ABS HTTP bridge listening on :${HTTP_PORT}`);
|
||||
|
||||
888
server.js
Normal file
888
server.js
Normal file
@ -0,0 +1,888 @@
|
||||
|
||||
const express = require("express");
|
||||
const net = require("net");
|
||||
|
||||
const ABS_HOST = process.env.ABS_HOST || "192.0.0.14";
|
||||
const ABS_PORT = Number(process.env.ABS_PORT || 7000);
|
||||
|
||||
const ABS_POLL = 178;
|
||||
const LOG = 100;
|
||||
const LOGBT = 6013;
|
||||
const BTI = 6014;
|
||||
const RPI = 6015;
|
||||
const SUCCESS = 0;
|
||||
|
||||
const PKT_SIZE = 512;
|
||||
const PAYLOAD_PER_PKT = PKT_SIZE - 1;
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = Number(process.env.BRIDGE_TIMEOUT_MS || 20000);
|
||||
const RETRIES = Number(process.env.BRIDGE_RETRIES || 2);
|
||||
const RETRY_DELAY_MS = Number(process.env.BRIDGE_RETRY_DELAY_MS || 500);
|
||||
|
||||
const MAX_VENUES = 10;
|
||||
const MAX_RACES = 14;
|
||||
const MAX_POOLS = 8;
|
||||
const MAX_HORSES = 24;
|
||||
|
||||
const app = express();
|
||||
|
||||
// Venue and pool mappings
|
||||
const venueMapping = {
|
||||
1: "BLR", 2: "MUM", 3: "MAA", 4: "CAL", 5: "HYD",
|
||||
6: "MYS", 7: "OTY", 8: "PUN", 9: "GYM", 10: "OVS"
|
||||
};
|
||||
|
||||
const poolMapping = {
|
||||
1: "WNP", 2: "PLP", 3: "SHP", 4: "THP", 5: "QNP",
|
||||
6: "FRP", 7: "TNP", 8: "EXP", 9: "TBP", 10: "MJP", 11: "JPP"
|
||||
};
|
||||
|
||||
const multiLegPoolMapping = {
|
||||
"T1": "TBP", "T2": "TBP", "T3": "TBP",
|
||||
"M1": "MJP", "M2": "MJP", "M3": "MJP",
|
||||
"J1": "JPP", "J2": "JPP", "J3": "JPP"
|
||||
};
|
||||
|
||||
function getDateStr(nDate) {
|
||||
const s = nDate.toString().padStart(8, "0");
|
||||
return `${s.slice(0, 4)}/${s.slice(4, 6)}/${s.slice(6, 8)}`;
|
||||
}
|
||||
|
||||
function getTimeStr(nTime) {
|
||||
const s = nTime.toString().padStart(6, "0");
|
||||
return `${s.slice(0, 2)}:${s.slice(2, 4)}:${s.slice(4, 6)}`;
|
||||
}
|
||||
|
||||
function getVenueIdStr(nVenue) {
|
||||
return venueMapping[nVenue] || "";
|
||||
}
|
||||
|
||||
function getPoolIdStr(nPoolId) {
|
||||
return poolMapping[nPoolId] || "";
|
||||
}
|
||||
|
||||
function getMultiLegPoolIdStr(szMultiLegRace) {
|
||||
return multiLegPoolMapping[szMultiLegRace] || "";
|
||||
}
|
||||
|
||||
function readJsonBody(req) {
|
||||
return new Promise((resolve) => {
|
||||
const chunks = [];
|
||||
req.on("data", (c) => chunks.push(c));
|
||||
req.on("end", () => {
|
||||
if (!chunks.length) return resolve({});
|
||||
const txt = Buffer.concat(chunks).toString("utf8").trim();
|
||||
if (!txt) return resolve({});
|
||||
try { resolve(JSON.parse(txt)); } catch { resolve({}); }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function writeFixedAscii(buf, offset, s, len) {
|
||||
const str = (s ?? "").toString();
|
||||
for (let i = 0; i < len; i++) buf[offset + i] = i < str.length ? (str.charCodeAt(i) & 0xff) : 0x00;
|
||||
return offset + len;
|
||||
}
|
||||
|
||||
function readFixedAscii(buf, offset, len) {
|
||||
let end = offset;
|
||||
const max = offset + len;
|
||||
while (end < max && buf[end] !== 0) end++;
|
||||
const raw = buf.subarray(offset, end).toString("ascii");
|
||||
return { value: raw.trimEnd(), next: offset + len };
|
||||
}
|
||||
|
||||
function hexSlice(buf, off, len) {
|
||||
const s = Math.max(0, off);
|
||||
const e = Math.min(buf.length, off + len);
|
||||
return buf.subarray(s, e).toString("hex");
|
||||
}
|
||||
|
||||
function remaining(buf, off) {
|
||||
return Math.max(0, buf.length - off);
|
||||
}
|
||||
|
||||
function safeAdvanceOrBreak(buf, off, want, outObj) {
|
||||
if (remaining(buf, off) < want) {
|
||||
outObj._truncated = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function looksLikeHeaderAt(buf, off, expected_nTxnCd = null) {
|
||||
if (remaining(buf, off) < 18) return false;
|
||||
try {
|
||||
const nTxnCd = buf.readInt32LE(off);
|
||||
const nRetCd = buf.readInt32LE(off + 4);
|
||||
const nNumRecs = buf.readInt32LE(off + 8);
|
||||
// Allow nTxnCd: 100 as a fallback for any section, since the server may send it
|
||||
const validTxnCd = expected_nTxnCd ? (nTxnCd === expected_nTxnCd || nTxnCd === 100) : (nTxnCd >= 100 && nTxnCd <= 105);
|
||||
return validTxnCd && nRetCd >= 0 && nRetCd < 10000 && nNumRecs >= 0 && nNumRecs < 100;
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
function alignToPossibleHeader(reply, off, expected_nTxnCd = null) {
|
||||
// Try current offset and additional offsets for alignment
|
||||
const tryOffsets = [0, 2, 4, -4, 8, 12];
|
||||
for (const d of tryOffsets) {
|
||||
const o = off + d;
|
||||
if (o >= 0 && looksLikeHeaderAt(reply, o, expected_nTxnCd)) return o;
|
||||
}
|
||||
// Fallback: align to valid nRaceDt (20000101–21001231)
|
||||
for (const d of tryOffsets) {
|
||||
const o = off + d;
|
||||
if (o >= 0 && remaining(reply, o) >= 4) {
|
||||
const nRaceDt = reply.readInt32LE(o);
|
||||
if (nRaceDt >= 20000101 && nRaceDt <= 21001231) return o;
|
||||
}
|
||||
}
|
||||
return off;
|
||||
}
|
||||
|
||||
function adjustForOptionalOp(replyBuf, bodyOffset) {
|
||||
if (replyBuf.length >= bodyOffset + 4) {
|
||||
const maybeOp = replyBuf.readInt32LE(bodyOffset);
|
||||
if (maybeOp === LOGBT || maybeOp === LOG || maybeOp === 0 || (maybeOp >= 100 && maybeOp <= 10000)) {
|
||||
return { offset: bodyOffset + 4, extraOp: maybeOp };
|
||||
}
|
||||
}
|
||||
return { offset: bodyOffset, extraOp: null };
|
||||
}
|
||||
|
||||
function absSend(sock, payload) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let off = 0;
|
||||
function sendNext() {
|
||||
const remaining = payload.length - off;
|
||||
const toCopy = Math.min(PAYLOAD_PER_PKT, Math.max(remaining, 0));
|
||||
const pkt = Buffer.alloc(PKT_SIZE, 0);
|
||||
pkt[0] = (off + toCopy >= payload.length) ? 48 : 49;
|
||||
if (toCopy > 0) payload.copy(pkt, 1, off, off + toCopy);
|
||||
off += toCopy;
|
||||
sock.write(pkt, (err) => {
|
||||
if (err) return reject(err);
|
||||
if (pkt[0] === 48) return resolve();
|
||||
sendNext();
|
||||
});
|
||||
}
|
||||
if (!payload || payload.length === 0) {
|
||||
const pkt = Buffer.alloc(PKT_SIZE, 0);
|
||||
pkt[0] = 48;
|
||||
sock.write(pkt, (err) => err ? reject(err) : resolve());
|
||||
} else {
|
||||
sendNext();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function absRecv(sock, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
let buf = Buffer.alloc(0);
|
||||
const timer = timeoutMs ? setTimeout(() => done(new Error("TCP timeout")), timeoutMs) : null;
|
||||
|
||||
function done(err) {
|
||||
if (timer) clearTimeout(timer);
|
||||
sock.off("data", onData);
|
||||
if (err) reject(err);
|
||||
else resolve(Buffer.concat(chunks));
|
||||
}
|
||||
function onData(data) {
|
||||
buf = Buffer.concat([buf, data]);
|
||||
while (buf.length >= PKT_SIZE) {
|
||||
const pkt = buf.subarray(0, PKT_SIZE);
|
||||
buf = buf.subarray(PKT_SIZE);
|
||||
const flag = pkt[0];
|
||||
chunks.push(pkt.subarray(1));
|
||||
if (flag === 48) return done();
|
||||
}
|
||||
}
|
||||
|
||||
sock.on("data", onData);
|
||||
sock.on("error", (e) => done(e));
|
||||
sock.on("end", () => done(new Error("Socket ended before final '0' packet")));
|
||||
});
|
||||
}
|
||||
|
||||
async function absRoundtrip(payload, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
||||
const sock = new net.Socket();
|
||||
await new Promise((res, rej) => sock.connect(ABS_PORT, ABS_HOST, res).once("error", rej));
|
||||
try {
|
||||
await absSend(sock, payload || Buffer.alloc(0));
|
||||
const replyConcat = await absRecv(sock, timeoutMs);
|
||||
sock.end();
|
||||
return replyConcat;
|
||||
} catch (e) {
|
||||
try { sock.destroy(); } catch {}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function withRetry(fn, tries = RETRIES, delayMs = RETRY_DELAY_MS) {
|
||||
let lastErr;
|
||||
for (let i = 0; i < tries; i++) {
|
||||
try { return await fn(); }
|
||||
catch (e) { lastErr = e; if (i + 1 < tries) await new Promise(r => setTimeout(r, delayMs)); }
|
||||
}
|
||||
throw lastErr;
|
||||
}
|
||||
|
||||
function encryptPasswordLikeMFC(plain) {
|
||||
const sec = new Date().getUTCSeconds();
|
||||
const key = Number(String(sec)[0] || "0");
|
||||
let out = "";
|
||||
const s = (plain ?? "").toString();
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const ch = s[i];
|
||||
const d = (ch >= "0" && ch <= "9") ? (ch.charCodeAt(0) - 48) : 0;
|
||||
out += String((d + key) % 10);
|
||||
}
|
||||
return { enc: out + String(key), key, utcSeconds: sec };
|
||||
}
|
||||
|
||||
function packSndHeader({ nTxnCd, nOpCd = 0, nNumRecsSent = 0, nNumRecsRqrd = 0, nTxnId = 0, cBtMake = 0 }) {
|
||||
const b = Buffer.alloc(24, 0);
|
||||
let o = 0;
|
||||
b.writeInt32LE(nTxnCd, o); o += 4;
|
||||
b.writeInt32LE(nOpCd, o); o += 4;
|
||||
b.writeInt32LE(nNumRecsSent, o); o += 4;
|
||||
b.writeInt32LE(nNumRecsRqrd, o); o += 4;
|
||||
b.writeInt32LE(nTxnId, o); o += 4;
|
||||
b.writeUInt8(cBtMake & 0xff, o);
|
||||
return b;
|
||||
}
|
||||
|
||||
function parseRcvHeaderFlexible(buf) {
|
||||
if (buf.length < 18) throw new Error(`Reply too short for RcvHeader: ${buf.length} bytes`);
|
||||
function readAs(len) {
|
||||
let o = 0;
|
||||
const nTxnCd = buf.readInt32LE(o); o += 4;
|
||||
const nRetCd = buf.readInt32LE(o); o += 4;
|
||||
const nNumRecs = buf.readInt32LE(o); o += 4;
|
||||
const nTxnId = buf.readInt32LE(o); o += 4;
|
||||
const cBtMake = buf.readUInt8(o);
|
||||
return { nTxnCd, nRetCd, nNumRecs, nTxnId, cBtMake, size: len };
|
||||
}
|
||||
const h18 = readAs(18);
|
||||
const ok18 = (h18.nTxnCd >= 50 && h18.nTxnCd < 10000) && (h18.nRetCd >= 0 && h18.nRetCd < 10000) && (h18.nNumRecs >= 0 && h18.nNumRecs < 100);
|
||||
let h24 = null, ok24 = false;
|
||||
if (buf.length >= 24) {
|
||||
h24 = readAs(24);
|
||||
ok24 = (h24.nTxnCd >= 50 && h24.nTxnCd < 10000) && (h24.nRetCd >= 0 && h24.nRetCd < 10000) && (h24.nNumRecs >= 0 && h24.nNumRecs < 100);
|
||||
}
|
||||
if (ok18 && !ok24) return h18;
|
||||
if (!ok18 && ok24) return h24;
|
||||
if (ok18 && ok24) return h18;
|
||||
return h18;
|
||||
}
|
||||
|
||||
function buildRcvHeader18({ nTxnCd, nRetCd, nNumRecs = 0, nTxnId = 0, cBtMake = 0 }) {
|
||||
const b = Buffer.alloc(18, 0);
|
||||
let o = 0;
|
||||
b.writeInt32LE(nTxnCd, o); o += 4;
|
||||
b.writeInt32LE(nRetCd, o); o += 4;
|
||||
b.writeInt32LE(nNumRecs, o); o += 4;
|
||||
b.writeInt32LE(nTxnId, o); o += 4;
|
||||
b.writeUInt8(cBtMake & 0xff, o);
|
||||
return b;
|
||||
}
|
||||
|
||||
function alignToAsciiYear(reply, off) {
|
||||
const tryOffsets = [0, 2, 4, -4, 8, 12];
|
||||
for (const d of tryOffsets) {
|
||||
const o = off + d;
|
||||
if (o >= 0 && o + 1 < reply.length) {
|
||||
const b0 = reply[o], b1 = reply[o + 1];
|
||||
if (b0 === 0x32 && b1 === 0x30) return o;
|
||||
if (reply[o] === 0x2f && o >= 4 && reply[o - 4] === 0x32 && reply[o - 3] === 0x30) return o - 4;
|
||||
}
|
||||
}
|
||||
return off;
|
||||
}
|
||||
|
||||
function packSndLog({ usrId = "", opCard, passwd, btId }) {
|
||||
const b = Buffer.alloc(38, 0);
|
||||
let o = 0;
|
||||
o = writeFixedAscii(b, o, usrId, 5);
|
||||
o = writeFixedAscii(b, o, opCard, 17);
|
||||
o = writeFixedAscii(b, o, passwd, 11);
|
||||
o = writeFixedAscii(b, o, btId, 5);
|
||||
return b;
|
||||
}
|
||||
|
||||
function parseRcvLog(buf, offset = 0) {
|
||||
let o = offset;
|
||||
const t1 = readFixedAscii(buf, o, 20); const cDateTime = t1.value; o = t1.next;
|
||||
const t2 = readFixedAscii(buf, o, 31); const cUsrNm = t2.value; o = t2.next;
|
||||
const t3 = readFixedAscii(buf, o, 5); const cUsrId = t3.value; o = t3.next;
|
||||
const t4 = readFixedAscii(buf, o, 31); const cSupNm = t4.value; o = t4.next;
|
||||
const t5 = readFixedAscii(buf, o, 5); const cSupId = t5.value; o = t5.next;
|
||||
const t6 = readFixedAscii(buf, o, 5); const cUsrTyp = t6.value; o = t6.next;
|
||||
|
||||
function rf() { const v = buf.readFloatLE(o); o += 4; return v; }
|
||||
function ri() { const v = buf.readInt32LE(o); o += 4; return v; }
|
||||
|
||||
const fOpenBal = rf();
|
||||
const fTktSalesByVoucher = rf();
|
||||
const fTktSalesByCash = rf();
|
||||
const fTktSalesByMemCard = rf();
|
||||
const nTktSalesByVoucherCount = ri();
|
||||
const nTktSalesByCashCount = ri();
|
||||
const nTktSalesByMemCardCount = ri();
|
||||
const fPayoutByVoucher = rf();
|
||||
const fPayoutByCash = rf();
|
||||
const fPayoutByMemCard = rf();
|
||||
const nPayoutByVoucherCount = ri();
|
||||
const nPayoutByCashCount = ri();
|
||||
const nPayoutByMemCardCount = ri();
|
||||
const fCancelByVoucher = rf();
|
||||
const fCancelByCash = rf();
|
||||
const fCancelByMemCard = rf();
|
||||
const nCancelByVoucherCount = ri();
|
||||
const nCancelByCashCount = ri();
|
||||
const nCancelByMemCardCount = ri();
|
||||
const fDeposit = rf();
|
||||
const fWithdrawAmt = rf();
|
||||
const fVoucherSales = rf();
|
||||
const fVoucherEncash = rf();
|
||||
const fCloseBal = rf();
|
||||
const fSaleTarget = rf();
|
||||
|
||||
return {
|
||||
cDateTime, cUsrNm, cUsrId, cSupNm, cSupId, cUsrTyp,
|
||||
fOpenBal, fTktSalesByVoucher, fTktSalesByCash, fTktSalesByMemCard,
|
||||
nTktSalesByVoucherCount, nTktSalesByCashCount, nTktSalesByMemCardCount,
|
||||
fPayoutByVoucher, fPayoutByCash, fPayoutByMemCard,
|
||||
nPayoutByVoucherCount, nPayoutByCashCount, nPayoutByMemCardCount,
|
||||
fCancelByVoucher, fCancelByCash, fCancelByMemCard,
|
||||
nCancelByVoucherCount, nCancelByCashCount, nCancelByMemCardCount,
|
||||
fDeposit, fWithdrawAmt, fVoucherSales, fVoucherEncash,
|
||||
fCloseBal, fSaleTarget,
|
||||
size: o - offset
|
||||
};
|
||||
}
|
||||
|
||||
function parseRcvBtiMst(buf, offset) {
|
||||
let o = offset;
|
||||
const a1 = readFixedAscii(buf, o, 5); const cBtId = a1.value; o = a1.next;
|
||||
const a2 = readFixedAscii(buf, o, 31); const cBtName = a2.value; o = a2.next;
|
||||
const a3 = readFixedAscii(buf, o, 5); const cEnclCd = a3.value; o = a3.next;
|
||||
const a4 = readFixedAscii(buf, o, 4); const cGrpCd = a4.value; o = a4.next;
|
||||
const cBtMode = String.fromCharCode(buf[o]); o += 1;
|
||||
const a5 = readFixedAscii(buf, o, 4); const cMoneyTyp = a5.value; o = a5.next;
|
||||
o = (o + 3) & ~3;
|
||||
const fMinSaleBet = buf.readFloatLE(o); o += 4;
|
||||
const fMaxSaleBet = buf.readFloatLE(o); o += 4;
|
||||
const fPayMaxAmt = buf.readFloatLE(o); o += 4;
|
||||
return { cBtId, cBtName, cEnclCd, cGrpCd, cBtMode, cMoneyTyp, fMinSaleBet, fMaxSaleBet, fPayMaxAmt, size: o - offset };
|
||||
}
|
||||
|
||||
function parseRcvBtiDtl(buf, offset) {
|
||||
let o = offset;
|
||||
const cDtlTyp = String.fromCharCode(buf[o]); o += 1;
|
||||
if (remaining(buf, o) < 4) {
|
||||
const avail = Math.max(0, remaining(buf, o));
|
||||
const aRem = readFixedAscii(buf, o, avail);
|
||||
const cPoolOrVenue = aRem.value;
|
||||
o = aRem.next;
|
||||
const cDtlSts = o < buf.length ? String.fromCharCode(buf[o]) : "";
|
||||
if (o < buf.length) o += 1;
|
||||
return { cDtlTyp, cPoolOrVenue, cDtlSts, size: o - offset };
|
||||
}
|
||||
const a3 = readFixedAscii(buf, o, 3);
|
||||
const cPoolOrVenue3 = a3.value;
|
||||
const after3 = a3.next;
|
||||
const statusCandidate = buf[after3];
|
||||
const isPrintable = (statusCandidate >= 0x20 && statusCandidate <= 0x7E);
|
||||
if (isPrintable) {
|
||||
o = after3;
|
||||
const cDtlSts = String.fromCharCode(buf[o]); o += 1;
|
||||
return { cDtlTyp, cPoolOrVenue: cPoolOrVenue3, cDtlSts, size: o - offset };
|
||||
}
|
||||
const a4 = readFixedAscii(buf, o, 4);
|
||||
const cPoolOrVenue4 = a4.value;
|
||||
o = a4.next;
|
||||
const cDtlSts = o < buf.length ? String.fromCharCode(buf[o]) : "";
|
||||
if (o < buf.length) o += 1;
|
||||
return { cDtlTyp, cPoolOrVenue: cPoolOrVenue4, cDtlSts, size: o - offset };
|
||||
}
|
||||
|
||||
function parseRcvRpi1(buf, offset) {
|
||||
let o = offset;
|
||||
const nRaceDt = buf.readInt32LE(o); o += 4;
|
||||
const cVenueNum = buf.readUInt8(o); o += 1;
|
||||
const cVenueSts = String.fromCharCode(buf[o]); o += 1;
|
||||
const aR = readFixedAscii(buf, o, 15); const cRaces = aR.value; o = aR.next;
|
||||
const aAd = readFixedAscii(buf, o, 41); const cAdvertisement = aAd.value; o = aAd.next;
|
||||
return { nRaceDt, cVenueNum, cVenueSts, cRaces, cAdvertisement, size: o - offset };
|
||||
}
|
||||
|
||||
function parseRcvRpi2(buf, offset) {
|
||||
let o = offset;
|
||||
const nRaceDt = buf.readInt32LE(o); o += 4;
|
||||
const cVenueNum = buf.readUInt8(o); o += 1;
|
||||
const cRaceNum = buf.readUInt8(o); o += 1;
|
||||
const aH = readFixedAscii(buf, o, 25); const cHorses = aH.value; o = aH.next;
|
||||
const nRaceStartTm = buf.readInt32LE(o); o += 4;
|
||||
return { nRaceDt, cVenueNum, cRaceNum, cHorses, nRaceStartTm, size: o - offset };
|
||||
}
|
||||
|
||||
function parseRcvRpi3(buf, offset) {
|
||||
let o = offset;
|
||||
const nRaceDt = buf.readInt32LE(o); o += 4;
|
||||
const cVenueNum = buf.readUInt8(o); o += 1;
|
||||
const cRaceNum = buf.readUInt8(o); o += 1;
|
||||
const aP = readFixedAscii(buf, o, 9); const cPools = aP.value; o = aP.next;
|
||||
return { nRaceDt, cVenueNum, cRaceNum, cPools, size: o - offset };
|
||||
}
|
||||
|
||||
function parseRcvRpi4(buf, offset) {
|
||||
let o = offset;
|
||||
const nRaceDt = buf.readInt32LE(o); o += 4;
|
||||
const cVenueNum = buf.readUInt8(o); o += 1;
|
||||
const cRaceNum = buf.subarray(o, o + 3).toString("ascii").replace(/\0/g, ""); o += 3;
|
||||
const aSel = readFixedAscii(buf, o, 11); const cSelectRaceNum = aSel.value; o = aSel.next;
|
||||
const cPoolSts = String.fromCharCode(buf[o]); o += 1;
|
||||
return { nRaceDt, cVenueNum, cRaceNum, cSelectRaceNum, cPoolSts, size: o - offset };
|
||||
}
|
||||
|
||||
function parseRcvPvm(buf, offset) {
|
||||
let o = offset;
|
||||
const a1 = readFixedAscii(buf, o, 4); const cRaceVenue = a1.value; o = a1.next;
|
||||
const a2 = readFixedAscii(buf, o, 4); const cPoolId = a2.value; o = a2.next;
|
||||
const fUnitPrice = buf.readFloatLE(o); o += 4;
|
||||
const fTaxRate = buf.readFloatLE(o); o += 4;
|
||||
const fDeductRate = buf.readFloatLE(o); o += 4;
|
||||
const a3 = readFixedAscii(buf, o, 31); const cRemarks = a3.value; o = a3.next;
|
||||
return { cRaceVenue, cPoolId, fUnitPrice, fTaxRate, fDeductRate, cRemarks, size: o - offset };
|
||||
}
|
||||
|
||||
app.get("/", (_req, res) => res.send("ABS bridge is up"));
|
||||
|
||||
app.get("/health", (_req, res) => {
|
||||
const s = new net.Socket();
|
||||
s.setTimeout(1500);
|
||||
s.connect(ABS_PORT, ABS_HOST, () => { s.destroy(); res.json({ ok: true, target: `${ABS_HOST}:${ABS_PORT}` }); });
|
||||
s.on("timeout", () => { s.destroy(); res.status(504).json({ ok: false, error: "TCP timeout" }); });
|
||||
s.on("error", (e) => { s.destroy(); res.status(502).json({ ok: false, error: String(e) }); });
|
||||
});
|
||||
|
||||
app.post("/abs/poll", async (req, res) => {
|
||||
try {
|
||||
const body = await readJsonBody(req);
|
||||
const btMake = (typeof body.btMake === "string")
|
||||
? body.btMake.charCodeAt(0)
|
||||
: (Number.isFinite(body.btMake) ? (body.btMake | 0) : 0x00);
|
||||
|
||||
const header = packSndHeader({ nTxnCd: ABS_POLL, nOpCd: 0, nNumRecsSent: 0, nNumRecsRqrd: 0, nTxnId: 0, cBtMake: btMake });
|
||||
|
||||
const reply = await withRetry(() => absRoundtrip(header, DEFAULT_TIMEOUT_MS), RETRIES, RETRY_DELAY_MS);
|
||||
const hdr = parseRcvHeaderFlexible(reply.subarray(0, Math.min(reply.length, 24)));
|
||||
|
||||
res.json({
|
||||
ok: true,
|
||||
target: `${ABS_HOST}:${ABS_PORT}`,
|
||||
sentHeaderHex: header.toString("hex"),
|
||||
replyBytes: reply.length,
|
||||
replyHexFirst64: reply.subarray(0, 64).toString("hex"),
|
||||
parsedRcvHeaderRaw: hdr,
|
||||
normalizedRcvHeader: { ...hdr, size: 18 },
|
||||
normalizedHeaderHex: buildRcvHeader18(hdr).toString("hex"),
|
||||
success: hdr.nRetCd === SUCCESS
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(502).json({ ok: false, error: String(e) });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/login", async (req, res) => {
|
||||
try {
|
||||
const body = await readJsonBody(req);
|
||||
const opCard = (body.opCard ?? "").toString();
|
||||
const btId = (body.btId ?? "").toString();
|
||||
const usrId = (body.usrId ?? "").toString();
|
||||
const plain = (body.password ?? "");
|
||||
const givenEnc = (body.passwordEnc ?? "");
|
||||
|
||||
if (!opCard || !btId || (!plain && !givenEnc)) {
|
||||
return res.status(400).json({ ok: false, error: "Missing opCard, btId, and either password or passwordEnc" });
|
||||
}
|
||||
|
||||
let encInfo = null;
|
||||
let passwd = givenEnc ? String(givenEnc) : (encInfo = encryptPasswordLikeMFC(String(plain))).enc;
|
||||
|
||||
const btMake = (typeof body.btMake === "string")
|
||||
? body.btMake.charCodeAt(0)
|
||||
: (Number.isFinite(body.btMake) ? (body.btMake | 0) : 0x00);
|
||||
|
||||
const sndHeader = packSndHeader({ nTxnCd: LOG, nOpCd: LOGBT, nNumRecsSent: 1, nNumRecsRqrd: 1, nTxnId: 0, cBtMake: btMake });
|
||||
const sndLog = packSndLog({ usrId, opCard, passwd, btId });
|
||||
const sendBuf = Buffer.concat([sndHeader, sndLog]);
|
||||
|
||||
const reply = await withRetry(() => absRoundtrip(sendBuf, DEFAULT_TIMEOUT_MS), RETRIES, RETRY_DELAY_MS);
|
||||
|
||||
const rcvHeader = parseRcvHeaderFlexible(reply.subarray(0, Math.min(reply.length, 24)));
|
||||
|
||||
const json = {
|
||||
ok: true,
|
||||
target: `${ABS_HOST}:${ABS_PORT}`,
|
||||
sentHeaderHex: sndHeader.toString("hex"),
|
||||
sentBodyHex: sndLog.toString("hex"),
|
||||
replyBytes: reply.length,
|
||||
replyHexFirst64: reply.subarray(0, 64).toString("hex"),
|
||||
rcvHeaderRaw: rcvHeader,
|
||||
success: rcvHeader.nRetCd === SUCCESS
|
||||
};
|
||||
|
||||
if (!givenEnc && encInfo) {
|
||||
json.encryption = { used: true, utcSeconds: encInfo.utcSeconds, key: encInfo.key, encPasswd: passwd };
|
||||
} else if (givenEnc) {
|
||||
json.encryption = { used: false, providedPasswordEnc: true };
|
||||
}
|
||||
|
||||
let bodyOffset = rcvHeader.size;
|
||||
bodyOffset = adjustForOptionalOp(reply, bodyOffset).offset;
|
||||
bodyOffset = alignToAsciiYear(reply, bodyOffset);
|
||||
|
||||
json.offsetProbe = {
|
||||
bodyOffset,
|
||||
around: reply.subarray(Math.max(0, bodyOffset - 8), Math.min(reply.length, bodyOffset + 48)).toString("ascii")
|
||||
};
|
||||
|
||||
if (json.success && reply.length >= bodyOffset + 20) {
|
||||
try { json.log = parseRcvLog(reply, bodyOffset); } catch (e) { json.parseLogError = String(e); }
|
||||
}
|
||||
|
||||
res.json(json);
|
||||
} catch (e) {
|
||||
res.status(502).json({ ok: false, error: String(e) });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/download/btinfo", async (req, res) => {
|
||||
try {
|
||||
const body = await readJsonBody(req);
|
||||
const opCard = (body.opCard ?? "").toString();
|
||||
const btId = (body.btId ?? "").toString();
|
||||
const usrId = (body.usrId ?? "").toString();
|
||||
const plain = (body.password ?? "");
|
||||
const encPass = (body.passwordEnc ?? "");
|
||||
if (!opCard || !btId || (!plain && !encPass)) {
|
||||
return res.status(400).json({ ok: false, error: "Missing opCard, btId, and either password or passwordEnc" });
|
||||
}
|
||||
const passwd = encPass ? String(encPass) : encryptPasswordLikeMFC(String(plain)).enc;
|
||||
|
||||
const sndHeader = packSndHeader({ nTxnCd: LOG, nOpCd: BTI, nNumRecsSent: 1, nNumRecsRqrd: 1, nTxnId: 0, cBtMake: 0 });
|
||||
const sndLog = packSndLog({ usrId, opCard, passwd, btId });
|
||||
const sendBuf = Buffer.concat([sndHeader, sndLog]);
|
||||
|
||||
const reply = await withRetry(() => absRoundtrip(sendBuf, DEFAULT_TIMEOUT_MS), RETRIES, RETRY_DELAY_MS);
|
||||
|
||||
const result = { ok: true, replyBytes: reply.length, replyHexFirst64: reply.subarray(0, 64).toString("hex") };
|
||||
|
||||
let offset = 0;
|
||||
if (remaining(reply, offset) < 18) return res.json({ ok: false, error: "truncated: no header" });
|
||||
|
||||
const hdr = parseRcvHeaderFlexible(reply.subarray(offset, Math.min(reply.length, offset + 24)));
|
||||
result.primaryHeader = hdr;
|
||||
|
||||
const hdrLenAligned = (hdr.size + 3) & ~3;
|
||||
if (!safeAdvanceOrBreak(reply, 0, hdrLenAligned, result)) {
|
||||
result.error = "truncated: incomplete header (after alignment)";
|
||||
return res.json(result);
|
||||
}
|
||||
offset = hdrLenAligned;
|
||||
|
||||
if (hdr.nRetCd !== SUCCESS) {
|
||||
result.error = `server returned nRetCd=${hdr.nRetCd}`;
|
||||
return res.json(result);
|
||||
}
|
||||
|
||||
if (!safeAdvanceOrBreak(reply, offset, 5 + 31 + 5 + 4 + 1 + 4 + 4 + 4 + 4, result)) {
|
||||
result.error = "truncated before BTI master";
|
||||
return res.json(result);
|
||||
}
|
||||
const btiMst = parseRcvBtiMst(reply, offset);
|
||||
offset += btiMst.size;
|
||||
result.btiMst = btiMst;
|
||||
|
||||
const details = [];
|
||||
let readCount = 0;
|
||||
while (readCount < hdr.nNumRecs) {
|
||||
if (looksLikeHeaderAt(reply, offset)) break;
|
||||
if (!safeAdvanceOrBreak(reply, offset, 1 + 3 + 1, result)) break;
|
||||
const dt = parseRcvBtiDtl(reply, offset);
|
||||
offset += dt.size;
|
||||
details.push(dt);
|
||||
readCount++;
|
||||
}
|
||||
result.btiDetails = details;
|
||||
result.btiReadCount = readCount;
|
||||
result.parsedBytes = offset;
|
||||
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
res.status(502).json({ ok: false, error: String(e), stack: e.stack });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/download/rpinfo", async (req, res) => {
|
||||
try {
|
||||
const body = await readJsonBody(req);
|
||||
const debug = String((req.query.debug ?? "")).toLowerCase() === "1";
|
||||
const opCard = (body.opCard ?? "").toString();
|
||||
const btId = (body.btId ?? "").toString();
|
||||
const usrId = (body.usrId ?? "").toString();
|
||||
const plain = (body.password ?? "");
|
||||
const encPass = (body.passwordEnc ?? "");
|
||||
|
||||
if (!opCard || !btId || (!plain && !encPass)) {
|
||||
return res.status(400).json({ ok: false, error: "Missing opCard, btId, and either password or passwordEnc" });
|
||||
}
|
||||
|
||||
const passwd = encPass ? String(encPass) : encryptPasswordLikeMFC(String(plain)).enc;
|
||||
const btAdvSalesFlg = body.btAdvSalesFlg || "N";
|
||||
const nBtAdvSaleEnb = btAdvSalesFlg === "A" ? 1 : 0;
|
||||
|
||||
const sndHeader = packSndHeader({ nTxnCd: LOG, nOpCd: RPI, nNumRecsSent: 1, nNumRecsRqrd: nBtAdvSaleEnb, nTxnId: 0, cBtMake: 0 });
|
||||
const sndLog = packSndLog({ usrId, opCard, passwd, btId });
|
||||
const sendBuf = Buffer.concat([sndHeader, sndLog]);
|
||||
|
||||
const reply = await withRetry(() => absRoundtrip(sendBuf, DEFAULT_TIMEOUT_MS), RETRIES, RETRY_DELAY_MS);
|
||||
|
||||
const result = { ok: true, replyBytes: reply.length, replyHexFirst64: reply.subarray(0, 64).toString("hex") };
|
||||
let offset = 0;
|
||||
|
||||
if (remaining(reply, offset) < 18) return res.json({ ok: false, error: "truncated: no top header" });
|
||||
const topHdr = parseRcvHeaderFlexible(reply.subarray(offset, Math.min(reply.length, offset + 24)));
|
||||
offset += topHdr.size;
|
||||
result.topHeader = topHdr;
|
||||
|
||||
if (topHdr.nRetCd !== SUCCESS) {
|
||||
result.error = `server returned nRetCd=${topHdr.nRetCd}`;
|
||||
return res.json(result);
|
||||
}
|
||||
|
||||
if (debug) result.debug_afterTopHdr = { offset, hex: hexSlice(reply, offset, 128) };
|
||||
|
||||
function parseSection(minRecBytes, recParser, outArrName, outHeaderName, expected_nTxnCd) {
|
||||
const items = [];
|
||||
let read = 0;
|
||||
let hdr = null;
|
||||
|
||||
if (remaining(reply, offset) < 18) {
|
||||
result[outArrName] = items;
|
||||
result[outHeaderName] = null;
|
||||
result[outArrName.replace(/s?$/, "ReadCount")] = read;
|
||||
if (debug) result[`debug_${outArrName}_no_data`] = `No data for ${outArrName}, offset=${offset}, remaining=${remaining(reply, offset)}`;
|
||||
return items;
|
||||
}
|
||||
|
||||
// Try parsing header
|
||||
offset = alignToPossibleHeader(reply, offset, expected_nTxnCd);
|
||||
if (looksLikeHeaderAt(reply, offset, expected_nTxnCd)) {
|
||||
hdr = parseRcvHeaderFlexible(reply.subarray(offset, Math.min(reply.length, offset + 24)));
|
||||
offset += hdr.size;
|
||||
const adj = adjustForOptionalOp(reply, offset);
|
||||
offset = adj.offset;
|
||||
|
||||
if (debug) result[`debug_${outArrName}_header`] = { offset, hdr, hex: hexSlice(reply, offset - hdr.size, 64), expected_nTxnCd };
|
||||
|
||||
while (read < hdr.nNumRecs && remaining(reply, offset) >= minRecBytes) {
|
||||
try {
|
||||
const rec = recParser(reply, offset);
|
||||
if (rec && rec.nRaceDt >= 20000101 && rec.nRaceDt <= 21001231 && rec.cVenueNum >= 1 && rec.cVenueNum <= MAX_VENUES) {
|
||||
items.push(rec);
|
||||
offset += rec.size;
|
||||
read++;
|
||||
} else {
|
||||
offset += 1;
|
||||
if (debug) result[`debug_${outArrName}_invalid`] = `Invalid record at offset ${offset}: ${JSON.stringify(rec)}`;
|
||||
}
|
||||
} catch (e) {
|
||||
if (debug) result[`debug_${outArrName}_error`] = `Error at offset ${offset}: ${e.message}`;
|
||||
offset += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback: try parsing data directly if no valid header
|
||||
if (remaining(reply, offset) >= minRecBytes) {
|
||||
try {
|
||||
const rec = recParser(reply, offset);
|
||||
if (rec && rec.nRaceDt >= 20000101 && rec.nRaceDt <= 21001231 && rec.cVenueNum >= 1 && rec.cVenueNum <= MAX_VENUES) {
|
||||
items.push(rec);
|
||||
offset += rec.size;
|
||||
read++;
|
||||
if (debug) result[`debug_${outArrName}_fallback`] = `Parsed ${outArrName} data without valid header at offset ${offset - rec.size}`;
|
||||
} else {
|
||||
if (debug) result[`debug_${outArrName}_fallback_invalid`] = `Fallback invalid record at offset ${offset}: ${JSON.stringify(rec)}`;
|
||||
}
|
||||
} catch (e) {
|
||||
if (debug) result[`debug_${outArrName}_fallback_error`] = `Fallback parse error at offset ${offset}: ${e.message}`;
|
||||
}
|
||||
}
|
||||
if (debug) result[`debug_${outArrName}_noheader`] = `No valid header for ${outArrName} at offset ${offset}, expected nTxnCd=${expected_nTxnCd}, hex=${hexSlice(reply, offset, 64)}`;
|
||||
}
|
||||
|
||||
result[outHeaderName] = hdr;
|
||||
result[outArrName] = items;
|
||||
result[outArrName.replace(/s?$/, "ReadCount")] = read;
|
||||
if (debug) result[`debug_${outArrName}_after`] = { offset, hex: hexSlice(reply, offset, 128) };
|
||||
return items;
|
||||
}
|
||||
|
||||
// Parse sections with expected nTxnCd
|
||||
const sections = [
|
||||
{ minRecBytes: 4 + 1 + 1 + 15 + 41, parser: parseRcvRpi1, arrName: "rpi1", headerName: "rpi1Header", nTxnCd: 101 },
|
||||
{ minRecBytes: 4 + 1 + 1 + 25 + 4, parser: parseRcvRpi2, arrName: "rpi2", headerName: "rpi2Header", nTxnCd: 102 },
|
||||
{ minRecBytes: 4 + 1 + 1 + 9, parser: parseRcvRpi3, arrName: "rpi3", headerName: "rpi3Header", nTxnCd: 103 },
|
||||
{ minRecBytes: 4 + 1 + 3 + 11 + 1, parser: parseRcvRpi4, arrName: "rpi4", headerName: "rpi4Header", nTxnCd: 104 },
|
||||
{ minRecBytes: 4 + 4 + 4 + 4 + 4 + 31, parser: parseRcvPvm, arrName: "pvms", headerName: "pvmHeader", nTxnCd: 105 }
|
||||
];
|
||||
|
||||
for (const section of sections) {
|
||||
parseSection(section.minRecBytes, section.parser, section.arrName, section.headerName, section.nTxnCd);
|
||||
}
|
||||
|
||||
result.parsedBytes = offset;
|
||||
|
||||
// Build raceCard
|
||||
const raceCard = [];
|
||||
for (const v of result.rpi1 || []) {
|
||||
const raceDate = getDateStr(v.nRaceDt);
|
||||
const venueId = getVenueIdStr(v.cVenueNum);
|
||||
const venueSts = v.cVenueSts;
|
||||
|
||||
for (let nRace = 0; nRace < MAX_RACES; nRace++) {
|
||||
if (v.cRaces[nRace] === "0" || !v.cRaces[nRace]) continue;
|
||||
const raceNum = (nRace + 1).toString().padStart(2, "0");
|
||||
raceCard.push({
|
||||
RACE_DATE: raceDate,
|
||||
RACE_VENUE_ID: venueId,
|
||||
RACE_NO: raceNum,
|
||||
STOPBET_STATUS: v.cRaces[nRace] === "C" ? "S" : v.cRaces[nRace],
|
||||
RACE_STATUS: v.cRaces[nRace] === "C" ? "C" : "V",
|
||||
VENUE_STATUS: venueSts,
|
||||
START_TIME: "",
|
||||
ADVT: v.cAdvertisement,
|
||||
horses: [],
|
||||
pools: []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const r of result.rpi2 || []) {
|
||||
const raceDate = getDateStr(r.nRaceDt);
|
||||
const venueId = getVenueIdStr(r.cVenueNum);
|
||||
const raceNum = r.cRaceNum.toString().padStart(2, "0");
|
||||
|
||||
for (let nHorse = 0; nHorse < MAX_HORSES; nHorse++) {
|
||||
if (r.cHorses[nHorse] === "0" || !r.cHorses[nHorse]) continue;
|
||||
const horseNum = (nHorse + 1).toString().padStart(2, "0");
|
||||
const entry = raceCard.find(e => e.RACE_DATE === raceDate && e.RACE_VENUE_ID === venueId && e.RACE_NO === raceNum);
|
||||
if (entry) {
|
||||
entry.horses.push({
|
||||
RACE_HORSE_NO: horseNum,
|
||||
HORSE_STATUS: r.cHorses[nHorse],
|
||||
REFUND_FILE_STATUS: ""
|
||||
});
|
||||
if (!entry.START_TIME) {
|
||||
entry.START_TIME = getTimeStr(r.nRaceStartTm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const p of result.rpi3 || []) {
|
||||
const raceDate = getDateStr(p.nRaceDt);
|
||||
const venueId = getVenueIdStr(p.cVenueNum);
|
||||
const raceNum = p.cRaceNum.toString().padStart(2, "0");
|
||||
|
||||
for (let nPoolId = 0; nPoolId < MAX_POOLS; nPoolId++) {
|
||||
if (p.cPools[nPoolId] === "0" || !p.cPools[nPoolId]) continue;
|
||||
const entry = raceCard.find(e => e.RACE_DATE === raceDate && e.RACE_VENUE_ID === venueId && e.RACE_NO === raceNum);
|
||||
if (entry) {
|
||||
entry.pools.push({
|
||||
POOL_ID: getPoolIdStr(nPoolId + 1),
|
||||
POOL_STATUS: p.cPools[nPoolId],
|
||||
BETTING_CENTRE_ID: "",
|
||||
SELECT_RACE_NO: ""
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const s of result.rpi4 || []) {
|
||||
const raceDate = getDateStr(s.nRaceDt);
|
||||
const venueId = getVenueIdStr(s.cVenueNum);
|
||||
const raceNum = s.cRaceNum;
|
||||
const venueSts = raceCard.find(e => e.RACE_DATE === raceDate && e.RACE_VENUE_ID === venueId)?.VENUE_STATUS || "V";
|
||||
|
||||
let entry = raceCard.find(e => e.RACE_DATE === raceDate && e.RACE_VENUE_ID === venueId && e.RACE_NO === raceNum);
|
||||
if (!entry) {
|
||||
entry = {
|
||||
RACE_DATE: raceDate,
|
||||
RACE_VENUE_ID: venueId,
|
||||
RACE_NO: raceNum,
|
||||
STOPBET_STATUS: "",
|
||||
RACE_STATUS: s.cPoolSts,
|
||||
VENUE_STATUS: venueSts,
|
||||
START_TIME: "",
|
||||
ADVT: "",
|
||||
horses: [],
|
||||
pools: []
|
||||
};
|
||||
raceCard.push(entry);
|
||||
}
|
||||
|
||||
entry.pools.push({
|
||||
POOL_ID: getMultiLegPoolIdStr(s.cRaceNum),
|
||||
POOL_STATUS: s.cPoolSts,
|
||||
BETTING_CENTRE_ID: "",
|
||||
SELECT_RACE_NO: s.cSelectRaceNum
|
||||
});
|
||||
}
|
||||
|
||||
for (const p of result.rpi3 || []) {
|
||||
if (!p.cPools.includes("TBP") && !p.cPools.includes("MJP") && !p.cPools.includes("JPP")) continue;
|
||||
const raceDate = getDateStr(p.nRaceDt);
|
||||
const venueId = getVenueIdStr(p.cVenueNum);
|
||||
const raceNum = p.cRaceNum.toString().padStart(2, "0");
|
||||
|
||||
for (const s of result.rpi4 || []) {
|
||||
if (s.cSelectRaceNum && s.nRaceDt === p.nRaceDt && s.cVenueNum === p.cVenueNum) {
|
||||
const selectRaceNum = s.cSelectRaceNum.slice(0, 2);
|
||||
const selectEntry = raceCard.find(e => e.RACE_DATE === raceDate && e.RACE_VENUE_ID === venueId && e.RACE_NO === selectRaceNum);
|
||||
const targetEntry = raceCard.find(e => e.RACE_DATE === raceDate && e.RACE_VENUE_ID === venueId && e.RACE_NO === raceNum);
|
||||
if (selectEntry && targetEntry) {
|
||||
targetEntry.STOPBET_STATUS = selectEntry.STOPBET_STATUS;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pvmData = (result.pvms || []).map(p => ({
|
||||
RACE_VENUE: p.cRaceVenue,
|
||||
POOL_ID: p.cPoolId,
|
||||
UNIT_PRICE: p.fUnitPrice,
|
||||
TAX_RATE: p.fTaxRate,
|
||||
DEDUCT_RATE: p.fDeductRate,
|
||||
REMARKS: p.cRemarks
|
||||
}));
|
||||
|
||||
result.raceCard = raceCard;
|
||||
result.pvm = pvmData;
|
||||
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
res.status(502).json({ ok: false, error: String(e), stack: e.stack });
|
||||
}
|
||||
});
|
||||
|
||||
const HTTP_PORT = Number(process.env.HTTP_BRIDGE_PORT || 8080);
|
||||
app.listen(HTTP_PORT, () => {
|
||||
console.log(`ABS HTTP bridge listening on :${HTTP_PORT}`);
|
||||
console.log(`Target ABS server: ${ABS_HOST}:${ABS_PORT}`);
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user