Port SMPP definitions: constants, errors, encodings and wire types
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
export type EncodingName = 'ASCII' | 'FLASH' | 'LATIN1' | 'UCS2';
|
||||
|
||||
export type Encoding = {
|
||||
decode: (buffer: Uint8Array) => string;
|
||||
encode: (value: string) => Buffer;
|
||||
match: (value: string) => boolean;
|
||||
};
|
||||
|
||||
const gsmChars =
|
||||
'@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ\x1BÆæßÉ !"#¤%&\'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà';
|
||||
|
||||
const gsmRegex =
|
||||
/^[@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ\x1BÆæßÉ !"#¤%&'()*+,\-./0-9:;<=>?¡A-ZÄÖÑܧ¿a-zäöñüà\f^{}\\[~\]|€]*$/;
|
||||
|
||||
const gsmExtended = /[\f^{}\\[~\]|€]/g;
|
||||
const gsmEscaped = /\x1B([\nΛ()/<=>¡e])/g;
|
||||
|
||||
const gsmCharCodes = new Map<string, number>();
|
||||
const gsmExtChars = new Map<string, string>();
|
||||
|
||||
for (let i = 0; i < gsmChars.length; i++) {
|
||||
gsmCharCodes.set(gsmChars[i], i);
|
||||
}
|
||||
|
||||
{
|
||||
const from = '\f^{}\\[~]|€';
|
||||
const to = '\nΛ()/<=>¡e';
|
||||
|
||||
for (let i = 0; i < from.length; i++) {
|
||||
gsmExtChars.set(from[i], to[i]);
|
||||
gsmExtChars.set(to[i], from[i]);
|
||||
}
|
||||
}
|
||||
|
||||
const ascii: Encoding = {
|
||||
decode(buffer) {
|
||||
let result = '';
|
||||
|
||||
for (const byte of buffer) {
|
||||
result += gsmChars[byte] ?? ' ';
|
||||
}
|
||||
|
||||
return result.replace(gsmEscaped, (match, escaped: string) => gsmExtChars.get(escaped) ?? match);
|
||||
},
|
||||
|
||||
encode(value) {
|
||||
const escaped = value.replace(gsmExtended, match => `\x1B${gsmExtChars.get(match) ?? ''}`);
|
||||
const result: number[] = [];
|
||||
|
||||
for (const char of escaped) {
|
||||
result.push(gsmCharCodes.get(char) ?? 0x20);
|
||||
}
|
||||
|
||||
return Buffer.from(result);
|
||||
},
|
||||
|
||||
match(value) {
|
||||
return gsmRegex.test(value);
|
||||
},
|
||||
};
|
||||
|
||||
const latin1: Encoding = {
|
||||
decode(buffer) {
|
||||
return Buffer.from(buffer).toString('latin1');
|
||||
},
|
||||
|
||||
encode(value) {
|
||||
return Buffer.from(value, 'latin1');
|
||||
},
|
||||
|
||||
// Deliberately never selected by detect(); Latin-1 is decoded when a peer asks for it, never
|
||||
// chosen for outgoing messages.
|
||||
match() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
const ucs2: Encoding = {
|
||||
decode(buffer) {
|
||||
return Buffer.from(buffer).swap16().toString('utf16le');
|
||||
},
|
||||
|
||||
encode(value) {
|
||||
return Buffer.from(value, 'utf16le').swap16();
|
||||
},
|
||||
|
||||
match() {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const encodings: Record<EncodingName, Encoding> = {
|
||||
ASCII: ascii,
|
||||
FLASH: ascii,
|
||||
LATIN1: latin1,
|
||||
UCS2: ucs2,
|
||||
};
|
||||
|
||||
export function detect(value: string): EncodingName {
|
||||
if (encodings.ASCII.match(value)) return 'ASCII';
|
||||
if (encodings.LATIN1.match(value)) return 'LATIN1';
|
||||
|
||||
return 'UCS2';
|
||||
}
|
||||
|
||||
/**
|
||||
* SMPP data_coding is a flat table for 0x00-0x0E, but the 0x1X and 0xFX ranges carry a GSM message
|
||||
* class and encode the alphabet in bits 3-2 (or bit 2) instead — which is how a flash UCS2 message
|
||||
* arrives as 0x18. Alphabets with no codec here fall back to ASCII.
|
||||
*/
|
||||
export function encodingByDataCoding(dataCoding: number): EncodingName {
|
||||
if ((dataCoding & 0xF0) === 0x10) {
|
||||
return ((dataCoding >> 2) & 0x03) === 0x02 ? 'UCS2' : 'ASCII';
|
||||
}
|
||||
|
||||
if ((dataCoding & 0xF0) === 0xF0) {
|
||||
return 'ASCII';
|
||||
}
|
||||
|
||||
if (dataCoding === 0x03) return 'LATIN1';
|
||||
if (dataCoding === 0x08) return 'UCS2';
|
||||
|
||||
return 'ASCII';
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Ordered by code, mirroring the SMPP 5.0 command_status table, so gaps stay visible.
|
||||
export const errors = {
|
||||
ESME_ROK: 0x0000,
|
||||
ESME_RINVMSGLEN: 0x0001,
|
||||
ESME_RINVCMDLEN: 0x0002,
|
||||
ESME_RINVCMDID: 0x0003,
|
||||
ESME_RINVBNDSTS: 0x0004,
|
||||
ESME_RALYBND: 0x0005,
|
||||
ESME_RINVPRTFLG: 0x0006,
|
||||
ESME_RINVREGDLVFLG: 0x0007,
|
||||
ESME_RSYSERR: 0x0008,
|
||||
ESME_RINVSRCADR: 0x000A,
|
||||
ESME_RINVDSTADR: 0x000B,
|
||||
ESME_RINVMSGID: 0x000C,
|
||||
ESME_RBINDFAIL: 0x000D,
|
||||
ESME_RINVPASWD: 0x000E,
|
||||
ESME_RINVSYSID: 0x000F,
|
||||
ESME_RCANCELFAIL: 0x0011,
|
||||
ESME_RREPLACEFAIL: 0x0013,
|
||||
ESME_RMSGQFUL: 0x0014,
|
||||
ESME_RINVSERTYP: 0x0015,
|
||||
ESME_RINVNUMDESTS: 0x0033,
|
||||
ESME_RINVDLNAME: 0x0034,
|
||||
ESME_RINVDESTFLAG: 0x0040,
|
||||
ESME_RINVSUBREP: 0x0042,
|
||||
ESME_RINVESMCLASS: 0x0043,
|
||||
ESME_RCNTSUBDL: 0x0044,
|
||||
ESME_RSUBMITFAIL: 0x0045,
|
||||
ESME_RINVSRCTON: 0x0048,
|
||||
ESME_RINVSRCNPI: 0x0049,
|
||||
ESME_RINVDSTTON: 0x0050,
|
||||
ESME_RINVDSTNPI: 0x0051,
|
||||
ESME_RINVSYSTYP: 0x0053,
|
||||
ESME_RINVREPFLAG: 0x0054,
|
||||
ESME_RINVNUMMSGS: 0x0055,
|
||||
ESME_RTHROTTLED: 0x0058,
|
||||
ESME_RINVSCHED: 0x0061,
|
||||
ESME_RINVEXPIRY: 0x0062,
|
||||
ESME_RINVDFTMSGID: 0x0063,
|
||||
ESME_RX_T_APPN: 0x0064,
|
||||
ESME_RX_P_APPN: 0x0065,
|
||||
ESME_RX_R_APPN: 0x0066,
|
||||
ESME_RQUERYFAIL: 0x0067,
|
||||
ESME_RINVTLVSTREAM: 0x00C0,
|
||||
ESME_RTLVNOTALLWD: 0x00C1,
|
||||
ESME_RINVTLVLEN: 0x00C2,
|
||||
ESME_RMISSINGTLV: 0x00C3,
|
||||
ESME_RINVTLVVAL: 0x00C4,
|
||||
ESME_RDELIVERYFAILURE: 0x00FE,
|
||||
ESME_RUNKNOWNERR: 0x00FF,
|
||||
ESME_RSERTYPUNAUTH: 0x0100,
|
||||
ESME_RPROHIBITED: 0x0101,
|
||||
ESME_RSERTYPUNAVAIL: 0x0102,
|
||||
ESME_RSERTYPDENIED: 0x0103,
|
||||
ESME_RINVDCS: 0x0104,
|
||||
ESME_RINVSRCADDRSUBUNIT: 0x0105,
|
||||
ESME_RINVDSTADDRSUBUNIT: 0x0106,
|
||||
ESME_RINVBCASTFREQINT: 0x0107,
|
||||
ESME_RINVBCASTALIAS_NAME: 0x0108,
|
||||
ESME_RINVBCASTAREAFMT: 0x0109,
|
||||
ESME_RINVNUMBCAST_AREAS: 0x010A,
|
||||
ESME_RINVBCASTCNTTYPE: 0x010B,
|
||||
ESME_RINVBCASTMSGCLASS: 0x010C,
|
||||
ESME_RBCASTFAIL: 0x010D,
|
||||
ESME_RBCASTQUERYFAIL: 0x010E,
|
||||
ESME_RBCASTCANCELFAIL: 0x010F,
|
||||
ESME_RINVBCAST_REP: 0x0110,
|
||||
ESME_RINVBCASTSRVGRP: 0x0111,
|
||||
ESME_RINVBCASTCHANIND: 0x0112,
|
||||
} as const;
|
||||
|
||||
export type ErrorName = keyof typeof errors;
|
||||
|
||||
export const errorsById: Record<number, string> = {};
|
||||
|
||||
for (const [name, code] of Object.entries(errors)) {
|
||||
errorsById[code] = name;
|
||||
}
|
||||
|
||||
export function isErrorName(value: unknown): value is ErrorName {
|
||||
return typeof value === 'string' && Object.hasOwn(errors, value);
|
||||
}
|
||||
|
||||
export function errorNameById(id: number): ErrorName | undefined {
|
||||
const name = errorsById[id];
|
||||
|
||||
return isErrorName(name) ? name : undefined;
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
import type { Result, VoidResult } from '../result.ts';
|
||||
|
||||
export type DestAddress =
|
||||
| { dest_addr_npi: number; dest_addr_ton: number; destination_addr: string }
|
||||
| { dl_name: string };
|
||||
|
||||
export type UnsuccessSme = {
|
||||
dest_addr_npi: number;
|
||||
dest_addr_ton: number;
|
||||
destination_addr: string;
|
||||
error_status_code: number;
|
||||
};
|
||||
|
||||
export type ParamValue = Buffer | DestAddress[] | UnsuccessSme[] | number | string;
|
||||
|
||||
export type WireType<TRead extends ParamValue, TWrite extends ParamValue = TRead> = {
|
||||
default: TRead;
|
||||
read: (buffer: Buffer, offset: number, length?: number) => Result<{ value: TRead }>;
|
||||
size: (value: TWrite) => number;
|
||||
write: (value: TWrite, buffer: Buffer, offset: number) => VoidResult;
|
||||
};
|
||||
|
||||
function outOfRange(buffer: Buffer, offset: number, needed: number): Error | undefined {
|
||||
if (offset < 0 || needed < 0 || offset + needed > buffer.length) {
|
||||
return new Error(
|
||||
`Out of range: need ${String(needed)} bytes at offset ${String(offset)} of a ${String(buffer.length)} byte buffer`,
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function asString(value: number | string): string {
|
||||
return typeof value === 'number' ? value.toString() : value;
|
||||
}
|
||||
|
||||
function readCstring(buffer: Buffer, offset: number): Result<{ value: string }> {
|
||||
let length = 0;
|
||||
|
||||
while (buffer[offset + length]) {
|
||||
length++;
|
||||
|
||||
if (offset + length >= buffer.length) {
|
||||
return { err: new Error('Unterminated C-Octet String') };
|
||||
}
|
||||
}
|
||||
|
||||
return { value: buffer.toString('ascii', offset, offset + length) };
|
||||
}
|
||||
|
||||
function writeCstring(value: number | string, buffer: Buffer, offset: number): VoidResult {
|
||||
const str = asString(value);
|
||||
const err = outOfRange(buffer, offset, str.length + 1);
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
buffer.write(str, offset, 'ascii');
|
||||
buffer[offset + str.length] = 0;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
function sizeCstring(value: number | string): number {
|
||||
return asString(value).length + 1;
|
||||
}
|
||||
|
||||
export const int8: WireType<number> = {
|
||||
default: 0,
|
||||
read(buffer, offset) {
|
||||
const err = outOfRange(buffer, offset, 1);
|
||||
|
||||
return err ? { err } : { value: buffer.readUInt8(offset) };
|
||||
},
|
||||
size() {
|
||||
return 1;
|
||||
},
|
||||
write(value, buffer, offset) {
|
||||
const err = outOfRange(buffer, offset, 1);
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
buffer.writeUInt8(value || 0, offset);
|
||||
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
export const int16: WireType<number> = {
|
||||
default: 0,
|
||||
read(buffer, offset) {
|
||||
const err = outOfRange(buffer, offset, 2);
|
||||
|
||||
return err ? { err } : { value: buffer.readUInt16BE(offset) };
|
||||
},
|
||||
size() {
|
||||
return 2;
|
||||
},
|
||||
write(value, buffer, offset) {
|
||||
const err = outOfRange(buffer, offset, 2);
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
buffer.writeUInt16BE(value || 0, offset);
|
||||
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
export const int32: WireType<number> = {
|
||||
default: 0,
|
||||
read(buffer, offset) {
|
||||
const err = outOfRange(buffer, offset, 4);
|
||||
|
||||
return err ? { err } : { value: buffer.readUInt32BE(offset) };
|
||||
},
|
||||
size() {
|
||||
return 4;
|
||||
},
|
||||
write(value, buffer, offset) {
|
||||
const err = outOfRange(buffer, offset, 4);
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
buffer.writeUInt32BE(value || 0, offset);
|
||||
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
/** Octet String: a length byte followed by that many octets. */
|
||||
export const string: WireType<string, number | string> = {
|
||||
default: '',
|
||||
read(buffer, offset) {
|
||||
const lengthErr = outOfRange(buffer, offset, 1);
|
||||
|
||||
if (lengthErr) return { err: lengthErr };
|
||||
|
||||
const length = buffer.readUInt8(offset);
|
||||
const err = outOfRange(buffer, offset + 1, length);
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
return { value: buffer.toString('ascii', offset + 1, offset + 1 + length) };
|
||||
},
|
||||
size(value) {
|
||||
return asString(value).length + 1;
|
||||
},
|
||||
write(value, buffer, offset) {
|
||||
const str = asString(value);
|
||||
const err = outOfRange(buffer, offset, str.length + 1);
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
buffer.writeUInt8(str.length, offset);
|
||||
buffer.write(str, offset + 1, 'ascii');
|
||||
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
/** C-Octet String: NULL-terminated. */
|
||||
export const cstring: WireType<string, number | string> = {
|
||||
default: '',
|
||||
read: readCstring,
|
||||
size: sizeCstring,
|
||||
write: writeCstring,
|
||||
};
|
||||
|
||||
export const buffer: WireType<Buffer, Buffer | number | string> = {
|
||||
default: Buffer.alloc(0),
|
||||
read(buf, offset, length = 0) {
|
||||
const err = outOfRange(buf, offset, length);
|
||||
|
||||
return err ? { err } : { value: buf.subarray(offset, offset + length) };
|
||||
},
|
||||
// A trailing NULL octet is not counted, mirroring how peers that append one to short_message
|
||||
// report sm_length. pduToObj relies on this when deciding whether to retry a parse.
|
||||
size(value) {
|
||||
const buf = Buffer.isBuffer(value) ? value : Buffer.from(asString(value), 'ascii');
|
||||
|
||||
return buf[buf.length - 1] === 0x00 ? buf.length - 1 : buf.length;
|
||||
},
|
||||
write(value, buf, offset) {
|
||||
const source = Buffer.isBuffer(value) ? value : Buffer.from(asString(value), 'ascii');
|
||||
const err = outOfRange(buf, offset, source.length);
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
source.copy(buf, offset);
|
||||
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
export const dest_address_array: WireType<DestAddress[]> = {
|
||||
default: [],
|
||||
read(buf, offset) {
|
||||
const countErr = outOfRange(buf, offset, 1);
|
||||
|
||||
if (countErr) return { err: countErr };
|
||||
|
||||
const result: DestAddress[] = [];
|
||||
let remaining = buf.readUInt8(offset++);
|
||||
|
||||
while (remaining-- > 0) {
|
||||
const flagErr = outOfRange(buf, offset, 1);
|
||||
|
||||
if (flagErr) return { err: flagErr };
|
||||
|
||||
const destFlag = buf.readUInt8(offset++);
|
||||
|
||||
if (destFlag === 1) {
|
||||
const headerErr = outOfRange(buf, offset, 2);
|
||||
|
||||
if (headerErr) return { err: headerErr };
|
||||
|
||||
const dest_addr_ton = buf.readUInt8(offset++);
|
||||
const dest_addr_npi = buf.readUInt8(offset++);
|
||||
const { err, value } = readCstring(buf, offset);
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
offset += sizeCstring(value);
|
||||
result.push({ dest_addr_npi, dest_addr_ton, destination_addr: value });
|
||||
} else {
|
||||
const { err, value } = readCstring(buf, offset);
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
offset += sizeCstring(value);
|
||||
result.push({ dl_name: value });
|
||||
}
|
||||
}
|
||||
|
||||
return { value: result };
|
||||
},
|
||||
size(value) {
|
||||
let size = 1;
|
||||
|
||||
for (const dest of value) {
|
||||
size += 'dl_name' in dest
|
||||
? sizeCstring(dest.dl_name) + 1
|
||||
: sizeCstring(dest.destination_addr) + 3;
|
||||
}
|
||||
|
||||
return size;
|
||||
},
|
||||
write(value, buf, offset) {
|
||||
const err = outOfRange(buf, offset, dest_address_array.size(value));
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
buf.writeUInt8(value.length, offset++);
|
||||
|
||||
for (const dest of value) {
|
||||
if ('dl_name' in dest) {
|
||||
buf.writeUInt8(2, offset++);
|
||||
writeCstring(dest.dl_name, buf, offset);
|
||||
offset += sizeCstring(dest.dl_name);
|
||||
} else {
|
||||
buf.writeUInt8(1, offset++);
|
||||
buf.writeUInt8(dest.dest_addr_ton || 0, offset++);
|
||||
buf.writeUInt8(dest.dest_addr_npi || 0, offset++);
|
||||
writeCstring(dest.destination_addr, buf, offset);
|
||||
offset += sizeCstring(dest.destination_addr);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
export const unsuccess_sme_array: WireType<UnsuccessSme[]> = {
|
||||
default: [],
|
||||
read(buf, offset) {
|
||||
const countErr = outOfRange(buf, offset, 1);
|
||||
|
||||
if (countErr) return { err: countErr };
|
||||
|
||||
const result: UnsuccessSme[] = [];
|
||||
let remaining = buf.readUInt8(offset++);
|
||||
|
||||
while (remaining-- > 0) {
|
||||
const headerErr = outOfRange(buf, offset, 2);
|
||||
|
||||
if (headerErr) return { err: headerErr };
|
||||
|
||||
const dest_addr_ton = buf.readUInt8(offset++);
|
||||
const dest_addr_npi = buf.readUInt8(offset++);
|
||||
const { err, value } = readCstring(buf, offset);
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
offset += sizeCstring(value);
|
||||
|
||||
const statusErr = outOfRange(buf, offset, 4);
|
||||
|
||||
if (statusErr) return { err: statusErr };
|
||||
|
||||
result.push({
|
||||
dest_addr_npi,
|
||||
dest_addr_ton,
|
||||
destination_addr: value,
|
||||
error_status_code: buf.readUInt32BE(offset),
|
||||
});
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
return { value: result };
|
||||
},
|
||||
size(value) {
|
||||
let size = 1;
|
||||
|
||||
for (const sme of value) {
|
||||
size += sizeCstring(sme.destination_addr) + 6;
|
||||
}
|
||||
|
||||
return size;
|
||||
},
|
||||
write(value, buf, offset) {
|
||||
const err = outOfRange(buf, offset, unsuccess_sme_array.size(value));
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
buf.writeUInt8(value.length, offset++);
|
||||
|
||||
for (const sme of value) {
|
||||
buf.writeUInt8(sme.dest_addr_ton || 0, offset++);
|
||||
buf.writeUInt8(sme.dest_addr_npi || 0, offset++);
|
||||
writeCstring(sme.destination_addr, buf, offset);
|
||||
offset += sizeCstring(sme.destination_addr);
|
||||
buf.writeUInt32BE(sme.error_status_code, offset);
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
/** TLV variants are length-prefixed by the TLV header, so they carry no length of their own. */
|
||||
export const tlv = {
|
||||
buffer: {
|
||||
default: Buffer.alloc(0),
|
||||
read(buf: Buffer, offset: number, length = 0): Result<{ value: Buffer }> {
|
||||
const err = outOfRange(buf, offset, length);
|
||||
|
||||
return err ? { err } : { value: buf.subarray(offset, offset + length) };
|
||||
},
|
||||
size(value: Buffer | number | string): number {
|
||||
return Buffer.isBuffer(value) ? value.length : asString(value).length;
|
||||
},
|
||||
write: buffer.write,
|
||||
} satisfies WireType<Buffer, Buffer | number | string>,
|
||||
cstring,
|
||||
int8,
|
||||
int16,
|
||||
int32,
|
||||
string: {
|
||||
default: '',
|
||||
read(buf: Buffer, offset: number, length = 0): Result<{ value: string }> {
|
||||
const err = outOfRange(buf, offset, length);
|
||||
|
||||
return err ? { err } : { value: buf.toString('ascii', offset, offset + length) };
|
||||
},
|
||||
size(value: number | string): number {
|
||||
return asString(value).length;
|
||||
},
|
||||
write(value: number | string, buf: Buffer, offset: number): VoidResult {
|
||||
const str = asString(value);
|
||||
const err = outOfRange(buf, offset, str.length);
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
buf.write(str, offset, 'ascii');
|
||||
|
||||
return {};
|
||||
},
|
||||
} satisfies WireType<string, number | string>,
|
||||
};
|
||||
|
||||
export const types = {
|
||||
buffer,
|
||||
cstring,
|
||||
dest_address_array,
|
||||
int8,
|
||||
int16,
|
||||
int32,
|
||||
string,
|
||||
tlv,
|
||||
unsuccess_sme_array,
|
||||
};
|
||||
Reference in New Issue
Block a user