diff --git a/.gitignore b/.gitignore index 5c10031..99db0bf 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ __pycache__/ # telegram bot token config telegram_bot_token.cfg +logs/ diff --git a/commands_admin.py b/commands_admin.py deleted file mode 100644 index ee387a4..0000000 --- a/commands_admin.py +++ /dev/null @@ -1,20 +0,0 @@ -from telegram import Bot, InlineKeyboardButton, InlineKeyboardMarkup, Update -from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes -import configparser -import json -import os -import datetime -import shutil - -# Function to send a message with inline buttons -async def newEvent(update: Update, context: ContextTypes.DEFAULT_TYPE): - keyboard = [ - [InlineKeyboardButton("Anmelden", callback_data='register')], - [InlineKeyboardButton("Abmelden", callback_data='cancelRegister')] - ] - - reply_markup = InlineKeyboardMarkup(keyboard) - - await update.message.reply_text("choose below", reply_markup=reply_markup) - return - diff --git a/commands_user.py b/commands_user.py deleted file mode 100644 index 7d191dc..0000000 --- a/commands_user.py +++ /dev/null @@ -1,29 +0,0 @@ -from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update -from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes, Updater, CallbackQueryHandler, CallbackContext - - -# Function to handle button clicks (callback queries) -async def button(update: Update, context: ContextTypes.DEFAULT_TYPE): - query = update.callback_query - - user = query.from_user - - await query.answer() # Acknowledge the button press - - if query.data == 'register': - try: - keyboard = [[InlineKeyboardButton("Abmelden", callback_data='cancelRegister')]] - reply_markup = InlineKeyboardMarkup(keyboard) - await context.bot.send_message(user.id, "Registered", reply_markup=reply_markup) - except Exception as e: - await context.bot.send_message(query.message.chat_id, f"@{user.username} Leider kam es zu einem Fehler: {str(e)}") - - if query.data == 'cancelRegister': - try: - await context.bot.send_message(user.id, "Cancelled") - except Exception as e: - await context.bot.send_message(query.message.chat_id, f"@{user.username} Leider kam es zu einem Fehler: {str(e)}") - - return - - diff --git a/config/config.py b/config/config.py new file mode 100644 index 0000000..78f212b --- /dev/null +++ b/config/config.py @@ -0,0 +1,40 @@ +import os + + + + +# -------------------------------------------# +# DO NOT TOUCH - will be adjusted on runtime # +# -------------------------------------------# + +BASE_DIR = dir_path = os.path.dirname(os.path.realpath(__file__)) + +# TODO: Avoid Race Conditions on event file edits (file locks?) + + + + +# -------------------------------------------# +# Configuration Settings # +# -------------------------------------------# + +# General +BOT_NAME = "PawHubBot" + +# Logging +LOG_FOLDER_PATH = os.path.abspath(os.path.join(BASE_DIR, '..', 'logs')) +LOG_FILE_NAME = os.path.join(LOG_FOLDER_PATH, 'log.txt') + +# Administration +ADMIN_IDS = [ + 1903773270, # SinusFox + 5781850368, # Karatarus + 30849386 # Goldwolf +] + +# Chats +ALLOWED_CHAT_IDS = [0] +ALLOW_DMS = True + +# Events +EVENTS_FOLDER = os.path.join(BASE_DIR, '..','events') diff --git a/config/telegram_bot_token.cfg b/config/telegram_bot_token.cfg new file mode 100644 index 0000000..2e7da25 --- /dev/null +++ b/config/telegram_bot_token.cfg @@ -0,0 +1,2 @@ +[telegram] +bot_token = MISSING_TOKEN diff --git a/direct_message_commands.py b/direct_message_commands.py new file mode 100644 index 0000000..b56c01c --- /dev/null +++ b/direct_message_commands.py @@ -0,0 +1,26 @@ + +import traceback +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update +from telegram.ext import ContextTypes +from config import config +import user_permissions +import log + +async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): + if not user_permissions.is_user_admin(update.effective_user.id): + log.warning(chat_id=update.effective_chat.id, message=f"Unauthorized direct message access attempt by user {update.effective_user.id}\n{traceback.format_exc()}") + await update.message.reply_text("You are not authorized to use this bot in this chat.") + return + + keyboard = [ + [InlineKeyboardButton("Eventmanagement", callback_data='list_event_actions')], + ] + + await update.message.reply_text( + f"""Willkommen beim {config.BOT_NAME} Admin Interface! +Hier kannst du verschiedene Verwaltungsaufgaben durchführen. Nutze die verfügbaren Befehle, um loszulegen. + +(c) SinusFox.dev 2025 + """, + reply_markup=InlineKeyboardMarkup(keyboard) + ) \ No newline at end of file diff --git a/event_management.py b/event_management.py new file mode 100644 index 0000000..6c4a86c --- /dev/null +++ b/event_management.py @@ -0,0 +1,64 @@ +import os +from config import config +import log +from telegram import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Update +from telegram.ext import ContextTypes +import user_permissions +import traceback + +os.makedirs(config.EVENTS_FOLDER, exist_ok=True) + +async def list_event_actions(update: Update, context: ContextTypes.DEFAULT_TYPE, query: CallbackQuery): + if not user_permissions.is_user_admin(update.effective_user.id): + log.warning(chat_id=update.effective_chat.id, message=f"Unauthorized direct message access attempt by user {update.effective_user.id}\n{traceback.format_exc()}") + await update.message.reply_text("You are not authorized to use this bot in this chat.") + return + + keyboard = [ + [InlineKeyboardButton("Neues Event anlegen", callback_data='new_event')], + [InlineKeyboardButton("Event bearbeiten", callback_data='edit_event')], + [InlineKeyboardButton("Event löschen", callback_data='delete_event')] + ] + + await query.message.reply_text( + f'Was möchtest du machen?', + reply_markup=InlineKeyboardMarkup(keyboard) + ) + +async def new_event(update: Update, context: ContextTypes.DEFAULT_TYPE, query: CallbackQuery): + if not user_permissions.is_user_admin(update.effective_user.id): + log.warning(chat_id=update.effective_chat.id, message=f"Unauthorized direct message access attempt by user {update.effective_user.id}\n{traceback.format_exc()}") + await update.message.reply_text("You are not authorized to use this bot in this chat.") + return + + keyboard = [ + [InlineKeyboardButton("Anmelden", callback_data='register')], + [InlineKeyboardButton("Abmelden", callback_data='cancelRegister')] + ] + + await query.message.reply_text( + f'Welches Event möchtest du anpassen?', + reply_markup=InlineKeyboardMarkup(keyboard) + ) + +async def edit_event(update: Update, context: ContextTypes.DEFAULT_TYPE, query: CallbackQuery): + if not user_permissions.is_user_admin(update.effective_user.id): + log.warning(chat_id=update.effective_chat.id, message=f"Unauthorized direct message access attempt by user {update.effective_user.id}\n{traceback.format_exc()}") + await update.message.reply_text("You are not authorized to use this bot in this chat.") + return + + keyboard = [ + [InlineKeyboardButton("Anmelden", callback_data='register')], + [InlineKeyboardButton("Abmelden", callback_data='cancelRegister')] + ] + + await query.message.reply_text( + f'Welches Event möchtest du anpassen?', + reply_markup=InlineKeyboardMarkup(keyboard) + ) + + # TODO: Implement edit event logic + +async def delete_event(update: Update, context: ContextTypes.DEFAULT_TYPE, query: CallbackQuery): + # TODO + pass diff --git a/events/template.json b/events/template.json new file mode 100644 index 0000000..b0798a6 --- /dev/null +++ b/events/template.json @@ -0,0 +1,28 @@ +[ + { + "date": "", + "time": "", + "custom_message": "", + "signup_deadline": "", + "ordered_food_change_deadline": "", + "ordered_food_change_closed": false, + "location": "", + "organizer_id": 0, + "attendees": [ + { + "is_guest": false, + "invited_by_user_id": 0, + "user_id_or_guest_id": 0, + "guest_name": "", + "signed_up": true, + "food_ordered": [ + { + "food_item": "", + "price": 0.0, + "quantity": 0 + } + ] + } + ] + } +] \ No newline at end of file diff --git a/food_management.py b/food_management.py new file mode 100644 index 0000000..47b3b85 --- /dev/null +++ b/food_management.py @@ -0,0 +1,2 @@ +# TODO +# add and remove foods, custom foods, automatic sum of prices, save to event \ No newline at end of file diff --git a/group_message_commands.py b/group_message_commands.py new file mode 100644 index 0000000..e30a793 --- /dev/null +++ b/group_message_commands.py @@ -0,0 +1,2 @@ +# TODO +# e.g. Nutzer nachtraeglich hinzufuegen? ODer wegen Privatsphaere in dms? \ No newline at end of file diff --git a/handler_participant_lists.py b/handler_participant_lists.py deleted file mode 100644 index c64ef93..0000000 --- a/handler_participant_lists.py +++ /dev/null @@ -1,6 +0,0 @@ -from telegram import Update -from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes -import configparser -import json -import os - diff --git a/log.py b/log.py index c64ef93..9e2b85c 100644 --- a/log.py +++ b/log.py @@ -1,6 +1,29 @@ -from telegram import Update -from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes -import configparser -import json import os +from config import config +from datetime import datetime +LOG_ID_INFO = 'I' +LOG_ID_WARNING = 'W' +LOG_ID_ERROR = 'E' + +def info(chat_id: str, message: str) -> None: + log_line = format_message(LOG_ID_INFO, chat_id, message) + write_log(log_line) + +def warning(chat_id: str, message: str) -> None: + log_line = format_message(LOG_ID_WARNING, chat_id, message) + write_log(log_line) + +def error(chat_id: str, message: str, stack_trace: str) -> None: + log_line = format_message(LOG_ID_ERROR, chat_id, message) + log_line += f'\nStack Trace:\n{stack_trace}' + write_log(log_line) + +def format_message (log_id: str, chat_id: str, message: str) -> str: + return f'{datetime.now().strftime("%Y-%m-%d @ %H:%M")} | {config.BOT_NAME} | {log_id} | {message}' + +def write_log(log_line: str): + os.makedirs(config.LOG_FOLDER_PATH, exist_ok=True) + with open(os.path.join(config.LOG_FOLDER_PATH, config.LOG_FILE_NAME), 'a', encoding='utf-8') as log_file: + log_file.write(log_line + '\n') + print(log_line) \ No newline at end of file diff --git a/pawhub-bot.py b/pawhub-bot.py index bbfc109..632817c 100644 --- a/pawhub-bot.py +++ b/pawhub-bot.py @@ -1,43 +1,65 @@ -# [ IMPORTS ] # -from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +import signal +import sys +import traceback +from telegram import Update from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes, CallbackQueryHandler, Application import configparser -import json -import os - -# other files in here +import direct_message_commands +import event_management +import log from errors import * -from commands_admin import * -from commands_user import * -from handler_participant_lists import * from log import * -############################################# -# [ CONFIG FILES ] # - -# Konfiguration config = configparser.ConfigParser() -config.read('telegram_bot_token.cfg') -config.read('telegram_bot_config.cfg') +config.read('config/telegram_bot_token.cfg') print("Configs loaded.") -############################################# -# [ MAIN LOOP ] # +def handle_signal(signal, frame): + log.info(chat_id="system", message="Shutdown command received. Stopping service...") + sys.exit(0) + +signal.signal(signal.SIGTERM, handle_signal) +signal.signal(signal.SIGINT, handle_signal) + +async def button_callback_query(update: Update, context: ContextTypes.DEFAULT_TYPE): + query = update.callback_query + await query.answer() + + try: + # Event Management + if query.data == "list_event_actions": + await event_management.list_event_actions(update, context, query) + return + if query.data == "edit_event": + await event_management.edit_event(update, context, query) + return + if query.data == "delete_event": + await event_management.delete_event(update, context, query) + return + if query.data == "new_event": + await event_management.new_event(update, context, query) + return + except Exception as e: + log.error(chat_id=update.effective_chat.id, message=f"Error handling button callback query: {e}", stack_trace=traceback.format_exc()) + await update.effective_message.reply_text(f'Leider gab es einen Fehler. Bitte melde die Uhrzeit bei den Admins: {datetime.now().strftime("%Y-%m-%d @ %H:%M")}.') def main(): + log.info(chat_id="system", message="Registering bot commands and starting service...") app = ApplicationBuilder().token(config['telegram']['bot_token']).build() - # Admin commands - app.add_handler(CommandHandler("newEvent", newEvent)) + # DM commands + app.add_handler(CommandHandler("start", direct_message_commands.start)) - # User commands - # app.add_handler(CommandHandler("start", start)) + # Event management + app.add_handler(CommandHandler("newEvent", event_management.new_event)) # buttons - app.add_handler(CallbackQueryHandler(button)) + app.add_handler(CallbackQueryHandler(button_callback_query)) - print("Bot läuft...") + log.info(chat_id="system", message="Service started") app.run_polling() if __name__ == "__main__": + log.info(chat_id="system", message="Starting service...") main() + log.info(chat_id="system", message="Service stopped.") diff --git a/telegram_bot_config.cfg b/telegram_bot_config.cfg deleted file mode 100644 index 467c39f..0000000 --- a/telegram_bot_config.cfg +++ /dev/null @@ -1,9 +0,0 @@ -[admin] -ids = [CHANGE_ME, CHANGE_ME] - -[chats] -# allowed_chat_ids = [CHANGEME] -# allow_dms_if_not_in_allowed_chats = True - -[list] -default = event_list.json diff --git a/telegram_bot_token.cfg b/telegram_bot_token.cfg deleted file mode 100644 index 458c0e3..0000000 --- a/telegram_bot_token.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[telegram] -bot_token = BOT_TOKEN_HERE diff --git a/user_management.py b/user_management.py new file mode 100644 index 0000000..d5fbed1 --- /dev/null +++ b/user_management.py @@ -0,0 +1,2 @@ +# TODO +# Add and remove users from event, but only set a flag configuring them as "attending" or "not attending" when "deletion" \ No newline at end of file diff --git a/user_permissions.py b/user_permissions.py new file mode 100644 index 0000000..83cdebb --- /dev/null +++ b/user_permissions.py @@ -0,0 +1,7 @@ +from config import config + +def is_user_admin(user_id: int) -> bool: + return user_id in config.ADMIN_IDS + +def is_chat_allowed(chat_id: int) -> bool: + return chat_id in config.ALLOWED_CHAT_IDS