bot dev
This commit is contained in:
23
bot/admin.py
23
bot/admin.py
@@ -1,3 +1,24 @@
|
||||
from django.contrib import admin
|
||||
from .models import TelegramBot, BotConfig, TelegramChat
|
||||
|
||||
# Register your models here.
|
||||
|
||||
@admin.register(TelegramBot)
|
||||
class TelegramBotAdmin(admin.ModelAdmin):
|
||||
list_display = ("id", "name", "username", "is_active")
|
||||
list_filter = ("is_active",)
|
||||
search_fields = ("name", "username")
|
||||
|
||||
|
||||
@admin.register(BotConfig)
|
||||
class BotConfigAdmin(admin.ModelAdmin):
|
||||
list_display = ("id", "bot", "parse_mode", "use_webhook")
|
||||
list_filter = ("use_webhook", "parse_mode")
|
||||
autocomplete_fields = ("bot",)
|
||||
|
||||
|
||||
@admin.register(TelegramChat)
|
||||
class TelegramChatAdmin(admin.ModelAdmin):
|
||||
list_display = ("id", "type", "title", "username", "is_member", "joined_at", "left_at", "last_message_at")
|
||||
list_filter = ("type", "is_member")
|
||||
search_fields = ("title", "username")
|
||||
readonly_fields = ("joined_at", "left_at", "last_message_at")
|
||||
|
||||
61
bot/handlers.py
Normal file
61
bot/handlers.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from datetime import datetime, timezone as py_tz
|
||||
from telegram import Update
|
||||
from telegram.ext import ContextTypes
|
||||
from django.utils import timezone
|
||||
from .models import TelegramChat
|
||||
|
||||
def _now_aware():
|
||||
return timezone.now()
|
||||
|
||||
async def upsert_chat_from_update(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""
|
||||
Вызывайте в message-хендлерах, чтобы обновлять last_message_at и тайтл.
|
||||
"""
|
||||
chat = update.effective_chat
|
||||
if not chat:
|
||||
return
|
||||
|
||||
obj, created = TelegramChat.objects.update_or_create(
|
||||
id=chat.id,
|
||||
defaults={
|
||||
"type": chat.type,
|
||||
"title": chat.title or "",
|
||||
"username": chat.username or "",
|
||||
"is_member": True, # если есть сообщения — бот, скорее всего, в чате
|
||||
"last_message_at": _now_aware(),
|
||||
}
|
||||
)
|
||||
# created можно игнорировать; при необходимости — логировать
|
||||
|
||||
|
||||
async def on_my_chat_member(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""
|
||||
Ловим изменение статуса бота в чатах (добавили/кикнули).
|
||||
"""
|
||||
if not update.my_chat_member:
|
||||
return
|
||||
|
||||
chat = update.my_chat_member.chat
|
||||
new_status = update.my_chat_member.new_chat_member.status # "member"/"administrator"/"kicked"/"left"/...
|
||||
is_in_chat = new_status in ("member", "administrator")
|
||||
|
||||
defaults = {
|
||||
"type": chat.type,
|
||||
"title": chat.title or "",
|
||||
"username": getattr(chat, "username", "") or "",
|
||||
"is_member": is_in_chat,
|
||||
"last_message_at": _now_aware(),
|
||||
}
|
||||
|
||||
if is_in_chat:
|
||||
defaults["left_at"] = None
|
||||
defaults.setdefault("joined_at", _now_aware())
|
||||
|
||||
obj, _ = TelegramChat.objects.update_or_create(
|
||||
id=chat.id,
|
||||
defaults=defaults
|
||||
)
|
||||
|
||||
if not is_in_chat and obj.left_at is None:
|
||||
obj.left_at = _now_aware()
|
||||
obj.save(update_fields=["is_member", "left_at"])
|
||||
72
bot/management/commands/runbot.py
Normal file
72
bot/management/commands/runbot.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from telegram.ext import ApplicationBuilder, CommandHandler, MessageHandler, ChatMemberHandler, filters
|
||||
from django.db import transaction
|
||||
from bot.models import TelegramBot, BotConfig
|
||||
from bot.handlers import on_my_chat_member, upsert_chat_from_update
|
||||
|
||||
async def cmd_start(update, context):
|
||||
await upsert_chat_from_update(update, context)
|
||||
await update.message.reply_text("Я на связи. Добавьте меня в группу — запомню чат автоматически.")
|
||||
|
||||
async def cmd_groups(update, context):
|
||||
"""
|
||||
/groups — покажем список чатов, где бот сейчас состоит.
|
||||
(Лучше ограничить доступ: например, только admin_user_ids из BotConfig)
|
||||
"""
|
||||
bot_conf = await context.application.bot_data.get("bot_conf")
|
||||
admin_ids = bot_conf.admin_user_ids if bot_conf else []
|
||||
if update.effective_user and admin_ids and update.effective_user.id not in admin_ids:
|
||||
return
|
||||
|
||||
from bot.models import TelegramChat
|
||||
qs = TelegramChat.objects.filter(is_member=True).exclude(type="private").order_by("type", "title")
|
||||
if not qs.exists():
|
||||
await update.message.reply_text("Пока ни одной группы/канала не найдено.")
|
||||
return
|
||||
|
||||
lines = []
|
||||
for ch in qs:
|
||||
label = ch.title or ch.username or ch.id
|
||||
lines.append(f"• {label} [{ch.type}] — {ch.id}")
|
||||
await update.message.reply_text("\n".join(lines)[:3900])
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Запуск телеграм-бота (polling) на основе активного TelegramBot"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("--bot-id", type=int, help="ID TelegramBot (если не указан — берём первый активный)")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
bot_id = options.get("bot_id")
|
||||
|
||||
try:
|
||||
if bot_id:
|
||||
bot = TelegramBot.objects.get(id=bot_id, is_active=True)
|
||||
else:
|
||||
bot = TelegramBot.objects.filter(is_active=True).first()
|
||||
if not bot:
|
||||
raise CommandError("Нет активного TelegramBot.")
|
||||
except TelegramBot.DoesNotExist:
|
||||
raise CommandError("Указанный TelegramBot не найден или неактивен.")
|
||||
|
||||
conf = getattr(bot, "config", None)
|
||||
|
||||
app = ApplicationBuilder().token(bot.token).build()
|
||||
# Сохраним конфиг в bot_data, чтобы был доступ в хендлерах:
|
||||
app.bot_data["bot_conf"] = conf
|
||||
|
||||
# Команды
|
||||
app.add_handler(CommandHandler("start", cmd_start))
|
||||
app.add_handler(CommandHandler("groups", cmd_groups))
|
||||
|
||||
# Обновление списка чатов при изменении статуса бота
|
||||
app.add_handler(ChatMemberHandler(on_my_chat_member, ChatMemberHandler.MY_CHAT_MEMBER))
|
||||
|
||||
# Любые входящие сообщения — обновляем last_message_at для чатов
|
||||
app.add_handler(MessageHandler(filters.ALL, upsert_chat_from_update))
|
||||
|
||||
# allowed_updates (если заданы в конфиге)
|
||||
allowed_updates = conf.allowed_updates if conf and conf.allowed_updates else None
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f"Бот {bot} запущен (polling)."))
|
||||
app.run_polling(allowed_updates=allowed_updates, drop_pending_updates=True)
|
||||
@@ -1,3 +1,61 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
# Create your models here.
|
||||
|
||||
class TelegramBot(models.Model):
|
||||
name = models.CharField(max_length=100, help_text="Произвольное имя бота в проекте")
|
||||
username = models.CharField(max_length=100, blank=True, help_text="@username в Telegram")
|
||||
token = models.CharField(max_length=200)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.username or 'no-username'})"
|
||||
|
||||
|
||||
class BotConfig(models.Model):
|
||||
bot = models.OneToOneField(TelegramBot, on_delete=models.CASCADE, related_name="config")
|
||||
parse_mode = models.CharField(
|
||||
max_length=20, default="HTML",
|
||||
choices=[("HTML", "HTML"), ("MarkdownV2", "MarkdownV2"), ("None", "None")]
|
||||
)
|
||||
allowed_updates = models.JSONField(default=list, blank=True)
|
||||
admin_user_ids = models.JSONField(default=list, blank=True, help_text="Список Telegram user_id админов")
|
||||
webhook_url = models.URLField(blank=True, default="", help_text="Если используете вебхуки")
|
||||
use_webhook = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return f"Config for {self.bot}"
|
||||
|
||||
|
||||
class TelegramChat(models.Model):
|
||||
"""
|
||||
Храним все чаты, где бот состоит/состоял.
|
||||
chat_id в TG может быть до ~52 бит => BigInteger.
|
||||
"""
|
||||
CHAT_TYPES = [
|
||||
("private", "private"),
|
||||
("group", "group"),
|
||||
("supergroup", "supergroup"),
|
||||
("channel", "channel"),
|
||||
]
|
||||
|
||||
id = models.BigIntegerField(primary_key=True) # chat_id как PK
|
||||
type = models.CharField(max_length=20, choices=CHAT_TYPES)
|
||||
title = models.CharField(max_length=255, blank=True, default="")
|
||||
username = models.CharField(max_length=255, blank=True, default="")
|
||||
|
||||
# Статус участия бота
|
||||
is_member = models.BooleanField(default=True)
|
||||
joined_at = models.DateTimeField(default=timezone.now)
|
||||
left_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
# Метаданные
|
||||
last_message_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
base = self.title or self.username or str(self.id)
|
||||
return f"{base} [{self.type}]"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Telegram чат"
|
||||
verbose_name_plural = "Telegram чаты"
|
||||
|
||||
Reference in New Issue
Block a user