change web service sms

This commit is contained in:
2026-05-30 11:13:52 +03:30
parent f213ad2453
commit 0673729a4d
3 changed files with 218 additions and 39 deletions
+144 -18
View File
@@ -1,20 +1,23 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
/** Legacy GET API — often returns HTTP 500; kept as optional fallback. */
/** Legacy 0098 GET API — kept when SMS_PROVIDER=0098. */
const SMS_0098_LINK_URL = 'http://www.0098sms.com/sendsmslink.aspx';
/** Official 0098 SOAP API (works when link endpoint is down). */
/** Legacy 0098 SOAP API — kept when SMS_PROVIDER=0098. */
const SMS_0098_SOAP_URL = 'https://webservice.0098sms.com/service.asmx';
/** SOAP SendSMSResult: 0 or positive = accepted (often SMS part count); negative = error. */
const SMS_0098_SOAP_ERROR_CODES: Record<string, string> = {
'-22': 'invalid mobile number format',
};
/** Trez / Raygan panel (default). */
const SMS_TREZ_DEFAULT_BASE_URL = 'http://smspanel.Trez.ir';
const SMS_TREZ_DEFAULT_SEND_PATH = '/SendMessageWithCode.ashx';
export type SmsSendResult = {
ok: boolean;
transport: 'soap' | 'link';
transport: 'trez' | 'soap' | 'link';
reason?: string;
};
@@ -46,7 +49,10 @@ export class SmsService {
}
async sendOtpSms(mobile: string, code: string): Promise<boolean> {
const text = `کد ورود منتایم: ${code}`;
const template =
this.config.get<string>('SMS_OTP_TEMPLATE') ??
'کد ورود منتایم: {code}';
const text = template.replace('{code}', code);
const result = await this.sendSmsDetailed(mobile, text);
return result.ok;
}
@@ -62,33 +68,153 @@ export class SmsService {
const sender = this.config.get<string>('SMS_SENDER');
if (!username || !password || !sender) {
return { ok: false, transport: 'soap', reason: 'SMS not configured' };
return { ok: false, transport: 'trez', reason: 'SMS not configured' };
}
const to = this.normalizePhoneForSms(phone);
const transport = this.resolveTransport();
const provider = this.resolveProvider();
if (transport === 'link') {
return this.sendViaLink(username, password, sender, to, text);
if (provider === 'trez') {
return this.sendViaTrez(username, password, sender, to, text);
}
const soapResult = await this.sendViaSoap(username, password, sender, to, text);
const transport = this.resolve0098Transport();
if (transport === 'link') {
return this.sendVia0098Link(username, password, sender, to, text);
}
const soapResult = await this.sendVia0098Soap(
username,
password,
sender,
to,
text,
);
if (soapResult.ok) {
return soapResult;
}
this.logger.warn(
`SOAP SMS failed to=${this.maskPhone(to)} reason=${soapResult.reason}; trying link fallback`,
`0098 SOAP failed to=${this.maskPhone(to)} reason=${soapResult.reason}; trying link fallback`,
);
return this.sendViaLink(username, password, sender, to, text);
return this.sendVia0098Link(username, password, sender, to, text);
}
private resolveTransport(): 'soap' | 'link' {
private resolveProvider(): 'trez' | '0098' {
const raw = (this.config.get<string>('SMS_PROVIDER') ?? 'trez').toLowerCase();
return raw === '0098' ? '0098' : 'trez';
}
private resolve0098Transport(): 'soap' | 'link' {
const raw = (this.config.get<string>('SMS_TRANSPORT') ?? 'soap').toLowerCase();
return raw === 'link' ? 'link' : 'soap';
}
private async sendViaSoap(
private async sendViaTrez(
username: string,
password: string,
sender: string,
to: string,
text: string,
): Promise<SmsSendResult> {
const baseUrl = (
this.config.get<string>('SMS_BASE_URL') ?? SMS_TREZ_DEFAULT_BASE_URL
).replace(/\/$/, '');
const path =
this.config.get<string>('SMS_SEND_PATH') ?? SMS_TREZ_DEFAULT_SEND_PATH;
const url = `${baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
const timeoutMs = this.readTimeoutMs();
const body = new URLSearchParams({
PhoneNumber: sender,
Username: username,
Password: password,
Mobile: to,
Message: text,
});
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: body.toString(),
signal: AbortSignal.timeout(timeoutMs),
});
const responseBody = (await res.text()).trim();
if (!res.ok) {
this.logger.warn(
`Trez HTTP ${res.status} to=${this.maskPhone(to)} body=${responseBody.slice(0, 200)}`,
);
return {
ok: false,
transport: 'trez',
reason: `HTTP ${res.status}`,
};
}
const parsed = this.parseTrezResponse(responseBody);
if (!parsed.ok) {
this.logger.warn(
`Trez rejected to=${this.maskPhone(to)} reason=${parsed.reason} body=${responseBody.slice(0, 200)}`,
);
}
return { ...parsed, transport: 'trez' };
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
this.logger.error(
`Trez request failed to=${this.maskPhone(to)}: ${message}`,
e instanceof Error ? e.stack : undefined,
);
return { ok: false, transport: 'trez', reason: 'Trez request failed' };
}
}
private parseTrezResponse(body: string): { ok: boolean; reason: string } {
if (!body) {
return { ok: true, reason: 'accepted (empty response)' };
}
const trimmed = body.trim();
const lower = trimmed.toLowerCase();
if (
lower.includes('error') ||
lower.includes('fail') ||
lower.includes('invalid') ||
lower.includes('خطا')
) {
return { ok: false, reason: trimmed.slice(0, 120) };
}
if (/^-?\d+$/.test(trimmed)) {
const n = Number(trimmed);
if (n === 1 || n > 1) {
return { ok: true, reason: 'accepted' };
}
if (n === 0) {
return { ok: false, reason: 'provider code 0' };
}
if (n < 0) {
return { ok: false, reason: `provider code ${trimmed}` };
}
}
if (lower === 'true' || lower === 'ok' || lower === 'success') {
return { ok: true, reason: 'accepted' };
}
if (lower === 'false') {
return { ok: false, reason: 'provider returned false' };
}
// Non-numeric IDs (message ref) are treated as success.
return { ok: true, reason: 'accepted' };
}
private async sendVia0098Soap(
username: string,
password: string,
sender: string,
@@ -139,20 +265,20 @@ export class SmsService {
SMS_0098_SOAP_ERROR_CODES[code ?? ''] ??
(code ? `provider code ${code}` : 'unknown SOAP response');
this.logger.warn(
`SOAP rejected to=${this.maskPhone(to)} code=${code ?? '?'} body=${body.slice(0, 200)}`,
`0098 SOAP rejected to=${this.maskPhone(to)} code=${code ?? '?'} body=${body.slice(0, 200)}`,
);
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}`,
`0098 SOAP request failed to=${this.maskPhone(to)}: ${message}`,
e instanceof Error ? e.stack : undefined,
);
return { ok: false, transport: 'soap', reason: 'SOAP request failed' };
}
}
private async sendViaLink(
private async sendVia0098Link(
username: string,
password: string,
sender: string,
@@ -192,7 +318,7 @@ export class SmsService {
return { ok: true, transport: 'link' };
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
this.logger.error(`Link SMS failed to=${this.maskPhone(to)}: ${message}`);
this.logger.error(`0098 link SMS failed to=${this.maskPhone(to)}: ${message}`);
return { ok: false, transport: 'link', reason: 'link request failed' };
}
}