diff --git a/environment/env.example b/environment/env.example index 034e360..d481f1e 100644 --- a/environment/env.example +++ b/environment/env.example @@ -359,3 +359,5 @@ PLUNDER_COOLDOWN_DURATION= PLUNDER_REPAY_MULTIPLIER= IMPEL_DOWN_BAIL_PER_MINUTE= + +AUTO_DELETE_DURATION_VALUES= \ No newline at end of file diff --git a/resources/Environment.py b/resources/Environment.py index 0a563ff..bfe87aa 100644 --- a/resources/Environment.py +++ b/resources/Environment.py @@ -1026,3 +1026,8 @@ def __str__(self) -> str: # How much is each remaining minute of the sentence for bail in Impel Down. Default: 100k IMPEL_DOWN_BAIL_PER_MINUTE = Environment("IMPEL_DOWN_BAIL_PER_MINUTE", default_value="100000") + +# Values in minute for auto delete. Default: 1|2|5|15|30|60|120|180|360 +AUTO_DELETE_DURATION_VALUES = Environment( + "AUTO_DELETE_DURATION_VALUES", default_value="1|2|5|15|30|60|120|180|360" +) diff --git a/resources/phrases.py b/resources/phrases.py index 233da90..b26063b 100644 --- a/resources/phrases.py +++ b/resources/phrases.py @@ -187,6 +187,7 @@ def surround_with_arrows(text: str) -> str: TEXT_YOU = "You" TEXT_STOLE = "stole" TEXT_OWE = "[owe]({})" +TEXT_NEVER = "Never" TEXT_DAY = "day" TEXT_DAYS = "days" @@ -362,7 +363,8 @@ def surround_with_arrows(text: str) -> str: PVT_KEY_SHOW_ALL = "Back to list" GRP_KEY_DEVIL_FRUIT_BUY = Emoji.MONEY + " Buy" -GRP_KEY_FEATURES = "Features" +GRP_KEY_SETTINGS_FEATURES = "Features" +GRP_KEY_SETTINGS_AUTO_DELETE = "Auto delete" GRP_TXT_FEATURES = "{}Which Bounty System features would you like to enable in this {}?" GRP_KEY_PREDICTION_BET_IN_PRIVATE_CHAT = "Bet in private chat" GRP_KEY_PREDICTION_VIEW_IN_PRIVATE_CHAT = "View in private chat" @@ -2152,3 +2154,8 @@ def surround_with_arrows(text: str) -> str: " prize!_" ) DAILY_REWARD_PRIZE_CONFIRM_BELLY = "Belly amount: ฿*{}*" + +AUTO_DELETE_SET = ( + "After how many minutes should the Bot's messages be deleted from the chat?" + "\n\nCurrent setting: *{}*" +) diff --git a/src/chat/group/group_chat_manager.py b/src/chat/group/group_chat_manager.py index 75e5728..6e02bf7 100644 --- a/src/chat/group/group_chat_manager.py +++ b/src/chat/group/group_chat_manager.py @@ -35,6 +35,9 @@ manage as manage_screen_prediction_bet_status, ) from src.chat.group.screens.screen_settings import manage as manage_screen_settings +from src.chat.group.screens.screen_settings_auto_delete import ( + manage as manage_screen_settings_auto_delete, +) from src.chat.group.screens.screen_settings_features import manage as manage_screen_features from src.chat.group.screens.screen_silence import manage as manage_screen_silence from src.chat.group.screens.screen_silence_end import manage as manage_screen_silence_end @@ -95,7 +98,7 @@ async def manage( if added_to_group: group.is_active = True group.save() - command = Command.GRP_FEATURES + command = Command.GRP_SETTINGS_FEATURES # Insert or update user, with message count try: @@ -211,7 +214,7 @@ async def dispatch_screens( update, context, user, inbound_keyboard, target_user, command, group_chat ) - case Screen.GRP_FEATURES: # Features + case Screen.GRP_SETTINGS_FEATURES: # Features await manage_screen_features( update, context, inbound_keyboard, group_chat, added_to_group ) @@ -238,6 +241,9 @@ async def dispatch_screens( case Screen.GRP_SETTINGS: # Settings await manage_screen_settings(update, context) + case Screen.GRP_SETTINGS_AUTO_DELETE: # Auto delete + await manage_screen_settings_auto_delete(update, context, inbound_keyboard, group_chat) + case _: # Unknown screen if update.callback_query is not None: raise GroupChatException(GroupChatError.UNRECOGNIZED_SCREEN) diff --git a/src/chat/group/screens/screen_settings.py b/src/chat/group/screens/screen_settings.py index 61258ec..4a82139 100644 --- a/src/chat/group/screens/screen_settings.py +++ b/src/chat/group/screens/screen_settings.py @@ -16,7 +16,10 @@ async def manage(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """ inline_keyboard: list[list[Keyboard]] = [ - [Keyboard(phrases.GRP_KEY_FEATURES, screen=Screen.GRP_FEATURES)], + # Features + [Keyboard(phrases.GRP_KEY_SETTINGS_FEATURES, screen=Screen.GRP_SETTINGS_FEATURES)], + # Auto delete + [Keyboard(phrases.GRP_KEY_SETTINGS_AUTO_DELETE, screen=Screen.GRP_SETTINGS_AUTO_DELETE)], ] await full_message_send( @@ -26,4 +29,5 @@ async def manage(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: keyboard=inline_keyboard, add_delete_button=True, use_close_delete=True, + should_auto_delete=False, ) diff --git a/src/chat/group/screens/screen_settings_auto_delete.py b/src/chat/group/screens/screen_settings_auto_delete.py new file mode 100644 index 0000000..5efb294 --- /dev/null +++ b/src/chat/group/screens/screen_settings_auto_delete.py @@ -0,0 +1,66 @@ +from telegram import Update +from telegram.ext import ContextTypes + +import resources.Environment as Env +import resources.phrases as phrases +from src.model.GroupChat import GroupChat +from src.model.enums.Emoji import Emoji +from src.model.enums.ReservedKeyboardKeys import ReservedKeyboardKeys +from src.model.pojo.Keyboard import Keyboard +from src.service.list_service import get_options_keyboard +from src.service.message_service import full_message_send + + +async def manage( + update: Update, + context: ContextTypes.DEFAULT_TYPE, + inbound_keyboard: Keyboard, + group_chat: GroupChat, +) -> None: + """ + Manage the auto delete screen + :param update: The update object + :param context: The context object + :param inbound_keyboard: The inbound keyboard + :param group_chat: The group chat + :return: None + """ + + if ReservedKeyboardKeys.DEFAULT_PRIMARY_KEY in inbound_keyboard.info: + duration = inbound_keyboard.get(ReservedKeyboardKeys.DEFAULT_PRIMARY_KEY) + group_chat.auto_delete_duration = duration + group_chat.save() + + current_value = group_chat.auto_delete_duration + # Create numeric keyboard with all possible values + numeric_keyboard: list[list[Keyboard]] = get_options_keyboard( + inbound_info=inbound_keyboard.info, + values=Env.AUTO_DELETE_DURATION_VALUES.get_list(), + current_selected=current_value, + ) + + # Add "Never" option as first option + numeric_keyboard.insert( + 0, + [ + Keyboard( + (Emoji.RADIO_BUTTON if current_value is None else "") + phrases.TEXT_NEVER, + info={ReservedKeyboardKeys.DEFAULT_PRIMARY_KEY: None}, + inbound_info=inbound_keyboard.info, + ) + ], + ) + + ot_text = phrases.AUTO_DELETE_SET.format( + phrases.TEXT_NEVER if current_value is None else current_value + ) + await full_message_send( + context, + ot_text, + update=update, + keyboard=numeric_keyboard, + add_delete_button=True, + use_close_delete=True, + inbound_keyboard=inbound_keyboard, + should_auto_delete=False, + ) diff --git a/src/chat/group/screens/screen_settings_features.py b/src/chat/group/screens/screen_settings_features.py index 6b9b435..783613e 100644 --- a/src/chat/group/screens/screen_settings_features.py +++ b/src/chat/group/screens/screen_settings_features.py @@ -97,6 +97,7 @@ async def manage( add_delete_button=True, use_close_delete=True, inbound_keyboard=inbound_keyboard, + should_auto_delete=False, ) @@ -138,7 +139,9 @@ def get_features_keyboard(group_chat: GroupChat) -> list[list[Keyboard]]: ReservedKeyboardKeys.TOGGLE: not is_enabled_feature, } button: Keyboard = Keyboard( - f"{emoji} {feature.get_description()}", info=button_info, screen=Screen.GRP_FEATURES + f"{emoji} {feature.get_description()}", + info=button_info, + screen=Screen.GRP_SETTINGS_FEATURES, ) # If feature is pinnable, add button in a new row with the pin toggle button @@ -153,7 +156,9 @@ def get_features_keyboard(group_chat: GroupChat) -> list[list[Keyboard]]: FeaturesReservedKeys.PIN_TOGGLE: not is_enabled_pin, } pin_toggle: Keyboard = Keyboard( - f"{is_enabled_emoji} {Emoji.PIN}", info=pin_button_info, screen=Screen.GRP_FEATURES + f"{is_enabled_emoji} {Emoji.PIN}", + info=pin_button_info, + screen=Screen.GRP_SETTINGS_FEATURES, ) # Relative feature button and pin button in the same new row diff --git a/src/chat/private/screens/screen_crew_davy_back_fight_request.py b/src/chat/private/screens/screen_crew_davy_back_fight_request.py index dd9c0ac..bd8a560 100644 --- a/src/chat/private/screens/screen_crew_davy_back_fight_request.py +++ b/src/chat/private/screens/screen_crew_davy_back_fight_request.py @@ -5,7 +5,6 @@ from telegram.error import TelegramError from telegram.ext import ContextTypes -import constants as c import resources.Environment as Env import resources.phrases as phrases from src.chat.private.screens.screen_crew_davy_back_fight_request_received import accept @@ -24,6 +23,7 @@ convert_minutes_to_duration, datetime_is_before, ) +from src.service.list_service import get_options_keyboard from src.service.message_service import full_message_send, get_yes_no_keyboard, get_deeplink @@ -249,23 +249,14 @@ async def edit_options( raise ValueError() # Create numeric keyboard with all possible values - numeric_keyboard: list[list[Keyboard]] = [] - line_keyboard: list[Keyboard] = [] - for i in range(minimum, maximum + 1): - line_keyboard.append( - Keyboard( - str(i), - info={key: i}, - inbound_info=inbound_keyboard.info, - exclude_key_from_inbound_info=[ReservedKeyboardKeys.SCREEN_STEP_NO_INPUT], - ) - ) - if len(line_keyboard) == c.STANDARD_LIST_KEYBOARD_ROW_SIZE: - numeric_keyboard.append(line_keyboard) - line_keyboard = [] - - if len(line_keyboard) > 0: - numeric_keyboard.append(line_keyboard) + numeric_keyboard: list[list[Keyboard]] = get_options_keyboard( + primary_key=key, + inbound_info=inbound_keyboard.info, + exclude_key_from_inbound_info=[ReservedKeyboardKeys.SCREEN_STEP_NO_INPUT], + generate_numbers=True, + start_number=minimum, + end_number=maximum, + ) user.private_screen_stay = True await full_message_send( diff --git a/src/model/GroupChat.py b/src/model/GroupChat.py index f97809d..2157cff 100644 --- a/src/model/GroupChat.py +++ b/src/model/GroupChat.py @@ -22,6 +22,7 @@ class GroupChat(BaseModel): last_error_message = CharField(null=True) is_active = BooleanField(default=True) is_muted = BooleanField(default=False) + auto_delete_duration = IntegerField(null=True) # In minutes # Backref enabled_features = None diff --git a/src/model/GroupChatAutoDelete.py b/src/model/GroupChatAutoDelete.py new file mode 100644 index 0000000..3698fcd --- /dev/null +++ b/src/model/GroupChatAutoDelete.py @@ -0,0 +1,26 @@ +import datetime + +from peewee import * + +from src.model.BaseModel import BaseModel +from src.model.GroupChat import GroupChat + + +class GroupChatAutoDelete(BaseModel): + """ + Group Chat Auto Delete class + """ + + id: int | PrimaryKeyField = PrimaryKeyField() + group_chat: GroupChat | ForeignKeyField = ForeignKeyField( + GroupChat, backref="auto_delete", on_delete="CASCADE" + ) + message_id: int | IntegerField = IntegerField() + date: datetime.datetime | DateTimeField = DateTimeField(default=datetime.datetime.now) + delete_date: datetime.datetime | DateTimeField = DateTimeField() + + class Meta: + db_table = "group_chat_auto_delete" + + +GroupChatAutoDelete.create_table() diff --git a/src/model/enums/Command.py b/src/model/enums/Command.py index 9800fff..aaae362 100644 --- a/src/model/enums/Command.py +++ b/src/model/enums/Command.py @@ -348,9 +348,9 @@ def __eq__(self, other): ) COMMANDS.append(GRP_SETTINGS) -GRP_FEATURES = Command( +GRP_SETTINGS_FEATURES = Command( CommandName.EMPTY, - Screen.GRP_FEATURES, + Screen.GRP_SETTINGS_FEATURES, allow_while_arrested=True, only_by_chat_admin=True, answer_callback=True, diff --git a/src/model/enums/Screen.py b/src/model/enums/Screen.py index f56245a..a7787ba 100644 --- a/src/model/enums/Screen.py +++ b/src/model/enums/Screen.py @@ -23,13 +23,14 @@ class Screen(StrEnum): GRP_SPEAK = "G17" GRP_BOUNTY_GIFT = "G18" GRP_DEVIL_FRUIT_COLLECT = "G19" # Deprecated - GRP_FEATURES = "G20" + GRP_SETTINGS_FEATURES = "G20" GRP_DEVIL_FRUIT_SELL = "G21" GRP_BOUNTY_LOAN = "G22" GRP_PLUNDER = "G23" GRP_DAILY_REWARD = "G24" GRP_DAILY_REWARD_PRIZE = "G25" GRP_SETTINGS = "G26" + GRP_SETTINGS_AUTO_DELETE = "G27" PVT_START = "P1" PVT_SETTINGS = "P2" diff --git a/src/service/generic_service.py b/src/service/generic_service.py index fe0487e..c9e2eb4 100644 --- a/src/service/generic_service.py +++ b/src/service/generic_service.py @@ -3,6 +3,7 @@ from src.model.DavyBackFight import DavyBackFight from src.service.crew_service import end_all_conscription from src.service.davy_back_fight_service import start_all as start_dbf, end_all as end_dbf +from src.service.group_service import auto_delete async def run_minute_tasks(context: ContextTypes.DEFAULT_TYPE) -> None: @@ -23,3 +24,6 @@ async def run_minute_tasks(context: ContextTypes.DEFAULT_TYPE) -> None: # End all Crew conscription context.application.create_task(end_all_conscription(context)) + + # Auto delete messages + context.application.create_task(auto_delete(context)) diff --git a/src/service/group_service.py b/src/service/group_service.py index 422fb3a..2b1350e 100644 --- a/src/service/group_service.py +++ b/src/service/group_service.py @@ -9,6 +9,7 @@ from src.model.BaseModel import BaseModel from src.model.Group import Group from src.model.GroupChat import GroupChat +from src.model.GroupChatAutoDelete import GroupChatAutoDelete from src.model.GroupChatDisabledFeature import GroupChatDisabledFeature from src.model.GroupChatEnabledFeaturePin import GroupChatEnabledFeaturePin from src.model.GroupChatFeaturePinMessage import GroupChatFeaturePinMessage @@ -323,3 +324,39 @@ def save_group_chat_error(group_chat: GroupChat, error: str) -> None: group.last_error_date = datetime.now() group.last_error_message = error group.save() + + +async def auto_delete(context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Auto delete messages + :param context: The context + """ + + # Get maximum 20 messages to auto delete + auto_deletes: list[GroupChatAutoDelete] = ( + GroupChatAutoDelete.select() + .where(GroupChatAutoDelete.delete_date < datetime.now()) + .limit(20) + ) + + for auto_delete_item in auto_deletes: + context.application.create_task(auto_delete_process(context, auto_delete_item)) + + +async def auto_delete_process( + context: ContextTypes.DEFAULT_TYPE, auto_delete_item: GroupChatAutoDelete +) -> None: + """ + Auto delete process + :param context: The context + :param auto_delete_item: The message + """ + + group_chat: GroupChat = auto_delete_item.group_chat + group: Group = group_chat.group + try: + await context.bot.delete_message(group.tg_group_id, auto_delete_item.message_id) + except TelegramError as te: + save_group_chat_error(group_chat, str(te)) + + auto_delete_item.delete_instance() diff --git a/src/service/list_service.py b/src/service/list_service.py index 244c8aa..e2380b7 100644 --- a/src/service/list_service.py +++ b/src/service/list_service.py @@ -8,6 +8,7 @@ from src.model.BaseModel import BaseModel from src.model.User import User from src.model.enums.ContextDataKey import ContextDataKey +from src.model.enums.Emoji import Emoji from src.model.enums.ListPage import ListPage, ListFilter, ListFilterType from src.model.enums.ReservedKeyboardKeys import ReservedKeyboardKeys from src.model.enums.Screen import Screen @@ -407,3 +408,65 @@ def get_show_list_button(inbound_keyboard: Keyboard) -> Keyboard: phrases.PVT_KEY_SHOW_ALL, inbound_info=inbound_keyboard.info, ) + + +def get_options_keyboard( + primary_key: str = ReservedKeyboardKeys.DEFAULT_PRIMARY_KEY, + inbound_info: dict = None, + exclude_key_from_inbound_info: list[str] = None, + row_size: int = c.STANDARD_LIST_KEYBOARD_ROW_SIZE, + values: list[str | int] = None, + generate_numbers: bool = False, + start_number: int = None, + end_number: int = None, + is_numeric_values: int = True, + current_selected: str | int = None, +) -> list[list[Keyboard]]: + """ + Get the options keyboard + :param primary_key: The primary key + :param inbound_info: The inbound info + :param exclude_key_from_inbound_info: The exclude key from inbound info + :param row_size: The row size + :param values: The values + :param generate_numbers: Whether to generate numbers + :param start_number: The start number + :param end_number: The end number + :param is_numeric_values: Whether the values are numeric + :param current_selected: The current selected value + :return: The numeric keyboard + """ + + if generate_numbers: + if start_number is None: + start_number = 1 + + if end_number is None: + raise ValueError("End number must be set if auto generating numbers") + + is_numeric_values = True + values = list(range(start_number, end_number + 1)) + + numeric_keyboard: list[list[Keyboard]] = [] + line_keyboard: list[Keyboard] = [] + for i in values: + button_info = {primary_key: int(i) if is_numeric_values else i} + text = str(i) + if current_selected is not None and str(i) == str(current_selected): + text = f"{Emoji.RADIO_BUTTON}{text}" + line_keyboard.append( + Keyboard( + text, + info=button_info, + inbound_info=inbound_info, + exclude_key_from_inbound_info=exclude_key_from_inbound_info, + ) + ) + if len(line_keyboard) == row_size: + numeric_keyboard.append(line_keyboard) + line_keyboard = [] + + if len(line_keyboard) > 0: + numeric_keyboard.append(line_keyboard) + + return numeric_keyboard diff --git a/src/service/message_service.py b/src/service/message_service.py index eb5d849..022cc31 100644 --- a/src/service/message_service.py +++ b/src/service/message_service.py @@ -2,6 +2,7 @@ import json import logging import re +import traceback from uuid import uuid4 from telegram import ( @@ -27,6 +28,7 @@ import resources.phrases as phrases from src.model.Group import Group from src.model.GroupChat import GroupChat +from src.model.GroupChatAutoDelete import GroupChatAutoDelete from src.model.User import User from src.model.enums.ContextDataKey import ContextDataKey from src.model.enums.MessageSource import MessageSource @@ -344,6 +346,7 @@ async def full_message_send( user: User = None, from_exception: bool = False, add_back_button: bool = True, + should_auto_delete: bool = True, ) -> Message | bool: """ Send a message @@ -387,9 +390,11 @@ async def full_message_send( :param user: User object :param from_exception: True if the message is sent from an exception :param add_back_button: True if the back button should be added to the keyboard if possible + :param should_auto_delete: True if the message should be auto deleted :return: Message """ + message_source: MessageSource = get_message_source(update) if show_alert: answer_callback = True @@ -407,6 +412,15 @@ async def full_message_send( topic_id = group_chat.tg_topic_id group: Group = group_chat.group chat_id = group.tg_group_id + elif message_source is MessageSource.GROUP: + group_chat = get_group_chat_for_auto_delete(update) + + should_auto_delete = ( + should_auto_delete + and add_delete_button + and message_source is message_source.GROUP + and group_chat + ) if previous_screens is not None and (inbound_keyboard is None or from_exception): inbound_keyboard = Keyboard( @@ -461,6 +475,11 @@ async def full_message_send( protect_content=protect_content, message_thread_id=topic_id, ) + + # Enqueue for auto deletion + if should_auto_delete: + context.application.create_task(enqueue_message_auto_delete(group_chat, message)) + return message except TelegramError as e: if ignore_exception: @@ -484,7 +503,7 @@ async def full_message_send( if edit_message_id is not None else update.callback_query.message.message_id ) - return await context.bot.edit_message_text( + message: Message = await context.bot.edit_message_text( text=text, chat_id=chat_id, reply_markup=keyboard_markup, @@ -493,6 +512,11 @@ async def full_message_send( message_id=edit_message_id, ) + if should_auto_delete: + context.application.create_task(enqueue_message_auto_delete(group_chat, message)) + + return message + def get_input_media_from_saved_media( saved_media: SavedMedia, caption: str = None, parse_mode: str = c.TG_DEFAULT_PARSE_MODE @@ -1400,3 +1424,72 @@ def get_message_url(message_id: int, group_chat: GroupChat = None, chat_id: str url = url.replace("/c/", "/") return url + + +def get_group_chat_from_update(update: Update) -> GroupChat | None: + """ + Gets the group chat from the update + :param update: The update + :return: The group chat + """ + + group = Group.get_or_none(Group.tg_group_id == update.effective_chat.id) + if group is None: + return + + tg_topic_id = None + if update.effective_chat.is_forum and update.effective_message.is_topic_message: + tg_topic_id = update.effective_message.message_thread_id + + group_chat = GroupChat.get_or_none( + (GroupChat.group == group) & (GroupChat.tg_topic_id == tg_topic_id) + ) + + return group_chat + + +def get_group_chat_for_auto_delete( + update: Update, group_chat: GroupChat = None +) -> GroupChat | None: + """ + Gets the group chat for auto delete + :param update: The update + :param group_chat: The group chat + :return: The group chat + """ + + if group_chat is not None: + return group_chat + + if update is None: + logging.warning("Cannot add delete button without an update or group chat object") + logging.warning(traceback.format_stack()) + return + + if not get_message_source(update) is MessageSource.GROUP: + return + + group_chat = get_group_chat_from_update(update) + + if group_chat is None: + raise ValueError("Group chat not found") + + return group_chat + + +async def enqueue_message_auto_delete(group_chat: GroupChat, message: Message): + """ + Enqueue a message for auto delete + :param group_chat: The group chat + :param message: The message + """ + from src.service.date_service import get_datetime_in_future_minutes + + if group_chat.auto_delete_duration is None: + return + + auto_delete: GroupChatAutoDelete = GroupChatAutoDelete() + auto_delete.group_chat = group_chat + auto_delete.message_id = message.message_id + auto_delete.delete_date = get_datetime_in_future_minutes(group_chat.auto_delete_duration) + auto_delete.save()