119 lines
5.3 KiB
Python
119 lines
5.3 KiB
Python
"""استعلام وضعیت تحقق پیامکهای ارسالشده (رسیده/نرسیده به گوشی).
|
|
|
|
فقط برای provider وبسرویس 0098SMS معنا دارد؛ تنها آن provider با
|
|
``SendSMSWithID`` شناسهٔ پیامک (smsid) برمیگرداند و ``smsdeliveryState``
|
|
وضعیت تحویل را میدهد. برای سایر providerها دستور no-op است.
|
|
"""
|
|
import logging
|
|
import signal
|
|
import time
|
|
from datetime import timedelta
|
|
|
|
from django.core.management.base import BaseCommand
|
|
from django.db.models import Q
|
|
from django.utils import timezone
|
|
|
|
from admin_panel.models import SMSProviderConfig, SMSQueue
|
|
from integrations.sms.factory import get_sms_provider
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class Command(BaseCommand):
|
|
help = "استعلام وضعیت تحویل پیامکهای ارسالشده از وبسرویس 0098SMS."
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self._stop = False
|
|
|
|
def add_arguments(self, parser):
|
|
parser.add_argument("--once", action="store_true", help="یک دور پردازش و خروج")
|
|
parser.add_argument("--sleep", type=float, default=30.0, help="فاصلهٔ بین دورها (ثانیه)")
|
|
parser.add_argument("--batch", type=int, default=100, help="تعداد ردیف در هر دور")
|
|
parser.add_argument(
|
|
"--min-age", type=int, default=60,
|
|
help="حداقل فاصله (ثانیه) از زمان ارسال پیش از نخستین استعلام",
|
|
)
|
|
|
|
def handle(self, *args, **options):
|
|
signal.signal(signal.SIGTERM, self._handle_signal)
|
|
signal.signal(signal.SIGINT, self._handle_signal)
|
|
once = options["once"]
|
|
sleep_seconds = options["sleep"]
|
|
|
|
self._emit("استعلام دلیوری پیامک آغاز شد.", style=self.style.SUCCESS)
|
|
while not self._stop:
|
|
try:
|
|
processed = self._drain_batch(
|
|
options["batch"], options["min_age"],
|
|
)
|
|
except Exception:
|
|
logger.exception("خطای غیرمنتظره در یک دور از استعلام دلیوری؛ دور بعد ادامه میدهد.")
|
|
processed = 0
|
|
if once:
|
|
break
|
|
if processed == 0:
|
|
self._sleep_interruptible(sleep_seconds)
|
|
self._emit("استعلام دلیوری پیامک متوقف شد.", style=self.style.SUCCESS)
|
|
|
|
# ------------------------------------------------------------------ helpers
|
|
def _handle_signal(self, signum, frame):
|
|
self._stop = True
|
|
|
|
def _emit(self, text, *, style=None, err=False):
|
|
"""چاپ با تحمل کدگذاری محدود کنسول (مثل worker پیامک)."""
|
|
stream = self.stderr if err else self.stdout
|
|
style = style or (lambda value: value)
|
|
for payload in (text, text.encode("ascii", "replace").decode("ascii")):
|
|
try:
|
|
stream.write(style(payload))
|
|
return
|
|
except (UnicodeEncodeError, ValueError, OSError):
|
|
continue
|
|
|
|
def _sleep_interruptible(self, seconds):
|
|
deadline = time.monotonic() + max(0, seconds)
|
|
while not self._stop and time.monotonic() < deadline:
|
|
time.sleep(min(0.5, deadline - time.monotonic()))
|
|
|
|
def _drain_batch(self, batch, min_age):
|
|
config = SMSProviderConfig.load()
|
|
if config.provider != "sms0098ws":
|
|
# smsid تنها متعلق به این provider است؛ استعلام با بقیه بیمعناست.
|
|
logger.info("provider فعلی رهگیری دلیوری ندارد؛ استعلام رد شد.")
|
|
return 0
|
|
|
|
now = timezone.now()
|
|
cutoff = now - timedelta(seconds=max(0, min_age))
|
|
# پیامکهای ارسالشده که شناسه دارند و وضعیتشان هنوز نهایی نشده است.
|
|
query = SMSQueue.objects.filter(
|
|
Q(status=SMSQueue.Status.SENT, provider_id__gt="", sent_at__lte=cutoff),
|
|
Q(delivery_checked_at__isnull=True)
|
|
| Q(delivery_status__in=["", SMSQueue.DeliveryStatus.PENDING,
|
|
SMSQueue.DeliveryStatus.UNKNOWN]),
|
|
)
|
|
|
|
try:
|
|
provider = get_sms_provider(config)
|
|
except Exception:
|
|
logger.exception("ساخت provider پیامک ناموفق بود؛ این دور رد شد.")
|
|
return 0
|
|
|
|
processed = 0
|
|
for row in query.order_by("sent_at")[:batch]:
|
|
if self._stop:
|
|
break
|
|
result = provider.get_delivery_status(row.provider_id)
|
|
if result is None:
|
|
# خطای ارتباط/تنظیمات: ردیف را رد کن تا دور بعد دوباره تلاش شود.
|
|
continue
|
|
row.delivery_status = result.status
|
|
row.delivery_checked_at = timezone.now()
|
|
row.save(update_fields=["delivery_status", "delivery_checked_at", "updated_at"])
|
|
processed += 1
|
|
self._emit(
|
|
f" دلیوری #{row.pk} ({row.provider_id}): {result.status} "
|
|
f"{'(نهایی)' if result.is_final else '(در انتظار)'}",
|
|
)
|
|
return processed
|