Files
shopship/apps/messaging/api/services.py
T
2026-08-24 15:54:08 +03:30

167 lines
6.9 KiB
Python

import re
from django.db import transaction
from apps.account.models import User
from apps.banker.models import Contact, ContactGroup, MessageTemplate
from apps.messaging.models import BulkMessage, Channel, MessageRecipient
from apps.messaging.providers.bale import send_bale
from apps.messaging.providers.eitaa import send_eitaa
from apps.messaging.providers.sms import send_bulk_sms
from apps.messaging.providers.telegram import send_telegram
from apps.messaging.providers.whatsapp import send_whatsapp
_FIELD_PATTERN = re.compile(r"\{(.*?)\}")
_BOT_CHANNEL_SENDERS = {
Channel.TELEGRAM: send_telegram,
Channel.BALE: send_bale,
Channel.EITAA: send_eitaa,
}
class TemplateRenderer:
@staticmethod
def field_values(contact: Contact) -> dict[str, str]:
"""برچسب فیلد -> مقدار، برای جایگزینی {برچسب} در متن قالب.
فیلدهای پایه همیشه هستند؛ فیلدهای سفارشی از گروه(های)ی می‌آیند که
این مخاطب عضوشان است (ContactGroup.member_ids شامل id این مخاطب).
"""
values = {"نام": contact.name, "شماره": contact.phone}
# فیلتر JSON-level (member_ids__contains) روی SQLite اصلا قابل‌کامپایل
# نیست (فقط PostgreSQL پشتیبانی می‌کند) — چون تعداد گروه‌های هر کاربر
# کم است، اینجا فیلتر را سمت پایتون انجام می‌دهیم تا روی هر دو دیتابیس
# کار کند.
groups = [
g
for g in ContactGroup.objects.filter(user_id=contact.user_id)
if contact.id in g.member_ids
]
for group in groups:
for field in group.fields.all():
value = contact.field_values.get(field.id)
if value:
values[field.label] = value
return values
@staticmethod
def render(contact: Contact, template: MessageTemplate | None, body_override: str) -> str:
body = template.body if template else body_override
values = TemplateRenderer.field_values(contact)
return _FIELD_PATTERN.sub(lambda m: values.get(m.group(1), m.group(0)), body)
class BulkMessageService:
@staticmethod
def _resolve_contacts(user: User, group_id: str | None, contact_ids: list[str] | None) -> list[Contact]:
if contact_ids:
return list(Contact.objects.filter(user=user, id__in=contact_ids))
if group_id:
group = ContactGroup.objects.get(user=user, id=group_id)
return list(Contact.objects.filter(user=user, id__in=group.member_ids))
return list(Contact.objects.filter(user=user))
@staticmethod
@transaction.atomic
def create_and_dispatch(
user: User,
channels: list[str],
group_id: str | None = None,
contact_ids: list[str] | None = None,
template_id: str | None = None,
body_override: str = "",
) -> BulkMessage:
template = MessageTemplate.objects.filter(user=user, id=template_id).first() if template_id else None
contacts = BulkMessageService._resolve_contacts(user, group_id, contact_ids)
bulk = BulkMessage.objects.create(
user=user,
group_id=group_id or None,
template=template,
body_override=body_override,
channels=channels,
)
rendered_by_contact = {c.id: TemplateRenderer.render(c, template, body_override) for c in contacts}
if Channel.SMS in channels:
BulkMessageService._dispatch_sms(bulk, contacts, rendered_by_contact)
for channel in (Channel.TELEGRAM, Channel.BALE, Channel.EITAA):
if channel in channels:
BulkMessageService._dispatch_bot_channel(bulk, contacts, rendered_by_contact, channel)
if Channel.WHATSAPP in channels:
BulkMessageService._dispatch_whatsapp(bulk, contacts, rendered_by_contact)
has_manual = bulk.recipients.filter(status=MessageRecipient.Status.NEEDS_MANUAL).exists()
bulk.status = BulkMessage.Status.PARTIALLY_MANUAL if has_manual else BulkMessage.Status.DISPATCHED
bulk.save(update_fields=["status"])
return bulk
@staticmethod
def _dispatch_sms(bulk: BulkMessage, contacts: list[Contact], rendered_by_contact: dict) -> None:
pairs = [(c.phone, rendered_by_contact[c.id]) for c in contacts]
results = send_bulk_sms(pairs)
MessageRecipient.objects.bulk_create(
MessageRecipient(
bulk_message=bulk,
contact=c,
channel=Channel.SMS,
status=MessageRecipient.Status.SENT if results.get(c.phone) else MessageRecipient.Status.FAILED,
rendered_body=rendered_by_contact[c.id],
)
for c in contacts
)
@staticmethod
def _dispatch_bot_channel(
bulk: BulkMessage, contacts: list[Contact], rendered_by_contact: dict, channel: str
) -> None:
sender = _BOT_CHANNEL_SENDERS[channel]
recipients = []
for c in contacts:
chat_id = c.channel_ids.get(channel)
body = rendered_by_contact[c.id]
if not chat_id:
status = MessageRecipient.Status.NEEDS_MANUAL
else:
status = MessageRecipient.Status.SENT if sender(chat_id, body) else MessageRecipient.Status.FAILED
recipients.append(
MessageRecipient(
bulk_message=bulk, contact=c, channel=channel, status=status, rendered_body=body
)
)
MessageRecipient.objects.bulk_create(recipients)
@staticmethod
def _dispatch_whatsapp(bulk: BulkMessage, contacts: list[Contact], rendered_by_contact: dict) -> None:
from django.conf import settings
recipients = []
configured = bool(settings.WHATSAPP_ACCESS_TOKEN and settings.WHATSAPP_PHONE_NUMBER_ID)
for c in contacts:
body = rendered_by_contact[c.id]
if not configured:
status = MessageRecipient.Status.NEEDS_MANUAL
else:
status = (
MessageRecipient.Status.SENT
if send_whatsapp(c.phone, body)
else MessageRecipient.Status.FAILED
)
recipients.append(
MessageRecipient(
bulk_message=bulk, contact=c, channel=Channel.WHATSAPP, status=status, rendered_body=body
)
)
MessageRecipient.objects.bulk_create(recipients)
@staticmethod
def confirm_manual(user: User, bulk_id: int, recipient_id: int) -> MessageRecipient:
recipient = MessageRecipient.objects.get(
id=recipient_id, bulk_message_id=bulk_id, bulk_message__user=user
)
recipient.status = MessageRecipient.Status.MANUAL_CONFIRMED
recipient.save(update_fields=["status"])
return recipient