Restore 0098 SOAP SMS with link fallback and clearer errors
This commit is contained in:
+6
-14
@@ -26,20 +26,12 @@ UPLOAD_DIR=uploads
|
|||||||
CORS_ORIGINS=https://montime.ir,https://adm.montime.ir,http://localhost:5173,http://localhost:5174
|
CORS_ORIGINS=https://montime.ir,https://adm.montime.ir,http://localhost:5173,http://localhost:5174
|
||||||
|
|
||||||
# 0098sms.com — used when AUTH_RETURN_OTP is not true
|
# 0098sms.com — used when AUTH_RETURN_OTP is not true
|
||||||
# Transport: soap (default, recommended) or link (legacy)
|
# From panel: account username, password (or web service password), approved sender line (pnlno)
|
||||||
# SMS_TRANSPORT=soap
|
|
||||||
# SMS_SOAP_URL=https://webservice.0098sms.com/service.asmx
|
|
||||||
# From panel: account username, password (or web service password), approved sender line (FROM)
|
|
||||||
SMS_USERNAME=
|
SMS_USERNAME=
|
||||||
SMS_PASSWORD=
|
SMS_PASSWORD=
|
||||||
# Alternative name supported for password (same as some older projects)
|
# SMS_API_KEY= # alias for SMS_PASSWORD
|
||||||
# SMS_API_KEY=
|
|
||||||
SMS_SENDER=
|
SMS_SENDER=
|
||||||
# Optional; default is 0098
|
# soap (default, recommended) | link (legacy GET API, often HTTP 500)
|
||||||
# SMS_DOMAIN=0098
|
SMS_TRANSPORT=soap
|
||||||
# Optional OTP text; {code} is replaced. ASCII-only if provider rejects Persian.
|
# SMS_DOMAIN=0098 # only for link transport
|
||||||
# SMS_OTP_TEMPLATE=Montime code: {code}
|
# SMS_TIMEOUT_MS=20000
|
||||||
# Optional request timeout in ms (default 15000)
|
|
||||||
# SMS_TIMEOUT_MS=15000
|
|
||||||
# Optional override for send URL
|
|
||||||
# SMS_SEND_URL=http://www.0098sms.com/sendsmslink.aspx
|
|
||||||
|
|||||||
+14
-8
@@ -1,18 +1,17 @@
|
|||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
|
|
||||||
const username = process.env.SMS_USERNAME;
|
const username = process.env.SMS_USERNAME;
|
||||||
const password = process.env.SMS_PASSWORD;
|
const password = process.env.SMS_PASSWORD ?? process.env.SMS_API_KEY;
|
||||||
const sender = process.env.SMS_SENDER;
|
const sender = process.env.SMS_SENDER;
|
||||||
|
|
||||||
if (!username || !password || !sender) {
|
if (!username || !password || !sender) {
|
||||||
console.error('Missing SMS env vars');
|
console.error('Set SMS_USERNAME, SMS_PASSWORD (or SMS_API_KEY), and SMS_SENDER in .env');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = 'Montime code: 123456';
|
|
||||||
const targets = process.argv.slice(2).length
|
const targets = process.argv.slice(2).length
|
||||||
? process.argv.slice(2)
|
? process.argv.slice(2)
|
||||||
: ['09397439581', '09128081392'];
|
: ['09397439581'];
|
||||||
|
|
||||||
function escapeXml(value) {
|
function escapeXml(value) {
|
||||||
return String(value)
|
return String(value)
|
||||||
@@ -23,9 +22,11 @@ function escapeXml(value) {
|
|||||||
.replace(/'/g, ''');
|
.replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const text = 'کد ورود منتایم: 123456';
|
||||||
|
|
||||||
for (const mobile of targets) {
|
for (const mobile of targets) {
|
||||||
const soapBody = `<?xml version="1.0" encoding="utf-8"?>
|
const soapBody = `<?xml version="1.0" encoding="utf-8"?>
|
||||||
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
||||||
<soap:Body>
|
<soap:Body>
|
||||||
<SendSMS xmlns="http://tempuri.org/">
|
<SendSMS xmlns="http://tempuri.org/">
|
||||||
<username>${escapeXml(username)}</username>
|
<username>${escapeXml(username)}</username>
|
||||||
@@ -44,10 +45,15 @@ for (const mobile of targets) {
|
|||||||
SOAPAction: 'http://tempuri.org/SendSMS',
|
SOAPAction: 'http://tempuri.org/SendSMS',
|
||||||
},
|
},
|
||||||
body: soapBody,
|
body: soapBody,
|
||||||
signal: AbortSignal.timeout(20000),
|
signal: AbortSignal.timeout(20_000),
|
||||||
});
|
});
|
||||||
|
|
||||||
const body = await res.text();
|
const body = await res.text();
|
||||||
const match = body.match(/<SendSMSResult[^>]*>([^<]*)<\/SendSMSResult>/);
|
const match = body.match(/<SendSMSResult[^>]*>([^<]*)<\/SendSMSResult>/i);
|
||||||
console.log(mobile, 'HTTP', res.status, 'RESULT', match?.[1] ?? body.slice(0, 200));
|
const code = match?.[1]?.trim() ?? '?';
|
||||||
|
|
||||||
|
console.log(`mobile=${mobile} http=${res.status} RESULT=${code}`);
|
||||||
|
if (code !== '0') {
|
||||||
|
console.log(body.slice(0, 400));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,13 +98,17 @@ export class AuthService {
|
|||||||
'SMS is not configured. Set SMS_USERNAME, SMS_PASSWORD (or SMS_API_KEY), and SMS_SENDER.',
|
'SMS is not configured. Set SMS_USERNAME, SMS_PASSWORD (or SMS_API_KEY), and SMS_SENDER.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const result = await this.sms.sendOtpSmsDetailed(mobile, code);
|
const result = await this.sms.sendSmsDetailed(
|
||||||
|
mobile,
|
||||||
|
`کد ورود منتایم: ${code}`,
|
||||||
|
);
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
|
const detail = result.reason ? ` (${result.reason})` : '';
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`OTP SMS failed for ${this.maskMobile(mobile)} transport=${result.transport} reason=${result.reason}`,
|
`OTP SMS failed for ${this.maskMobile(mobile)} transport=${result.transport} reason=${result.reason}`,
|
||||||
);
|
);
|
||||||
throw new ServiceUnavailableException(
|
throw new ServiceUnavailableException(
|
||||||
`Could not send SMS. Please try again later. (${result.reason})`,
|
`Could not send SMS. Please try again later.${detail}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+89
-131
@@ -1,10 +1,10 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
/** Legacy link API — often returns HTTP 500; kept as fallback only. */
|
/** Legacy GET API — often returns HTTP 500; kept as optional fallback. */
|
||||||
const SMS_0098_LINK_URL = 'http://www.0098sms.com/sendsmslink.aspx';
|
const SMS_0098_LINK_URL = 'http://www.0098sms.com/sendsmslink.aspx';
|
||||||
|
|
||||||
/** Current 0098 SOAP endpoint (works reliably). */
|
/** Official 0098 SOAP API (works when link endpoint is down). */
|
||||||
const SMS_0098_SOAP_URL = 'https://webservice.0098sms.com/service.asmx';
|
const SMS_0098_SOAP_URL = 'https://webservice.0098sms.com/service.asmx';
|
||||||
|
|
||||||
const SMS_0098_ERROR_CODES: Record<string, string> = {
|
const SMS_0098_ERROR_CODES: Record<string, string> = {
|
||||||
@@ -23,11 +23,10 @@ const SMS_0098_ERROR_CODES: Record<string, string> = {
|
|||||||
'13': 'invalid characters in message text',
|
'13': 'invalid characters in message text',
|
||||||
};
|
};
|
||||||
|
|
||||||
type SendResult = {
|
export type SmsSendResult = {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
code?: string;
|
|
||||||
reason: string;
|
|
||||||
transport: 'soap' | 'link';
|
transport: 'soap' | 'link';
|
||||||
|
reason?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -36,7 +35,6 @@ export class SmsService {
|
|||||||
|
|
||||||
constructor(private readonly config: ConfigService) {}
|
constructor(private readonly config: ConfigService) {}
|
||||||
|
|
||||||
/** True when username, password, and sender line are all set. */
|
|
||||||
isConfigured(): boolean {
|
isConfigured(): boolean {
|
||||||
const username = this.config.get<string>('SMS_USERNAME');
|
const username = this.config.get<string>('SMS_USERNAME');
|
||||||
const password = this.getPassword();
|
const password = this.getPassword();
|
||||||
@@ -44,9 +42,6 @@ export class SmsService {
|
|||||||
return Boolean(username && password && sender);
|
return Boolean(username && password && sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats mobile for 0098 (Iranian mobiles as 09xxxxxxxxx).
|
|
||||||
*/
|
|
||||||
normalizePhoneForSms(mobile: string): string {
|
normalizePhoneForSms(mobile: string): string {
|
||||||
const digits = String(mobile ?? '').replace(/\D/g, '');
|
const digits = String(mobile ?? '').replace(/\D/g, '');
|
||||||
if (digits.startsWith('98') && digits.length === 12) {
|
if (digits.startsWith('98') && digits.length === 12) {
|
||||||
@@ -62,67 +57,46 @@ export class SmsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sendOtpSms(mobile: string, code: string): Promise<boolean> {
|
async sendOtpSms(mobile: string, code: string): Promise<boolean> {
|
||||||
const result = await this.sendOtpSmsDetailed(mobile, code);
|
const text = `کد ورود منتایم: ${code}`;
|
||||||
|
const result = await this.sendSmsDetailed(mobile, text);
|
||||||
return result.ok;
|
return result.ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendOtpSmsDetailed(mobile: string, code: string): Promise<SendResult> {
|
|
||||||
const template =
|
|
||||||
this.config.get<string>('SMS_OTP_TEMPLATE') ??
|
|
||||||
'کد ورود منتایم: {code}';
|
|
||||||
const text = template.replace('{code}', code);
|
|
||||||
return this.sendSmsDetailed(mobile, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendSms(phone: string, text: string): Promise<boolean> {
|
async sendSms(phone: string, text: string): Promise<boolean> {
|
||||||
const result = await this.sendSmsDetailed(phone, text);
|
const result = await this.sendSmsDetailed(phone, text);
|
||||||
return result.ok;
|
return result.ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendSmsDetailed(phone: string, text: string): Promise<SendResult> {
|
async sendSmsDetailed(phone: string, text: string): Promise<SmsSendResult> {
|
||||||
const username = this.config.get<string>('SMS_USERNAME');
|
const username = this.config.get<string>('SMS_USERNAME');
|
||||||
const password = this.getPassword();
|
const password = this.getPassword();
|
||||||
const sender = this.config.get<string>('SMS_SENDER');
|
const sender = this.config.get<string>('SMS_SENDER');
|
||||||
|
|
||||||
if (!username || !password || !sender) {
|
if (!username || !password || !sender) {
|
||||||
return { ok: false, reason: 'SMS not configured', transport: 'soap' };
|
return { ok: false, transport: 'soap', reason: 'SMS not configured' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const to = this.normalizePhoneForSms(phone);
|
const to = this.normalizePhoneForSms(phone);
|
||||||
const transport =
|
const transport = this.resolveTransport();
|
||||||
(this.config.get<string>('SMS_TRANSPORT') ?? 'soap').toLowerCase() ===
|
|
||||||
'link'
|
|
||||||
? 'link'
|
|
||||||
: 'soap';
|
|
||||||
|
|
||||||
if (transport === 'link') {
|
if (transport === 'link') {
|
||||||
const linkResult = await this.sendViaLink(
|
return this.sendViaLink(username, password, sender, to, text);
|
||||||
username,
|
|
||||||
password,
|
|
||||||
sender,
|
|
||||||
to,
|
|
||||||
text,
|
|
||||||
);
|
|
||||||
if (linkResult.ok) {
|
|
||||||
return linkResult;
|
|
||||||
}
|
|
||||||
this.logger.warn(
|
|
||||||
`SMS link transport failed to=${this.maskPhone(to)} reason=${linkResult.reason}; trying SOAP`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const soapResult = await this.sendViaSoap(
|
const soapResult = await this.sendViaSoap(username, password, sender, to, text);
|
||||||
username,
|
if (soapResult.ok) {
|
||||||
password,
|
|
||||||
sender,
|
|
||||||
to,
|
|
||||||
text,
|
|
||||||
);
|
|
||||||
if (soapResult.ok || transport === 'soap') {
|
|
||||||
return soapResult;
|
return soapResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
return soapResult;
|
this.logger.warn(
|
||||||
|
`SOAP SMS failed to=${this.maskPhone(to)} reason=${soapResult.reason}; trying link fallback`,
|
||||||
|
);
|
||||||
|
return this.sendViaLink(username, password, sender, to, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveTransport(): 'soap' | 'link' {
|
||||||
|
const raw = (this.config.get<string>('SMS_TRANSPORT') ?? 'soap').toLowerCase();
|
||||||
|
return raw === 'link' ? 'link' : 'soap';
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sendViaSoap(
|
private async sendViaSoap(
|
||||||
@@ -131,12 +105,8 @@ export class SmsService {
|
|||||||
sender: string,
|
sender: string,
|
||||||
to: string,
|
to: string,
|
||||||
text: string,
|
text: string,
|
||||||
): Promise<SendResult> {
|
): Promise<SmsSendResult> {
|
||||||
const soapUrl =
|
const soapBody = `<?xml version="1.0" encoding="utf-8"?>
|
||||||
this.config.get<string>('SMS_SOAP_URL') ?? SMS_0098_SOAP_URL;
|
|
||||||
const timeoutMs = this.readPositiveInt('SMS_TIMEOUT_MS', 20000);
|
|
||||||
|
|
||||||
const body = `<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
||||||
<soap:Body>
|
<soap:Body>
|
||||||
<SendSMS xmlns="http://tempuri.org/">
|
<SendSMS xmlns="http://tempuri.org/">
|
||||||
@@ -149,46 +119,47 @@ export class SmsService {
|
|||||||
</soap:Body>
|
</soap:Body>
|
||||||
</soap:Envelope>`;
|
</soap:Envelope>`;
|
||||||
|
|
||||||
|
const timeoutMs = this.readTimeoutMs();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(soapUrl, {
|
const res = await fetch(SMS_0098_SOAP_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/xml; charset=utf-8',
|
'Content-Type': 'text/xml; charset=utf-8',
|
||||||
SOAPAction: 'http://tempuri.org/SendSMS',
|
SOAPAction: 'http://tempuri.org/SendSMS',
|
||||||
},
|
},
|
||||||
body,
|
body: soapBody,
|
||||||
signal: AbortSignal.timeout(timeoutMs),
|
signal: AbortSignal.timeout(timeoutMs),
|
||||||
});
|
});
|
||||||
|
|
||||||
const responseBody = (await res.text()).trim();
|
const body = (await res.text()).trim();
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
this.logger.warn(
|
|
||||||
`SMS SOAP HTTP ${res.status} to=${this.maskPhone(to)} body=${responseBody.slice(0, 300)}`,
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
reason: `SOAP HTTP ${res.status}`,
|
|
||||||
transport: 'soap',
|
transport: 'soap',
|
||||||
|
reason: `HTTP ${res.status}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = responseBody.match(
|
const code = this.parseSoapResultCode(body);
|
||||||
/<SendSMSResult[^>]*>([^<]*)<\/SendSMSResult>/i,
|
if (code === '0') {
|
||||||
);
|
return { ok: true, transport: 'soap' };
|
||||||
const code = match?.[1]?.trim() ?? responseBody;
|
|
||||||
const parsed = this.parse0098Response(code);
|
|
||||||
if (!parsed.ok) {
|
|
||||||
this.logger.warn(
|
|
||||||
`SMS SOAP rejected to=${this.maskPhone(to)} code=${parsed.code ?? 'unknown'} reason=${parsed.reason}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return { ...parsed, transport: 'soap' };
|
|
||||||
} catch (e) {
|
const reason =
|
||||||
this.logger.error(
|
SMS_0098_ERROR_CODES[code ?? ''] ??
|
||||||
`SMS SOAP request failed to=${this.maskPhone(to)}`,
|
(code ? `provider code ${code}` : 'unknown SOAP response');
|
||||||
e instanceof Error ? e.stack : e,
|
this.logger.warn(
|
||||||
|
`SOAP rejected to=${this.maskPhone(to)} code=${code ?? '?'} body=${body.slice(0, 200)}`,
|
||||||
);
|
);
|
||||||
return { ok: false, reason: 'SOAP request failed', transport: 'soap' };
|
return { ok: false, transport: 'soap', reason };
|
||||||
|
} catch (e) {
|
||||||
|
const message = e instanceof Error ? e.message : String(e);
|
||||||
|
this.logger.error(
|
||||||
|
`SOAP request failed to=${this.maskPhone(to)}: ${message}`,
|
||||||
|
e instanceof Error ? e.stack : undefined,
|
||||||
|
);
|
||||||
|
return { ok: false, transport: 'soap', reason: 'SOAP request failed' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,88 +169,75 @@ export class SmsService {
|
|||||||
sender: string,
|
sender: string,
|
||||||
to: string,
|
to: string,
|
||||||
text: string,
|
text: string,
|
||||||
): Promise<SendResult> {
|
): Promise<SmsSendResult> {
|
||||||
const domain = this.config.get<string>('SMS_DOMAIN') ?? '0098';
|
const domain = this.config.get<string>('SMS_DOMAIN') ?? '0098';
|
||||||
const sendUrl = this.config.get<string>('SMS_SEND_URL') ?? SMS_0098_LINK_URL;
|
|
||||||
const timeoutMs = this.readPositiveInt('SMS_TIMEOUT_MS', 20000);
|
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
FROM: sender,
|
FROM: sender,
|
||||||
To: to,
|
TO: to,
|
||||||
Text: text,
|
TEXT: text,
|
||||||
USERNAME: username,
|
USERNAME: username,
|
||||||
PASSWORD: password,
|
PASSWORD: password,
|
||||||
DOMAIN: domain,
|
DOMAIN: domain,
|
||||||
});
|
});
|
||||||
|
|
||||||
const url = `${sendUrl}?${params.toString()}`;
|
const url = `${SMS_0098_LINK_URL}?${params.toString()}`;
|
||||||
|
const timeoutMs = this.readTimeoutMs();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
||||||
signal: AbortSignal.timeout(timeoutMs),
|
|
||||||
});
|
|
||||||
const body = (await res.text()).trim();
|
const body = (await res.text()).trim();
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
this.logger.warn(
|
|
||||||
`SMS link HTTP ${res.status} to=${this.maskPhone(to)} body=${body.slice(0, 300)}`,
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
reason: `link HTTP ${res.status}`,
|
|
||||||
transport: 'link',
|
transport: 'link',
|
||||||
|
reason: `HTTP ${res.status}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (!this.isLikelySuccessLinkBody(body)) {
|
||||||
const parsed = this.parse0098Response(body);
|
return {
|
||||||
if (!parsed.ok) {
|
ok: false,
|
||||||
this.logger.warn(
|
transport: 'link',
|
||||||
`SMS link rejected to=${this.maskPhone(to)} code=${parsed.code ?? 'unknown'} reason=${parsed.reason} body=${body.slice(0, 300)}`,
|
reason: body.slice(0, 120) || 'link API rejected',
|
||||||
);
|
};
|
||||||
}
|
}
|
||||||
return { ...parsed, transport: 'link' };
|
return { ok: true, transport: 'link' };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.error(
|
const message = e instanceof Error ? e.message : String(e);
|
||||||
`SMS link request failed to=${this.maskPhone(to)}`,
|
this.logger.error(`Link SMS failed to=${this.maskPhone(to)}: ${message}`);
|
||||||
e instanceof Error ? e.stack : e,
|
return { ok: false, transport: 'link', reason: 'link request failed' };
|
||||||
);
|
|
||||||
return { ok: false, reason: 'link request failed', transport: 'link' };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private parse0098Response(body: string): {
|
private parseSoapResultCode(xml: string): string | null {
|
||||||
ok: boolean;
|
const match =
|
||||||
code?: string;
|
xml.match(/<SendSMSResult[^>]*>([^<]*)<\/SendSMSResult>/i) ??
|
||||||
reason: string;
|
xml.match(/<SendSMSResult>([^<]*)<\/SendSMSResult>/i);
|
||||||
} {
|
if (match?.[1] != null) {
|
||||||
|
return match[1].trim();
|
||||||
|
}
|
||||||
|
if (xml.includes('>0<')) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isLikelySuccessLinkBody(body: string): boolean {
|
||||||
if (!body) {
|
if (!body) {
|
||||||
return { ok: false, reason: 'empty response' };
|
return true;
|
||||||
}
|
}
|
||||||
|
const lower = body.toLowerCase();
|
||||||
const trimmed = body.trim();
|
|
||||||
|
|
||||||
if (/^\d+$/.test(trimmed)) {
|
|
||||||
if (trimmed === '0') {
|
|
||||||
return { ok: true, code: '0', reason: 'successful' };
|
|
||||||
}
|
|
||||||
const reason = SMS_0098_ERROR_CODES[trimmed] ?? `provider code ${trimmed}`;
|
|
||||||
return { ok: false, code: trimmed, reason };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trimmed.startsWith('0')) {
|
|
||||||
return { ok: true, code: '0', reason: 'successful' };
|
|
||||||
}
|
|
||||||
|
|
||||||
const lower = trimmed.toLowerCase();
|
|
||||||
if (
|
if (
|
||||||
lower.includes('error') ||
|
lower.includes('error') ||
|
||||||
lower.includes('fail') ||
|
lower.includes('fail') ||
|
||||||
lower.includes('invalid')
|
lower.includes('invalid')
|
||||||
) {
|
) {
|
||||||
return { ok: false, reason: trimmed.slice(0, 120) };
|
return false;
|
||||||
}
|
}
|
||||||
|
const code = body.trim();
|
||||||
return { ok: true, reason: 'accepted' };
|
if (/^\d+$/.test(code) && code !== '0') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private escapeXml(value: string): string {
|
private escapeXml(value: string): string {
|
||||||
@@ -291,10 +249,10 @@ export class SmsService {
|
|||||||
.replace(/'/g, ''');
|
.replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
private readPositiveInt(key: string, fallback: number): number {
|
private readTimeoutMs(): number {
|
||||||
const raw = this.config.get<string>(key);
|
const raw = this.config.get<string>('SMS_TIMEOUT_MS');
|
||||||
const n = raw ? Number.parseInt(raw, 10) : NaN;
|
const n = raw ? Number.parseInt(raw, 10) : 20_000;
|
||||||
return Number.isFinite(n) && n > 0 ? n : fallback;
|
return Number.isFinite(n) && n > 0 ? n : 20_000;
|
||||||
}
|
}
|
||||||
|
|
||||||
private maskPhone(phone: string): string {
|
private maskPhone(phone: string): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user