This commit is contained in:
2026-08-24 15:54:08 +03:30
commit 236002a87c
133 changed files with 5946 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# بله Bot API تلگرام‌سازگار است. اگر base_url حساب تو فرق دارد، همین یک
# مقدار را عوض کن — بقیه‌ی منطق در bot_api.py مشترک است.
from django.conf import settings
from .bot_api import send_bot_message
BALE_BASE_URL = "https://tapi.bale.ai"
def send_bale(chat_id: str, text: str) -> bool:
return send_bot_message(BALE_BASE_URL, settings.BALE_BOT_TOKEN, chat_id, text)
+39
View File
@@ -0,0 +1,39 @@
# apps/messaging/providers/bot_api.py
"""
پایه‌ی مشترک ارسال از طریق Bot API — تلگرام، بله و ایتا هرکدام یک Bot API
تلگرام‌سازگار دارند (endpoint یکسان: POST {base}/bot{token}/sendMessage با
بدنه‌ی {chat_id, text}). این یعنی این سه پلتفرم فقط base_url و token
متفاوت دارند؛ منطق ارسال یکی است.
نکته‌ی مهم — opt-in: بات فقط می‌تواند به chat_id هایی پیام بدهد که قبلا
خودشان چت با بات را با /start شروع کرده‌اند (دقیقا مثل قانون خودِ تلگرام).
یعنی این تابع فقط برای مخاطبینی کار می‌کند که chat_id شان قبلا (از طریق
webhook خود بات، خارج از این فایل) در Contact.channel_ids ثبت شده باشد؛ برای
بقیه، صدازننده باید needs_manual برگرداند — نه اینکه اینجا حدس بزند.
"""
import logging
import requests
logger = logging.getLogger(__name__)
REQUEST_TIMEOUT_SECONDS = 10
def send_bot_message(base_url: str, token: str, chat_id: str, text: str) -> bool:
if not base_url or not token:
logger.error("توکن یا آدرس Bot API تنظیم نشده است")
return False
url = f"{base_url.rstrip('/')}/bot{token}/sendMessage"
try:
response = requests.post(
url, json={"chat_id": chat_id, "text": text}, timeout=REQUEST_TIMEOUT_SECONDS
)
response.raise_for_status()
body = response.json()
return bool(body.get("ok", True))
except (requests.RequestException, ValueError) as exc:
logger.error(f"❌ خطا در ارسال Bot API به {url}: {exc}")
return False
+11
View File
@@ -0,0 +1,11 @@
# ایتا هم یک Bot API تلگرام‌سازگار (Eitaa Payam) دارد. اگر ساختار حساب تو
# فرق دارد (مثلا آدرس دیگری برای endpoint)، همین base_url را عوض کن.
from django.conf import settings
from .bot_api import send_bot_message
EITAA_BASE_URL = "https://eitaayar.ir/api"
def send_eitaa(chat_id: str, text: str) -> bool:
return send_bot_message(EITAA_BASE_URL, settings.EITAA_BOT_TOKEN, chat_id, text)
+64
View File
@@ -0,0 +1,64 @@
# apps/messaging/providers/sms.py
"""
ارسال پیامک گروهی با متن آزاد (نه پترن OTP) — از همان کاوه‌نگاری که
apps/account/send_sms.py برای OTP استفاده می‌کند، ولی از API متفاوتی: پترن
OTP فقط اجازه‌ی ارسال یک قالب از پیش تاییدشده را می‌دهد، برای متن دلخواه باید
از API عادی ارسال (sms/sendarray.json برای چندگیرنده‌ی هم‌زمان با متن‌های
مجزا) استفاده کرد. این دو مسیر کاملا مستقل‌اند؛ send_sms.py دست‌نخورده ماند.
فقط دو مقدار از .env لازم است: KAVENEGAR_API_KEY و KAVENEGAR_SENDER_LINE.
اگر ساختار پاسخ/درخواست کاوه‌نگار حساب تو فرق دارد، فقط تابع
`_call_sendarray` را با مستندات https://kavenegar.com/rest.html هماهنگ کن.
"""
import json
import logging
import requests
from django.conf import settings
logger = logging.getLogger(__name__)
REQUEST_TIMEOUT_SECONDS = 15
KAVENEGAR_BASE = "https://api.kavenegar.com/v1"
def _call_sendarray(receptors: list[str], messages: list[str]) -> dict[str, bool]:
"""ارسال بالک؛ خروجی: {receptor: True/False} برای هر گیرنده."""
api_key = settings.KAVENEGAR_API_KEY
sender = settings.KAVENEGAR_SENDER_LINE
result = {r: False for r in receptors}
if not api_key or not sender:
logger.error("KAVENEGAR_API_KEY یا KAVENEGAR_SENDER_LINE در .env تنظیم نشده است")
return result
url = f"{KAVENEGAR_BASE}/{api_key}/sms/sendarray.json"
payload = {
"receptor": json.dumps(receptors),
"sender": json.dumps([sender] * len(receptors)),
"message": json.dumps(messages),
}
try:
response = requests.post(url, data=payload, timeout=REQUEST_TIMEOUT_SECONDS)
response.raise_for_status()
body = response.json()
if body.get("return", {}).get("status") == 200:
for r in receptors:
result[r] = True
else:
logger.error(f"❌ کاوه‌نگار خطا برگرداند: {body.get('return')}")
except (requests.RequestException, ValueError) as exc:
logger.error(f"❌ خطا در ارسال پیامک بالک: {exc}")
return result
def send_bulk_sms(recipients: list[tuple[str, str]]) -> dict[str, bool]:
"""recipients: [(phone, message), ...] -> {phone: sent_ok}"""
if not recipients:
return {}
phones = [p for p, _ in recipients]
messages = [m for _, m in recipients]
return _call_sendarray(phones, messages)
+9
View File
@@ -0,0 +1,9 @@
from django.conf import settings
from .bot_api import send_bot_message
TELEGRAM_BASE_URL = "https://api.telegram.org"
def send_telegram(chat_id: str, text: str) -> bool:
return send_bot_message(TELEGRAM_BASE_URL, settings.TELEGRAM_BOT_TOKEN, chat_id, text)
+63
View File
@@ -0,0 +1,63 @@
# apps/messaging/providers/whatsapp.py
"""
ارسال از طریق WhatsApp Business Cloud API رسمی (Meta) —
https://developers.facebook.com/docs/whatsapp/cloud-api
برای ارسال گروهی/تبلیغاتی (business-initiated، نه پاسخ به پیام کاربر)،
واتساپ فقط اجازه‌ی ارسال «Template Message» از قبل تاییدشده را می‌دهد؛ پیام
آزاد فقط داخل پنجره‌ی ۲۴ ساعته‌ی پاسخ به مشتری مجاز است. یعنی این تابع یک
Template با یک متغیر ({{1}}) صدا می‌زند که همان متن رندرشده‌ی قالب داخل اپ
در آن جای می‌گیرد — این Template باید از قبل در Meta Business Manager ثبت و
تایید شده باشد.
سه مقدار از .env لازم است: WHATSAPP_ACCESS_TOKEN، WHATSAPP_PHONE_NUMBER_ID،
WHATSAPP_TEMPLATE_NAME. اگر ساختار Template حساب تو (تعداد متغیرها، زبان)
فرق دارد، فقط بدنه‌ی payload در send_whatsapp را هماهنگ کن.
"""
import logging
import requests
from django.conf import settings
logger = logging.getLogger(__name__)
REQUEST_TIMEOUT_SECONDS = 15
GRAPH_API_VERSION = "v20.0"
def send_whatsapp(phone: str, text: str) -> bool:
token = settings.WHATSAPP_ACCESS_TOKEN
phone_number_id = settings.WHATSAPP_PHONE_NUMBER_ID
template_name = settings.WHATSAPP_TEMPLATE_NAME
if not token or not phone_number_id or not template_name:
logger.error("تنظیمات WhatsApp Business API کامل نیست")
return False
url = f"https://graph.facebook.com/{GRAPH_API_VERSION}/{phone_number_id}/messages"
payload = {
"messaging_product": "whatsapp",
"to": phone,
"type": "template",
"template": {
"name": template_name,
"language": {"code": "fa"},
"components": [
{"type": "body", "parameters": [{"type": "text", "text": text}]},
],
},
}
try:
response = requests.post(
url,
json=payload,
headers={"Authorization": f"Bearer {token}"},
timeout=REQUEST_TIMEOUT_SECONDS,
)
response.raise_for_status()
return True
except requests.RequestException as exc:
logger.error(f"❌ خطا در ارسال WhatsApp به {phone}: {exc}")
return False