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