154 lines
7.0 KiB
Python
154 lines
7.0 KiB
Python
"""ارسال و پیگیری پیامک 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,
|
|
)
|