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,14 @@
.git/
bin/
backups/
# Исключаем хранилище БД
db_storage/
app/var/lib/mysql
# Остальное лишнее
.git
.venv
__pycache__
*.pyc
*.pyo
.env

View File

@@ -0,0 +1,28 @@
/*M!999999\- enable the sandbox mode */
-- MariaDB dump 10.19-11.6.2-MariaDB, for debian-linux-gnu (x86_64)
--
-- Host: 127.0.0.1 Database: tg_autopost
-- ------------------------------------------------------
-- Server version 11.6.2-MariaDB-ubu2404-log
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*M!100616 SET @OLD_NOTE_VERBOSITY=@@NOTE_VERBOSITY, NOTE_VERBOSITY=0 */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*M!100616 SET NOTE_VERBOSITY=@OLD_NOTE_VERBOSITY */;
-- Dump completed on 2025-08-08 10:20:40

View File

@@ -0,0 +1,28 @@
/*M!999999\- enable the sandbox mode */
-- MariaDB dump 10.19-11.6.2-MariaDB, for debian-linux-gnu (x86_64)
--
-- Host: 127.0.0.1 Database: tg_autopost
-- ------------------------------------------------------
-- Server version 11.6.2-MariaDB-ubu2404-log
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*M!100616 SET @OLD_NOTE_VERBOSITY=@@NOTE_VERBOSITY, NOTE_VERBOSITY=0 */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*M!100616 SET NOTE_VERBOSITY=@OLD_NOTE_VERBOSITY */;
-- Dump completed on 2025-08-08 10:21:15

View File

@@ -0,0 +1,28 @@
/*M!999999\- enable the sandbox mode */
-- MariaDB dump 10.19-11.6.2-MariaDB, for debian-linux-gnu (x86_64)
--
-- Host: 127.0.0.1 Database: tg_autopost
-- ------------------------------------------------------
-- Server version 11.6.2-MariaDB-ubu2404-log
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*M!100616 SET @OLD_NOTE_VERBOSITY=@@NOTE_VERBOSITY, NOTE_VERBOSITY=0 */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*M!100616 SET NOTE_VERBOSITY=@OLD_NOTE_VERBOSITY */;
-- Dump completed on 2025-08-08 10:30:15

View File

@@ -1,17 +1,31 @@
#!/bin/bash
set -e
# Загружаем переменные окружения
source .env
BACKUP_DIR="./backups"
TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
FILENAME="backup_${TIMESTAMP}.sql"
echo "💾 1. Создаём бэкап базы..."
mkdir -p $BACKUP_DIR
docker compose exec db mysqldump -u"$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" > "${BACKUP_DIR}/${FILENAME}"
echo "✅ Бэкап сохранён: ${BACKUP_DIR}/${FILENAME}"
mkdir -p "$BACKUP_DIR"
# гарантируем, что db поднята
docker compose up -d db
# ждём доступности сервиса (не обязательно, но полезно)
# docker compose healthcheck можно добавить в compose, но тут просто пауза:
sleep 3
# Пытаемся mariadb-dump, если нет — mysqldump. Берём MYSQL_* из окружения контейнера.
if ! docker compose exec -T db sh -lc 'command -v mariadb-dump >/dev/null 2>&1 || command -v mysqldump >/dev/null 2>&1'; then
echo "⚠️ В контейнере нет mariadb-dump/mysqldump. Пропускаю бэкап."
else
docker compose exec -T db sh -lc '
DUMP_BIN=$(command -v mariadb-dump || command -v mysqldump);
"$DUMP_BIN" \
-h127.0.0.1 \
-u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE" \
--single-transaction --quick --lock-tables=false
' > "${BACKUP_DIR}/${FILENAME}" || echo "⚠️ Бэкап не получился (возможно, ещё пустая БД). Продолжаю..."
fi
echo "📥 2. Получаем обновления из git..."
git pull
@@ -20,11 +34,10 @@ echo "🔨 3. Собираем контейнеры..."
docker compose build
echo "⬆ 4. Применяем миграции..."
docker compose exec django python manage.py migrate
docker compose up -d django
docker compose exec -T django python manage.py makemigrations && docker compose exec -T django python manage.py migrate
echo "🔄 5. Перезапускаем сервисы..."
docker compose down
docker compose up -d
echo "🎉 Деплой завершён успешно!"
echo "✅ Все операции выполнены успешно."

View File

@@ -5,7 +5,7 @@ echo "📥 Получаем обновления..."
git pull
echo "🔨 Собираем контейнеры..."
docker compose build
docker compose build --no-cache
echo "⬆ Обновляем БД..."
docker compose exec django python manage.py migrate

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

View File

@@ -0,0 +1,6 @@
[mariadb-client]
port=3306
socket=/run/mysqld/mysqld.sock
user=healthcheck
password=~QH*.$7^|l>R!J0Sol"T@G]*S9.:${E]

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,148 @@
5,3
4,3
7,3
1,45
3,44
2,44
1,44
3,43
2,43
1,43
3,42
2,42
1,42
3,41
2,41
1,41
3,40
2,40
1,40
3,39
2,39
1,39
3,38
2,38
1,38
3,37
2,37
1,37
3,36
2,36
1,36
3,35
2,35
1,35
3,34
2,34
1,34
3,33
2,33
1,33
3,32
2,32
1,32
3,31
2,31
1,31
3,30
2,30
1,30
3,29
2,29
1,29
3,28
2,28
1,28
3,27
2,27
1,27
3,26
2,26
1,26
3,25
2,25
1,25
3,24
2,24
1,24
3,23
2,23
1,23
3,22
2,22
1,22
3,21
2,21
1,21
3,20
2,20
1,20
3,19
2,19
1,19
3,18
2,18
1,18
3,17
2,17
1,17
3,16
2,16
1,16
3,15
2,15
1,15
3,14
2,14
1,14
3,13
2,13
1,13
3,12
2,12
1,12
3,11
2,11
1,11
3,10
2,10
1,10
3,9
2,9
1,9
3,8
2,8
1,8
3,7
2,7
1,7
3,6
2,6
1,6
3,5
2,5
1,5
3,4
2,4
1,4
3,3
3,0
2,3
2,0
1,3
1,0
0,6
0,0
0,47
0,46
0,49
0,48
0,45
0,12
0,10
0,8
0,11
0,5
0,7
0,4
0,3

Binary file not shown.

BIN
db_storage/mysql/ibdata1 Normal file

Binary file not shown.

BIN
db_storage/mysql/ibtmp1 Normal file

Binary file not shown.

View File

@@ -0,0 +1 @@
11.6.2-MariaDB

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,2 @@
default-character-set=utf8mb4
default-collation=utf8mb4_unicode_ci

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More