"""ارسال و پیگیری پیامک 0098SMS از طریق وب‌سرویس (SOAP). تفاوت با provider لینکی (``sms0098``): متد ``SendSMSWithID`` هم‌زمان با ارسال **شناسهٔ پیامک (smsid)** را برمی‌گرداند و متد ``smsdeliveryState`` وضعیت تحویل به گوشی را می‌دهد. به همین دلیل تنها این provider قابلیت رهگیری «رسیده/نرسیده به گوشی» دارد. آدرس وب‌سرویس، نام کاربری، رمز و شمارهٔ پنل از ``SMSProviderConfig`` خوانده می‌شوند (نه هیچ مقدار ثابتی در کد). """ import logging import requests import zeep from zeep.transports import Transport from .base import SMSDeliveryResult, SMSProvider, SMSResult from .normalize import to_local from .sms0098_codes import classify_delivery, describe_send_code logger = logging.getLogger(__name__) # نام پارامترها را خودِ WSDL سرویس تعیین می‌کند و **تماماً با حرف کوچک** است # (username/password/text/mobileno/pnlno/smsid). راهنمای vendor آن‌ها را با # حرف بزرگ نوشته؛ zeep نام راهنما را نمی‌پذیرد و «unexpected keyword argument» # می‌دهد. پس این‌جا دقیقاً همان امضای WSDL استفاده می‌شود. _SEND_OPERATION = "SendSMSWithID" _DELIVERY_OPERATION = "smsdeliveryState" def _as_text(value): """پاسخ zeep/وب‌سرویس را به رشتهٔ ساده تبدیل می‌کند.""" if value is None: return "" if isinstance(value, bool): return str(value) if isinstance(value, float) and value.is_integer(): return str(int(value)) return str(value).strip() class ZeroZeroNinetyEightWebServiceProvider(SMSProvider): """وب‌سرویس SOAP 0098SMS: ارسال با شناسه + استعلام دلیوری.""" def __init__(self, config=None): self.config = config self.username = (getattr(config, "username", "") or "").strip() self.password = (getattr(config, "password", "") or "").strip() # شمارهٔ پنل وب‌سرویس؛ در نبود مقدار صریح، همان خط فرستنده است. self.panel_no = (getattr(config, "panel_no", "") or "").strip() or \ (getattr(config, "sender", "") or "").strip() self.url = (getattr(config, "webservice_url", "") or "").strip() self.timeout = getattr(config, "timeout_seconds", None) or 30 self._client = None # ------------------------------------------------------------------ helpers def _configuration_error(self): missing = [ name for name, value in ( ("نام کاربری", self.username), ("رمز عبور", self.password), ("شماره پنل", self.panel_no), ("آدرس وب‌سرویس", self.url), ) if not value ] return f"تنظیمات وب‌سرویس 0098SMS ناقص است: {', '.join(missing)}" if missing else "" def _safe_exception(self, exc): """متن خطا را از لو رفتن نام کاربری/رمز پاک می‌کند.""" message = str(exc) for secret in (self.username, self.password): if secret: message = message.replace(secret, "***") return message[:500] @property def client(self): if self._client is None: # ``trust_env=False`` پراکسی محیط (مثلاً برای سایت‌های خارجی) را برای # تماس با سرویس داخلی ایران خنثی می‌کند. session = requests.Session() session.trust_env = False transport = Transport( session=session, timeout=self.timeout, operation_timeout=self.timeout, ) wsdl = self.url if self.url.lower().endswith("wsdl") else f"{self.url}?WSDL" self._client = zeep.Client(wsdl=wsdl, transport=transport) return self._client # ------------------------------------------------------------------ sending def send_message(self, mobile, message): error = self._configuration_error() if error: return SMSResult(False, error=error, provider_code="CONFIG") recipient = to_local(mobile) if not recipient: return SMSResult( False, provider_code="INVALID_NUMBER", error="شمارهٔ موبایل گیرنده معتبر نیست.", ) try: response = getattr(self.client.service, _SEND_OPERATION)( username=self.username, password=self.password, text=str(message), mobileno=recipient, pnlno=self.panel_no, ) except Exception as exc: # (Connect|Timeout|zeep) — هر چه بود، ارسال تأیید نشد logger.exception("ارسال پیامک 0098SMS (وب‌سرویس) شکست خورد") return SMSResult( False, provider_code="REQUEST_ERROR", error=f"خطا در ارتباط با وب‌سرویس 0098SMS: {self._safe_exception(exc)}", ) code = _as_text(response) # موفقیت: یا ۰، یا خودِ شناسهٔ پیامک (عدد حداقل ۹ رقمی). if code == "0": return SMSResult(True, provider_code="0") if code.isdigit() and len(code) >= 9: return SMSResult(True, provider_id=code, provider_code=code) description = describe_send_code(code) if description: return SMSResult(False, provider_code=code[:50] or "REJECTED", error=description) return SMSResult( False, provider_code=code[:50] or "UNKNOWN", error=f"پاسخ نامشخص وب‌سرویس 0098SMS: {code[:200] or 'خالی'}", ) # ----------------------------------------------------------------- delivery def get_delivery_status(self, provider_id): """وضعیت تحویل پیامک را با شناسهٔ آن استعلام می‌کند. در خطای ارتباط/تنظیمات ``None`` برمی‌گرداند تا فراخوان (دستور ``sms_delivery``) این ردیف را رد کند و در دور بعد دوباره تلاش نماید. """ if not provider_id or self._configuration_error(): return None try: response = getattr(self.client.service, _DELIVERY_OPERATION)( username=self.username, password=self.password, smsid=str(provider_id), ) except Exception: logger.exception("استعلام دلیوری 0098SMS برای smsid=%s شکست خورد", provider_id) return None code = _as_text(response) result = classify_delivery(code) return SMSDeliveryResult( status=result.status, provider_code=code, description=result.description, is_final=result.is_final, )