Files
smpp-js/src/sms.ts
T

156 lines
4.4 KiB
TypeScript

import type { ErrorName } from './defs/errors.ts';
import type { MessageState } from './defs/constants.ts';
import type { PduObject, TlvInput } from './pdu.ts';
import type { Result, VoidResult } from './result.ts';
import type { Session } from './session.ts';
import { consts } from './defs/constants.ts';
import { receiptCodes } from './dlr.ts';
import { smppDate } from './message.ts';
import { uuidv7 } from './uuid.ts';
export type SendRespOptions = {
/** The id the peer correlates a later delivery receipt by. Defaults to a generated UUID v7. */
smsId?: string;
status?: ErrorName;
};
/**
* A received SMS, and the handle for answering it. Multipart messages arrive as one Sms carrying
* every segment's PDU.
*/
export type Sms = {
dlr: boolean;
flash: boolean;
from: string;
message: string;
pduObjs: PduObject[];
/** Sends a delivery report back to the sender. Defaults to DELIVERED. */
sendDlr: (status?: MessageState) => Promise<Result<{ pduObjs: PduObject[] }>>;
/** Answers every segment. Part of the protocol, not optional. Defaults to ESME_ROK. */
sendResp: (options?: SendRespOptions) => Promise<VoidResult>;
session: Session;
/** The id answered to the peer: what sendResp() was given, or a generated UUID v7. */
readonly smsId: string;
submitTime: Date;
to: string;
};
export type SmsInput = {
from: string;
message: string;
pduObjs: PduObject[];
session: Session;
to: string;
};
/** Each segment of a multipart message gets its own message_id, as a separate submit_sm must. */
function segmentId(smsId: string, index: number, total: number): string {
return total === 1 ? smsId : `${smsId}-${String(index + 1)}`;
}
export function createSms(input: SmsInput): Sms {
const first = input.pduObjs[0];
const registered = first?.params.registered_delivery;
const dataCoding = first?.params.data_coding;
const answered = { smsId: uuidv7() };
const sms: Sms = {
dlr: typeof registered === 'number' && registered !== 0,
flash: typeof dataCoding === 'number' && (dataCoding & 0xF0) === 0x10,
from: input.from,
message: input.message,
pduObjs: input.pduObjs,
sendDlr: status => sendDlr(sms, status),
sendResp: options => sendResp(sms, answered, options ?? {}),
session: input.session,
get smsId(): string {
return answered.smsId;
},
submitTime: new Date(),
to: input.to,
};
return sms;
}
async function sendResp(
sms: Sms,
answered: { smsId: string },
options: SendRespOptions,
): Promise<VoidResult> {
const total = sms.pduObjs.length;
if (total === 0) {
return { err: new Error('No PDUs to answer') };
}
if (options.smsId === '') {
return { err: new Error('smsId must not be empty') };
}
if (options.smsId !== undefined) answered.smsId = options.smsId;
const results = await Promise.all(sms.pduObjs.map((pduObj, index) => sms.session.sendReturn(
pduObj,
options.status ?? 'ESME_ROK',
{ message_id: segmentId(answered.smsId, index, total) },
)));
return results.find(result => result.err) ?? {};
}
/** The receipt as text, which is all of it a peer below SMPP 3.4 is allowed to be sent. */
function receiptText(sms: Sms, smsId: string, status: MessageState): string {
const delivered = status === 'DELIVERED';
return [
`id:${smsId}`,
'sub:001',
`dlvrd:${delivered ? '001' : '000'}`,
`submit date:${smppDate(sms.submitTime)}`,
`done date:${smppDate(new Date())}`,
`stat:${receiptCodes[status]}`,
`err:${delivered ? '000' : '001'}`,
'text:',
].join(' ');
}
function receiptTlvs(smsId: string, status: MessageState): Record<string, TlvInput> {
return {
message_state: { tagValue: consts.MESSAGE_STATE[status] },
receipted_message_id: { tagValue: smsId },
};
}
async function sendDlr(
sms: Sms,
status: MessageState = 'DELIVERED',
): Promise<Result<{ pduObjs: PduObject[] }>> {
if (!sms.session.bindAllows('deliver_sm')) {
return { err: new Error('A transmitter-bound session does not carry deliver_sm') };
}
const total = sms.pduObjs.length;
const pduObjs: PduObject[] = [];
for (let index = 0; index < total; index++) {
const smsId = segmentId(sms.smsId, index, total);
const sent = await sms.session.send({
cmdName: 'deliver_sm',
params: {
destination_addr: sms.from,
esm_class: consts.ESM_CLASS.MC_DELIVERY_RECEIPT,
short_message: receiptText(sms, smsId, status),
source_addr: sms.to,
},
...(sms.session.acceptsOptionalParams() ? { tlvs: receiptTlvs(smsId, status) } : {}),
});
if (sent.err) return { err: sent.err };
pduObjs.push(sent.pduObj);
}
return { pduObjs };
}