diff --git a/.gitignore b/.gitignore
index 41f6e38..49b651c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,9 @@
sync.ffs_lock
.sync.ffs_db
+# Copyparty
+*.hist/
+
# Volumes and data directories
gitea/gitea-db/
gitea/gitea-data/*
@@ -88,4 +91,4 @@ temp/*
.env.anna
.env.forust
.env.*
-!*example
+!*example
\ No newline at end of file
diff --git a/edu_master/webinar-checker/checker.py b/edu_master/webinar-checker/checker.py
index 2efd86a..60c3dbb 100644
--- a/edu_master/webinar-checker/checker.py
+++ b/edu_master/webinar-checker/checker.py
@@ -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👑 Режим администратора активен",
'access_denied': "⛔ Доступ запрещен. Вас нет в белом списке.",
'help_title': "🤖 Помощь по боту\n\n",
- 'help_commands': "/start - Подписаться на уведомления\n/help - Показать это сообщение\n/language - Сменить язык",
+ 'help_commands': "/start - Подписаться на уведомления\n/stop - Отписаться от уведомлений\n/help - Показать это сообщение\n/language - Сменить язык",
'help_admin': "\nКоманды администратора:\n/adduser [user_id] - Добавить пользователя в белый список\n/removeuser [user_id] - Удалить пользователя из белого списка\nИли используйте панель ниже для управления настройками.",
'admin_only': "⛔ Только для администратора!",
'user_added': "✅ Пользователь {user_id} добавлен в белый список",
@@ -94,7 +95,7 @@ TRANSLATIONS = {
'welcome_admin': "\n\n👑 Режим адміністратора активний",
'access_denied': "⛔ Доступ заборонено. Вас немає в білому списку.",
'help_title': "🤖 Довідка по боту\n\n",
- 'help_commands': "/start - Підписатися на сповіщення\n/help - Показати це повідомлення\n/language - Змінити мову",
+ 'help_commands': "/start - Підписатися на сповіщення\n/stop - Відписатися від сповіщень\n/help - Показати це повідомлення\n/language - Змінити мову",
'help_admin': "\nКоманди адміністратора:\n/adduser [user_id] - Додати користувача до білого списку\n/removeuser [user_id] - Видалити користувача з білого списку\nАбо використовуйте панель нижче для керування налаштуваннями.",
'admin_only': "⛔ Тільки для адміністратора!",
'user_added': "✅ Користувач {user_id} доданий до білого списку",
@@ -131,7 +132,7 @@ TRANSLATIONS = {
'welcome_admin': "\n\n👑 Admin Mode Active",
'access_denied': "⛔ Access denied. You are not on the whitelist.",
'help_title': "🤖 Bot Help\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': "\nAdmin Commands:\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))