first
This commit is contained in:
commit
fa9d154db9
260 files changed
+17643
No files matched your search
Whitespace-only changes.
@@ -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
|
||||
@@ -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])
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
@@ -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))
|
||||
@@ -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"]
|
||||
@@ -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])
|
||||
Reference in new issue
Block a user