├── telegram_periodic_msg_bot ├── bot │ ├── __init__.py │ ├── bot_handlers_config_typing.py │ ├── bot_config_types.py │ ├── bot_base.py │ ├── bot_config.py │ └── bot_handlers_config.py ├── misc │ ├── __init__.py │ ├── helpers.py │ └── chat_members.py ├── utils │ ├── __init__.py │ ├── utils.py │ ├── key_value_converter.py │ ├── wrapped_list.py │ └── pyrogram_wrapper.py ├── command │ ├── __init__.py │ ├── command_data.py │ ├── command_dispatcher.py │ ├── command_base.py │ └── commands.py ├── config │ ├── __init__.py │ ├── config_typing.py │ ├── config_loader_ex.py │ ├── config_file_sections_loader.py │ ├── config_sections_loader.py │ ├── config_object.py │ └── config_section_loader.py ├── logger │ ├── __init__.py │ └── logger.py ├── message │ ├── __init__.py │ ├── message_deleter.py │ ├── message_sender.py │ └── message_dispatcher.py ├── translator │ ├── __init__.py │ └── translation_loader.py ├── periodic_msg │ ├── __init__.py │ ├── periodic_msg_parser.py │ ├── periodic_msg_sender.py │ ├── periodic_msg_job.py │ └── periodic_msg_scheduler.py ├── _version.py ├── __init__.py ├── periodic_msg_bot.py └── lang │ └── lang_en.xml ├── MANIFEST.in ├── requirements-dev.txt ├── requirements.txt ├── .gitignore ├── .github └── workflows │ ├── code-analysis.yml │ └── build.yml ├── app ├── conf │ └── config.ini ├── bot.py └── lang │ └── lang_it.xml ├── LICENSE ├── CHANGELOG.md ├── pyproject.toml ├── pyproject_legacy.toml └── README.md /telegram_periodic_msg_bot/bot/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/misc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include pyproject_legacy.toml 2 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/command/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/logger/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/message/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/translator/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | mypy>=0.900 2 | ruff>=0.1 3 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/periodic_msg/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/_version.py: -------------------------------------------------------------------------------- 1 | __version__: str = "0.3.5" 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyrotgfork 2 | pytgcrypto 3 | apscheduler 4 | defusedxml 5 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Imports 3 | # 4 | from telegram_periodic_msg_bot._version import __version__ 5 | from telegram_periodic_msg_bot.periodic_msg_bot import PeriodicMsgBot 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled 2 | *~ 3 | *.pyc 4 | *.pyo 5 | __pycache__/ 6 | 7 | # Distribution 8 | build/ 9 | dist/ 10 | eggs/ 11 | .eggs/ 12 | sdist/ 13 | *.egg-info/ 14 | *.egg 15 | telegram_periodic_msg_bot-*/ 16 | 17 | # Unit test / Coverage reports 18 | htmlcov 19 | .coverage 20 | .coverage.* 21 | .pytest_cache/ 22 | .tox/ 23 | 24 | # Environments 25 | .idea 26 | .env 27 | .venv 28 | env/ 29 | venv/ 30 | 31 | # mypy 32 | .mypy_cache/ 33 | 34 | # Ruff 35 | .ruff_cache/ 36 | 37 | # Additional 38 | *.log 39 | *.session 40 | *.session-journal 41 | -------------------------------------------------------------------------------- /.github/workflows/code-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies and run code analysis tools 2 | 3 | name: Code Analysis 4 | 5 | on: 6 | push: 7 | branches: [ "master" ] 8 | paths-ignore: 9 | - '*.md' 10 | pull_request: 11 | branches: [ "master" ] 12 | paths-ignore: 13 | - '*.md' 14 | 15 | jobs: 16 | code_analysis: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Set up Python 3.13 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: "3.13" 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -r requirements.txt 28 | pip install -r requirements-dev.txt 29 | - name: Run code analysis 30 | run: | 31 | # Run mypy 32 | mypy . 33 | # Run ruff 34 | ruff check . 35 | -------------------------------------------------------------------------------- /app/conf/config.ini: -------------------------------------------------------------------------------- 1 | # Configuration for Pyrogram 2 | [pyrogram] 3 | session_name = crypto_price_bot_client 4 | api_id = 0000000 5 | api_hash = 00000000000000000000000000000000 6 | bot_token = 0000000000:AAAAAAAAAAAA-0000000000000000000000 7 | 8 | # App configuration 9 | [app] 10 | app_test_mode = False 11 | # Example with custom translation 12 | #app_lang_file = lang/lang_it.xml 13 | 14 | # Task configuration 15 | [task] 16 | tasks_max_num = 10 17 | 18 | # Message configuration 19 | [message] 20 | message_max_len = 4000 21 | 22 | # Configuration for logging 23 | [logging] 24 | log_level = INFO 25 | log_console_enabled = True 26 | log_file_enabled = True 27 | log_file_name = logs/payment_bot.log 28 | log_file_use_rotating = True 29 | log_file_max_bytes = 5242880 30 | log_file_backup_cnt = 10 31 | 32 | # Only if log file rotating is not used 33 | #log_file_append = False 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Emanuele Bellocchia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/config/config_typing.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | from typing import Any, Dict, List 25 | 26 | 27 | # 28 | # Types 29 | # 30 | ConfigFieldType = Dict[str, Any] 31 | ConfigSectionType = List[ConfigFieldType] 32 | ConfigSectionsType = Dict[str, ConfigSectionType] 33 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/config/config_loader_ex.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Classes 23 | # 24 | 25 | # Configuration field not existent error class 26 | class ConfigFieldNotExistentError(Exception): 27 | pass 28 | 29 | 30 | # Configuration field value error class 31 | class ConfigFieldValueError(Exception): 32 | pass 33 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.3.5 2 | 3 | - Use `pyrotgfork`, since `pyrogram` was archived 4 | 5 | # 0.3.4 6 | 7 | - Update Python versions 8 | 9 | # 0.3.3 10 | 11 | - Fix replying to commands in topics 12 | 13 | # 0.3.2 14 | 15 | - Fix usage in channels 16 | 17 | # 0.3.1 18 | 19 | - Fix some _mypy_ and _prospector_ warnings 20 | - Add configuration for _isort_ and run it on project 21 | 22 | # 0.3.0 23 | 24 | - Add support for _pyrogram_ version 2 (version 1 still supported) 25 | 26 | # 0.2.4 27 | 28 | - Bot can now work in channels 29 | 30 | # 0.2.3 31 | 32 | - Handle anonymous user case when executing a command 33 | 34 | # 0.2.2 35 | 36 | - Add command for showing bot version 37 | 38 | # 0.2.1 39 | 40 | - Project re-organized into folders 41 | 42 | # 0.2.0 43 | 44 | - Add possibility to specify a starting hour for message tasks 45 | 46 | # 0.1.5 47 | 48 | - Add single handlers for message updates, to avoid being notified of each single message sent in groups 49 | 50 | # 0.1.4 51 | 52 | - Rename commands by adding the `msgbot_` prefix, to avoid conflicts with other bots 53 | 54 | # 0.1.3 55 | 56 | - Add configuration files for _flake8_ and prospector 57 | - Fix all _flake8_ warnings 58 | - Fix the vast majority of _prospector_ warnings 59 | - Remove all star imports (`import *`) 60 | 61 | # 0.1.2 62 | 63 | - Fix wrong imports 64 | - Add typing to class members 65 | - Fix _mypy_ errors 66 | 67 | # 0.1.1 68 | 69 | - Minor bug fixes 70 | 71 | # 0.1.0 72 | 73 | First release 74 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/bot/bot_handlers_config_typing.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | from typing import Callable, Dict, List, Optional, Type, Union 25 | 26 | from pyrogram.filters import Filter 27 | from pyrogram.handlers.handler import Handler 28 | 29 | 30 | # 31 | # Types 32 | # 33 | 34 | # Bot handlers configuration type 35 | BotHandlersConfigType = Dict[ 36 | Type[Handler], 37 | List[ 38 | Dict[ 39 | str, 40 | Optional[Union[Callable[..., None], Filter]] 41 | ] 42 | ] 43 | ] 44 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/utils/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Classes 23 | # 24 | 25 | # Wrapper for utility functions 26 | class Utils: 27 | # Convert string to bool 28 | @staticmethod 29 | def StrToBool(s: str) -> bool: 30 | s = s.lower() 31 | if s in ["true", "on", "yes", "y"]: 32 | res = True 33 | elif s in ["false", "off", "no", "n"]: 34 | res = False 35 | else: 36 | raise ValueError("Invalid string") 37 | return res 38 | 39 | # Convert string to integer 40 | @staticmethod 41 | def StrToInt(s: str) -> int: 42 | return int(s) 43 | 44 | # Convert string to float 45 | @staticmethod 46 | def StrToFloat(s: str) -> float: 47 | return float(s) 48 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/utils/key_value_converter.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | from typing import Any, Dict 25 | 26 | 27 | # 28 | # Classes 29 | # 30 | 31 | # Key-Value converter class 32 | class KeyValueConverter: 33 | 34 | kv_dict: Dict[str, Any] 35 | 36 | # Constructor 37 | def __init__(self, 38 | kv_dict: Dict[str, Any]) -> None: 39 | self.kv_dict = kv_dict 40 | 41 | # Convert key to value 42 | def KeyToValue(self, 43 | key: str) -> Any: 44 | return self.kv_dict[key] 45 | 46 | # Convert value to key 47 | def ValueToKey(self, 48 | value: Any) -> str: 49 | idx = list(self.kv_dict.values()).index(value) 50 | return list(self.kv_dict.keys())[idx] 51 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/bot/bot_config_types.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | from enum import auto, unique 25 | 26 | from telegram_periodic_msg_bot.config.config_object import ConfigTypes 27 | 28 | 29 | # 30 | # Enumerations 31 | # 32 | 33 | # Bot configuration types 34 | @unique 35 | class BotConfigTypes(ConfigTypes): 36 | API_ID = auto() 37 | API_HASH = auto() 38 | BOT_TOKEN = auto() 39 | SESSION_NAME = auto() 40 | # App 41 | APP_TEST_MODE = auto() 42 | APP_LANG_FILE = auto() 43 | # Task 44 | TASKS_MAX_NUM = auto() 45 | # Message 46 | MESSAGE_MAX_LEN = auto() 47 | # Logging 48 | LOG_LEVEL = auto() 49 | LOG_CONSOLE_ENABLED = auto() 50 | LOG_FILE_ENABLED = auto() 51 | LOG_FILE_NAME = auto() 52 | LOG_FILE_USE_ROTATING = auto() 53 | LOG_FILE_APPEND = auto() 54 | LOG_FILE_MAX_BYTES = auto() 55 | LOG_FILE_BACKUP_CNT = auto() 56 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/config/config_file_sections_loader.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | import configparser 25 | 26 | from telegram_periodic_msg_bot.config.config_object import ConfigObject 27 | from telegram_periodic_msg_bot.config.config_sections_loader import ConfigSectionsLoader 28 | from telegram_periodic_msg_bot.config.config_typing import ConfigSectionsType 29 | 30 | 31 | # 32 | # Classes 33 | # 34 | 35 | # Configuration file sections loader class 36 | class ConfigFileSectionsLoader: 37 | # Load 38 | @staticmethod 39 | def Load(file_name: str, 40 | sections: ConfigSectionsType) -> ConfigObject: 41 | print(f"\nLoading configuration file {file_name}...\n") 42 | 43 | # Read file 44 | config_parser = configparser.ConfigParser() 45 | config_parser.read(file_name, encoding="utf-8") 46 | 47 | # Load sections 48 | return ConfigSectionsLoader(config_parser).LoadSections(sections) 49 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/periodic_msg_bot.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | from telegram_periodic_msg_bot.bot.bot_base import BotBase 25 | from telegram_periodic_msg_bot.bot.bot_config import BotConfig 26 | from telegram_periodic_msg_bot.bot.bot_handlers_config import BotHandlersConfig 27 | from telegram_periodic_msg_bot.periodic_msg.periodic_msg_scheduler import PeriodicMsgScheduler 28 | 29 | 30 | # 31 | # Classes 32 | # 33 | 34 | # Periodic message bot class 35 | class PeriodicMsgBot(BotBase): 36 | 37 | periodic_msg_scheduler: PeriodicMsgScheduler 38 | 39 | # Constructor 40 | def __init__(self, 41 | config_file: str) -> None: 42 | super().__init__( 43 | config_file, 44 | BotConfig, 45 | BotHandlersConfig 46 | ) 47 | # Initialize periodic message scheduler 48 | self.periodic_msg_scheduler = PeriodicMsgScheduler( 49 | self.client, 50 | self.config, 51 | self.logger, 52 | self.translator 53 | ) 54 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/config/config_sections_loader.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | import configparser 25 | 26 | from telegram_periodic_msg_bot.config.config_object import ConfigObject 27 | from telegram_periodic_msg_bot.config.config_section_loader import ConfigSectionLoader 28 | from telegram_periodic_msg_bot.config.config_typing import ConfigSectionsType 29 | 30 | 31 | # 32 | # Classes 33 | # 34 | 35 | # Configuration sections loader class 36 | class ConfigSectionsLoader: 37 | 38 | config_section_loader: ConfigSectionLoader 39 | 40 | # Constructor 41 | def __init__(self, 42 | config_parser: configparser.ConfigParser) -> None: 43 | self.config_section_loader = ConfigSectionLoader(config_parser) 44 | 45 | # Load sections 46 | def LoadSections(self, 47 | sections: ConfigSectionsType) -> ConfigObject: 48 | config_obj = ConfigObject() 49 | 50 | # For each section 51 | for section_name, section in sections.items(): 52 | # Print section 53 | print(f"Section [{section_name}]") 54 | # Load fields 55 | self.config_section_loader.LoadSection(config_obj, section_name, section) 56 | 57 | return config_obj 58 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/config/config_object.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | from enum import Enum 25 | from typing import Any, Dict 26 | 27 | 28 | # 29 | # Enumerations 30 | # 31 | 32 | # Configuration types 33 | class ConfigTypes(Enum): 34 | pass 35 | 36 | 37 | # 38 | # Classes 39 | # 40 | 41 | # Configuration object class 42 | class ConfigObject: 43 | 44 | config: Dict[ConfigTypes, Any] 45 | 46 | # Constructor 47 | def __init__(self) -> None: 48 | self.config = {} 49 | 50 | # Get value 51 | def GetValue(self, 52 | config_type: ConfigTypes) -> Any: 53 | if not isinstance(config_type, ConfigTypes): 54 | raise TypeError("BotConfig type is not an enumerative of ConfigTypes") 55 | return self.config[config_type] 56 | 57 | # Set value 58 | def SetValue(self, 59 | config_type: ConfigTypes, 60 | value: Any) -> None: 61 | if not isinstance(config_type, ConfigTypes): 62 | raise TypeError("BotConfig type is not an enumerative of ConfigTypes") 63 | self.config[config_type] = value 64 | 65 | # Get if value is set 66 | def IsValueSet(self, 67 | config_type: ConfigTypes) -> bool: 68 | return config_type in self.config 69 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/message/message_deleter.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | from typing import List 25 | 26 | import pyrogram 27 | import pyrogram.errors.exceptions as pyrogram_ex 28 | 29 | from telegram_periodic_msg_bot.logger.logger import Logger 30 | from telegram_periodic_msg_bot.utils.pyrogram_wrapper import PyrogramWrapper 31 | 32 | 33 | # 34 | # Classes 35 | # 36 | 37 | # Message deleter class 38 | class MessageDeleter: 39 | 40 | client: pyrogram.Client 41 | logger: Logger 42 | 43 | # Constructor 44 | def __init__(self, 45 | client: pyrogram.Client, 46 | logger: Logger) -> None: 47 | self.client = client 48 | self.logger = logger 49 | 50 | # Delete message 51 | def DeleteMessage(self, 52 | message: pyrogram.types.Message) -> bool: 53 | try: 54 | if message.chat is not None: 55 | self.client.delete_messages(message.chat.id, PyrogramWrapper.MessageId(message)) 56 | return True 57 | except pyrogram_ex.forbidden_403.MessageDeleteForbidden: 58 | self.logger.GetLogger().exception(f"Unable to delete message {PyrogramWrapper.MessageId(message)}") 59 | return False 60 | 61 | # Delete messages 62 | def DeleteMessages(self, 63 | messages: List[pyrogram.types.Message]) -> None: 64 | for message in messages: 65 | self.DeleteMessage(message) 66 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/periodic_msg/periodic_msg_parser.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | import pyrogram 25 | 26 | from telegram_periodic_msg_bot.bot.bot_config_types import BotConfigTypes 27 | from telegram_periodic_msg_bot.config.config_object import ConfigObject 28 | 29 | 30 | # 31 | # Classes 32 | # 33 | 34 | # Invalid message error 35 | class PeriodicMsgParserInvalidError(Exception): 36 | pass 37 | 38 | 39 | # Too long message error 40 | class PeriodicMsgParserTooLongError(Exception): 41 | pass 42 | 43 | 44 | # Periodic message parser class 45 | class PeriodicMsgParser: 46 | 47 | config: ConfigObject 48 | 49 | # Constructor 50 | def __init__(self, 51 | config: ConfigObject) -> None: 52 | self.config = config 53 | 54 | # Parse message 55 | def Parse(self, 56 | message: pyrogram.types.Message) -> str: 57 | if message.text is None: 58 | raise PeriodicMsgParserInvalidError() 59 | 60 | try: 61 | # The message shall start on a new line 62 | msg = message.text[message.text.index("\n"):].strip() 63 | 64 | # Check message 65 | if msg == "": 66 | raise PeriodicMsgParserInvalidError() 67 | if len(msg) > self.config.GetValue(BotConfigTypes.MESSAGE_MAX_LEN): 68 | raise PeriodicMsgParserTooLongError() 69 | 70 | return msg 71 | 72 | except ValueError as ex: 73 | raise PeriodicMsgParserInvalidError() from ex 74 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/utils/wrapped_list.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | import typing 25 | from abc import ABC 26 | from typing import Iterator, List 27 | 28 | 29 | # 30 | # Classes 31 | # 32 | 33 | # Wrapped list class 34 | class WrappedList(ABC): 35 | 36 | list_elements: List[typing.Any] 37 | 38 | # Constructor 39 | def __init__(self) -> None: 40 | self.list_elements = [] 41 | 42 | # Add single element 43 | def AddSingle(self, 44 | element: typing.Any) -> None: 45 | self.list_elements.append(element) 46 | 47 | # Add multiple elements 48 | def AddMultiple(self, 49 | elements: List[typing.Any]) -> None: 50 | self.list_elements.extend(elements) 51 | 52 | # Remove single element 53 | def RemoveSingle(self, 54 | element: typing.Any) -> None: 55 | self.list_elements.remove(element) 56 | 57 | # Get if element is present 58 | def IsElem(self, 59 | element: typing.Any) -> bool: 60 | return element in self.list_elements 61 | 62 | # Clear element 63 | def Clear(self) -> None: 64 | self.list_elements.clear() 65 | 66 | # Get elements count 67 | def Count(self) -> int: 68 | return len(self.list_elements) 69 | 70 | # Get if any 71 | def Any(self) -> bool: 72 | return self.Count() > 0 73 | 74 | # Get if empty 75 | def Empty(self) -> bool: 76 | return self.Count() == 0 77 | 78 | # Get list 79 | def GetList(self) -> List[typing.Any]: 80 | return self.list_elements 81 | 82 | # Get iterator 83 | def __iter__(self) -> Iterator[typing.Any]: 84 | yield from self.list_elements 85 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/periodic_msg/periodic_msg_sender.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | from typing import List, Optional 25 | 26 | import pyrogram 27 | 28 | from telegram_periodic_msg_bot.logger.logger import Logger 29 | from telegram_periodic_msg_bot.message.message_deleter import MessageDeleter 30 | from telegram_periodic_msg_bot.message.message_sender import MessageSender 31 | 32 | 33 | # 34 | # Classes 35 | # 36 | 37 | # Periodic message sender class 38 | class PeriodicMsgSender: 39 | 40 | logger: Logger 41 | delete_last_sent_msg: bool 42 | last_sent_msgs: Optional[List[pyrogram.types.Message]] 43 | message_deleter: MessageDeleter 44 | message_sender: MessageSender 45 | 46 | # Constructor 47 | def __init__(self, 48 | client: pyrogram.Client, 49 | logger: Logger) -> None: 50 | self.logger = logger 51 | self.delete_last_sent_msg = True 52 | self.last_sent_msgs = None 53 | self.message_deleter = MessageDeleter(client, logger) 54 | self.message_sender = MessageSender(client, logger) 55 | 56 | # Set delete last sent message 57 | def DeleteLastSentMessage(self, 58 | flag: bool) -> None: 59 | self.delete_last_sent_msg = flag 60 | 61 | # Send message 62 | def SendMessage(self, 63 | chat: pyrogram.types.Chat, 64 | msg: str) -> None: 65 | if self.delete_last_sent_msg: 66 | self.__DeleteLastSentMessage() 67 | 68 | self.last_sent_msgs = self.message_sender.SendMessage(chat, msg) 69 | 70 | # Delete last sent message 71 | def __DeleteLastSentMessage(self) -> None: 72 | if self.last_sent_msgs is not None: 73 | self.message_deleter.DeleteMessages(self.last_sent_msgs) 74 | 75 | self.last_sent_msgs = None 76 | -------------------------------------------------------------------------------- /app/bot.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | import argparse 25 | 26 | from telegram_periodic_msg_bot import PeriodicMsgBot, __version__ 27 | 28 | 29 | # 30 | # Variables 31 | # 32 | 33 | # Default configuration file 34 | DEF_CONFIG_FILE = "conf/config.ini" 35 | 36 | 37 | # 38 | # Classes 39 | # 40 | 41 | # Argument parser 42 | class ArgumentsParser: 43 | 44 | parser: argparse.ArgumentParser 45 | 46 | # Constructor 47 | def __init__(self) -> None: 48 | self.parser = argparse.ArgumentParser() 49 | self.parser.add_argument( 50 | "-c", "--config", 51 | type=str, 52 | default=DEF_CONFIG_FILE, 53 | help="configuration file" 54 | ) 55 | 56 | # Parse arguments 57 | def Parse(self) -> argparse.Namespace: 58 | return self.parser.parse_args() 59 | 60 | 61 | # 62 | # Functions 63 | # 64 | 65 | # Print header 66 | def print_header() -> None: 67 | print("") 68 | print("***************************************") 69 | print("**** ****") 70 | print("*** ***") 71 | print("** **") 72 | print("* Telegram Periodic Message Bot *") 73 | print("* Author: Emanuele Bellocchia *") 74 | print("* Version: %s *" % __version__) 75 | print("** **") 76 | print("*** ***") 77 | print("**** ****") 78 | print("***************************************") 79 | print("") 80 | 81 | 82 | # 83 | # Main 84 | # 85 | if __name__ == "__main__": 86 | # Print header 87 | print_header() 88 | 89 | # Get arguments 90 | args_parser = ArgumentsParser() 91 | args = args_parser.Parse() 92 | 93 | # Create and run bot 94 | bot = PeriodicMsgBot(args.config) 95 | bot.Run() 96 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/misc/helpers.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | from typing import Optional 25 | 26 | import pyrogram 27 | 28 | from telegram_periodic_msg_bot.utils.pyrogram_wrapper import PyrogramWrapper 29 | 30 | 31 | # 32 | # Classes 33 | # 34 | 35 | # Chat helper class 36 | class ChatHelper: 37 | # Get if channel 38 | @staticmethod 39 | def IsChannel(chat: pyrogram.types.Chat) -> bool: 40 | return PyrogramWrapper.IsChannel(chat) 41 | 42 | # Get title 43 | @staticmethod 44 | def GetTitle(chat: pyrogram.types.Chat) -> str: 45 | return chat.title if chat.title is not None else "" 46 | 47 | # Get title or ID 48 | @staticmethod 49 | def GetTitleOrId(chat: pyrogram.types.Chat) -> str: 50 | return f"'{chat.title}' (ID: {chat.id})" if chat.title is not None else f"{chat.id}" 51 | 52 | # Get if private chat 53 | @staticmethod 54 | def IsPrivateChat(chat: pyrogram.types.Chat, 55 | user: pyrogram.types.User): 56 | if ChatHelper.IsChannel(chat): 57 | return False 58 | return chat.id == user.id 59 | 60 | 61 | # User helper class 62 | class UserHelper: 63 | # Get user name or ID 64 | @staticmethod 65 | def GetNameOrId(user: Optional[pyrogram.types.User]) -> str: 66 | if user is None: 67 | return "Anonymous user" 68 | 69 | if user.username is not None: 70 | return f"@{user.username} ({UserHelper.GetName(user)} - ID: {user.id})" 71 | 72 | name = UserHelper.GetName(user) 73 | return f"{name} (ID: {user.id})" if name is not None else f"ID: {user.id}" 74 | 75 | # Get user name 76 | @staticmethod 77 | def GetName(user: Optional[pyrogram.types.User]) -> str: 78 | if user is None: 79 | return "Anonymous user" 80 | 81 | if user.first_name is not None: 82 | return f"{user.first_name} {user.last_name}" if user.last_name is not None else f"{user.first_name}" 83 | return user.last_name if user.last_name is not None else "" 84 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies and the package with different Python versions and OS 2 | 3 | name: Build 4 | 5 | on: 6 | push: 7 | branches: [ "master" ] 8 | paths-ignore: 9 | - '*.md' 10 | pull_request: 11 | branches: [ "master" ] 12 | paths-ignore: 13 | - '*.md' 14 | 15 | jobs: 16 | build_py307: 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | python-version: ["3.7"] 21 | os: 22 | - ubuntu-22.04 23 | - macOS-13 24 | - windows-latest 25 | runs-on: ${{ matrix.os }} 26 | steps: 27 | - uses: actions/checkout@v4 28 | - name: Set up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v4 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | - name: Install dependencies 33 | shell: bash 34 | run: | 35 | mv -f pyproject_legacy.toml pyproject.toml 36 | python -m pip install --upgrade pip 37 | pip install -r requirements.txt 38 | - name: Install package 39 | run: | 40 | pip install . 41 | 42 | build_py308: 43 | strategy: 44 | fail-fast: false 45 | matrix: 46 | python-version: ["3.8"] 47 | os: 48 | - ubuntu-latest 49 | - macOS-13 50 | - windows-latest 51 | runs-on: ${{ matrix.os }} 52 | steps: 53 | - uses: actions/checkout@v4 54 | - name: Set up Python ${{ matrix.python-version }} 55 | uses: actions/setup-python@v4 56 | with: 57 | python-version: ${{ matrix.python-version }} 58 | - name: Install dependencies 59 | shell: bash 60 | run: | 61 | mv -f pyproject_legacy.toml pyproject.toml 62 | python -m pip install --upgrade pip 63 | pip install -r requirements.txt 64 | - name: Install package 65 | run: | 66 | pip install . 67 | 68 | build_py309_py310: 69 | strategy: 70 | fail-fast: false 71 | matrix: 72 | python-version: ["3.9", "3.10"] 73 | os: 74 | - ubuntu-latest 75 | - macOS-13 76 | - windows-latest 77 | runs-on: ${{ matrix.os }} 78 | steps: 79 | - uses: actions/checkout@v4 80 | - name: Set up Python ${{ matrix.python-version }} 81 | uses: actions/setup-python@v4 82 | with: 83 | python-version: ${{ matrix.python-version }} 84 | - name: Install dependencies 85 | run: | 86 | python -m pip install --upgrade pip 87 | pip install -r requirements.txt 88 | - name: Install package 89 | run: | 90 | pip install . 91 | 92 | build_newer_than_py311: 93 | strategy: 94 | fail-fast: false 95 | matrix: 96 | python-version: ["3.11", "3.12", "3.13"] 97 | os: 98 | - ubuntu-latest 99 | - macOS-latest 100 | - windows-latest 101 | runs-on: ${{ matrix.os }} 102 | steps: 103 | - uses: actions/checkout@v4 104 | - name: Set up Python ${{ matrix.python-version }} 105 | uses: actions/setup-python@v4 106 | with: 107 | python-version: ${{ matrix.python-version }} 108 | - name: Install dependencies 109 | run: | 110 | python -m pip install --upgrade pip 111 | pip install -r requirements.txt 112 | - name: Install package 113 | run: | 114 | pip install . 115 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/utils/pyrogram_wrapper.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # Old wrapper, not needed anymore with pyrotgfork 22 | 23 | # 24 | # Imports 25 | # 26 | from typing import Iterator 27 | 28 | import pyrogram 29 | 30 | from telegram_periodic_msg_bot.utils.utils import Utils 31 | 32 | 33 | if int(pyrogram.__version__[0]) == 2: 34 | from pyrogram.enums import ChatMembersFilter, ChatType 35 | else: 36 | from enum import Enum 37 | 38 | # Fake enum 39 | class ChatMembersFilter(Enum): # type: ignore 40 | pass 41 | 42 | 43 | # 44 | # Classes 45 | # 46 | 47 | # Wrapper for pyrogram for handling different versions 48 | class PyrogramWrapper: 49 | # Get message id 50 | @staticmethod 51 | def MessageId(message: pyrogram.types.Message) -> int: 52 | if PyrogramWrapper.__Version() == 2: 53 | return message.id 54 | if PyrogramWrapper.__Version() == 1: 55 | return message.message_id 56 | raise RuntimeError("Unsupported pyrogram version") 57 | 58 | # Get if channel 59 | @staticmethod 60 | def IsChannel(chat: pyrogram.types.Chat) -> bool: 61 | if PyrogramWrapper.__Version() == 2: 62 | return chat.type == ChatType.CHANNEL 63 | if PyrogramWrapper.__Version() == 1: 64 | return chat["type"] == "channel" 65 | raise RuntimeError("Unsupported pyrogram version") 66 | 67 | # Get if channel 68 | @staticmethod 69 | def GetChatMembers(client: pyrogram.Client, 70 | chat: pyrogram.types.Chat, 71 | filter_str: str) -> Iterator[pyrogram.types.ChatMember]: 72 | if PyrogramWrapper.__Version() == 2: 73 | return client.get_chat_members(chat.id, filter=PyrogramWrapper.__StrToChatMembersFilter(filter_str)) 74 | if PyrogramWrapper.__Version() == 1: 75 | return client.iter_chat_members(chat.id, filter=filter_str) 76 | raise RuntimeError("Unsupported pyrogram version") 77 | 78 | @staticmethod 79 | def __StrToChatMembersFilter(filter_str: str) -> ChatMembersFilter: 80 | str_to_enum = { 81 | "all": ChatMembersFilter.SEARCH, 82 | "banned": ChatMembersFilter.BANNED, 83 | "bots": ChatMembersFilter.BOTS, 84 | "restricted": ChatMembersFilter.RESTRICTED, 85 | "administrators": ChatMembersFilter.ADMINISTRATORS, 86 | } 87 | 88 | return str_to_enum[filter_str] 89 | 90 | # Get version 91 | @staticmethod 92 | def __Version() -> int: 93 | return Utils.StrToInt(pyrogram.__version__[0]) 94 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/translator/translation_loader.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | import os 25 | from typing import Any, Dict, Optional 26 | 27 | from defusedxml import ElementTree 28 | 29 | from telegram_periodic_msg_bot.logger.logger import Logger 30 | 31 | 32 | # 33 | # Classes 34 | # 35 | 36 | # Constants for translation loader class 37 | class TranslationLoaderConst: 38 | # Default language folder 39 | DEF_LANG_FOLDER: str = "../lang" 40 | # Default file name 41 | DEF_FILE_NAME: str = "lang_en.xml" 42 | # XML tag for sentences 43 | SENTENCE_XML_TAG: str = "sentence" 44 | 45 | 46 | # Translation loader class 47 | class TranslationLoader: 48 | 49 | logger: Logger 50 | sentences: Dict[str, str] 51 | 52 | # Constructor 53 | def __init__(self, 54 | logger: Logger) -> None: 55 | self.logger = logger 56 | self.sentences = {} 57 | 58 | # Load translation file 59 | def Load(self, 60 | file_name: Optional[str] = None) -> None: 61 | def_file_path = os.path.join(os.path.dirname(__file__), 62 | TranslationLoaderConst.DEF_LANG_FOLDER, 63 | TranslationLoaderConst.DEF_FILE_NAME) 64 | 65 | if file_name is not None: 66 | try: 67 | self.logger.GetLogger().info(f"Loading language file '{file_name}'...") 68 | self.__LoadFile(file_name) 69 | except FileNotFoundError: 70 | self.logger.GetLogger().error( 71 | f"Language file '{file_name}' not found, loading default language..." 72 | ) 73 | self.__LoadFile(def_file_path) 74 | else: 75 | self.logger.GetLogger().info("Loading default language file...") 76 | self.__LoadFile(def_file_path) 77 | 78 | # Get sentence 79 | def GetSentence(self, 80 | sentence_id: str, 81 | **kwargs: Any) -> str: 82 | return self.sentences[sentence_id].format(**kwargs) 83 | 84 | # Load file 85 | def __LoadFile(self, 86 | file_name: str) -> None: 87 | # Parse xml 88 | tree = ElementTree.parse(file_name) 89 | root = tree.getroot() 90 | 91 | # Load all sentences 92 | for child in root: 93 | if child.tag == TranslationLoaderConst.SENTENCE_XML_TAG and child.text is not None: 94 | sentence_id = child.attrib["id"] 95 | self.sentences[sentence_id] = child.text.replace("\\n", "\n") 96 | 97 | self.logger.GetLogger().debug( 98 | f"Loaded sentence '{sentence_id}': {self.sentences[sentence_id]}" 99 | ) 100 | 101 | self.logger.GetLogger().info( 102 | f"Language file successfully loaded, number of sentences: {len(self.sentences)}" 103 | ) 104 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/message/message_sender.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | import time 25 | from typing import Any, List, Union 26 | 27 | import pyrogram 28 | 29 | from telegram_periodic_msg_bot.logger.logger import Logger 30 | 31 | 32 | # 33 | # Classes 34 | # 35 | 36 | # Constants for message sender class 37 | class MessageSenderConst: 38 | # Maximum message length 39 | MSG_MAX_LEN: int = 4096 40 | # Sleep time for sending messages 41 | SEND_MSG_SLEEP_TIME_SEC: float = 0.1 42 | 43 | 44 | # Message sender class 45 | class MessageSender: 46 | 47 | client: pyrogram.Client 48 | logger: Logger 49 | 50 | # Constructor 51 | def __init__(self, 52 | client: pyrogram.Client, 53 | logger: Logger) -> None: 54 | self.client = client 55 | self.logger = logger 56 | 57 | # Send message 58 | def SendMessage(self, 59 | receiver: Union[pyrogram.types.Chat, pyrogram.types.User], 60 | msg: str, 61 | **kwargs: Any) -> List[pyrogram.types.Message]: 62 | # Log 63 | self.logger.GetLogger().info(f"Sending message (length: {len(msg)}):\n{msg}") 64 | # Split and send message 65 | return self.__SendSplitMessage(receiver, self.__SplitMessage(msg), **kwargs) 66 | 67 | # Send split message 68 | def __SendSplitMessage(self, 69 | receiver: Union[pyrogram.types.Chat, pyrogram.types.User], 70 | split_msg: List[str], 71 | **kwargs: Any) -> List[pyrogram.types.Message]: 72 | sent_msgs = [] 73 | 74 | # Send message 75 | for msg_part in split_msg: 76 | sent_msgs.append(self.client.send_message(receiver.id, msg_part, **kwargs)) 77 | time.sleep(MessageSenderConst.SEND_MSG_SLEEP_TIME_SEC) 78 | 79 | return sent_msgs # type: ignore 80 | 81 | # Split message 82 | def __SplitMessage(self, 83 | msg: str) -> List[str]: 84 | msg_parts = [] 85 | 86 | while len(msg) > 0: 87 | # If length is less than maximum, the operation is completed 88 | if len(msg) <= MessageSenderConst.MSG_MAX_LEN: 89 | msg_parts.append(msg) 90 | break 91 | 92 | # Take the current part 93 | curr_part = msg[:MessageSenderConst.MSG_MAX_LEN] 94 | # Get the last occurrence of a new line 95 | idx = curr_part.rfind("\n") 96 | 97 | # Split with respect to the found occurrence 98 | if idx != -1: 99 | msg_parts.append(curr_part[:idx]) 100 | msg = msg[idx + 1:] 101 | else: 102 | msg_parts.append(curr_part) 103 | msg = msg[MessageSenderConst.MSG_MAX_LEN + 1:] 104 | 105 | # Log 106 | self.logger.GetLogger().info(f"Message split into {len(msg_parts)} part(s)") 107 | 108 | return msg_parts 109 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/config/config_section_loader.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | import configparser 25 | 26 | from telegram_periodic_msg_bot.config.config_loader_ex import ConfigFieldNotExistentError, ConfigFieldValueError 27 | from telegram_periodic_msg_bot.config.config_object import ConfigObject 28 | from telegram_periodic_msg_bot.config.config_typing import ConfigFieldType, ConfigSectionType 29 | 30 | 31 | # 32 | # Classes 33 | # 34 | 35 | # Configuration section loader class 36 | class ConfigSectionLoader: 37 | 38 | config_parser: configparser.ConfigParser 39 | 40 | # Constructor 41 | def __init__(self, 42 | config_parser: configparser.ConfigParser) -> None: 43 | self.config_parser = config_parser 44 | 45 | # Load section 46 | def LoadSection(self, 47 | config_obj: ConfigObject, 48 | section_name: str, 49 | section: ConfigSectionType) -> None: 50 | # For each field 51 | for field in section: 52 | # Load if needed 53 | if self.__FieldShallBeLoaded(config_obj, field): 54 | # Set field value and print it 55 | self.__SetFieldValue(config_obj, section_name, field) 56 | self.__PrintFieldValue(config_obj, field) 57 | # Otherwise set the default value 58 | elif "def_val" in field: 59 | config_obj.SetValue(field["type"], field["def_val"]) 60 | 61 | # Get if field shall be loaded 62 | @staticmethod 63 | def __FieldShallBeLoaded(config_obj: ConfigObject, 64 | field: ConfigFieldType) -> bool: 65 | return field["load_if"](config_obj) if "load_if" in field else True 66 | 67 | # Set field value 68 | def __SetFieldValue(self, 69 | config_obj: ConfigObject, 70 | section: str, 71 | field: ConfigFieldType) -> None: 72 | try: 73 | field_val = self.config_parser[section][field["name"]] 74 | # Field not present, set default value if specified 75 | except KeyError as ex: 76 | if "def_val" not in field: 77 | raise ConfigFieldNotExistentError(f"Configuration field \"{field['name']}\" not found") from ex 78 | field_val = field["def_val"] 79 | else: 80 | # Convert value if needed 81 | if "conv_fct" in field: 82 | field_val = field["conv_fct"](field_val) 83 | 84 | # Validate value if needed 85 | if "valid_if" in field and not field["valid_if"](config_obj, field_val): 86 | raise ConfigFieldValueError(f"Value '{field_val}' is not valid for field \"{field['name']}\"") 87 | 88 | # Set value 89 | config_obj.SetValue(field["type"], field_val) 90 | 91 | # Print field value 92 | @staticmethod 93 | def __PrintFieldValue(config_obj: ConfigObject, 94 | field: ConfigFieldType) -> None: 95 | if "print_fct" in field: 96 | print(f"- {field['name']}: {field['print_fct'](config_obj.GetValue(field['type']))}") 97 | else: 98 | print(f"- {field['name']}: {config_obj.GetValue(field['type'])}") 99 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/command/command_data.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | from typing import Any, Callable, Optional, Union 25 | 26 | import pyrogram 27 | 28 | from telegram_periodic_msg_bot.utils.utils import Utils 29 | from telegram_periodic_msg_bot.utils.wrapped_list import WrappedList 30 | 31 | 32 | # 33 | # Classes 34 | # 35 | 36 | # Command parameter error class 37 | class CommandParameterError(Exception): 38 | pass 39 | 40 | 41 | # Command parameters list class 42 | class CommandParametersList(WrappedList): 43 | # Get parameter as bool 44 | def GetAsBool(self, 45 | idx: int, 46 | def_val: Optional[bool] = None) -> bool: 47 | return self.__GetGenericParam(Utils.StrToBool, idx, def_val) 48 | 49 | # Get parameter as int 50 | def GetAsInt(self, 51 | idx: int, 52 | def_val: Optional[int] = None) -> int: 53 | return self.__GetGenericParam(Utils.StrToInt, idx, def_val) 54 | 55 | # Get parameter as string 56 | def GetAsString(self, 57 | idx: int, 58 | def_val: Optional[str] = None) -> str: 59 | return self.__GetGenericParam(str, idx, def_val) 60 | 61 | # Check if last parameter is the specified value 62 | def IsLast(self, 63 | value: Union[int, str]) -> bool: 64 | try: 65 | return value == self.list_elements[self.Count() - 1] 66 | except IndexError: 67 | return False 68 | 69 | # Check if value is present 70 | def IsValue(self, 71 | value: Union[int, str]) -> bool: 72 | return value in self.list_elements 73 | 74 | # Get generic parameter 75 | def __GetGenericParam(self, 76 | conv_fct: Callable[[str], Any], 77 | idx: int, 78 | def_val: Optional[Any]) -> Any: 79 | try: 80 | return conv_fct(self.list_elements[idx]) 81 | except (ValueError, IndexError) as ex: 82 | if def_val is not None: 83 | return def_val 84 | raise CommandParameterError(f"Invalid command parameter #{idx}") from ex 85 | 86 | 87 | # Command data 88 | class CommandData: 89 | 90 | cmd_name: str 91 | cmd_params: CommandParametersList 92 | cmd_chat: pyrogram.types.Chat 93 | cmd_user: Optional[pyrogram.types.User] 94 | 95 | # Constructor 96 | def __init__(self, 97 | message: pyrogram.types.Message) -> None: 98 | if message.command is None or message.chat is None: 99 | raise ValueError("Invalid command") 100 | 101 | self.cmd_name = message.command[0] 102 | self.cmd_params = CommandParametersList() 103 | self.cmd_params.AddMultiple(message.command[1:]) 104 | self.cmd_chat = message.chat 105 | self.cmd_user = message.from_user 106 | 107 | # Get name 108 | def Name(self) -> str: 109 | return self.cmd_name 110 | 111 | # Get chat 112 | def Chat(self) -> pyrogram.types.Chat: 113 | return self.cmd_chat 114 | 115 | # Get user 116 | def User(self) -> Optional[pyrogram.types.User]: 117 | return self.cmd_user 118 | 119 | # Get parameters 120 | def Params(self) -> CommandParametersList: 121 | return self.cmd_params 122 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/misc/chat_members.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | from typing import Callable, Optional 25 | 26 | import pyrogram 27 | 28 | from telegram_periodic_msg_bot.misc.helpers import UserHelper 29 | from telegram_periodic_msg_bot.utils.pyrogram_wrapper import PyrogramWrapper 30 | from telegram_periodic_msg_bot.utils.wrapped_list import WrappedList 31 | 32 | 33 | # 34 | # Classes 35 | # 36 | 37 | # Chat members list class 38 | class ChatMembersList(WrappedList): 39 | # Get by user ID 40 | def GetByUserId(self, 41 | user_id: int) -> Optional[pyrogram.types.ChatMember]: 42 | res = list(filter(lambda member: user_id == member.user.id, self.list_elements)) 43 | return None if len(res) == 0 else res[0] 44 | 45 | # Get by username 46 | def GetByUsername(self, 47 | username: str) -> Optional[pyrogram.types.ChatMember]: 48 | res = list(filter(lambda member: username == member.user.username, self.list_elements)) 49 | return None if len(res) == 0 else res[0] 50 | 51 | # Get if ID is present 52 | def IsUserIdPresent(self, 53 | user_id: int) -> bool: 54 | return self.GetByUserId(user_id) is not None 55 | 56 | # Get if username is present 57 | def IsUsernamePresent(self, 58 | username: str) -> bool: 59 | return self.GetByUsername(username) is not None 60 | 61 | # Convert to string 62 | def ToString(self) -> str: 63 | return "\n".join( 64 | [f"- {UserHelper.GetNameOrId(member.user)}" for member in self.list_elements] 65 | ) 66 | 67 | # Convert to string 68 | def __str__(self) -> str: 69 | return self.ToString() 70 | 71 | 72 | # Chat members getter class 73 | class ChatMembersGetter: 74 | 75 | client: pyrogram.Client 76 | 77 | # Constructor 78 | def __init__(self, 79 | client: pyrogram.Client) -> None: 80 | self.client = client 81 | 82 | # Get the list of chat members by applying the specified filter 83 | def FilterMembers(self, 84 | chat: pyrogram.types.Chat, 85 | filter_fct: Optional[Callable[[pyrogram.types.ChatMember], bool]] = None, 86 | filter_str: str = "all") -> ChatMembersList: 87 | # Get members 88 | filtered_members = list(PyrogramWrapper.GetChatMembers(self.client, chat, filter_str)) 89 | # Filter them if necessary 90 | if filter_fct is not None: 91 | filtered_members = list(filter(filter_fct, filtered_members)) # type: ignore 92 | # Order filtered members 93 | filtered_members.sort( # type: ignore 94 | key=lambda member: member.user.username.lower() if member.user.username is not None else str(member.user.id) 95 | ) 96 | 97 | # Build chat members 98 | chat_members = ChatMembersList() 99 | chat_members.AddMultiple(filtered_members) # type: ignore 100 | 101 | return chat_members 102 | 103 | # Get all 104 | def GetAll(self, 105 | chat: pyrogram.types.Chat) -> ChatMembersList: 106 | return self.FilterMembers(chat) 107 | 108 | # Get admins 109 | def GetAdmins(self, 110 | chat: pyrogram.types.Chat) -> ChatMembersList: 111 | return self.FilterMembers(chat, 112 | lambda member: True, 113 | "administrators") 114 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/periodic_msg/periodic_msg_job.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | from threading import Lock 25 | 26 | import pyrogram 27 | 28 | from telegram_periodic_msg_bot.logger.logger import Logger 29 | from telegram_periodic_msg_bot.misc.helpers import ChatHelper 30 | from telegram_periodic_msg_bot.periodic_msg.periodic_msg_sender import PeriodicMsgSender 31 | 32 | 33 | # 34 | # Classes 35 | # 36 | 37 | # Periodic message job data class 38 | class PeriodicMsgJobData: 39 | 40 | chat: pyrogram.types.Chat 41 | period_hours: int 42 | start_hour: int 43 | msg_id: str 44 | running: bool 45 | 46 | # Constructor 47 | def __init__(self, 48 | chat: pyrogram.types.Chat, 49 | period_hours: int, 50 | start_hour: int, 51 | msg_id: str) -> None: 52 | self.chat = chat 53 | self.period_hours = period_hours 54 | self.start_hour = start_hour 55 | self.msg_id = msg_id 56 | self.running = True 57 | 58 | # Get chat 59 | def Chat(self) -> pyrogram.types.Chat: 60 | return self.chat 61 | 62 | # Get period hours 63 | def PeriodHours(self) -> int: 64 | return self.period_hours 65 | 66 | # Get start hour 67 | def StartHour(self) -> int: 68 | return self.start_hour 69 | 70 | # Get message ID 71 | def MessageId(self) -> str: 72 | return self.msg_id 73 | 74 | # Set if running 75 | def SetRunning(self, 76 | flag: bool) -> None: 77 | self.running = flag 78 | 79 | # Get if running 80 | def IsRunning(self) -> bool: 81 | return self.running 82 | 83 | 84 | # Periodic message job class 85 | class PeriodicMsgJob: 86 | 87 | data: PeriodicMsgJobData 88 | logger: Logger 89 | message: str 90 | message_lock: Lock 91 | message_sender: PeriodicMsgSender 92 | 93 | # Constructor 94 | def __init__(self, 95 | client: pyrogram.Client, 96 | logger: Logger, 97 | data: PeriodicMsgJobData) -> None: 98 | self.data = data 99 | self.logger = logger 100 | self.message = "" 101 | self.message_lock = Lock() 102 | self.message_sender = PeriodicMsgSender(client, logger) 103 | 104 | # Get data 105 | def Data(self) -> PeriodicMsgJobData: 106 | return self.data 107 | 108 | # Set if running 109 | def SetRunning(self, 110 | flag: bool) -> None: 111 | self.data.SetRunning(flag) 112 | 113 | # Set delete last sent message 114 | def DeleteLastSentMessage(self, 115 | flag: bool) -> None: 116 | self.message_sender.DeleteLastSentMessage(flag) 117 | 118 | # Get message 119 | def GetMessage(self) -> str: 120 | return self.message 121 | 122 | # Set message 123 | def SetMessage(self, 124 | message: str) -> None: 125 | # Prevent accidental modifications while job is executing 126 | with self.message_lock: 127 | self.message = message 128 | 129 | # Do job 130 | def DoJob(self, 131 | chat: pyrogram.types.Chat) -> None: 132 | self.logger.GetLogger().info(f"Periodic message job started in chat '{ChatHelper.GetTitleOrId(chat)}'") 133 | 134 | with self.message_lock: 135 | if self.message == "": 136 | self.logger.GetLogger().info("No message set, exiting...") 137 | return 138 | 139 | self.message_sender.SendMessage(chat, self.message) 140 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/logger/logger.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | import logging 25 | import logging.handlers 26 | import os 27 | from typing import Union 28 | 29 | from telegram_periodic_msg_bot.bot.bot_config_types import BotConfigTypes 30 | from telegram_periodic_msg_bot.config.config_object import ConfigObject 31 | 32 | 33 | # 34 | # Classes 35 | # 36 | 37 | # Constants for logger class 38 | class LoggerConst: 39 | # Logger name 40 | LOGGER_NAME: str = "" 41 | # Log formats 42 | LOG_CONSOLE_FORMAT: str = "%(asctime)-15s %(levelname)s - %(message)s" 43 | LOG_FILE_FORMAT: str = "%(asctime)-15s %(levelname)s - [%(name)s.%(funcName)s:%(lineno)d] %(message)s" 44 | 45 | 46 | # Logger class 47 | class Logger: 48 | 49 | config: ConfigObject 50 | logger: logging.Logger 51 | 52 | # Constructor 53 | def __init__(self, 54 | config: ConfigObject) -> None: 55 | self.config = config 56 | self.logger = logging.getLogger(LoggerConst.LOGGER_NAME) 57 | self.__Init() 58 | 59 | # Get logger 60 | def GetLogger(self) -> logging.Logger: 61 | return self.logger 62 | 63 | # Initialize 64 | def __Init(self) -> None: 65 | # Configure loggers 66 | self.__ConfigureRootLogger() 67 | self.__ConfigureConsoleLogger() 68 | self.__ConfigureFileLogger() 69 | # Log 70 | self.logger.info("Logger initialized") 71 | 72 | # Configure root logger 73 | def __ConfigureRootLogger(self) -> None: 74 | self.logger.setLevel(self.config.GetValue(BotConfigTypes.LOG_LEVEL)) 75 | 76 | # Configure console logger 77 | def __ConfigureConsoleLogger(self) -> None: 78 | # Configure console handler if required 79 | if self.config.GetValue(BotConfigTypes.LOG_CONSOLE_ENABLED): 80 | # Create handler 81 | ch = logging.StreamHandler() 82 | ch.setLevel(self.config.GetValue(BotConfigTypes.LOG_LEVEL)) 83 | ch.setFormatter(logging.Formatter(LoggerConst.LOG_CONSOLE_FORMAT)) 84 | # Add handler 85 | self.logger.addHandler(ch) 86 | 87 | # Configure file logger 88 | def __ConfigureFileLogger(self) -> None: 89 | # Configure file handler if required 90 | if self.config.GetValue(BotConfigTypes.LOG_FILE_ENABLED): 91 | # Get file name 92 | log_file_name = self.config.GetValue(BotConfigTypes.LOG_FILE_NAME) 93 | # Create log directories if needed 94 | self.__MakeLogDir(log_file_name) 95 | 96 | # Create file handler 97 | fh: Union[logging.handlers.RotatingFileHandler, logging.FileHandler] 98 | if self.config.GetValue(BotConfigTypes.LOG_FILE_USE_ROTATING): 99 | fh = logging.handlers.RotatingFileHandler(log_file_name, 100 | maxBytes=self.config.GetValue(BotConfigTypes.LOG_FILE_MAX_BYTES), 101 | backupCount=self.config.GetValue(BotConfigTypes.LOG_FILE_BACKUP_CNT), 102 | encoding="utf-8") 103 | else: 104 | fh = logging.FileHandler(log_file_name, 105 | mode="a" if self.config.GetValue(BotConfigTypes.LOG_FILE_APPEND) else "w", 106 | encoding="utf-8") 107 | 108 | fh.setLevel(self.config.GetValue(BotConfigTypes.LOG_LEVEL)) 109 | fh.setFormatter(logging.Formatter(LoggerConst.LOG_FILE_FORMAT)) 110 | # Add handler 111 | self.logger.addHandler(fh) 112 | 113 | # Make log directories 114 | @staticmethod 115 | def __MakeLogDir(file_name: str) -> None: 116 | try: 117 | os.makedirs(os.path.dirname(file_name)) 118 | except FileExistsError: 119 | pass 120 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/message/message_dispatcher.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | from enum import Enum, auto, unique 25 | from typing import Any 26 | 27 | import pyrogram 28 | 29 | from telegram_periodic_msg_bot.config.config_object import ConfigObject 30 | from telegram_periodic_msg_bot.logger.logger import Logger 31 | from telegram_periodic_msg_bot.message.message_sender import MessageSender 32 | from telegram_periodic_msg_bot.translator.translation_loader import TranslationLoader 33 | 34 | 35 | # 36 | # Enumerations 37 | # 38 | 39 | # Message types 40 | @unique 41 | class MessageTypes(Enum): 42 | GROUP_CHAT_CREATED = auto() 43 | LEFT_CHAT_MEMBER = auto() 44 | NEW_CHAT_MEMBERS = auto() 45 | 46 | 47 | # 48 | # Classes 49 | # 50 | 51 | # Message dispatcher class 52 | class MessageDispatcher: 53 | 54 | config: ConfigObject 55 | logger: Logger 56 | translator: TranslationLoader 57 | 58 | # Constructor 59 | def __init__(self, 60 | config: ConfigObject, 61 | logger: Logger, 62 | translator: TranslationLoader) -> None: 63 | self.config = config 64 | self.logger = logger 65 | self.translator = translator 66 | 67 | # Dispatch command 68 | def Dispatch(self, 69 | client: pyrogram.Client, 70 | message: pyrogram.types.Message, 71 | msg_type: MessageTypes, 72 | **kwargs: Any) -> None: 73 | if not isinstance(msg_type, MessageTypes): 74 | raise TypeError("Message type is not an enumerative of MessageTypes") 75 | 76 | # Log 77 | self.logger.GetLogger().info(f"Dispatching message type: {msg_type}") 78 | 79 | # New chat created 80 | if msg_type == MessageTypes.GROUP_CHAT_CREATED: 81 | self.__OnCreatedChat(client, message, **kwargs) 82 | # A member left the chat 83 | elif msg_type == MessageTypes.LEFT_CHAT_MEMBER: 84 | self.__OnLeftMember(client, message, **kwargs) 85 | # A member joined the chat 86 | elif msg_type == MessageTypes.NEW_CHAT_MEMBERS: 87 | self.__OnJoinedMember(client, message, **kwargs) 88 | 89 | # Function called when a new chat is created 90 | def __OnCreatedChat(self, 91 | client, 92 | message: pyrogram.types.Message, 93 | **kwargs: Any) -> None: 94 | if message.chat is None: 95 | return 96 | 97 | # Send the welcome message 98 | MessageSender(client, self.logger).SendMessage( 99 | message.chat, 100 | self.translator.GetSentence("BOT_WELCOME_MSG") 101 | ) 102 | 103 | # Function called when a member left the chat 104 | def __OnLeftMember(self, 105 | client, 106 | message: pyrogram.types.Message, 107 | **kwargs: Any) -> None: 108 | # If the member is the bot itself, remove the chat from the scheduler 109 | if message.left_chat_member is not None and message.left_chat_member.is_self: 110 | kwargs["periodic_msg_scheduler"].ChatLeft(message.chat) 111 | 112 | # Function called when a member joined the chat 113 | def __OnJoinedMember(self, 114 | client, 115 | message: pyrogram.types.Message, 116 | **kwargs: Any) -> None: 117 | if message.new_chat_members is None or message.chat is None: 118 | return 119 | 120 | # If the member is the bot itself, send the welcome message 121 | for member in message.new_chat_members: 122 | if member.is_self: 123 | MessageSender(client, self.logger).SendMessage( 124 | message.chat, 125 | self.translator.GetSentence("BOT_WELCOME_MSG") 126 | ) 127 | break 128 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=77", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "telegram_periodic_msg_bot" 7 | dynamic = ["version", "dependencies"] 8 | authors = [ 9 | {name = "Emanuele Bellocchia", email = "ebellocchia@gmail.com"} 10 | ] 11 | maintainers = [ 12 | {name = "Emanuele Bellocchia", email = "ebellocchia@gmail.com"} 13 | ] 14 | description = "Telegram bot for sending periodic messages" 15 | readme = "README.md" 16 | license = "MIT" 17 | license-files = [ 18 | "LICENSE", 19 | ] 20 | requires-python = ">=3.7" 21 | keywords = ["telegram", "bot", "telegram bot", "periodic messages"] 22 | classifiers = [ 23 | "Programming Language :: Python :: 3.7", 24 | "Programming Language :: Python :: 3.8", 25 | "Programming Language :: Python :: 3.9", 26 | "Programming Language :: Python :: 3.10", 27 | "Programming Language :: Python :: 3.11", 28 | "Programming Language :: Python :: 3.12", 29 | "Programming Language :: Python :: 3.13", 30 | "Development Status :: 5 - Production/Stable", 31 | "Operating System :: OS Independent", 32 | "Intended Audience :: Developers", 33 | ] 34 | 35 | [project.urls] 36 | Homepage = "https://github.com/ebellocchia/telegram_periodic_msg_bot" 37 | Changelog = "https://github.com/ebellocchia/telegram_periodic_msg_bot/blob/master/CHANGELOG.md" 38 | Repository = "https://github.com/ebellocchia/telegram_periodic_msg_bot" 39 | Download = "https://github.com/ebellocchia/telegram_periodic_msg_bot/archive/v{version}.tar.gz" 40 | 41 | [tool.setuptools] 42 | packages = {find = {}} 43 | 44 | [tool.setuptools.package-data] 45 | telegram_periodic_msg_bot = ["lang/lang_en.xml"] 46 | 47 | [tool.setuptools.dynamic] 48 | version = {attr = "telegram_periodic_msg_bot._version.__version__"} 49 | dependencies = {file = ["requirements.txt"]} 50 | optional-dependencies.develop = {file = ["requirements-dev.txt"]} 51 | 52 | # 53 | # Tools configuration 54 | # 55 | 56 | [tool.ruff] 57 | target-version = "py37" 58 | line-length = 140 59 | exclude = [ 60 | ".github", 61 | ".eggs", 62 | ".egg-info", 63 | ".idea", 64 | ".mypy_cache", 65 | ".tox", 66 | "build", 67 | "dist", 68 | "venv", 69 | ] 70 | 71 | [tool.ruff.lint] 72 | select = [ 73 | "E", # pycodestyle errors 74 | "W", # pycodestyle warnings 75 | "F", # pyflakes 76 | "I", # pyflakes 77 | "N", # pep8-naming 78 | "D", # pydocstyle 79 | "UP", # pyupgrade 80 | "C90", # mccabe complexity 81 | "PL", # pylint 82 | ] 83 | ignore = [ 84 | "N802", # Function name should be lowercase 85 | "E231", # Missing whitespace after ':' 86 | "F821", # Undefined name (Literal import for Python 3.7 compatibility) 87 | "UP006", # Use `type` instead of `Type` for type annotation (Python <3.9 compatibility) 88 | "UP007", # Use `X | Y` for type annotations (Python <3.10 compatibility) 89 | "UP037", # Remove quotes from type annotation (Literal import for Python 3.7 compatibility) 90 | "UP045", # Use `X | None` for type annotations (Python <3.10 compatibility) 91 | # pydocstyle 92 | "D100", # Missing docstring 93 | "D101", # Missing docstring 94 | "D102", # Missing docstring 95 | "D103", # Missing docstring 96 | "D104", # Missing docstring 97 | "D105", # Missing docstring 98 | "D107", # Missing docstring 99 | "D202", # No blank lines allowed after function docstring 100 | "D203", # 1 blank line required before class docstring 101 | "D205", # 1 blank line required between summary line and description 102 | "D212", # Multi-line docstring summary should start at the first line 103 | "D406", # Section name should end with a newline 104 | "D407", # Missing dashed underline after section 105 | "D413", # Missing blank line after last section 106 | "D415", # First line should end with a period, question mark, or exclamation point 107 | "D417", # Missing argument description in the docstring: **kwargs 108 | # pylint 109 | "PLR0911", # Too many return statements 110 | "PLR0912", # Too many branches 111 | "PLR0913", # Too many arguments 112 | "PLR0915", # Too many statements 113 | "PLR2004", # Magic value used in comparison 114 | ] 115 | 116 | [tool.ruff.lint.per-file-ignores] 117 | "__init__.py" = ["F401", "D104"] # Imported but unused, missing docstring 118 | "app/bot.py" = ["UP031"] # Use format specifiers instead of percent format 119 | 120 | [tool.ruff.lint.isort] 121 | known-first-party = [] 122 | lines-after-imports = 2 123 | combine-as-imports = false 124 | force-single-line = false 125 | 126 | [tool.ruff.lint.pydocstyle] 127 | convention = "google" 128 | 129 | [tool.ruff.lint.mccabe] 130 | max-complexity = 10 131 | 132 | [tool.mypy] 133 | python_version = "3.7" 134 | ignore_missing_imports = true 135 | follow_imports = "skip" 136 | exclude = [ 137 | "\\.github", 138 | "\\.eggs", 139 | "\\.egg-info", 140 | "\\.idea", 141 | "\\.ruff_cache", 142 | "\\.tox", 143 | "build", 144 | "dist", 145 | "venv", 146 | ] 147 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/bot/bot_base.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | from typing import Any 25 | 26 | import pyrogram 27 | from pyrogram import Client 28 | 29 | from telegram_periodic_msg_bot.bot.bot_config_types import BotConfigTypes 30 | from telegram_periodic_msg_bot.bot.bot_handlers_config_typing import BotHandlersConfigType 31 | from telegram_periodic_msg_bot.command.command_dispatcher import CommandDispatcher, CommandTypes 32 | from telegram_periodic_msg_bot.config.config_file_sections_loader import ConfigFileSectionsLoader 33 | from telegram_periodic_msg_bot.config.config_object import ConfigObject 34 | from telegram_periodic_msg_bot.config.config_typing import ConfigSectionsType 35 | from telegram_periodic_msg_bot.logger.logger import Logger 36 | from telegram_periodic_msg_bot.message.message_dispatcher import MessageDispatcher, MessageTypes 37 | from telegram_periodic_msg_bot.translator.translation_loader import TranslationLoader 38 | 39 | 40 | # 41 | # Classes 42 | # 43 | 44 | 45 | # Bot base class 46 | class BotBase: 47 | 48 | config: ConfigObject 49 | logger: Logger 50 | translator: TranslationLoader 51 | client: pyrogram.Client 52 | cmd_dispatcher: CommandDispatcher 53 | msg_dispatcher: MessageDispatcher 54 | 55 | # Constructor 56 | def __init__(self, 57 | config_file: str, 58 | config_sections: ConfigSectionsType, 59 | handlers_config: BotHandlersConfigType) -> None: 60 | # Load configuration 61 | self.config = ConfigFileSectionsLoader.Load(config_file, config_sections) 62 | # Initialize logger 63 | self.logger = Logger(self.config) 64 | # Initialize translations 65 | self.translator = TranslationLoader(self.logger) 66 | self.translator.Load(self.config.GetValue(BotConfigTypes.APP_LANG_FILE)) 67 | # Initialize client 68 | self.client = Client( 69 | self.config.GetValue(BotConfigTypes.SESSION_NAME), 70 | api_id=self.config.GetValue(BotConfigTypes.API_ID), 71 | api_hash=self.config.GetValue(BotConfigTypes.API_HASH), 72 | bot_token=self.config.GetValue(BotConfigTypes.BOT_TOKEN) 73 | ) 74 | # Initialize helper classes 75 | self.cmd_dispatcher = CommandDispatcher(self.config, self.logger, self.translator) 76 | self.msg_dispatcher = MessageDispatcher(self.config, self.logger, self.translator) 77 | # Setup handlers 78 | self._SetupHandlers(handlers_config) 79 | # Log 80 | self.logger.GetLogger().info("Bot initialization completed") 81 | 82 | # Run bot 83 | def Run(self) -> None: 84 | # Print 85 | self.logger.GetLogger().info("Bot started!\n") 86 | # Run client 87 | self.client.run() 88 | 89 | # Setup handlers 90 | def _SetupHandlers(self, 91 | handlers_config: BotHandlersConfigType) -> None: 92 | def create_handler(handler_type, handler_cfg): 93 | return handler_type( 94 | lambda client, message: handler_cfg["callback"](self, client, message), 95 | handler_cfg["filters"] 96 | ) 97 | 98 | # Add all configured handlers 99 | for curr_hnd_type, curr_hnd_cfg in handlers_config.items(): 100 | for handler_cfg in curr_hnd_cfg: 101 | self.client.add_handler( 102 | create_handler(curr_hnd_type, handler_cfg) 103 | ) 104 | # Log 105 | self.logger.GetLogger().info("Bot handlers set") 106 | 107 | # Dispatch command 108 | def DispatchCommand(self, 109 | client: pyrogram.Client, 110 | message: pyrogram.types.Message, 111 | cmd_type: CommandTypes, 112 | **kwargs: Any) -> None: 113 | self.cmd_dispatcher.Dispatch(client, message, cmd_type, **kwargs) 114 | 115 | # Handle message 116 | def HandleMessage(self, 117 | client: pyrogram.Client, 118 | message: pyrogram.types.Message, 119 | msg_type: MessageTypes, 120 | **kwargs: Any) -> None: 121 | self.msg_dispatcher.Dispatch(client, message, msg_type, **kwargs) 122 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/command/command_dispatcher.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | from enum import Enum, auto, unique 25 | from typing import Any, Dict, Type 26 | 27 | import pyrogram 28 | 29 | from telegram_periodic_msg_bot.command.command_base import CommandBase 30 | from telegram_periodic_msg_bot.command.commands import ( 31 | AliveCmd, 32 | HelpCmd, 33 | IsTestModeCmd, 34 | MessageTaskDeleteLastMsgCmd, 35 | MessageTaskGetCmd, 36 | MessageTaskInfoCmd, 37 | MessageTaskPauseCmd, 38 | MessageTaskResumeCmd, 39 | MessageTaskSetCmd, 40 | MessageTaskStartCmd, 41 | MessageTaskStopAllCmd, 42 | MessageTaskStopCmd, 43 | SetTestModeCmd, 44 | VersionCmd, 45 | ) 46 | from telegram_periodic_msg_bot.config.config_object import ConfigObject 47 | from telegram_periodic_msg_bot.logger.logger import Logger 48 | from telegram_periodic_msg_bot.translator.translation_loader import TranslationLoader 49 | 50 | 51 | # 52 | # Enumerations 53 | # 54 | 55 | # Command types 56 | @unique 57 | class CommandTypes(Enum): 58 | START_CMD = auto() 59 | HELP_CMD = auto() 60 | ALIVE_CMD = auto() 61 | SET_TEST_MODE_CMD = auto() 62 | IS_TEST_MODE_CMD = auto() 63 | VERSION_CMD = auto() 64 | MESSAGE_TASK_START_CMD = auto() 65 | MESSAGE_TASK_STOP_CMD = auto() 66 | MESSAGE_TASK_STOP_ALL_CMD = auto() 67 | MESSAGE_TASK_PAUSE_CMD = auto() 68 | MESSAGE_TASK_RESUME_CMD = auto() 69 | MESSAGE_TASK_GET_CMD = auto() 70 | MESSAGE_TASK_SET_CMD = auto() 71 | MESSAGE_TASK_DELETE_LAST_MSG_CMD = auto() 72 | MESSAGE_TASK_INFO_CMD = auto() 73 | 74 | 75 | # 76 | # Classes 77 | # 78 | 79 | # Comstant for command dispatcher class 80 | class CommandDispatcherConst: 81 | # Command to class map 82 | CMD_TYPE_TO_CLASS: Dict[CommandTypes, Type[CommandBase]] = { 83 | CommandTypes.START_CMD: HelpCmd, 84 | CommandTypes.HELP_CMD: HelpCmd, 85 | CommandTypes.ALIVE_CMD: AliveCmd, 86 | CommandTypes.SET_TEST_MODE_CMD: SetTestModeCmd, 87 | CommandTypes.IS_TEST_MODE_CMD: IsTestModeCmd, 88 | CommandTypes.VERSION_CMD: VersionCmd, 89 | CommandTypes.MESSAGE_TASK_START_CMD: MessageTaskStartCmd, 90 | CommandTypes.MESSAGE_TASK_STOP_CMD: MessageTaskStopCmd, 91 | CommandTypes.MESSAGE_TASK_STOP_ALL_CMD: MessageTaskStopAllCmd, 92 | CommandTypes.MESSAGE_TASK_PAUSE_CMD: MessageTaskPauseCmd, 93 | CommandTypes.MESSAGE_TASK_RESUME_CMD: MessageTaskResumeCmd, 94 | CommandTypes.MESSAGE_TASK_GET_CMD: MessageTaskGetCmd, 95 | CommandTypes.MESSAGE_TASK_SET_CMD: MessageTaskSetCmd, 96 | CommandTypes.MESSAGE_TASK_DELETE_LAST_MSG_CMD: MessageTaskDeleteLastMsgCmd, 97 | CommandTypes.MESSAGE_TASK_INFO_CMD: MessageTaskInfoCmd, 98 | } 99 | 100 | 101 | # Command dispatcher class 102 | class CommandDispatcher: 103 | 104 | config: ConfigObject 105 | logger: Logger 106 | translator: TranslationLoader 107 | 108 | # Constructor 109 | def __init__(self, 110 | config: ConfigObject, 111 | logger: Logger, 112 | translator: TranslationLoader) -> None: 113 | self.config = config 114 | self.logger = logger 115 | self.translator = translator 116 | 117 | # Dispatch command 118 | def Dispatch(self, 119 | client: pyrogram.Client, 120 | message: pyrogram.types.Message, 121 | cmd_type: CommandTypes, 122 | **kwargs: Any) -> None: 123 | if not isinstance(cmd_type, CommandTypes): 124 | raise TypeError("Command type is not an enumerative of CommandTypes") 125 | 126 | # Log 127 | self.logger.GetLogger().info(f"Dispatching command type: {cmd_type}") 128 | 129 | # Create and execute command if existent 130 | if cmd_type in CommandDispatcherConst.CMD_TYPE_TO_CLASS: 131 | cmd_class = CommandDispatcherConst.CMD_TYPE_TO_CLASS[cmd_type](client, 132 | self.config, 133 | self.logger, 134 | self.translator) 135 | cmd_class.Execute(message, **kwargs) 136 | -------------------------------------------------------------------------------- /pyproject_legacy.toml: -------------------------------------------------------------------------------- 1 | # 2 | # pyproject.toml with the old license syntax, compatible with older versions of setuptools 3 | # Use this to install the package from the archive or repo for Python 3.7 and 3.8 4 | # Just rename this file to `pyproject.toml`, overwriting the existent one, and install as usual with pip: 5 | # 6 | # pip install . 7 | # 8 | # NOTE: This file is only for installing the package from the folder. 9 | # You DON'T need it if you install the package from PyPi (i.e. pip install telegram_periodic_msg_bot) 10 | # 11 | [build-system] 12 | requires = ["setuptools>=61", "wheel"] 13 | build-backend = "setuptools.build_meta" 14 | 15 | [project] 16 | name = "telegram_periodic_msg_bot" 17 | dynamic = ["version", "dependencies"] 18 | authors = [ 19 | {name = "Emanuele Bellocchia", email = "ebellocchia@gmail.com"} 20 | ] 21 | maintainers = [ 22 | {name = "Emanuele Bellocchia", email = "ebellocchia@gmail.com"} 23 | ] 24 | description = "Telegram bot for sending periodic messages" 25 | readme = "README.md" 26 | license = {text = "MIT"} 27 | requires-python = ">=3.7" 28 | keywords = ["telegram", "bot", "telegram bot", "periodic messages"] 29 | classifiers = [ 30 | "Programming Language :: Python :: 3.7", 31 | "Programming Language :: Python :: 3.8", 32 | "Programming Language :: Python :: 3.9", 33 | "Programming Language :: Python :: 3.10", 34 | "Programming Language :: Python :: 3.11", 35 | "Programming Language :: Python :: 3.12", 36 | "Programming Language :: Python :: 3.13", 37 | "Development Status :: 5 - Production/Stable", 38 | "License :: OSI Approved :: MIT License", 39 | "Operating System :: OS Independent", 40 | "Intended Audience :: Developers", 41 | ] 42 | 43 | [project.urls] 44 | Homepage = "https://github.com/ebellocchia/telegram_periodic_msg_bot" 45 | Changelog = "https://github.com/ebellocchia/telegram_periodic_msg_bot/blob/master/CHANGELOG.md" 46 | Repository = "https://github.com/ebellocchia/telegram_periodic_msg_bot" 47 | Download = "https://github.com/ebellocchia/telegram_periodic_msg_bot/archive/v{version}.tar.gz" 48 | 49 | [tool.setuptools] 50 | packages = {find = {}} 51 | 52 | [tool.setuptools.package-data] 53 | telegram_periodic_msg_bot = ["lang/lang_en.xml"] 54 | 55 | [tool.setuptools.dynamic] 56 | version = {attr = "telegram_periodic_msg_bot._version.__version__"} 57 | dependencies = {file = ["requirements.txt"]} 58 | optional-dependencies.develop = {file = ["requirements-dev.txt"]} 59 | 60 | # 61 | # Tools configuration 62 | # 63 | 64 | [tool.ruff] 65 | target-version = "py37" 66 | line-length = 140 67 | exclude = [ 68 | ".github", 69 | ".eggs", 70 | ".egg-info", 71 | ".idea", 72 | ".mypy_cache", 73 | ".tox", 74 | "build", 75 | "dist", 76 | "venv", 77 | ] 78 | 79 | [tool.ruff.lint] 80 | select = [ 81 | "E", # pycodestyle errors 82 | "W", # pycodestyle warnings 83 | "F", # pyflakes 84 | "I", # pyflakes 85 | "N", # pep8-naming 86 | "D", # pydocstyle 87 | "UP", # pyupgrade 88 | "C90", # mccabe complexity 89 | "PL", # pylint 90 | ] 91 | ignore = [ 92 | "N802", # Function name should be lowercase 93 | "E231", # Missing whitespace after ':' 94 | "F821", # Undefined name (Literal import for Python 3.7 compatibility) 95 | "UP006", # Use `type` instead of `Type` for type annotation (Python <3.9 compatibility) 96 | "UP007", # Use `X | Y` for type annotations (Python <3.10 compatibility) 97 | "UP037", # Remove quotes from type annotation (Literal import for Python 3.7 compatibility) 98 | "UP045", # Use `X | None` for type annotations (Python <3.10 compatibility) 99 | # pydocstyle 100 | "D100", # Missing docstring 101 | "D101", # Missing docstring 102 | "D102", # Missing docstring 103 | "D103", # Missing docstring 104 | "D104", # Missing docstring 105 | "D105", # Missing docstring 106 | "D107", # Missing docstring 107 | "D202", # No blank lines allowed after function docstring 108 | "D203", # 1 blank line required before class docstring 109 | "D205", # 1 blank line required between summary line and description 110 | "D212", # Multi-line docstring summary should start at the first line 111 | "D406", # Section name should end with a newline 112 | "D407", # Missing dashed underline after section 113 | "D413", # Missing blank line after last section 114 | "D415", # First line should end with a period, question mark, or exclamation point 115 | "D417", # Missing argument description in the docstring: **kwargs 116 | # pylint 117 | "PLR0911", # Too many return statements 118 | "PLR0912", # Too many branches 119 | "PLR0913", # Too many arguments 120 | "PLR0915", # Too many statements 121 | "PLR2004", # Magic value used in comparison 122 | ] 123 | 124 | [tool.ruff.lint.per-file-ignores] 125 | "__init__.py" = ["F401", "D104"] # Imported but unused, missing docstring 126 | "app/bot.py" = ["UP031"] # Use format specifiers instead of percent format 127 | 128 | [tool.ruff.lint.isort] 129 | known-first-party = [] 130 | lines-after-imports = 2 131 | combine-as-imports = false 132 | force-single-line = false 133 | 134 | [tool.ruff.lint.pydocstyle] 135 | convention = "google" 136 | 137 | [tool.ruff.lint.mccabe] 138 | max-complexity = 10 139 | 140 | [tool.mypy] 141 | python_version = "3.7" 142 | ignore_missing_imports = true 143 | follow_imports = "skip" 144 | exclude = [ 145 | "\\.github", 146 | "\\.eggs", 147 | "\\.egg-info", 148 | "\\.idea", 149 | "\\.ruff_cache", 150 | "\\.tox", 151 | "build", 152 | "dist", 153 | "venv", 154 | ] 155 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/bot/bot_config.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | import logging 25 | 26 | from telegram_periodic_msg_bot.bot.bot_config_types import BotConfigTypes 27 | from telegram_periodic_msg_bot.config.config_typing import ConfigSectionsType 28 | from telegram_periodic_msg_bot.utils.key_value_converter import KeyValueConverter 29 | from telegram_periodic_msg_bot.utils.utils import Utils 30 | 31 | 32 | # 33 | # Variables 34 | # 35 | 36 | # Logging level converter 37 | LoggingLevelConverter = KeyValueConverter({ 38 | "DEBUG": logging.DEBUG, 39 | "INFO": logging.INFO, 40 | "WARNING": logging.WARNING, 41 | "ERROR": logging.ERROR, 42 | "CRITICAL": logging.CRITICAL, 43 | }) 44 | 45 | 46 | # Bot configuration 47 | BotConfig: ConfigSectionsType = { 48 | # Pyrogram 49 | "pyrogram": [ 50 | { 51 | "type": BotConfigTypes.API_ID, 52 | "name": "api_id", 53 | }, 54 | { 55 | "type": BotConfigTypes.API_HASH, 56 | "name": "api_hash", 57 | }, 58 | { 59 | "type": BotConfigTypes.BOT_TOKEN, 60 | "name": "bot_token", 61 | }, 62 | { 63 | "type": BotConfigTypes.SESSION_NAME, 64 | "name": "session_name", 65 | }, 66 | ], 67 | # App 68 | "app": [ 69 | { 70 | "type": BotConfigTypes.APP_TEST_MODE, 71 | "name": "app_test_mode", 72 | "conv_fct": Utils.StrToBool, 73 | }, 74 | { 75 | "type": BotConfigTypes.APP_LANG_FILE, 76 | "name": "app_lang_file", 77 | "def_val": None, 78 | }, 79 | ], 80 | # Task 81 | "task": [ 82 | { 83 | "type": BotConfigTypes.TASKS_MAX_NUM, 84 | "name": "tasks_max_num", 85 | "conv_fct": Utils.StrToInt, 86 | "def_val": 20, 87 | "valid_if": lambda cfg, val: val > 0, 88 | }, 89 | ], 90 | # Message 91 | "message": [ 92 | { 93 | "type": BotConfigTypes.MESSAGE_MAX_LEN, 94 | "name": "message_max_len", 95 | "conv_fct": Utils.StrToInt, 96 | "def_val": 4000, 97 | "valid_if": lambda cfg, val: val > 0, 98 | }, 99 | ], 100 | # Logging 101 | "logging": [ 102 | { 103 | "type": BotConfigTypes.LOG_LEVEL, 104 | "name": "log_level", 105 | "conv_fct": LoggingLevelConverter.KeyToValue, 106 | "print_fct": LoggingLevelConverter.ValueToKey, 107 | "def_val": logging.INFO, 108 | }, 109 | { 110 | "type": BotConfigTypes.LOG_CONSOLE_ENABLED, 111 | "name": "log_console_enabled", 112 | "conv_fct": Utils.StrToBool, 113 | "def_val": True, 114 | }, 115 | { 116 | "type": BotConfigTypes.LOG_FILE_ENABLED, 117 | "name": "log_file_enabled", 118 | "conv_fct": Utils.StrToBool, 119 | "def_val": False, 120 | }, 121 | { 122 | "type": BotConfigTypes.LOG_FILE_NAME, 123 | "name": "log_file_name", 124 | "load_if": lambda cfg: cfg.GetValue(BotConfigTypes.LOG_FILE_ENABLED), 125 | }, 126 | { 127 | "type": BotConfigTypes.LOG_FILE_USE_ROTATING, 128 | "name": "log_file_use_rotating", 129 | "conv_fct": Utils.StrToBool, 130 | "load_if": lambda cfg: cfg.GetValue(BotConfigTypes.LOG_FILE_ENABLED), 131 | }, 132 | { 133 | "type": BotConfigTypes.LOG_FILE_APPEND, 134 | "name": "log_file_append", 135 | "conv_fct": Utils.StrToBool, 136 | "load_if": lambda cfg: (cfg.GetValue(BotConfigTypes.LOG_FILE_ENABLED) and 137 | not cfg.GetValue(BotConfigTypes.LOG_FILE_USE_ROTATING)), 138 | }, 139 | { 140 | "type": BotConfigTypes.LOG_FILE_MAX_BYTES, 141 | "name": "log_file_max_bytes", 142 | "conv_fct": Utils.StrToInt, 143 | "load_if": lambda cfg: (cfg.GetValue(BotConfigTypes.LOG_FILE_ENABLED) and 144 | cfg.GetValue(BotConfigTypes.LOG_FILE_USE_ROTATING)), 145 | }, 146 | { 147 | "type": BotConfigTypes.LOG_FILE_BACKUP_CNT, 148 | "name": "log_file_backup_cnt", 149 | "conv_fct": Utils.StrToInt, 150 | "load_if": lambda cfg: (cfg.GetValue(BotConfigTypes.LOG_FILE_ENABLED) and 151 | cfg.GetValue(BotConfigTypes.LOG_FILE_USE_ROTATING)), 152 | }, 153 | ], 154 | } 155 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/command/command_base.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | from abc import ABC, abstractmethod 25 | from typing import Any 26 | 27 | import pyrogram 28 | from pyrogram.errors import RPCError 29 | from pyrogram.errors.exceptions.bad_request_400 import BadRequest 30 | 31 | from telegram_periodic_msg_bot.command.command_data import CommandData 32 | from telegram_periodic_msg_bot.config.config_object import ConfigObject 33 | from telegram_periodic_msg_bot.logger.logger import Logger 34 | from telegram_periodic_msg_bot.message.message_sender import MessageSender 35 | from telegram_periodic_msg_bot.misc.chat_members import ChatMembersGetter 36 | from telegram_periodic_msg_bot.misc.helpers import ChatHelper, UserHelper 37 | from telegram_periodic_msg_bot.translator.translation_loader import TranslationLoader 38 | 39 | 40 | # 41 | # Classes 42 | # 43 | 44 | # 45 | # Generic command base class 46 | # 47 | class CommandBase(ABC): 48 | 49 | client: pyrogram.Client 50 | config: ConfigObject 51 | logger: Logger 52 | translator: TranslationLoader 53 | message: pyrogram.types.Message 54 | cmd_data: CommandData 55 | message_sender: MessageSender 56 | 57 | # Constructor 58 | def __init__(self, 59 | client: pyrogram.Client, 60 | config: ConfigObject, 61 | logger: Logger, 62 | translator: TranslationLoader) -> None: 63 | self.client = client 64 | self.config = config 65 | self.logger = logger 66 | self.translator = translator 67 | # Helper classes 68 | self.message_sender = MessageSender(client, logger) 69 | 70 | # Execute command 71 | def Execute(self, 72 | message: pyrogram.types.Message, 73 | **kwargs: Any) -> None: 74 | self.message = message 75 | self.cmd_data = CommandData(message) 76 | 77 | # Log command 78 | self.__LogCommand() 79 | 80 | # Check if user is anonymous 81 | if self._IsUserAnonymous() and not self._IsChannel(): 82 | self.logger.GetLogger().warning("An anonymous user tried to execute the command, exiting") 83 | return 84 | 85 | # Check if user is authorized 86 | if not self._IsUserAuthorized(): 87 | if self._IsPrivateChat(): 88 | self._SendMessage(self.translator.GetSentence("AUTH_ONLY_ERR_MSG")) 89 | 90 | self.logger.GetLogger().warning( 91 | f"User {UserHelper.GetNameOrId(self.cmd_data.User())} tried to execute the command but it's not authorized" 92 | ) 93 | return 94 | 95 | # Try to execute command 96 | try: 97 | self._ExecuteCommand(**kwargs) 98 | except RPCError: 99 | self._SendMessage(self.translator.GetSentence("GENERIC_ERR_MSG")) 100 | self.logger.GetLogger().exception( 101 | f"An error occurred while executing command {self.cmd_data.Name()}" 102 | ) 103 | 104 | # Send message 105 | def _SendMessage(self, 106 | msg: str) -> None: 107 | try: 108 | self.message_sender.SendMessage( 109 | self.cmd_data.Chat(), 110 | msg, 111 | reply_to_message_id=self.message.reply_to_message_id 112 | ) 113 | # Send message privately if topic is closed 114 | except BadRequest: 115 | self.message_sender.SendMessage(self.cmd_data.User(), msg) 116 | 117 | # Get if channel 118 | def _IsChannel(self) -> bool: 119 | return ChatHelper.IsChannel(self.cmd_data.Chat()) 120 | 121 | # Get if user is anonymous 122 | def _IsUserAnonymous(self) -> bool: 123 | return self.cmd_data.User() is None 124 | 125 | # Get if user is authorized 126 | def _IsUserAuthorized(self) -> bool: 127 | # In channels only admins can write, so we consider the user authorized since there is no way to know the specific user 128 | # This is a limitation for channels only 129 | if self._IsChannel(): 130 | return True 131 | 132 | # Anonymous user 133 | cmd_user = self.cmd_data.User() 134 | if cmd_user is None: 135 | return False 136 | # Private chat is always authorized 137 | if ChatHelper.IsPrivateChat(self.cmd_data.Chat(), cmd_user): 138 | return True 139 | # Check if admin 140 | admin_members = ChatMembersGetter(self.client).GetAdmins(self.cmd_data.Chat()) 141 | return any(cmd_user.id == member.user.id for member in admin_members if member.user is not None) 142 | 143 | # Get if chat is private 144 | def _IsPrivateChat(self) -> bool: 145 | cmd_user = self.cmd_data.User() 146 | if cmd_user is None: 147 | return False 148 | return ChatHelper.IsPrivateChat(self.cmd_data.Chat(), cmd_user) 149 | 150 | # Log command 151 | def __LogCommand(self) -> None: 152 | self.logger.GetLogger().info(f"Command: {self.cmd_data.Name()}") 153 | self.logger.GetLogger().info(f"Executed by user: {UserHelper.GetNameOrId(self.cmd_data.User())}") 154 | self.logger.GetLogger().debug(f"Received message: {self.message}") 155 | 156 | # Execute command - Abstract method 157 | @abstractmethod 158 | def _ExecuteCommand(self, 159 | **kwargs: Any) -> None: 160 | pass 161 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/lang/lang_en.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | **HELP** 5 | Hi {name}, 6 | welcome to the Telegram Periodic Message Bot. 7 | 8 | ℹ️ Here is the list of supported commands. 9 | 10 | 🔘 **/help** : show this message 11 | 🔘 **/alive** : show if bot is active 12 | 🔘 **/msgbot_set_test_mode** __true/false__ : enable/disable test mode 13 | 🔘 **/msgbot_is_test_mode** : show if test mode is enabled 14 | 🔘 **/msgbot_version** : show the bot version 15 | 🔘 **/msgbot_task_start** __MSG_ID PERIOD_HOURS [START_HOUR] MSG__ : start a message task in the current chat (the message shall be in a new line) 16 | 🔘 **/msgbot_task_stop** __MSG_ID__ : stop the specified message task in the current chat 17 | 🔘 **/msgbot_task_stop_all** : stop all message tasks in the current chat 18 | 🔘 **/msgbot_task_pause** __MSG_ID__ : pause the specified message task in the current chat 19 | 🔘 **/msgbot_task_resume** __MSG_ID__ : resume the specified message task in the current chat 20 | 🔘 **/msgbot_task_get** __MSG_ID__ : show the message set for the specified message task in the current chat 21 | 🔘 **/msgbot_task_set** __MSG_ID MSG__ : set the message of the specified message task in the current chat (the message shall be in a new line) 22 | 🔘 **/msgbot_task_delete_last_msg** __MSG_ID true/false__ : enable/disable the deletion of last messages for the specified message task in the current chat 23 | 🔘 **/msgbot_task_info** : show the list of active message tasks in the current chat 24 | 25 | Parameters in square brakets are optional. 26 | 27 | **STATUS** 28 | ✅ The bot is alive. 29 | 30 | 31 | **TEST MODE** 32 | ✅ Test mode enabled. 33 | 34 | **TEST MODE** 35 | ✅ Test mode disabled. 36 | 37 | 38 | **TEST MODE** 39 | ℹ️ Test mode is currently enabled. 40 | 41 | **TEST MODE** 42 | ℹ️ Test mode is currently disabled. 43 | 44 | 45 | **Telegram Periodic Message Bot** 46 | 47 | Author: Emanuele Bellocchia (ebellocchia@gmail.com) 48 | Version: **{version}** 49 | 50 | 51 | **TASK CONTROL** 52 | ✅ Message task successfully started. 53 | 54 | Parameters: 55 | - Period: __{period}h__ 56 | - Start: __{start:02d}:00__ 57 | - Message ID: __{msg_id}__ 58 | 59 | 60 | **TASK CONTROL** 61 | ✅ Message task __{msg_id}__ successfully stopped. 62 | 63 | 64 | **TASK CONTROL** 65 | ✅ All message tasks successfully stopped. 66 | 67 | 68 | **TASK CONTROL** 69 | ✅ Message task __{msg_id}__ successfully paused. 70 | 71 | 72 | **TASK CONTROL** 73 | ✅ Message task __{msg_id}__ successfully resumed. 74 | 75 | 76 | **TASK CONTROL** 77 | Message set for task __{msg_id}__: 78 | 79 | {msg} 80 | 81 | **TASK CONTROL** 82 | No message set for task __{msg_id}__. 83 | 84 | 85 | **TASK CONTROL** 86 | ✅ Message successfully set for task __{msg_id}__. 87 | 88 | 89 | **TASK CONTROL** 90 | ✅ Message task __{msg_id}__ delete last message set to: {flag}. 91 | 92 | 93 | **TASKS INFO** 94 | Number of active tasks in this chat: **{tasks_num}** 95 | Tasks list: 96 | {tasks_list} 97 | 98 | **TASKS INFO** 99 | No task is active in this chat. 100 | 101 | 102 | Hi! 103 | Thanks for choosing the **Telegram Periodic Message Bot**. 104 | Use __/help__ to see the list of supported commands. 105 | Do not forget to **make me administrator of the group** or I won't work properly. 106 | 107 | 108 | **ERROR** 109 | ❌ An error occurred while executing command. 110 | 111 | **ERROR** 112 | ❌ You are not authorized to use the command. 113 | 114 | **ERROR** 115 | ❌ This command can be executed only in the chat group. 116 | 117 | **ERROR** 118 | ❌ Invalid parameters. 119 | 120 | **ERROR** 121 | ❌ Invalid message. 122 | 123 | **ERROR** 124 | ❌ Message is too long (maximum accepted length: {msg_max_len}). 125 | 126 | **ERROR** 127 | ❌ Message task __{msg_id}__ is already active in this chat. Stop the task to start a new one with the same ID again. 128 | 129 | **ERROR** 130 | ❌ Message task __{msg_id}__ is not active in this chat. 131 | 132 | **ERROR** 133 | ❌ Period shall be between 1 and 24. 134 | 135 | **ERROR** 136 | ❌ Start hour shall be between 0 and 23. 137 | 138 | **ERROR** 139 | ❌ Maximum number of tasks reached. Stop some tasks to start new ones. 140 | 141 | - ID: __{msg_id}__, period: __{period}h__, start: __{start:02d}:00__, state: __{state}__ 142 | 143 | runnnig 144 | 145 | paused 146 | 147 | -------------------------------------------------------------------------------- /app/lang/lang_it.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | **MENU DI AIUTO** 5 | Ciao {name}, 6 | benvenuto nel Telegram Periodic Message Bot. 7 | 8 | ℹ️ Qui trovi la lista dei comandi supportati. 9 | 10 | 🔘 **/help** : mostra questo messaggio 11 | 🔘 **/alive** : mostra se il bot è attivo 12 | 🔘 **/msgbot_set_test_mode** __true/false__ : attiva/disattiva la modalità di test 13 | 🔘 **/msgbot_is_test_mode** : mostra se la modalità di test è attiva 14 | 🔘 **/msgbot_version** : mostra la versione del bot 15 | 🔘 **/msgbot_task_start** __MSG_ID PERIOD_HOURS [START_HOUR] MSG__ : avvia un task di avviso nella chat corrente (il messaggio deve essere su una linea a capo) 16 | 🔘 **/msgbot_task_stop** __MSG_ID__ : ferma il task specificato nella chat corrente 17 | 🔘 **/msgbot_task_stop_all** : ferma tutti i task nella chat corrente 18 | 🔘 **/msgbot_task_pause** __MSG_ID__ : mette in pausa il task specificato nella chat corrente 19 | 🔘 **/msgbot_task_resume** __MSG_ID__ : riavvia il task specificato nella chat corrente 20 | 🔘 **/msgbot_task_get** __MSG_ID__ : mostra il messaggio impostato per il task specificato nella chat corrente 21 | 🔘 **/msgbot_task_set** __MSG_ID MSG__ : imposta il messaggio del task specificato nella chat corrente (il messaggio deve essere su una linea a capo) 22 | 🔘 **/msgbot_task_delete_last_msg** __MSG_ID true/false__ : attiva/disattiva la rimozione degli ultimi messaggi inviati per il task specificato nella chat corrente 23 | 🔘 **/msgbot_task_info** : mostra la lista di tutti i task attivi nella chat corrente 24 | 25 | I parametri tra parentesi quadre sono opzionali. 26 | 27 | **STATO** 28 | ✅ Il bot è attivo. 29 | 30 | 31 | **MODALITÀ TEST** 32 | ✅ Modalità di test attivata. 33 | SINGLE_TASK_INFO_MSG 34 | **MODALITÀ TEST** 35 | ✅ Modalità di test disattivata. 36 | 37 | 38 | **MODALITÀ TEST** 39 | ℹ️ La modalità di test è attualmente attivata. 40 | 41 | **MODALITÀ TEST** 42 | ℹ️ La modalità di test è attualmente disattivata. 43 | 44 | 45 | **Telegram Periodic Message Bot** 46 | 47 | Autore: Emanuele Bellocchia (ebellocchia@gmail.com) 48 | Versione: **{version}** 49 | 50 | 51 | **CONTROLLO TASK** 52 | ✅ Task di avviso avviato con successo. 53 | 54 | Parametri: 55 | - Periodo: __{period}h__ 56 | - Inizio: __{start:02d}:00__ 57 | - ID messaggio: __{msg_id}__ 58 | 59 | 60 | **CONTROLLO TASK** 61 | ✅ Task di avviso __{msg_id}__ fermato con successo. 62 | 63 | 64 | **CONTROLLO TASK** 65 | ✅ Tutti i task di avviso fermati con successo. 66 | 67 | 68 | **CONTROLLO TASK** 69 | ✅ Task di avviso __{msg_id}__ messo in pausa con successo. 70 | 71 | 72 | **CONTROLLO TASK** 73 | ✅ Task di avviso __{msg_id}__ riavviato con successo. 74 | 75 | 76 | 77 | **CONTROLLO TASK** 78 | Messaggio impostato per il task di avviso __{msg_id}__: 79 | 80 | {msg} 81 | 82 | **CONTROLLO TASK** 83 | Nessun messaggio impostato pr il task di avviso __{msg_id}__. 84 | 85 | 86 | **CONTROLLO TASK** 87 | ✅ Messaggio impostato con successo per il task di avviso __{msg_id}__. 88 | 89 | 90 | **CONTROLLO TASK** 91 | ✅ Task di avviso __{msg_id}__ cancella ultimo messaggio impostato a: {flag}. 92 | 93 | 94 | **INFORMAZIONI TASK** 95 | Numero di task attivi in questa chat: **{tasks_num}** 96 | Lista dei task: 97 | {tasks_list} 98 | 99 | **INFORMAZIONI TASK** 100 | Nessun task attivo in questa chat. 101 | 102 | 103 | Ciao! 104 | Grazie per aver scelto il **Telegram Periodic Message Bot**. 105 | Usa __/help__ per vedere la lista dei comandi supportati. 106 | Non dimenticarti di **farmi amministratore del gruppo** o non potrò funzionare correttamente. 107 | 108 | 109 | **ERRORE** 110 | ❌ Si è verificato un errore durante l'esecuzione del comando. 111 | 112 | **ERRORE** 113 | ❌ Non sei autorizzato a utilizzare il comando. 114 | 115 | **ERRORE** 116 | ❌ Questo comando può essere eseguito solo nel gruppo. 117 | 118 | **ERRORE** 119 | ❌ Parametri non validi. 120 | 121 | **ERRORE** 122 | ❌ Messaggio non valido. 123 | 124 | **ERRORE** 125 | ❌ Il messaggio è troppo lungo (lunghezza massima consentita: {msg_max_len}). 126 | 127 | **ERRORE** 128 | ❌ Task di avviso __{msg_id}__ già attivo in questa chat. Ferma il task per avviarne un altro con lo stesso ID. 129 | 130 | **ERRORE** 131 | ❌ Task di avviso __{msg_id}__ non attivo in questa chat. 132 | 133 | **ERRORE** 134 | ❌ Il periodo deve essere compreso tra 1 e 24. 135 | 136 | **ERRORE** 137 | ❌ L'ora di inizio deve essere compresa tra 0 e 23. 138 | 139 | **ERRORE** 140 | ❌ Massimo numero di task raggiunto. Ferma qualche task per avviarne dei nuovi. 141 | 142 | - ID: __{msg_id}__, periodo: __{period}h__, inizio: __{start:02d}:00__, stato: __{state}__ 143 | 144 | attivo 145 | 146 | in pausa 147 | 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telegram Periodic Message Bot 2 | 3 | | | 4 | |---| 5 | | [![PyPI - Version](https://img.shields.io/pypi/v/telegram_periodic_msg_bot.svg?logo=pypi&label=PyPI&logoColor=gold)](https://pypi.org/project/telegram_periodic_msg_bot/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/telegram_periodic_msg_bot.svg?logo=python&label=Python&logoColor=gold)](https://pypi.org/project/telegram_periodic_msg_bot/) [![GitHub License](https://img.shields.io/github/license/ebellocchia/telegram_periodic_msg_bot?label=License)](https://github.com/ebellocchia/telegram_periodic_msg_bot?tab=MIT-1-ov-file) | 6 | | [![Build](https://github.com/ebellocchia/telegram_periodic_msg_bot/actions/workflows/build.yml/badge.svg)](https://github.com/ebellocchia/telegram_periodic_msg_bot/actions/workflows/build.yml) [![Code Analysis](https://github.com/ebellocchia/telegram_periodic_msg_bot/actions/workflows/code-analysis.yml/badge.svg)](https://github.com/ebellocchia/telegram_periodic_msg_bot/actions/workflows/code-analysis.yml) | 7 | | [![Codacy grade](https://img.shields.io/codacy/grade/d1cc7c1692de4939a23e626981923e83?label=Codacy%20Grade)](https://app.codacy.com/gh/ebellocchia/telegram_periodic_msg_bot/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/ebellocchia/telegram_periodic_msg_bot?label=CodeFactor%20Grade)](https://www.codefactor.io/repository/github/ebellocchia/telegram_periodic_msg_bot) | 8 | | | 9 | 10 | ## Introduction 11 | 12 | Telegram bot for sending periodic messages in groups based on *pyrogram*.\ 13 | A single bot instance can be used with multiple periodic messages (with different periods) and in multiple groups. 14 | 15 | ## Setup 16 | 17 | ### Create Telegram app 18 | 19 | In order to use the bot, in addition to the bot token you also need an APP ID and hash.\ 20 | To get them, create an app using the following website: [https://my.telegram.org/apps](https://my.telegram.org/apps). 21 | 22 | ### Installation 23 | 24 | The package requires Python >= 3.7.\ 25 | To install it: 26 | 27 | pip install telegram_periodic_msg_bot 28 | 29 | To run the bot, edit the configuration file by specifying the API ID/hash and bot token. Then, move to the *app* folder and run the *bot.py* script: 30 | 31 | cd app 32 | python bot.py 33 | 34 | When run with no parameter, *conf/config.ini* will be the default configuration file (in this way it can be used for different groups).\ 35 | To specify a different configuration file: 36 | 37 | python bot.py -c another_conf.ini 38 | python bot.py --config another_conf.ini 39 | 40 | Of course, the *app* folder can be moved elsewhere if needed. 41 | 42 | To run code analysis: 43 | 44 | mypy . 45 | ruff check . 46 | 47 | ## Configuration 48 | 49 | An example of configuration file is provided in the *app/conf* folder.\ 50 | The list of all possible fields that can be set is shown below. 51 | 52 | |Name|Description| 53 | |---|---| 54 | |**[pyrogram]**|Configuration for pyrogram| 55 | |`session_name`|Name of the file used to store the session| 56 | |`api_id`|API ID from [https://my.telegram.org/apps](https://my.telegram.org/apps)| 57 | |`api_hash`|API hash from [https://my.telegram.org/apps](https://my.telegram.org/apps)| 58 | |`bot_token`|Bot token from BotFather| 59 | |**[app]**|Configuration for app| 60 | |`app_is_test_mode`|True to activate test mode false otherwise| 61 | |`app_lang_file`|Language file in XML format (default: English)| 62 | |**[task]**|Configuration for tasks| 63 | |`tasks_max_num`|Maximum number of running tasks (totally, in all groups). Default: 20.| 64 | |**[message]**|Configuration for message| 65 | |`message_max_len`|Maximum message length in characters. Default: 4000.| 66 | |**[logging]**|Configuration for logging| 67 | |`log_level`|Log level, same of python logging (`DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`). Default: `INFO`.| 68 | |`log_console_enabled`|True to enable logging to console, false otherwise (default: `true`)| 69 | |`log_file_enabled`|True to enable logging to file, false otherwise (default: `false`). If false, all the next fields will be skipped.| 70 | |`log_file_name`|Log file name| 71 | |`log_file_use_rotating`|True for using a rotating log file, false otherwise| 72 | |`log_file_max_bytes`|Maximum size in bytes for a log file. When reached, a new log file is created up to `log_file_backup_cnt`. Valid only if `log_file_use_rotating` is true.| 73 | |`log_file_backup_cnt`|Maximum number of log files. Valid only if `log_file_use_rotating` is true.| 74 | |`log_file_append`|True to append to log file, false to start from a new file each time. Valid only if `log_file_use_rotating` is false.| 75 | 76 | ## Supported Commands 77 | 78 | List of supported commands: 79 | - `help`: show this message 80 | - `alive`: show if bot is active 81 | - `msgbot_set_test_mode true/false`: enable/disable test mode 82 | - `msgbot_is_test_mode`: show if test mode is enabled 83 | - `msgbot_version`: show bot version 84 | - `msgbot_task_start MSG_ID PERIOD_HOURS [START_HOUR] MSG`: start a message task in the current chat. If the task `MSG_ID` already exists in the current chat, an error message will be shown. To start it again, it shall be stopped with the `msgbot_task_stop` command.\ 85 | Parameters: 86 | - `MSG_ID`: Message ID 87 | - `PERIOD_HOURS`: Task period in hours, it shall be between 1 and 24 88 | - `START_HOUR` (optional): Task start hour, it shall be between 0 and 23. Default value: 0. 89 | - `MSG`: Message to be sent periodically, it shall be on a new line 90 | - `msgbot_task_stop MSG_ID`: stop the specified message task in the current chat. If the task `MSG_ID` does not exist in the current chat, an error message will be shown.\ 91 | Parameters: 92 | - `MSG_ID`: CoinGecko *ID* 93 | - `msgbot_task_stop_all`: stop all message tasks in the current chat 94 | - `msgbot_task_pause MSG_ID`: pause the specified message task in the current chat. If the task `MSG_ID` does not exist in the current chat, an error message will be shown.\ 95 | Parameters: 96 | - `MSG_ID`: Message ID 97 | - `msgbot_task_resume MSG_ID`: resume the specified message task in the current chat. If the task `MSG_ID` does not exist in the current chat, an error message will be shown.\ 98 | Parameters: 99 | - `MSG_ID`: Message ID 100 | - `msgbot_task_get MSG_ID`: show the message set for the specified message task in the current chat.\ 101 | Parameters: 102 | - `MSG_ID`: Message ID 103 | - `msgbot_task_set MSG_ID MSG`: set the message of the specified message task in the current chat.\ 104 | Parameters: 105 | - `MSG_ID`: Message ID 106 | - `MSG`: Message to be sent periodically, it shall be on a new line 107 | - `msgbot_task_delete_last_msg MSG_ID true/false`: enable/disable the deletion of last messages for the specified message task in the current chat. If the task `MSG_ID` does not exist in the current chat, an error message will be shown.\ 108 | Parameters: 109 | - `MSG_ID`: Message ID 110 | - `flag`: true or false 111 | - `msgbot_task_info`: show the list of active message tasks in the current chat 112 | 113 | Messages can contain HTML tags if needed (e.g. for bold/italic text), while Markdown tags are not supported.\ 114 | By default, a message task will delete the last sent message when sending a new one. This can be enabled/disabled with the `msgbot_task_delete_last_msg` command. 115 | 116 | The task period starts from the specified starting hour (be sure to set the correct time on the VPS), for example: 117 | - A task period of 8 hours starting from 00:00 will send the message at: 00:00, 08:00 and 16:00 118 | - A task period of 6 hours starting from 08:00 will send the message at: 08:00, 14:00, 20:00 and 02:00 119 | 120 | **Examples** 121 | 122 | Send a periodical message every 8 hours starting from 00:00 in the current chat: 123 | 124 | /msgbot_task_start test_msg 8 125 | Hi, 126 | This is a periodic message. 127 | Bye! 128 | 129 | Pause/Resume/Stop the previous task: 130 | 131 | /msgbot_task_pause test_msg 132 | /msgbot_task_resume test_msg 133 | /msgbot_task_stop test_msg 134 | 135 | Show the message set for the previous task: 136 | 137 | /msgbot_task_get test_msg 138 | 139 | Set a new message set for the previous task: 140 | 141 | /msgbot_task_set test_msg 142 | Hello, 143 | This is a different periodic message. 144 | Bye bye! 145 | 146 | Set task so that it doesn't delete the last sent message: 147 | 148 | /msgbot_task_delete_last_msg test_msg false 149 | 150 | Send a periodical message every 6 hours starting from 10:00 in the current chat: 151 | 152 | /msgbot_task_start test_msg 6 10 153 | Periodic message with start hour 154 | 155 | ## Run the Bot 156 | 157 | Since the bot deletes the last sent messages, it'd be better if it's an administrator of the group (otherwise the last messages cannot be deleted).\ 158 | In order to send messages periodically, the bot shall run 24h/24h. So, it's suggested to run it on a VPS (there is no performance requirements, so a cheap VPS will suffice). 159 | 160 | ## Test Mode 161 | 162 | During test mode, the bot will work as usual but the task period will be applied in minutes instead of hours. This allows to quickly check if it is working. 163 | 164 | ## Translation 165 | 166 | The messages sent by the bot on Telegram can be translated into different languages (the default language is English) by providing a custom XML file.\ 167 | The XML file path is specified in the configuration file (`app_lang_file` field).\ 168 | An example XML file in italian is provided in the folder *app/lang*. 169 | 170 | # License 171 | 172 | This software is available under the MIT license. 173 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/bot/bot_handlers_config.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | from pyrogram import filters 25 | from pyrogram.handlers import MessageHandler 26 | 27 | from telegram_periodic_msg_bot.bot.bot_handlers_config_typing import BotHandlersConfigType 28 | from telegram_periodic_msg_bot.command.command_dispatcher import CommandTypes 29 | from telegram_periodic_msg_bot.message.message_dispatcher import MessageTypes 30 | 31 | 32 | # 33 | # Classes 34 | # 35 | 36 | # Bot handlers configuration 37 | BotHandlersConfig: BotHandlersConfigType = { 38 | # Handlers for MessageHandler 39 | MessageHandler: [ 40 | 41 | # 42 | # Generic commands 43 | # 44 | 45 | { 46 | "callback": lambda self, client, message: self.DispatchCommand(client, 47 | message, 48 | CommandTypes.START_CMD), 49 | "filters": filters.private & filters.command(["start"]), 50 | }, 51 | { 52 | "callback": lambda self, client, message: self.DispatchCommand(client, 53 | message, 54 | CommandTypes.HELP_CMD), 55 | "filters": filters.command(["help"]), 56 | }, 57 | { 58 | "callback": lambda self, client, message: self.DispatchCommand(client, 59 | message, 60 | CommandTypes.ALIVE_CMD), 61 | "filters": filters.command(["alive"]), 62 | }, 63 | { 64 | "callback": lambda self, client, message: self.DispatchCommand(client, 65 | message, 66 | CommandTypes.SET_TEST_MODE_CMD), 67 | "filters": filters.command(["msgbot_set_test_mode"]), 68 | }, 69 | { 70 | "callback": lambda self, client, message: self.DispatchCommand(client, 71 | message, 72 | CommandTypes.IS_TEST_MODE_CMD), 73 | "filters": filters.command(["msgbot_is_test_mode"]), 74 | }, 75 | { 76 | "callback": lambda self, client, message: self.DispatchCommand(client, 77 | message, 78 | CommandTypes.VERSION_CMD), 79 | "filters": filters.command(["msgbot_version"]), 80 | }, 81 | 82 | # 83 | # Message commands (task) 84 | # 85 | 86 | { 87 | "callback": (lambda self, client, message: self.DispatchCommand(client, 88 | message, 89 | CommandTypes.MESSAGE_TASK_START_CMD, 90 | periodic_msg_scheduler=self.periodic_msg_scheduler)), 91 | "filters": filters.command(["msgbot_task_start"]), 92 | }, 93 | { 94 | "callback": (lambda self, client, message: self.DispatchCommand(client, 95 | message, 96 | CommandTypes.MESSAGE_TASK_STOP_CMD, 97 | periodic_msg_scheduler=self.periodic_msg_scheduler)), 98 | "filters": filters.command(["msgbot_task_stop"]), 99 | }, 100 | { 101 | "callback": (lambda self, client, message: self.DispatchCommand(client, 102 | message, 103 | CommandTypes.MESSAGE_TASK_STOP_ALL_CMD, 104 | periodic_msg_scheduler=self.periodic_msg_scheduler)), 105 | "filters": filters.command(["msgbot_task_stop_all"]), 106 | }, 107 | { 108 | "callback": (lambda self, client, message: self.DispatchCommand(client, 109 | message, 110 | CommandTypes.MESSAGE_TASK_PAUSE_CMD, 111 | periodic_msg_scheduler=self.periodic_msg_scheduler)), 112 | "filters": filters.command(["msgbot_task_pause"]), 113 | }, 114 | { 115 | "callback": (lambda self, client, message: self.DispatchCommand(client, 116 | message, 117 | CommandTypes.MESSAGE_TASK_RESUME_CMD, 118 | periodic_msg_scheduler=self.periodic_msg_scheduler)), 119 | "filters": filters.command(["msgbot_task_resume"]), 120 | }, 121 | { 122 | "callback": (lambda self, client, message: self.DispatchCommand(client, 123 | message, 124 | CommandTypes.MESSAGE_TASK_GET_CMD, 125 | periodic_msg_scheduler=self.periodic_msg_scheduler)), 126 | "filters": filters.command(["msgbot_task_get"]), 127 | }, 128 | { 129 | "callback": (lambda self, client, message: self.DispatchCommand(client, 130 | message, 131 | CommandTypes.MESSAGE_TASK_SET_CMD, 132 | periodic_msg_scheduler=self.periodic_msg_scheduler)), 133 | "filters": filters.command(["msgbot_task_set"]), 134 | }, 135 | { 136 | "callback": (lambda self, client, message: self.DispatchCommand(client, 137 | message, 138 | CommandTypes.MESSAGE_TASK_DELETE_LAST_MSG_CMD, 139 | periodic_msg_scheduler=self.periodic_msg_scheduler)), 140 | "filters": filters.command(["msgbot_task_delete_last_msg"]), 141 | }, 142 | { 143 | "callback": (lambda self, client, message: self.DispatchCommand(client, 144 | message, 145 | CommandTypes.MESSAGE_TASK_INFO_CMD, 146 | periodic_msg_scheduler=self.periodic_msg_scheduler)), 147 | "filters": filters.command(["msgbot_task_info"]), 148 | }, 149 | 150 | # 151 | # Update status messages 152 | # 153 | 154 | { 155 | "callback": (lambda self, client, message: self.HandleMessage(client, 156 | message, 157 | MessageTypes.GROUP_CHAT_CREATED)), 158 | "filters": filters.group_chat_created, 159 | }, 160 | { 161 | "callback": (lambda self, client, message: self.HandleMessage(client, 162 | message, 163 | MessageTypes.NEW_CHAT_MEMBERS)), 164 | "filters": filters.new_chat_members, 165 | }, 166 | { 167 | "callback": (lambda self, client, message: self.HandleMessage(client, 168 | message, 169 | MessageTypes.LEFT_CHAT_MEMBER, 170 | periodic_msg_scheduler=self.periodic_msg_scheduler)), 171 | "filters": filters.left_chat_member, 172 | }, 173 | ], 174 | } 175 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/command/commands.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | from typing import Any, Callable 25 | 26 | from telegram_periodic_msg_bot._version import __version__ 27 | from telegram_periodic_msg_bot.bot.bot_config_types import BotConfigTypes 28 | from telegram_periodic_msg_bot.command.command_base import CommandBase 29 | from telegram_periodic_msg_bot.command.command_data import CommandParameterError 30 | from telegram_periodic_msg_bot.misc.helpers import UserHelper 31 | from telegram_periodic_msg_bot.periodic_msg.periodic_msg_parser import PeriodicMsgParserInvalidError, PeriodicMsgParserTooLongError 32 | from telegram_periodic_msg_bot.periodic_msg.periodic_msg_scheduler import ( 33 | PeriodicMsgJobAlreadyExistentError, 34 | PeriodicMsgJobInvalidPeriodError, 35 | PeriodicMsgJobInvalidStartError, 36 | PeriodicMsgJobMaxNumError, 37 | PeriodicMsgJobNotExistentError, 38 | ) 39 | 40 | 41 | # 42 | # Decorators 43 | # 44 | 45 | # Decorator for group-only commands 46 | def GroupChatOnly(exec_cmd_fct: Callable[..., None]) -> Callable[..., None]: 47 | def decorated(self, 48 | **kwargs: Any): 49 | # Check if private chat 50 | if self._IsPrivateChat(): 51 | self._SendMessage(self.translator.GetSentence("GROUP_ONLY_ERR_MSG")) 52 | else: 53 | exec_cmd_fct(self, **kwargs) 54 | 55 | return decorated 56 | 57 | 58 | # 59 | # Classes 60 | # 61 | 62 | # 63 | # Command for getting help 64 | # 65 | class HelpCmd(CommandBase): 66 | # Execute command 67 | def _ExecuteCommand(self, 68 | **kwargs: Any) -> None: 69 | self._SendMessage( 70 | self.translator.GetSentence("HELP_CMD", 71 | name=UserHelper.GetName(self.cmd_data.User())) 72 | ) 73 | 74 | 75 | # 76 | # Command for checking if bot is alive 77 | # 78 | class AliveCmd(CommandBase): 79 | # Execute command 80 | def _ExecuteCommand(self, 81 | **kwargs: Any) -> None: 82 | self._SendMessage(self.translator.GetSentence("ALIVE_CMD")) 83 | 84 | 85 | # 86 | # Command for setting test mode 87 | # 88 | class SetTestModeCmd(CommandBase): 89 | # Execute command 90 | @GroupChatOnly 91 | def _ExecuteCommand(self, 92 | **kwargs: Any) -> None: 93 | try: 94 | # Get parameters 95 | flag = self.cmd_data.Params().GetAsBool(0) 96 | except CommandParameterError: 97 | self._SendMessage(self.translator.GetSentence("PARAM_ERR_MSG")) 98 | else: 99 | # Set test mode 100 | self.config.SetValue(BotConfigTypes.APP_TEST_MODE, flag) 101 | 102 | # Send message 103 | if self.config.GetValue(BotConfigTypes.APP_TEST_MODE): 104 | self._SendMessage(self.translator.GetSentence("SET_TEST_MODE_EN_CMD")) 105 | else: 106 | self._SendMessage(self.translator.GetSentence("SET_TEST_MODE_DIS_CMD")) 107 | 108 | 109 | # 110 | # Command for checking if test mode 111 | # 112 | class IsTestModeCmd(CommandBase): 113 | # Execute command 114 | def _ExecuteCommand(self, 115 | **kwargs: Any) -> None: 116 | if self.config.GetValue(BotConfigTypes.APP_TEST_MODE): 117 | self._SendMessage(self.translator.GetSentence("IS_TEST_MODE_EN_CMD")) 118 | else: 119 | self._SendMessage(self.translator.GetSentence("IS_TEST_MODE_DIS_CMD")) 120 | 121 | 122 | # 123 | # Command for showing bot version 124 | # 125 | class VersionCmd(CommandBase): 126 | # Execute command 127 | def _ExecuteCommand(self, 128 | **kwargs: Any) -> None: 129 | self._SendMessage( 130 | self.translator.GetSentence("VERSION_CMD", 131 | version=__version__) 132 | ) 133 | 134 | 135 | # 136 | # Message task start command 137 | # 138 | class MessageTaskStartCmd(CommandBase): 139 | # Execute command 140 | @GroupChatOnly 141 | def _ExecuteCommand(self, 142 | **kwargs: Any) -> None: 143 | # Get parameters 144 | try: 145 | msg_id = self.cmd_data.Params().GetAsString(0) 146 | period_hours = self.cmd_data.Params().GetAsInt(1) 147 | start_hour = self.cmd_data.Params().GetAsInt(2, 0) 148 | except CommandParameterError: 149 | self._SendMessage(self.translator.GetSentence("PARAM_ERR_MSG")) 150 | else: 151 | try: 152 | kwargs["periodic_msg_scheduler"].Start(self.cmd_data.Chat(), 153 | period_hours, 154 | start_hour, 155 | msg_id, 156 | self.message) 157 | self._SendMessage( 158 | self.translator.GetSentence("MESSAGE_TASK_START_OK_CMD", 159 | period=period_hours, 160 | start=start_hour, 161 | msg_id=msg_id) 162 | ) 163 | except PeriodicMsgJobInvalidPeriodError: 164 | self._SendMessage(self.translator.GetSentence("TASK_PERIOD_ERR_MSG")) 165 | except PeriodicMsgJobInvalidStartError: 166 | self._SendMessage(self.translator.GetSentence("TASK_START_ERR_MSG")) 167 | except PeriodicMsgJobMaxNumError: 168 | self._SendMessage(self.translator.GetSentence("MAX_TASK_ERR_MSG")) 169 | except PeriodicMsgJobAlreadyExistentError: 170 | self._SendMessage( 171 | self.translator.GetSentence("TASK_EXISTENT_ERR_MSG", 172 | msg_id=msg_id) 173 | ) 174 | except PeriodicMsgParserInvalidError: 175 | self._SendMessage(self.translator.GetSentence("MESSAGE_INVALID_ERR_MSG")) 176 | except PeriodicMsgParserTooLongError: 177 | self._SendMessage( 178 | self.translator.GetSentence("MESSAGE_TOO_LONG_ERR_MSG", 179 | msg_max_len=self.config.GetValue(BotConfigTypes.MESSAGE_MAX_LEN)) 180 | ) 181 | 182 | 183 | # 184 | # Message task stop command 185 | # 186 | class MessageTaskStopCmd(CommandBase): 187 | # Execute command 188 | @GroupChatOnly 189 | def _ExecuteCommand(self, 190 | **kwargs: Any) -> None: 191 | # Get parameters 192 | try: 193 | msg_id = self.cmd_data.Params().GetAsString(0) 194 | except CommandParameterError: 195 | self._SendMessage(self.translator.GetSentence("PARAM_ERR_MSG")) 196 | else: 197 | try: 198 | kwargs["periodic_msg_scheduler"].Stop(self.cmd_data.Chat(), msg_id) 199 | self._SendMessage( 200 | self.translator.GetSentence("MESSAGE_TASK_STOP_OK_CMD", 201 | msg_id=msg_id) 202 | ) 203 | except PeriodicMsgJobNotExistentError: 204 | self._SendMessage( 205 | self.translator.GetSentence("TASK_NOT_EXISTENT_ERR_MSG", 206 | msg_id=msg_id) 207 | ) 208 | 209 | 210 | # 211 | # Message task stop all command 212 | # 213 | class MessageTaskStopAllCmd(CommandBase): 214 | # Execute command 215 | @GroupChatOnly 216 | def _ExecuteCommand(self, 217 | **kwargs: Any) -> None: 218 | kwargs["periodic_msg_scheduler"].StopAll(self.cmd_data.Chat()) 219 | self._SendMessage( 220 | self.translator.GetSentence("MESSAGE_TASK_STOP_ALL_CMD") 221 | ) 222 | 223 | 224 | # 225 | # Message task pause command 226 | # 227 | class MessageTaskPauseCmd(CommandBase): 228 | # Execute command 229 | @GroupChatOnly 230 | def _ExecuteCommand(self, 231 | **kwargs: Any) -> None: 232 | # Get parameters 233 | try: 234 | msg_id = self.cmd_data.Params().GetAsString(0) 235 | except CommandParameterError: 236 | self._SendMessage(self.translator.GetSentence("PARAM_ERR_MSG")) 237 | else: 238 | try: 239 | kwargs["periodic_msg_scheduler"].Pause(self.cmd_data.Chat(), msg_id) 240 | self._SendMessage( 241 | self.translator.GetSentence("MESSAGE_TASK_PAUSE_OK_CMD", 242 | msg_id=msg_id) 243 | ) 244 | except PeriodicMsgJobNotExistentError: 245 | self._SendMessage( 246 | self.translator.GetSentence("TASK_NOT_EXISTENT_ERR_MSG", 247 | msg_id=msg_id) 248 | ) 249 | 250 | 251 | # 252 | # Message task resume command 253 | # 254 | class MessageTaskResumeCmd(CommandBase): 255 | # Execute command 256 | @GroupChatOnly 257 | def _ExecuteCommand(self, 258 | **kwargs: Any) -> None: 259 | # Get parameters 260 | try: 261 | msg_id = self.cmd_data.Params().GetAsString(0) 262 | except CommandParameterError: 263 | self._SendMessage(self.translator.GetSentence("PARAM_ERR_MSG")) 264 | else: 265 | try: 266 | kwargs["periodic_msg_scheduler"].Resume(self.cmd_data.Chat(), msg_id) 267 | self._SendMessage( 268 | self.translator.GetSentence("MESSAGE_TASK_RESUME_OK_CMD", 269 | msg_id=msg_id) 270 | ) 271 | except PeriodicMsgJobNotExistentError: 272 | self._SendMessage( 273 | self.translator.GetSentence("TASK_NOT_EXISTENT_ERR_MSG", 274 | msg_id=msg_id) 275 | ) 276 | 277 | 278 | # 279 | # Message task get command 280 | # 281 | class MessageTaskGetCmd(CommandBase): 282 | # Execute command 283 | @GroupChatOnly 284 | def _ExecuteCommand(self, 285 | **kwargs: Any) -> None: 286 | # Get parameters 287 | try: 288 | msg_id = self.cmd_data.Params().GetAsString(0) 289 | except CommandParameterError: 290 | self._SendMessage(self.translator.GetSentence("PARAM_ERR_MSG")) 291 | else: 292 | try: 293 | msg = kwargs["periodic_msg_scheduler"].GetMessage(self.cmd_data.Chat(), msg_id) 294 | 295 | if msg != "": 296 | self._SendMessage( 297 | self.translator.GetSentence("MESSAGE_TASK_GET_OK_CMD", 298 | msg_id=msg_id, 299 | msg=msg) 300 | ) 301 | else: 302 | self._SendMessage( 303 | self.translator.GetSentence("MESSAGE_TASK_GET_NO_CMD", 304 | msg_id=msg_id) 305 | ) 306 | except PeriodicMsgJobNotExistentError: 307 | self._SendMessage( 308 | self.translator.GetSentence("TASK_NOT_EXISTENT_ERR_MSG", 309 | msg_id=msg_id) 310 | ) 311 | 312 | 313 | # 314 | # Message task set command 315 | # 316 | class MessageTaskSetCmd(CommandBase): 317 | # Execute command 318 | @GroupChatOnly 319 | def _ExecuteCommand(self, 320 | **kwargs: Any) -> None: 321 | # Get parameters 322 | try: 323 | msg_id = self.cmd_data.Params().GetAsString(0) 324 | except CommandParameterError: 325 | self._SendMessage(self.translator.GetSentence("PARAM_ERR_MSG")) 326 | else: 327 | try: 328 | kwargs["periodic_msg_scheduler"].SetMessage(self.cmd_data.Chat(), 329 | msg_id, 330 | self.message) 331 | self._SendMessage( 332 | self.translator.GetSentence("MESSAGE_TASK_SET_OK_CMD", 333 | msg_id=msg_id) 334 | ) 335 | except PeriodicMsgJobNotExistentError: 336 | self._SendMessage( 337 | self.translator.GetSentence("TASK_NOT_EXISTENT_ERR_MSG", 338 | msg_id=msg_id) 339 | ) 340 | except PeriodicMsgParserInvalidError: 341 | self._SendMessage(self.translator.GetSentence("MESSAGE_INVALID_ERR_MSG")) 342 | except PeriodicMsgParserTooLongError: 343 | self._SendMessage( 344 | self.translator.GetSentence("MESSAGE_TOO_LONG_ERR_MSG", 345 | msg_max_len=self.config.GetValue(BotConfigTypes.MESSAGE_MAX_LEN)) 346 | ) 347 | 348 | 349 | # 350 | # Message task delete last message command 351 | # 352 | class MessageTaskDeleteLastMsgCmd(CommandBase): 353 | # Execute command 354 | @GroupChatOnly 355 | def _ExecuteCommand(self, 356 | **kwargs: Any) -> None: 357 | # Get parameters 358 | try: 359 | msg_id = self.cmd_data.Params().GetAsString(0) 360 | flag = self.cmd_data.Params().GetAsBool(1) 361 | except CommandParameterError: 362 | self._SendMessage(self.translator.GetSentence("PARAM_ERR_MSG")) 363 | else: 364 | try: 365 | kwargs["periodic_msg_scheduler"].DeleteLastSentMessage(self.cmd_data.Chat(), msg_id, flag) 366 | self._SendMessage( 367 | self.translator.GetSentence("MESSAGE_TASK_DELETE_LAST_MSG_OK_CMD", 368 | msg_id=msg_id, 369 | flag=flag) 370 | ) 371 | except PeriodicMsgJobNotExistentError: 372 | self._SendMessage( 373 | self.translator.GetSentence("TASK_NOT_EXISTENT_ERR_MSG", 374 | msg_id=msg_id) 375 | ) 376 | 377 | 378 | # 379 | # Message task info command 380 | # 381 | class MessageTaskInfoCmd(CommandBase): 382 | # Execute command 383 | @GroupChatOnly 384 | def _ExecuteCommand(self, 385 | **kwargs: Any) -> None: 386 | jobs_list = kwargs["periodic_msg_scheduler"].GetJobsInChat(self.cmd_data.Chat()) 387 | 388 | if jobs_list.Any(): 389 | self._SendMessage( 390 | self.translator.GetSentence("MESSAGE_TASK_INFO_CMD", 391 | tasks_num=jobs_list.Count(), 392 | tasks_list=str(jobs_list)) 393 | ) 394 | else: 395 | self._SendMessage(self.translator.GetSentence("MESSAGE_TASK_INFO_NO_TASK_CMD")) 396 | -------------------------------------------------------------------------------- /telegram_periodic_msg_bot/periodic_msg/periodic_msg_scheduler.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | from typing import Dict 25 | 26 | import pyrogram 27 | from apscheduler.schedulers.background import BackgroundScheduler 28 | 29 | from telegram_periodic_msg_bot.bot.bot_config_types import BotConfigTypes 30 | from telegram_periodic_msg_bot.config.config_object import ConfigObject 31 | from telegram_periodic_msg_bot.logger.logger import Logger 32 | from telegram_periodic_msg_bot.misc.helpers import ChatHelper 33 | from telegram_periodic_msg_bot.periodic_msg.periodic_msg_job import PeriodicMsgJob, PeriodicMsgJobData 34 | from telegram_periodic_msg_bot.periodic_msg.periodic_msg_parser import PeriodicMsgParser 35 | from telegram_periodic_msg_bot.translator.translation_loader import TranslationLoader 36 | from telegram_periodic_msg_bot.utils.wrapped_list import WrappedList 37 | 38 | 39 | # 40 | # Classes 41 | # 42 | 43 | # Job already existent error 44 | class PeriodicMsgJobAlreadyExistentError(Exception): 45 | pass 46 | 47 | 48 | # Job not existent error 49 | class PeriodicMsgJobNotExistentError(Exception): 50 | pass 51 | 52 | 53 | # Job invalid period error 54 | class PeriodicMsgJobInvalidPeriodError(Exception): 55 | pass 56 | 57 | 58 | # Job invalid start error 59 | class PeriodicMsgJobInvalidStartError(Exception): 60 | pass 61 | 62 | 63 | # Job maximum number error 64 | class PeriodicMsgJobMaxNumError(Exception): 65 | pass 66 | 67 | 68 | # Constants for periodic message scheduler 69 | class PeriodicMsgSchedulerConst: 70 | # Minimum/Maximum start hour 71 | MIN_START_HOUR: int = 0 72 | MAX_START_HOUR: int = 23 73 | # Minimum/Maximum periods 74 | MIN_PERIOD_HOURS: int = 1 75 | MAX_PERIOD_HOURS: int = 24 76 | 77 | 78 | # Periodic message jobs list class 79 | class PeriodicMsgJobsList(WrappedList): 80 | 81 | translator: TranslationLoader 82 | 83 | # Constructor 84 | def __init__(self, 85 | translator: TranslationLoader) -> None: 86 | super().__init__() 87 | self.translator = translator 88 | 89 | # Convert to string 90 | def ToString(self) -> str: 91 | return "\n".join( 92 | [self.translator.GetSentence("SINGLE_TASK_INFO_MSG", 93 | msg_id=job_data.MessageId(), 94 | period=job_data.PeriodHours(), 95 | start=job_data.StartHour(), 96 | state=(self.translator.GetSentence("TASK_RUNNING_MSG") 97 | if job_data.IsRunning() 98 | else self.translator.GetSentence("TASK_PAUSED_MSG")) 99 | ) 100 | for job_data in self.list_elements] 101 | ) 102 | 103 | # Convert to string 104 | def __str__(self) -> str: 105 | return self.ToString() 106 | 107 | 108 | # Periodic message scheduler class 109 | class PeriodicMsgScheduler: 110 | 111 | client: pyrogram.Client 112 | config: ConfigObject 113 | logger: Logger 114 | translator: TranslationLoader 115 | jobs: Dict[int, Dict[str, PeriodicMsgJob]] 116 | scheduler: BackgroundScheduler 117 | 118 | # Constructor 119 | def __init__(self, 120 | client: pyrogram.Client, 121 | config: ConfigObject, 122 | logger: Logger, 123 | translator: TranslationLoader) -> None: 124 | self.client = client 125 | self.config = config 126 | self.logger = logger 127 | self.translator = translator 128 | self.jobs = {} 129 | self.scheduler = BackgroundScheduler() 130 | self.scheduler.start() 131 | 132 | # Get the list of active jobs in chat 133 | def GetJobsInChat(self, 134 | chat: pyrogram.types.Chat) -> PeriodicMsgJobsList: 135 | jobs_list = PeriodicMsgJobsList(self.translator) 136 | jobs_list.AddMultiple( 137 | [job.Data() for (_, job) in self.jobs[chat.id].items()] if chat.id in self.jobs else [] 138 | ) 139 | 140 | return jobs_list 141 | 142 | # Get if job is active in chat 143 | def IsActiveInChat(self, 144 | chat: pyrogram.types.Chat, 145 | msg_id: str) -> bool: 146 | job_id = self.__GetJobId(chat, msg_id) 147 | return (chat.id in self.jobs and 148 | job_id in self.jobs[chat.id] and 149 | self.scheduler.get_job(job_id) is not None) 150 | 151 | # Start job 152 | def Start(self, 153 | chat: pyrogram.types.Chat, 154 | period_hours: int, 155 | start_hour: int, 156 | msg_id: str, 157 | message: pyrogram.types.Message) -> None: 158 | job_id = self.__GetJobId(chat, msg_id) 159 | 160 | # Check if existent 161 | if self.IsActiveInChat(chat, msg_id): 162 | self.logger.GetLogger().error( 163 | f"Job \"{job_id}\" already active in chat {ChatHelper.GetTitleOrId(chat)}, cannot start it" 164 | ) 165 | raise PeriodicMsgJobAlreadyExistentError() 166 | 167 | # Check period 168 | if period_hours < PeriodicMsgSchedulerConst.MIN_PERIOD_HOURS or period_hours > PeriodicMsgSchedulerConst.MAX_PERIOD_HOURS: 169 | self.logger.GetLogger().error( 170 | f"Invalid period {period_hours} for job \"{job_id}\", cannot start it" 171 | ) 172 | raise PeriodicMsgJobInvalidPeriodError() 173 | 174 | # Check start hour 175 | if start_hour < PeriodicMsgSchedulerConst.MIN_START_HOUR or start_hour > PeriodicMsgSchedulerConst.MAX_START_HOUR: 176 | self.logger.GetLogger().error( 177 | f"Invalid start hour {start_hour} for job \"{job_id}\", cannot start it" 178 | ) 179 | raise PeriodicMsgJobInvalidStartError() 180 | 181 | # Check total jobs number 182 | tot_job_cnt = self.__GetTotalJobCount() 183 | if tot_job_cnt >= self.config.GetValue(BotConfigTypes.TASKS_MAX_NUM): 184 | self.logger.GetLogger().error("Maximum number of jobs reached, cannot start a new one") 185 | raise PeriodicMsgJobMaxNumError() 186 | 187 | # Create job 188 | self.__CreateJob(job_id, chat, period_hours, start_hour, msg_id, message) 189 | # Add job 190 | self.__AddJob(job_id, chat, period_hours, start_hour, msg_id) 191 | 192 | # Get message 193 | def GetMessage(self, 194 | chat: pyrogram.types.Chat, 195 | msg_id: str) -> str: 196 | job_id = self.__GetJobId(chat, msg_id) 197 | 198 | # Check if existent 199 | if not self.IsActiveInChat(chat, msg_id): 200 | self.logger.GetLogger().error( 201 | f"Job \"{job_id}\" not active in chat {ChatHelper.GetTitleOrId(chat)}, cannot get message" 202 | ) 203 | raise PeriodicMsgJobNotExistentError() 204 | 205 | return self.jobs[chat.id][job_id].GetMessage() 206 | 207 | # Set message 208 | def SetMessage(self, 209 | chat: pyrogram.types.Chat, 210 | msg_id: str, 211 | message: pyrogram.types.Message) -> None: 212 | job_id = self.__GetJobId(chat, msg_id) 213 | 214 | # Check if existent 215 | if not self.IsActiveInChat(chat, msg_id): 216 | self.logger.GetLogger().error( 217 | f"Job \"{job_id}\" not active in chat {ChatHelper.GetTitleOrId(chat)}, cannot set message" 218 | ) 219 | raise PeriodicMsgJobNotExistentError() 220 | 221 | # Parse message 222 | msg = PeriodicMsgParser(self.config).Parse(message) 223 | 224 | self.jobs[chat.id][job_id].SetMessage(msg) 225 | self.logger.GetLogger().info( 226 | f"Set message to job \"{job_id}\" in chat {ChatHelper.GetTitleOrId(chat)}: {msg}" 227 | ) 228 | 229 | # Stop job 230 | def Stop(self, 231 | chat: pyrogram.types.Chat, 232 | msg_id: str) -> None: 233 | job_id = self.__GetJobId(chat, msg_id) 234 | 235 | if not self.IsActiveInChat(chat, msg_id): 236 | self.logger.GetLogger().error( 237 | f"Job \"{job_id}\" not active in chat {ChatHelper.GetTitleOrId(chat)}, cannot stop it" 238 | ) 239 | raise PeriodicMsgJobNotExistentError() 240 | 241 | del self.jobs[chat.id][job_id] 242 | self.scheduler.remove_job(job_id) 243 | self.logger.GetLogger().info( 244 | f"Stopped job \"{job_id}\" in chat {ChatHelper.GetTitleOrId(chat)}, " 245 | f"number of active jobs: {self.__GetTotalJobCount()}" 246 | ) 247 | 248 | # Stop all jobs 249 | def StopAll(self, 250 | chat: pyrogram.types.Chat) -> None: 251 | # Check if there are jobs to stop 252 | if chat.id not in self.jobs: 253 | self.logger.GetLogger().info( 254 | f"No job to stop in chat {ChatHelper.GetTitleOrId(chat)}, exiting..." 255 | ) 256 | return 257 | 258 | # Stop all jobs 259 | for job_id in self.jobs[chat.id].keys(): 260 | self.scheduler.remove_job(job_id) 261 | self.logger.GetLogger().info( 262 | f"Stopped job \"{job_id}\" in chat {ChatHelper.GetTitleOrId(chat)}" 263 | ) 264 | # Delete entry 265 | del self.jobs[chat.id] 266 | # Log 267 | self.logger.GetLogger().info( 268 | f"Removed all jobs in chat {ChatHelper.GetTitleOrId(chat)}, number of active jobs: {self.__GetTotalJobCount()}" 269 | ) 270 | 271 | # Called when chat is left by the bot 272 | def ChatLeft(self, 273 | chat: pyrogram.types.Chat) -> None: 274 | self.logger.GetLogger().info( 275 | f"Left chat {ChatHelper.GetTitleOrId(chat)}, stopping all jobs..." 276 | ) 277 | self.StopAll(chat) 278 | 279 | # Pause job 280 | def Pause(self, 281 | chat: pyrogram.types.Chat, 282 | msg_id: str) -> None: 283 | job_id = self.__GetJobId(chat, msg_id) 284 | 285 | if not self.IsActiveInChat(chat, msg_id): 286 | self.logger.GetLogger().error( 287 | f"Job \"{job_id}\" not active in chat {ChatHelper.GetTitleOrId(chat)}, cannot pause it" 288 | ) 289 | raise PeriodicMsgJobNotExistentError() 290 | 291 | self.jobs[chat.id][job_id].SetRunning(False) 292 | self.scheduler.pause_job(job_id) 293 | self.logger.GetLogger().info( 294 | f"Paused job \"{job_id}\" in chat {ChatHelper.GetTitleOrId(chat)}" 295 | ) 296 | 297 | # Resume job 298 | def Resume(self, 299 | chat: pyrogram.types.Chat, 300 | msg_id: str) -> None: 301 | job_id = self.__GetJobId(chat, msg_id) 302 | 303 | if not self.IsActiveInChat(chat, msg_id): 304 | self.logger.GetLogger().error( 305 | f"Job \"{job_id}\" not active in chat {ChatHelper.GetTitleOrId(chat)}, cannot resume it" 306 | ) 307 | raise PeriodicMsgJobNotExistentError() 308 | 309 | self.jobs[chat.id][job_id].SetRunning(True) 310 | self.scheduler.resume_job(job_id) 311 | self.logger.GetLogger().info( 312 | f"Resumed job \"{job_id}\" in chat {ChatHelper.GetTitleOrId(chat)}" 313 | ) 314 | 315 | # Set delete last sent message flag 316 | def DeleteLastSentMessage(self, 317 | chat: pyrogram.types.Chat, 318 | msg_id: str, 319 | flag: bool) -> None: 320 | job_id = self.__GetJobId(chat, msg_id) 321 | 322 | if not self.IsActiveInChat(chat, msg_id): 323 | self.logger.GetLogger().error( 324 | f"Job \"{job_id}\" not active in chat {ChatHelper.GetTitleOrId(chat)}" 325 | ) 326 | raise PeriodicMsgJobNotExistentError() 327 | 328 | self.jobs[chat.id][job_id].DeleteLastSentMessage(flag) 329 | self.logger.GetLogger().info( 330 | f"Set delete last message to {flag} for job \"{job_id}\" in chat {ChatHelper.GetTitleOrId(chat)}" 331 | ) 332 | 333 | # Create job 334 | def __CreateJob(self, 335 | job_id: str, 336 | chat: pyrogram.types.Chat, 337 | period: int, 338 | start: int, 339 | msg_id: str, 340 | message: pyrogram.types.Message) -> None: 341 | # Parse message 342 | msg = PeriodicMsgParser(self.config).Parse(message) 343 | 344 | if chat.id not in self.jobs: 345 | self.jobs[chat.id] = {} 346 | 347 | self.jobs[chat.id][job_id] = PeriodicMsgJob(self.client, 348 | self.logger, 349 | PeriodicMsgJobData(chat, period, start, msg_id)) 350 | self.jobs[chat.id][job_id].SetMessage(msg) 351 | 352 | # Add job 353 | def __AddJob(self, 354 | job_id: str, 355 | chat: pyrogram.types.Chat, 356 | period: int, 357 | start: int, 358 | msg_id: str) -> None: 359 | is_test_mode = self.config.GetValue(BotConfigTypes.APP_TEST_MODE) 360 | cron_str = self.__BuildCronString(period, start, is_test_mode) 361 | if is_test_mode: 362 | self.scheduler.add_job(self.jobs[chat.id][job_id].DoJob, 363 | "cron", 364 | args=(chat,), 365 | minute=cron_str, 366 | id=job_id) 367 | else: 368 | self.scheduler.add_job(self.jobs[chat.id][job_id].DoJob, 369 | "cron", 370 | args=(chat,), 371 | hour=cron_str, 372 | id=job_id) 373 | # Log 374 | per_sym = "minute(s)" if is_test_mode else "hour(s)" 375 | self.logger.GetLogger().info( 376 | f"Started job \"{job_id}\" in chat {ChatHelper.GetTitleOrId(chat)} ({period} {per_sym}, " 377 | f"{msg_id}), number of active jobs: {self.__GetTotalJobCount()}, cron: {cron_str}" 378 | ) 379 | 380 | # Get job ID 381 | @staticmethod 382 | def __GetJobId(chat: pyrogram.types.Chat, 383 | msg_id: str) -> str: 384 | return f"{chat.id}-{msg_id}" 385 | 386 | # Get total job count 387 | def __GetTotalJobCount(self) -> int: 388 | return sum([len(jobs) for (_, jobs) in self.jobs.items()]) 389 | 390 | # Build cron string 391 | @staticmethod 392 | def __BuildCronString(period: int, 393 | start_val: int, 394 | is_test_mode: bool) -> str: 395 | max_val = 24 if not is_test_mode else 60 396 | 397 | cron_str = "" 398 | 399 | loop_cnt = max_val // period 400 | if max_val % period != 0: 401 | loop_cnt += 1 402 | 403 | t = start_val 404 | for _ in range(loop_cnt): 405 | cron_str += f"{t}," 406 | t = (t + period) % max_val 407 | 408 | return cron_str[:-1] 409 | --------------------------------------------------------------------------------