Decode a receipt body before reading it and stop an unnameable state TLV overriding it
This commit is contained in:
@@ -33,6 +33,10 @@ export function paramText(value: ParamValue | undefined): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
export function paramNumber(value: ParamValue | undefined, fallback: number): number {
|
||||
return typeof value === 'number' ? value : fallback;
|
||||
}
|
||||
|
||||
function outOfRange(buffer: Buffer, offset: number, needed: number): Error | undefined {
|
||||
if (offset < 0 || needed < 0 || offset + needed > buffer.length) {
|
||||
return new Error(
|
||||
|
||||
+2
-8
@@ -3,7 +3,7 @@ import type { MessageState } from './defs/constants.ts';
|
||||
import type { SmppLog } from './log.ts';
|
||||
import { ExpiringGroups } from './expiring-groups.ts';
|
||||
|
||||
export type MessageDlr = Dlr & { segments: Dlr[] };
|
||||
export type MessageDlr = Dlr & { segments: Dlr[]; smsId: string };
|
||||
|
||||
export type DlrMergerOptions = {
|
||||
log: SmppLog;
|
||||
@@ -42,12 +42,6 @@ const severity: Record<MessageState, number> = {
|
||||
UNDELIVERABLE: 9,
|
||||
};
|
||||
|
||||
function severityOf(dlr: Dlr): number {
|
||||
const ranked: Record<string, number | undefined> = severity;
|
||||
|
||||
return ranked[dlr.statusMsg] ?? severity.UNKNOWN;
|
||||
}
|
||||
|
||||
export class DlrMerger {
|
||||
private readonly groups: ExpiringGroups<Group>;
|
||||
private readonly log: SmppLog;
|
||||
@@ -112,7 +106,7 @@ export class DlrMerger {
|
||||
this.groups.delete(base);
|
||||
|
||||
const segments = [...group.parts.entries()].sort(([a], [b]) => a - b).map(([, one]) => one);
|
||||
const worst = segments.reduce((carry, one) => (severityOf(one) > severityOf(carry) ? one : carry));
|
||||
const worst = segments.reduce((carry, one) => (severity[one.statusMsg] > severity[carry.statusMsg] ? one : carry));
|
||||
|
||||
return { ...worst, segments, smsId: base };
|
||||
}
|
||||
|
||||
+36
-17
@@ -2,6 +2,8 @@ import type { MessageState } from './defs/constants.ts';
|
||||
import type { ParamValue } from './defs/types.ts';
|
||||
import type { PduObject } from './pdu.ts';
|
||||
import { consts, constsById } from './defs/constants.ts';
|
||||
import { decodeMessage } from './message.ts';
|
||||
import { paramNumber, paramText } from './defs/types.ts';
|
||||
|
||||
/**
|
||||
* The seven-character status codes carried in a receipt's `stat:` field, mapped to the
|
||||
@@ -49,7 +51,7 @@ export type Dlr = {
|
||||
receipt: Receipt | undefined;
|
||||
smsId: string | undefined;
|
||||
statusId: number;
|
||||
statusMsg: string;
|
||||
statusMsg: MessageState;
|
||||
};
|
||||
|
||||
const field = (name: string) => new RegExp(`\\b${name}:([^ ]*)`, 'i');
|
||||
@@ -127,15 +129,25 @@ const messageTypeBits = 0x3c;
|
||||
type MessageType = 'other' | 'receipt' | 'unmarked';
|
||||
|
||||
function messageType(pduObj: PduObject): MessageType {
|
||||
const esmClass = pduObj.params.esm_class;
|
||||
|
||||
if (typeof esmClass !== 'number') return 'unmarked';
|
||||
|
||||
const type = esmClass & messageTypeBits;
|
||||
const type = paramNumber(pduObj.params.esm_class, 0) & messageTypeBits;
|
||||
|
||||
if (type === consts.ESM_CLASS.MC_DELIVERY_RECEIPT) return 'receipt';
|
||||
if (type !== 0) return 'other';
|
||||
|
||||
return type === 0 ? 'unmarked' : 'other';
|
||||
return pduObj.tlvs.receipted_message_id === undefined ? 'unmarked' : 'receipt';
|
||||
}
|
||||
|
||||
/** A UDH-carrying short_message reaches here as a buffer, header and all. */
|
||||
function receiptBody(pduObj: PduObject): string {
|
||||
const message = pduObj.params.short_message;
|
||||
|
||||
if (!Buffer.isBuffer(message)) return paramText(message);
|
||||
|
||||
return decodeMessage(
|
||||
message,
|
||||
paramNumber(pduObj.params.data_coding, 0),
|
||||
paramNumber(pduObj.params.esm_class, 0),
|
||||
).message;
|
||||
}
|
||||
|
||||
function receiptId(tlvId: ParamValue | undefined, receipt: Receipt | undefined): string | undefined {
|
||||
@@ -144,32 +156,39 @@ function receiptId(tlvId: ParamValue | undefined, receipt: Receipt | undefined):
|
||||
return receipt?.id === '' ? undefined : receipt?.id;
|
||||
}
|
||||
|
||||
function isMessageState(name: string | undefined): name is MessageState {
|
||||
return name !== undefined && name in consts.MESSAGE_STATE;
|
||||
}
|
||||
|
||||
/** The state TLV wins where it names a state we know; an unnameable one leaves the body to say. */
|
||||
function receiptStatus(
|
||||
tlvState: ParamValue | undefined,
|
||||
receipt: Receipt | undefined,
|
||||
): { statusId: number; statusMsg: string | undefined } {
|
||||
if (typeof tlvState === 'number') {
|
||||
return { statusId: tlvState, statusMsg: constsById.MESSAGE_STATE?.[tlvState] };
|
||||
): { statusId: number; statusMsg: MessageState | undefined } {
|
||||
const scraped = receiptStates[receipt?.stat?.toUpperCase() ?? ''];
|
||||
|
||||
if (typeof tlvState !== 'number') {
|
||||
return { statusId: consts.MESSAGE_STATE[scraped ?? 'UNKNOWN'], statusMsg: scraped };
|
||||
}
|
||||
|
||||
const state = receiptStates[receipt?.stat?.toUpperCase() ?? ''];
|
||||
const named = constsById.MESSAGE_STATE?.[tlvState];
|
||||
|
||||
return { statusId: consts.MESSAGE_STATE[state ?? 'UNKNOWN'], statusMsg: state };
|
||||
return { statusId: tlvState, statusMsg: isMessageState(named) ? named : scraped };
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a delivery report from a deliver_sm, or nothing if the PDU carries a message rather than a
|
||||
* receipt. `esm_class` decides that where the peer sets a message type; where it sets none, the body
|
||||
* is read for the standard receipt fields, which is the only thing Kannel and several other SMSCs
|
||||
* send. The message_state and receipted_message_id TLVs are authoritative over the body.
|
||||
* receipt. `esm_class` decides that where the peer names a message type and a receipted_message_id
|
||||
* TLV where it names none; failing both, the body is read for the standard receipt fields, which is
|
||||
* the only thing Kannel and several other SMSCs send.
|
||||
*/
|
||||
export function dlrFromPdu(pduObj: PduObject): Dlr | undefined {
|
||||
const type = messageType(pduObj);
|
||||
|
||||
if (type === 'other') return undefined;
|
||||
|
||||
const message = pduObj.params.short_message;
|
||||
const receipt = typeof message === 'string' ? parseReceipt(message) : undefined;
|
||||
const body = receiptBody(pduObj);
|
||||
const receipt = body === '' ? undefined : parseReceipt(body);
|
||||
const smsId = receiptId(pduObj.tlvs.receipted_message_id?.tagValue, receipt);
|
||||
const { statusId, statusMsg } = receiptStatus(pduObj.tlvs.message_state?.tagValue, receipt);
|
||||
|
||||
|
||||
+4
-7
@@ -8,6 +8,7 @@ import { consts } from './defs/constants.ts';
|
||||
import { decodeMessage, encodeMessage } from './message.ts';
|
||||
import { detect, encodingByDataCoding } from './defs/encodings.ts';
|
||||
import { errorNameById, errors, isErrorName } from './defs/errors.ts';
|
||||
import { paramNumber } from './defs/types.ts';
|
||||
import { tlvDefault, tlvs, tlvsById } from './defs/tlvs.ts';
|
||||
|
||||
/** Sequence numbers are a 31-bit field; 0x7fffffff is reserved. */
|
||||
@@ -63,10 +64,6 @@ export function isCommand<C extends CommandName>(
|
||||
return pduObj.cmdName === cmdName;
|
||||
}
|
||||
|
||||
function numberOr(value: ParamValue | undefined, fallback: number): number {
|
||||
return typeof value === 'number' ? value : fallback;
|
||||
}
|
||||
|
||||
function tagIdOf(name: string, input: TlvInput): Result<{ tagId: number }> {
|
||||
const tagId = input.tagId ?? tlvs[name]?.id;
|
||||
|
||||
@@ -283,7 +280,7 @@ function readParams(
|
||||
let offset = 16;
|
||||
|
||||
for (const [name, type] of Object.entries(cmds[cmdName]?.params ?? {})) {
|
||||
const read = type.read(pdu, offset, numberOr(params.sm_length, 0));
|
||||
const read = type.read(pdu, offset, paramNumber(params.sm_length, 0));
|
||||
|
||||
if (read.err) {
|
||||
return { err: new Error(`Parameter "${name}" of "${cmdName}": ${read.err.message}`) };
|
||||
@@ -327,11 +324,11 @@ function parseOnce(pdu: Buffer, trailingNull: boolean): Result<{ aligned: boolea
|
||||
|
||||
const params = read.params;
|
||||
const message = params.short_message;
|
||||
const esmClass = numberOr(params.esm_class, 0);
|
||||
const esmClass = paramNumber(params.esm_class, 0);
|
||||
|
||||
// A message carrying a UDH stays a buffer; the session needs the header intact to reassemble.
|
||||
if (Buffer.isBuffer(message) && (esmClass & consts.ESM_CLASS.UDH_INDICATOR) !== consts.ESM_CLASS.UDH_INDICATOR) {
|
||||
params.short_message = decodeMessage(message, numberOr(params.data_coding, 0)).message;
|
||||
params.short_message = decodeMessage(message, paramNumber(params.data_coding, 0)).message;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
+3
-7
@@ -5,7 +5,7 @@ import type { SmppLog } from './log.ts';
|
||||
import type { Tlv } from './defs/tlvs.ts';
|
||||
import { ExpiringGroups } from './expiring-groups.ts';
|
||||
import { decodeMessage } from './message.ts';
|
||||
import { paramText } from './defs/types.ts';
|
||||
import { paramNumber, paramText } from './defs/types.ts';
|
||||
|
||||
export type ReassemblerOptions = {
|
||||
log: SmppLog;
|
||||
@@ -71,10 +71,6 @@ function groupKey(pduObj: PduObject, reference: number): string {
|
||||
].join('_');
|
||||
}
|
||||
|
||||
function numberOr(value: ParamValue | undefined, fallback: number): number {
|
||||
return typeof value === 'number' ? value : fallback;
|
||||
}
|
||||
|
||||
/** The text of a message, joining its segments in the order they were reassembled. */
|
||||
export function decodeSegments(pduObjs: PduObject[]): string {
|
||||
let message = '';
|
||||
@@ -85,8 +81,8 @@ export function decodeSegments(pduObjs: PduObject[]): string {
|
||||
message += Buffer.isBuffer(part)
|
||||
? decodeMessage(
|
||||
part,
|
||||
numberOr(pduObj.params.data_coding, 0),
|
||||
numberOr(pduObj.params.esm_class, 0),
|
||||
paramNumber(pduObj.params.data_coding, 0),
|
||||
paramNumber(pduObj.params.esm_class, 0),
|
||||
).message
|
||||
: paramText(part);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user