diff --git a/AGENTS.md b/AGENTS.md index 62a9b4b..bf74705 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -206,6 +206,8 @@ exactly 140. keeps the scrape: SMSCs that send text-only receipts leave `esm_class` at 0, and reading that as the spec's "default message type" would lose every one of them. A `receipted_message_id` TLV marks a receipt on the same footing where the message type is 0, since nothing but a receipt carries one. + What gets scraped is the decoded `short_message` with any UDH stripped, so a concatenated receipt + is read like any other. The `message_state` TLV is authoritative only where it names a state in the table — SMPP reserves 0x80-0xFF for MC-vendor-specific values, so an unnameable one keeps its raw `statusId` and leaves `statusMsg` to the body. diff --git a/src/defs/constants.ts b/src/defs/constants.ts index 8980057..4c38276 100644 --- a/src/defs/constants.ts +++ b/src/defs/constants.ts @@ -103,6 +103,15 @@ export const consts = { }, } as const; +/** esm_class bits 5-2 name the message type; bit 6 is the UDH indicator. */ +export function hasUdh(esmClass: number): boolean { + return (esmClass & consts.ESM_CLASS.UDH_INDICATOR) === consts.ESM_CLASS.UDH_INDICATOR; +} + +export function messageTypeOf(esmClass: number): number { + return esmClass & 0x3c; +} + export type ConstGroup = keyof typeof consts; export type MessageState = keyof typeof consts.MESSAGE_STATE; diff --git a/src/dlr.ts b/src/dlr.ts index f909c7f..55b78b2 100644 --- a/src/dlr.ts +++ b/src/dlr.ts @@ -1,7 +1,7 @@ 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 { consts, constsById, messageTypeOf } from './defs/constants.ts'; import { decodeMessage } from './message.ts'; import { paramNumber, paramText } from './defs/types.ts'; @@ -123,13 +123,10 @@ export function parseReceipt(message: string): Receipt { }; } -/** esm_class bits 5-2 name the message type; the rest are the messaging mode and the GSM features. */ -const messageTypeBits = 0x3c; - type MessageType = 'other' | 'receipt' | 'unmarked'; function messageType(pduObj: PduObject): MessageType { - const type = paramNumber(pduObj.params.esm_class, 0) & messageTypeBits; + const type = messageTypeOf(paramNumber(pduObj.params.esm_class, 0)); if (type === consts.ESM_CLASS.MC_DELIVERY_RECEIPT) return 'receipt'; if (type !== 0) return 'other'; diff --git a/src/incoming-requests.ts b/src/incoming-requests.ts index 9a187da..4bccb59 100644 --- a/src/incoming-requests.ts +++ b/src/incoming-requests.ts @@ -6,10 +6,10 @@ import type { SmppLog } from './log.ts'; import { Reassembler, decodeSegments } from './reassembly.ts'; import { bindCommands, defaults } from './session-options.ts'; import { concatInfo } from './udh.ts'; -import { consts } from './defs/constants.ts'; +import { hasUdh } from './defs/constants.ts'; import { createSms } from './sms.ts'; import { dlrFromPdu } from './dlr.ts'; -import { paramText } from './defs/types.ts'; +import { paramNumber, paramText } from './defs/types.ts'; export type IncomingRequestsOptions = { dlrMerger: DlrMerger; @@ -115,10 +115,8 @@ export class IncomingRequests { private onMessage(pduObj: PduObject): void { const message = pduObj.params.short_message; - const esmClass = pduObj.params.esm_class; - const hasUdh = typeof esmClass === 'number' - && (esmClass & consts.ESM_CLASS.UDH_INDICATOR) === consts.ESM_CLASS.UDH_INDICATOR; - const concat = hasUdh && Buffer.isBuffer(message) ? concatInfo(message) : undefined; + const carriesUdh = hasUdh(paramNumber(pduObj.params.esm_class, 0)); + const concat = carriesUdh && Buffer.isBuffer(message) ? concatInfo(message) : undefined; if (!concat) { this.emitSms([pduObj]); diff --git a/src/message.ts b/src/message.ts index 621d1b0..792aca5 100644 --- a/src/message.ts +++ b/src/message.ts @@ -1,6 +1,6 @@ import type { Result } from './result.ts'; import type { EncodingName } from './defs/encodings.ts'; -import { consts } from './defs/constants.ts'; +import { hasUdh } from './defs/constants.ts'; import { detect, encodingByDataCoding, encodings } from './defs/encodings.ts'; /** A single SMS carries 1120 bits, whatever the alphabet. */ @@ -33,7 +33,7 @@ export function decodeMessage( ): { message: string; udh: Buffer | undefined } { const encoding = encodingByDataCoding(dataCoding); - if ((esmClass & consts.ESM_CLASS.UDH_INDICATOR) !== consts.ESM_CLASS.UDH_INDICATOR) { + if (!hasUdh(esmClass)) { return { message: encodings[encoding].decode(buffer), udh: undefined }; } diff --git a/src/pdu.ts b/src/pdu.ts index 206e58e..5ff6339 100644 --- a/src/pdu.ts +++ b/src/pdu.ts @@ -4,7 +4,7 @@ import type { ParamValue } from './defs/types.ts'; import type { Result, VoidResult } from './result.ts'; import type { Tlv } from './defs/tlvs.ts'; import { cmds, commandNameById, isCommandName } from './defs/commands.ts'; -import { consts } from './defs/constants.ts'; +import { consts, hasUdh } 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'; @@ -327,7 +327,7 @@ function parseOnce(pdu: Buffer, trailingNull: boolean): Result<{ aligned: boolea 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) { + if (Buffer.isBuffer(message) && !hasUdh(esmClass)) { params.short_message = decodeMessage(message, paramNumber(params.data_coding, 0)).message; } diff --git a/todo.md b/todo.md index 116ab7b..2e4cce1 100644 --- a/todo.md +++ b/todo.md @@ -147,12 +147,6 @@ session message is a change to every call site. `reassembly`, `dlr-merger`, `send-window`, `link-timers`, `reconnect-loop`, `pending-requests` and `send-sms`, so the directory would make that boundary visible. Do it on the next extraction out of `session.ts`, not as a move of its own. -- [ ] **Nothing owns the `esm_class` bits.** Three modules read them with their own literals: - `pdu.ts` decides decode-or-not, `incoming-requests.ts` reassemble-or-not and `dlr.ts` - receipt-or-not. That divergence is what let a UDH-carrying receipt reach `dlrFromPdu()` as an - undecoded buffer. Two predicates next to the constants would collapse it without touching - `index.ts`. - - [ ] **`submit_multi` and the broadcast commands** encode and decode, but nothing exercises them end to end. The interop suite is the natural place. - [ ] **Move to TypeScript 7** once `typescript-eslint` supports it; `renovate.json` pins TypeScript