fix sms and ticket user frindly
This commit is contained in:
1 parent
1ee83374fd
commit
b8b3aba855
30 files changed
+2220
-119
No files matched your search
@@ -7,6 +7,14 @@ class SMSResult:
|
||||
success: bool
|
||||
provider_id: str = ""
|
||||
error: str = ""
|
||||
provider_code: str = ""
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SMSDeliveryResult:
|
||||
status: str
|
||||
provider_code: str
|
||||
description: str
|
||||
is_final: bool = True
|
||||
def webotp_message(code):
|
||||
host=settings.WEBOTP_HOST or urlparse(settings.PUBLIC_BASE_URL).hostname
|
||||
if not host: raise ValueError("WEBOTP_HOST یا PUBLIC_BASE_URL معتبر تنظیم نشده است.")
|
||||
@@ -16,3 +24,7 @@ class SMSProvider(ABC):
|
||||
def send_otp(self,mobile,code): return self.send_message(mobile,webotp_message(code))
|
||||
@abstractmethod
|
||||
def send_message(self,mobile,message): raise NotImplementedError
|
||||
|
||||
def get_delivery_status(self, provider_id):
|
||||
"""Return None when a provider does not support delivery tracking."""
|
||||
return None
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Backward-compatible public facade for the SMS integration."""
|
||||
from .base import SMSProvider,SMSResult,webotp_message
|
||||
from .base import SMSDeliveryResult,SMSProvider,SMSResult,webotp_message
|
||||
from .behin import BehinSMSProvider
|
||||
from .console import ConsoleSMSProvider
|
||||
from .factory import get_sms_provider,reset_sms_provider
|
||||
from .http import HTTPSMSProvider
|
||||
__all__=["SMSProvider","SMSResult","webotp_message","BehinSMSProvider","ConsoleSMSProvider","HTTPSMSProvider","get_sms_provider","reset_sms_provider"]
|
||||
__all__=["SMSProvider","SMSResult","SMSDeliveryResult","webotp_message","BehinSMSProvider","ConsoleSMSProvider","HTTPSMSProvider","get_sms_provider","reset_sms_provider"]
|
||||
@@ -1,36 +1,172 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
import requests
|
||||
import zeep
|
||||
from django.conf import settings
|
||||
from zeep.transports import Transport
|
||||
|
||||
from apps.accounts.models import normalize_mobile
|
||||
from .base import SMSProvider, SMSResult
|
||||
from .base import SMSDeliveryResult, SMSProvider, SMSResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Source: the vendor's "کدهای خطا وب سرویس" table. Keeping the mapping here
|
||||
# makes worker logs and the staff UI report the same exact reason.
|
||||
SEND_RESULT_MESSAGES = {
|
||||
"2": "ارسال با موفقیت انجام شد.",
|
||||
"-3": "نام کاربری و کلمه عبور با هم تطابق ندارند؛ لطفاً با پشتیبانی 0098SMS تماس بگیرید.",
|
||||
"10": "نام کاربری یا کلمه عبور اشتباه است؛ لطفاً با پشتیبانی 0098SMS تماس بگیرید.",
|
||||
"11": "در متن پیامک کاراکتر غیرمجاز وجود دارد.",
|
||||
"-17": "متن پیامک خالی است.",
|
||||
"-18": "خطای شارژ رخ داده است؛ لطفاً با پشتیبانی 0098SMS تماس بگیرید.",
|
||||
"-19": "شارژ پنل برای ارسال کافی نیست؛ لطفاً پنل را شارژ کنید.",
|
||||
"-22": "شماره موبایل گیرنده صحیح نیست.",
|
||||
"66": "نام کاربری و کلمه عبور با هم تطابق ندارند؛ لطفاً با پشتیبانی 0098SMS تماس بگیرید.",
|
||||
"1111": "در متن پیامک کاراکتر غیرمجاز وجود دارد.",
|
||||
"hang": "حساب کاربری مسدود است؛ لطفاً با پشتیبانی 0098SMS تماس بگیرید.",
|
||||
"no doc": "مرحله دوم ثبتنام انجام نشده است؛ لطفاً ثبتنام را کامل کنید.",
|
||||
"-23": "شماره پنل مجوز ارسال از لینک را ندارد.",
|
||||
"-24": "در شماره پنل خطا وجود دارد؛ لطفاً با پشتیبانی 0098SMS تماس بگیرید.",
|
||||
"-25": "اعتبار شماره پنل به پایان رسیده است و باید تمدید شود.",
|
||||
"-26": "تنظیمات کد OTP انجام نشده است.",
|
||||
"-27": "فرمت کد OTP صحیح نیست.",
|
||||
"-28": "تنظیمات کد OTP توسط ادمین تأیید نشده است.",
|
||||
"wrong timestamp": "مقدار timestamp ارسالشده صحیح نیست.",
|
||||
"wrong time": "زمان ارسال خارج از بازه مجاز است.",
|
||||
"-29": "فرمت کد OTP صحیح نیست.",
|
||||
"-31": "شناسه پیامک (smsid) یافت نشد.",
|
||||
"-32": "شناسه پیامک (smsid) مطابقت ندارد.",
|
||||
"13": "ارسال از IP غیرمجاز انجام شده است.",
|
||||
}
|
||||
|
||||
DELIVERY_RESULTS = {
|
||||
"4": ("BLOCKED", "مخاطب دریافت پیامکهای تبلیغاتی را مسدود کرده است.", True),
|
||||
"5": ("DELIVERED", "پیامک به گوشی گیرنده رسیده است.", True),
|
||||
# 0098SMS can initially return 6 or 8 and publish the definitive result
|
||||
# much later. Keep polling these states instead of freezing a stale result.
|
||||
"6": ("UNDELIVERED", "هنوز تحویل پیامک به گوشی تأیید نشده است.", False),
|
||||
"7": ("EXPIRED", "مهلت دریافت پیامک منقضی شده است.", True),
|
||||
"8": ("UNKNOWN", "وضعیت تحویل هنوز توسط اپراتور مشخص نشده است.", False),
|
||||
"9": ("ERROR", "اطلاعات مالک شماره پیامک ثبت یا تأیید نشده است.", True),
|
||||
"12": ("ERROR", "هنوز اجازه ارسال با این شماره پنل داده نشده است.", True),
|
||||
"13": ("ERROR", "ارسال از IP غیرمجاز انجام شده است.", True),
|
||||
}
|
||||
|
||||
|
||||
def _scalar(value):
|
||||
if isinstance(value, (list, tuple)) and value:
|
||||
value = value[0]
|
||||
return str(value if value is not None else "").strip()
|
||||
|
||||
|
||||
class ZeroZeroNinetyEightSMSProvider(SMSProvider):
|
||||
"""Adapter for the 0098SMS HTTP gateway."""
|
||||
"""SOAP adapter for 0098SMS (0098vip-compatible web service)."""
|
||||
|
||||
def __init__(self):
|
||||
self.username = settings.SMS0098_USERNAME
|
||||
self.password = settings.SMS0098_PASSWORD
|
||||
self.sender = settings.SMS0098_FROM
|
||||
self.api_url = settings.SMS0098_API_URL
|
||||
self.wsdl_url = settings.SMS0098_WSDL_URL
|
||||
self._client = None
|
||||
|
||||
def _configuration_error(self):
|
||||
missing = [name for name, value in (
|
||||
("SMS0098_USERNAME", self.username),
|
||||
("SMS0098_PASSWORD", self.password),
|
||||
("SMS0098_FROM", self.sender),
|
||||
) if not value]
|
||||
return f"تنظیمات 0098SMS ناقص است: {', '.join(missing)}" if missing else ""
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
if self._client is None:
|
||||
timeout = settings.INTEGRATION_HTTP_TIMEOUT_SECONDS
|
||||
self._client = zeep.Client(
|
||||
wsdl=self.wsdl_url,
|
||||
transport=Transport(timeout=timeout, operation_timeout=timeout),
|
||||
)
|
||||
return self._client
|
||||
|
||||
def _safe_exception(self, exc):
|
||||
message = str(exc)
|
||||
for secret in (self.username, self.password):
|
||||
if secret:
|
||||
message = message.replace(secret, "***")
|
||||
return message[:500]
|
||||
|
||||
def _call(self, operation, parameters):
|
||||
method = getattr(self.client.service, operation)
|
||||
# The live 0098sms.com WSDL exposes separate scalar arguments. This
|
||||
# deliberately follows the WSDL signature instead of the outdated
|
||||
# dict-shaped invocation printed in the vendor PDF sample.
|
||||
return method(**parameters)
|
||||
|
||||
def _parse_send_result(self, response):
|
||||
raw = _scalar(response)
|
||||
normalized = raw.casefold()
|
||||
if re.fullmatch(r"\d{9,}", raw):
|
||||
return SMSResult(True, provider_id=raw, provider_code=raw)
|
||||
# 2 is the SOAP success code. 0 is retained only for compatibility
|
||||
# with the vendor's older link endpoint.
|
||||
if raw in {"0", "2"}:
|
||||
return SMSResult(True, provider_code=raw)
|
||||
reason = SEND_RESULT_MESSAGES.get(normalized)
|
||||
if reason:
|
||||
return SMSResult(False, error=f"0098SMS [کد {raw}]: {reason}", provider_code=raw)
|
||||
shown = raw[:120] or "پاسخ خالی"
|
||||
return SMSResult(False, error=f"0098SMS پاسخ ناشناخته برگرداند: {shown}", provider_code=raw[:50])
|
||||
|
||||
def send_message(self, mobile, message):
|
||||
missing = [name for name, value in (("SMS0098_USERNAME", self.username), ("SMS0098_PASSWORD", self.password), ("SMS0098_FROM", self.sender)) if not value]
|
||||
if missing:
|
||||
return SMSResult(False, error=f"تنظیمات 0098SMS ناقص است: {', '.join(missing)}")
|
||||
error = self._configuration_error()
|
||||
if error:
|
||||
return SMSResult(False, error=error, provider_code="CONFIG")
|
||||
try:
|
||||
recipient = int(normalize_mobile(str(mobile)).removeprefix("+98"))
|
||||
response = requests.get(self.api_url, params={"USERNAME": self.username, "PASSWORD": self.password, "FROM": self.sender, "TO": recipient, "TEXT": str(message)}, timeout=settings.INTEGRATION_HTTP_TIMEOUT_SECONDS)
|
||||
response.raise_for_status()
|
||||
# This legacy endpoint may append an HTML page containing the
|
||||
# original query string. Persist only its first scalar result so
|
||||
# credentials can never leak into notification logs.
|
||||
provider_id = response.text.strip().splitlines()[0][:100]
|
||||
return SMSResult(True, provider_id=provider_id)
|
||||
except (ValueError, requests.RequestException) as exc:
|
||||
logger.exception("0098SMS delivery failed")
|
||||
return SMSResult(False, error=str(exc)[:1000])
|
||||
recipient = normalize_mobile(str(mobile)).removeprefix("+98")
|
||||
if not recipient.startswith("0"):
|
||||
recipient = f"0{recipient}"
|
||||
response = self._call("SendSMSWithID", {
|
||||
"username": self.username,
|
||||
"password": self.password,
|
||||
"mobileno": recipient,
|
||||
"pnlno": str(self.sender),
|
||||
"text": str(message),
|
||||
})
|
||||
return self._parse_send_result(response)
|
||||
except Exception as exc:
|
||||
logger.exception("0098SMS send failed")
|
||||
return SMSResult(
|
||||
False,
|
||||
error=f"خطا در ارتباط با وبسرویس 0098SMS: {self._safe_exception(exc)}",
|
||||
provider_code="CONNECTION",
|
||||
)
|
||||
|
||||
def get_delivery_status(self, provider_id):
|
||||
error = self._configuration_error()
|
||||
if error:
|
||||
return SMSDeliveryResult("ERROR", "CONFIG", error)
|
||||
if not re.fullmatch(r"\d{9,}", str(provider_id or "")):
|
||||
return SMSDeliveryResult("ERROR", "INVALID_ID", "شناسه رهگیری 0098SMS معتبر نیست.")
|
||||
try:
|
||||
response = self._call("smsdeliveryState", {
|
||||
"username": self.username,
|
||||
"password": self.password,
|
||||
"smsid": str(provider_id),
|
||||
})
|
||||
code = _scalar(response)
|
||||
mapped = DELIVERY_RESULTS.get(code)
|
||||
if mapped:
|
||||
status, description, is_final = mapped
|
||||
return SMSDeliveryResult(status, code, f"0098SMS [کد {code}]: {description}", is_final=is_final)
|
||||
reason = SEND_RESULT_MESSAGES.get(code.casefold())
|
||||
if reason:
|
||||
return SMSDeliveryResult("ERROR", code, f"0098SMS [کد {code}]: {reason}")
|
||||
return SMSDeliveryResult("UNKNOWN", code[:50], f"0098SMS وضعیت دلیوری ناشناخته برگرداند: {code[:120] or 'پاسخ خالی'}")
|
||||
except Exception as exc:
|
||||
logger.exception("0098SMS delivery status lookup failed")
|
||||
return SMSDeliveryResult(
|
||||
"CHECK_FAILED",
|
||||
"CONNECTION",
|
||||
f"خطا در استعلام دلیوری 0098SMS: {self._safe_exception(exc)}",
|
||||
is_final=False,
|
||||
)
|
||||
Reference in new issue
Block a user