sms
This commit is contained in:
1 parent
250cdbab7f
commit
5c6069c3ef
87 files changed
+29209
-3697
No files matched your search
@@ -0,0 +1,153 @@
|
||||
"""ارسال پیامک از طریق سرویس لینکی 0098SMS (HTTP GET).
|
||||
|
||||
سرویس 0098SMS برای ارسال، یک درخواست GET به ``sendsmslink.aspx`` با پارامترهای
|
||||
``USERNAME/PASSWORD/FROM/TO/TEXT`` میپذیرد و در بدنهٔ پاسخ یک کد کوتاه (مثلاً ``0``)
|
||||
برمیگرداند. این سرویس SOAP/WSDL ندارد؛ اگر آدرس WSDL در تنظیمات گذاشته شود،
|
||||
``zeep`` صفحهٔ HTML میگیرد و خطای «Invalid XML content received» میدهد.
|
||||
|
||||
مقادیر اتصال (URL، نام کاربری، رمز و شمارهٔ خط) از ``SMSProviderConfig`` خوانده
|
||||
میشوند؛ یعنی همانهایی که ادمین در Django Admin یا ``/panel/sms/settings`` وارد میکند.
|
||||
"""
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
|
||||
from .base import SMSProvider, SMSResult
|
||||
from .normalize import to_local
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# آدرس پیشفرض سرویس لینکی؛ اگر ادمین آدرس دیگری وارد کند، همان استفاده میشود.
|
||||
LINK_SEND_URL = "http://www.0098sms.com/sendsmslink.aspx"
|
||||
|
||||
# نام پارامترها قرارداد خود vendor است؛ حروف بزرگ عمدیاند و نباید تغییر کنند.
|
||||
USERNAME_PARAM = "USERNAME"
|
||||
PASSWORD_PARAM = "PASSWORD"
|
||||
SENDER_PARAM = "FROM"
|
||||
RECIPIENT_PARAM = "TO"
|
||||
TEXT_PARAM = "TEXT"
|
||||
|
||||
# کدهای پیشفرض پاسخ. با تنظیمات ``SMS_SUCCESS_CODES``/``SMS_FAILURE_CODES``
|
||||
# (رشتهٔ جداشده با کاما یا لیست) قابل تغییرند، بدون دستزدن به کد.
|
||||
DEFAULT_SUCCESS_CODES = ("0",)
|
||||
DEFAULT_FAILURE_CODES = ("error",)
|
||||
|
||||
|
||||
def _configured_codes(name, default):
|
||||
raw = getattr(settings, name, default)
|
||||
if isinstance(raw, str):
|
||||
raw = raw.split(",")
|
||||
return {str(value).strip().casefold() for value in raw if str(value).strip()}
|
||||
|
||||
|
||||
def _response_code(response):
|
||||
"""کد برگشتی را از بدنهٔ پاسخ جدا میکند (BOM و فاصلههای اضافی حذف میشوند)."""
|
||||
return (response.text or "").strip().lstrip("\ufeff").strip()
|
||||
|
||||
|
||||
class ZeroZeroNinetyEightSMSProvider(SMSProvider):
|
||||
"""سرویس لینکی 0098SMS: یک درخواست GET و یک کد کوتاه در پاسخ."""
|
||||
|
||||
def __init__(self, config=None):
|
||||
self.config = config
|
||||
self.username = getattr(config, "username", "") or ""
|
||||
self.password = getattr(config, "password", "") or ""
|
||||
self.sender = getattr(config, "sender", "") or ""
|
||||
self.url = (getattr(config, "api_url", "") or "").strip() or LINK_SEND_URL
|
||||
self.timeout = getattr(config, "timeout_seconds", None) or 30
|
||||
|
||||
def _configuration_error(self):
|
||||
missing = [
|
||||
name for name, value in (
|
||||
("نام کاربری", self.username),
|
||||
("رمز عبور", self.password),
|
||||
("شماره خط فرستنده", self.sender),
|
||||
) if not value
|
||||
]
|
||||
return f"تنظیمات 0098SMS ناقص است: {', '.join(missing)}" if missing else ""
|
||||
|
||||
def _safe_exception(self, exc):
|
||||
"""متن خطا را از لو رفتن نام کاربری/رمز در لاگ و UI پاک میکند."""
|
||||
message = str(exc)
|
||||
for secret in (self.username, self.password):
|
||||
if secret:
|
||||
message = message.replace(secret, "***")
|
||||
return message[:500]
|
||||
|
||||
def _unconfirmed(self, detail):
|
||||
"""پاسخی که «ارسالشده» را تأیید نمیکند ولی «ردشده» هم نیست.
|
||||
|
||||
وضعیت را ناموفق برمیگردانیم تا صف با backoff دوباره تلاش کند؛ در برابر
|
||||
جایگزینِ «موفق فرضکردن»، این انتخاب صادقانهتر است و سبب گمشدن یادآوری
|
||||
نمیشود. (vendor ممکن است پیامک را فرستاده و پاسخش گم شده باشد.)
|
||||
"""
|
||||
logger.warning("پاسخ نامشخص سرویس 0098SMS: %s", detail)
|
||||
return SMSResult(
|
||||
False, provider_code="UNCONFIRMED",
|
||||
error=f"وضعیت ارسال 0098SMS تأیید نشد (پاسخ: {str(detail)[:120] or 'خالی'}).",
|
||||
)
|
||||
|
||||
def send_message(self, mobile, message):
|
||||
error = self._configuration_error()
|
||||
if error:
|
||||
return SMSResult(False, error=error, provider_code="CONFIG")
|
||||
|
||||
# این سرویس شماره را به شکل محلی «09xxxxxxxxx» میخواهد (نه +98 و نه 98).
|
||||
recipient = to_local(mobile)
|
||||
# تضمین صفر ابتدایی: عدد هرگز به int تبدیل نمیشود (int صفر ابتدایی را حذف میکند)
|
||||
# و همیشه بهصورت رشتهٔ «09xxxxxxxxx» فرستاده میشود.
|
||||
if recipient and not recipient.startswith("0"):
|
||||
recipient = "0" + recipient.lstrip("0")
|
||||
if not recipient:
|
||||
# بدون این گارد، شمارهٔ نامعتبر به vendor میرفت (هزینهٔ ارسال + خطای گنگ).
|
||||
return SMSResult(
|
||||
False, provider_code="INVALID_NUMBER",
|
||||
error="شمارهٔ موبایل گیرنده معتبر نیست.",
|
||||
)
|
||||
|
||||
params = {
|
||||
USERNAME_PARAM: self.username,
|
||||
PASSWORD_PARAM: self.password,
|
||||
SENDER_PARAM: str(self.sender),
|
||||
RECIPIENT_PARAM: recipient,
|
||||
TEXT_PARAM: str(message),
|
||||
}
|
||||
try:
|
||||
# سرویس 0098 داخل ایران میزبانی میشود؛ روی ماشینهایی که
|
||||
# ``HTTP_PROXY`` برای دسترسی به سایتهای خارجی تنظیم شده، عبور از
|
||||
# پراکسی باعث timeout در تماس با این سرویس میشود. مقدار ``None``
|
||||
# برای هر scheme، پراکسی محیط را برای همین درخواست بیاثر میکند.
|
||||
response = requests.get(
|
||||
self.url, params=params, timeout=self.timeout,
|
||||
proxies={"http": None, "https": None},
|
||||
)
|
||||
except (requests.Timeout, requests.ConnectionError) as exc:
|
||||
# مهلت تمامشده/قطعی ارتباط: ممکن است پیامک رفته باشد؛ قطعی «ناموفق» نیست.
|
||||
return self._unconfirmed(self._safe_exception(exc))
|
||||
except requests.RequestException as exc:
|
||||
return SMSResult(
|
||||
False, provider_code="CONNECTION",
|
||||
error=f"خطا در ارتباط با سرویس 0098SMS: {self._safe_exception(exc)}",
|
||||
)
|
||||
|
||||
if response.status_code >= 500:
|
||||
# خطای سمت vendor؛ ممکن است پیش از خطا ارسال شده باشد.
|
||||
return self._unconfirmed(f"HTTP {response.status_code}")
|
||||
|
||||
code = _response_code(response)
|
||||
folded = code.casefold()
|
||||
if folded in _configured_codes("SMS_SUCCESS_CODES", DEFAULT_SUCCESS_CODES):
|
||||
return SMSResult(True, provider_code=code or "0")
|
||||
if folded in _configured_codes("SMS_FAILURE_CODES", DEFAULT_FAILURE_CODES):
|
||||
return SMSResult(
|
||||
False, provider_code="REJECTED",
|
||||
error=f"سرویس 0098SMS ارسال را نپذیرفت (پاسخ: {code[:120] or 'خالی'}).",
|
||||
)
|
||||
if response.status_code >= 400:
|
||||
return SMSResult(
|
||||
False, provider_code=f"HTTP_{response.status_code}",
|
||||
error=f"سرویس 0098SMS درخواست را رد کرد (HTTP {response.status_code}).",
|
||||
)
|
||||
# بدنهٔ ناشناخته نشانهٔ رد نیست؛ اما «ارسالشده» هم قابل اثبات نیست.
|
||||
return self._unconfirmed(code)
|
||||
Reference in new issue
Block a user