This commit is contained in:
2025-08-08 10:39:10 +09:00
parent 05bc50269d
commit c8068aea1d
223 changed files with 2199 additions and 13 deletions

View File

@@ -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
View 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"])

View 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)

View File

@@ -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 чаты"