This commit is contained in:
javad committed 2026-09-10 11:34:19 +03:30
commit fa9d154db9
260 files changed
+17643

No files matched your search

View File
Whitespace-only changes.
+18
View File
@@ -0,0 +1,18 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
from urllib.parse import urlparse
from django.conf import settings
@dataclass(frozen=True, slots=True)
class SMSResult:
success: bool
provider_id: str = ""
error: str = ""
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 معتبر تنظیم نشده است.")
return f"کد شما: {code}\n@{host} #{code}"
class SMSProvider(ABC):
"""Stable application-facing contract for every SMS vendor."""
def send_otp(self,mobile,code): return self.send_message(mobile,webotp_message(code))
@abstractmethod
def send_message(self,mobile,message): raise NotImplementedError
+69
View File
@@ -0,0 +1,69 @@
import logging
import zeep
from django.conf import settings
from zeep.helpers import serialize_object
from .base import SMSProvider, SMSResult
logger = logging.getLogger(__name__)
class BehinSMSProvider(SMSProvider):
"""SOAP adapter for panel.behinsms.com, isolated from application code."""
def __init__(self):
self.username = settings.BEHIN_SMS_USERNAME
self.password = settings.BEHIN_SMS_PASSWORD
self.special_number = settings.BEHIN_SMS_SPECIAL_NUMBER
self.wsdl_url = settings.BEHIN_SMS_WSDL_URL
self._client = None
def _configuration_error(self):
missing = [name for name, value in (
("BEHIN_SMS_USERNAME", self.username),
("BEHIN_SMS_PASSWORD", self.password),
("BEHIN_SMS_SPECIAL_NUMBER", self.special_number)
) if not value]
return f"تنظیمات پیامک بهین ناقص است: {', '.join(missing)}" if missing else ""
@property
def client(self):
if self._client is None:
# استفاده از تنظیمات پیش‌فرض Zeep (مشابه نمونهٔ کاری)
self._client = zeep.Client(wsdl=self.wsdl_url)
return self._client
def get_credit(self):
error = self._configuration_error()
if error:
raise ValueError(error)
return self.client.service.GetCredit(UserName=self.username, Password=self.password)
def send_message(self, mobile, message):
return self.send_many([mobile], message)
def send_many(self, mobile_numbers, message, is_flash=False):
error = self._configuration_error()
if error:
return SMSResult(False, error=error)
recipients = list(dict.fromkeys(str(number).strip() for number in mobile_numbers if str(number).strip()))
if not recipients:
return SMSResult(False, error="شماره گیرنده پیامک خالی است.")
try:
response = self.client.service.SendArray(
UserName=self.username,
Password=self.password,
RecipientNumber={"string": recipients},
MessageBody=message,
SpecialNumber=self.special_number,
IsFlashMessage=is_flash,
CheckingMessageID={"long": [0] * len(recipients)}
)
serialized = serialize_object(response)
if serialized is False or serialized is None:
return SMSResult(False, error="وب‌سرویس بهین ارسال پیامک را تأیید نکرد.")
return SMSResult(True, provider_id=str(serialized)[:500])
except Exception as exc:
logger.exception("Behin SMS delivery failed")
return SMSResult(False, error=str(exc)[:1000])
+10
View File
@@ -0,0 +1,10 @@
import logging
from django.conf import settings
from .base import SMSProvider,SMSResult,webotp_message
logger=logging.getLogger(__name__)
class ConsoleSMSProvider(SMSProvider):
def send_otp(self,mobile,code):
if settings.DEBUG: print(f"[DEV OTP] {mobile}: {webotp_message(code)}")
return SMSResult(True,"console")
def send_message(self,mobile,message):
logger.info("[DEV SMS] destination=***%s message=%s",mobile[-4:],message); return SMSResult(True,"console")
+10
View File
@@ -0,0 +1,10 @@
from functools import lru_cache
from django.conf import settings
from django.utils.module_loading import import_string
from .base import SMSProvider
@lru_cache(maxsize=1)
def get_sms_provider():
provider_class=import_string(settings.SMS_PROVIDER_CLASS)
if not issubclass(provider_class,SMSProvider): raise TypeError("SMS_PROVIDER_CLASS باید از SMSProvider ارث‌بری کند.")
return provider_class()
def reset_sms_provider(): get_sms_provider.cache_clear()
+13
View File
@@ -0,0 +1,13 @@
import requests
from django.conf import settings
from .base import SMSProvider,SMSResult
class HTTPSMSProvider(SMSProvider):
def send_message(self,mobile,message):
if not settings.SMS_API_URL: return SMSResult(False,error="SMS_API_URL تنظیم نشده است.")
headers={"Accept":"application/json","Content-Type":"application/json"}
if settings.SMS_API_TOKEN: headers["Authorization"]=f"Bearer {settings.SMS_API_TOKEN}"
try:
response=requests.post(settings.SMS_API_URL,json={"to":mobile,"message":message,"sender":settings.SMS_SENDER},headers=headers,timeout=settings.INTEGRATION_HTTP_TIMEOUT_SECONDS); response.raise_for_status(); data=response.json() if response.content else {}
if data.get("success") is False: return SMSResult(False,error=str(data.get("error","Provider rejected SMS")))
return SMSResult(True,str(data.get("id") or data.get("message_id") or "http"))
except requests.RequestException as exc: return SMSResult(False,error=str(exc))
+7
View File
@@ -0,0 +1,7 @@
"""Backward-compatible public facade for the SMS integration."""
from .base import 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"]
+36
View File
@@ -0,0 +1,36 @@
import logging
import requests
from django.conf import settings
from apps.accounts.models import normalize_mobile
from .base import SMSProvider, SMSResult
logger = logging.getLogger(__name__)
class ZeroZeroNinetyEightSMSProvider(SMSProvider):
"""Adapter for the 0098SMS HTTP gateway."""
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
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)}")
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])