Merge pull request 'feat: add group support for webinar notifier' (#5) from feat/edu-group into main

Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
2026-01-14 16:46:37 +01:00
2 changed files with 59 additions and 16 deletions
+3
View File
@@ -2,6 +2,9 @@
sync.ffs_lock
.sync.ffs_db
# Copyparty
*.hist/
# Volumes and data directories
gitea/gitea-db/
gitea/gitea-data/*
+55 -15
View File
@@ -3,7 +3,8 @@ import logging
import redis
import json
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ChatMember
from telegram.constants import ChatType
from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes
from playwright.async_api import async_playwright
@@ -57,7 +58,7 @@ TRANSLATIONS = {
'welcome_admin': "\n\n👑 <b>Режим администратора активен</b>",
'access_denied': "⛔ Доступ запрещен. Вас нет в белом списке.",
'help_title': "🤖 <b>Помощь по боту</b>\n\n",
'help_commands': "/start - Подписаться на уведомления\n/help - Показать это сообщение\n/language - Сменить язык",
'help_commands': "/start - Подписаться на уведомления\n/stop - Отписаться от уведомлений\n/help - Показать это сообщение\n/language - Сменить язык",
'help_admin': "\n<b>Команды администратора:</b>\n/adduser [user_id] - Добавить пользователя в белый список\n/removeuser [user_id] - Удалить пользователя из белого списка\nИли используйте панель ниже для управления настройками.",
'admin_only': "⛔ Только для администратора!",
'user_added': "✅ Пользователь {user_id} добавлен в белый список",
@@ -94,7 +95,7 @@ TRANSLATIONS = {
'welcome_admin': "\n\n👑 <b>Режим адміністратора активний</b>",
'access_denied': "⛔ Доступ заборонено. Вас немає в білому списку.",
'help_title': "🤖 <b>Довідка по боту</b>\n\n",
'help_commands': "/start - Підписатися на сповіщення\n/help - Показати це повідомлення\n/language - Змінити мову",
'help_commands': "/start - Підписатися на сповіщення\n/stop - Відписатися від сповіщень\n/help - Показати це повідомлення\n/language - Змінити мову",
'help_admin': "\n<b>Команди адміністратора:</b>\n/adduser [user_id] - Додати користувача до білого списку\n/removeuser [user_id] - Видалити користувача з білого списку\nАбо використовуйте панель нижче для керування налаштуваннями.",
'admin_only': "⛔ Тільки для адміністратора!",
'user_added': "✅ Користувач {user_id} доданий до білого списку",
@@ -131,7 +132,7 @@ TRANSLATIONS = {
'welcome_admin': "\n\n👑 <b>Admin Mode Active</b>",
'access_denied': "⛔ Access denied. You are not on the whitelist.",
'help_title': "🤖 <b>Bot Help</b>\n\n",
'help_commands': "/start - Subscribe to notifications\n/help - Show this message\n/language - Change language",
'help_commands': "/start - Subscribe to notifications\n/stop - Unsubscribe from notifications\n/help - Show this message\n/language - Change language",
'help_admin': "\n<b>Admin Commands:</b>\n/adduser [user_id] - Add user to whitelist\n/removeuser [user_id] - Remove user from whitelist\nOr use the panel below to manage settings.",
'admin_only': "⛔ Admin only!",
'user_added': "✅ User {user_id} added to whitelist",
@@ -212,6 +213,21 @@ def is_whitelisted(user_id: int) -> bool:
return redis_client.sismember(KEY_WHITELIST, str(user_id))
async def is_group_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
"""Check if the user is an administrator in the group."""
user = update.effective_user
chat = update.effective_chat
if chat.type in [ChatType.PRIVATE, "private"]:
return True
try:
member = await context.bot.get_chat_member(chat.id, user.id)
return member.status in [ChatMember.OWNER, ChatMember.ADMINISTRATOR]
except Exception as e:
logger.error(f"Failed to check admin status: {e}")
return False
def get_admin_keyboard(user_id: int):
"""Generate admin panel keyboard."""
whitelist_enabled = redis_client.get(KEY_WHITELIST_ENABLED) != "0"
@@ -230,19 +246,21 @@ def get_admin_keyboard(user_id: int):
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle /start command."""
user = update.effective_user
logger.info(f"User {user.id} ({user.username}) started the bot.")
chat = update.effective_chat
logger.info(f"User {user.id} ({user.username}) started the bot in chat {chat.id} ({chat.type}).")
# Check whitelist - MUST be the user executing the command
if not is_whitelisted(user.id):
await update.message.reply_text(t(user.id, 'access_denied'))
return
# Add to subscribers
redis_client.sadd(KEY_SUBSCRIBERS, user.id)
# Add to subscribers (Chat ID!)
redis_client.sadd(KEY_SUBSCRIBERS, chat.id)
msg = t(user.id, 'welcome', name=user.first_name)
msg = t(chat.id, 'welcome', name=user.first_name)
if user.id == ADMIN_ID:
msg += t(user.id, 'welcome_admin')
if user.id == ADMIN_ID and chat.type == "private":
msg += t(chat.id, 'welcome_admin')
await update.message.reply_text(msg, parse_mode='HTML', reply_markup=get_admin_keyboard(user.id))
else:
await update.message.reply_text(msg, parse_mode='HTML')
@@ -250,19 +268,39 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle /help command."""
user_id = update.effective_user.id
msg = t(user_id, 'help_title') + t(user_id, 'help_commands')
chat_id = update.effective_chat.id
msg = t(chat_id, 'help_title') + t(chat_id, 'help_commands')
if user_id == ADMIN_ID:
msg += t(user_id, 'help_admin')
if user_id == ADMIN_ID and update.effective_chat.type == "private":
msg += t(chat_id, 'help_admin')
await update.message.reply_text(msg, parse_mode='HTML', reply_markup=get_admin_keyboard(user_id))
else:
await update.message.reply_text(msg, parse_mode='HTML')
async def stop_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle /stop command (unsubscribe)."""
user = update.effective_user
chat = update.effective_chat
# Permission check: Whitelisted user OR Group Admin
if not (is_whitelisted(user.id) or await is_group_admin(update, context)):
await update.message.reply_text(t(chat.id, 'access_denied')) # Or specific "admin only" message
return
redis_client.srem(KEY_SUBSCRIBERS, chat.id)
await update.message.reply_text(t(chat.id, 'whitelist_disabled').replace(" whitelist", " notifications").replace("Білий список", "Сповіщення").replace("Белый список", "Уведомления") if chat.id else "Unsubscribed")
async def language_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle /language command."""
user_id = update.effective_user.id
user = update.effective_user
chat = update.effective_chat
# Permission check for groups
if not (is_whitelisted(user.id) or await is_group_admin(update, context)):
return
await update.message.reply_text(
t(user_id, 'select_language'),
t(chat.id, 'select_language'),
parse_mode='HTML',
reply_markup=get_language_keyboard()
)
@@ -544,6 +582,7 @@ async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE):
for sub_id in subscribers:
try:
# Build message in user's language
# sub_id comes from redis set as string, convert to int for translation lookup
webinar_items = "\n\n".join([
t(int(sub_id), 'webinar_item', name=w['name'], url=w['url'])
for w in new_webinars
@@ -578,6 +617,7 @@ def main():
# Handlers
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("stop", stop_command))
app.add_handler(CommandHandler("help", help_command))
app.add_handler(CommandHandler("language", language_command))
app.add_handler(CommandHandler("adduser", add_user))