167 lines
6.9 KiB
Python
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
|