This commit is contained in:
2026-08-24 15:54:08 +03:30
commit 236002a87c
133 changed files with 5946 additions and 0 deletions
+166
View File
@@ -0,0 +1,166 @@
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