├── telegram_payment_bot ├── bot │ ├── __init__.py │ ├── bot_handlers_config_typing.py │ ├── bot_config_types.py │ └── bot_base.py ├── email │ ├── __init__.py │ ├── subscription_emailer.py │ └── smtp_emailer.py ├── misc │ ├── __init__.py │ ├── ban_helper.py │ ├── helpers.py │ ├── user.py │ └── chat_members.py ├── utils │ ├── __init__.py │ ├── utils.py │ ├── key_value_converter.py │ ├── wrapped_list.py │ ├── wrapped_dict.py │ └── pyrogram_wrapper.py ├── auth_user │ ├── __init__.py │ ├── authorized_users_list.py │ ├── authorized_users_getter.py │ └── authorized_users_message_sender.py ├── command │ ├── __init__.py │ ├── command_data.py │ ├── command_dispatcher.py │ └── command_base.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 ├── google │ ├── __init__.py │ ├── google_cred_types.py │ ├── google_sheet_rows_getter.py │ └── google_sheet_opener.py ├── logger │ ├── __init__.py │ └── logger.py ├── member │ ├── __init__.py │ ├── members_username_getter.py │ ├── joined_members_checker.py │ ├── members_kicker.py │ └── members_payment_getter.py ├── message │ ├── __init__.py │ ├── message_sender.py │ └── message_dispatcher.py ├── payment │ ├── __init__.py │ ├── payment_types.py │ ├── payments_loader_base.py │ ├── payments_loader_factory.py │ ├── payments_emailer.py │ ├── payments_check_job.py │ ├── payments_google_sheet_loader.py │ ├── payments_excel_loader.py │ ├── payments_check_scheduler.py │ └── payments_data.py ├── translator │ ├── __init__.py │ └── translation_loader.py ├── _version.py ├── __init__.py └── payment_bot.py ├── MANIFEST.in ├── requirements-dev.txt ├── requirements.txt ├── app ├── conf │ ├── email_body.txt │ ├── email_body.html │ └── config.ini └── bot.py ├── .gitignore ├── .github └── workflows │ ├── code-analysis.yml │ └── build.yml ├── LICENSE ├── CHANGELOG.md ├── pyproject.toml └── pyproject_legacy.toml /telegram_payment_bot/bot/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_payment_bot/email/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_payment_bot/misc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_payment_bot/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_payment_bot/auth_user/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_payment_bot/command/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_payment_bot/config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_payment_bot/google/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_payment_bot/logger/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_payment_bot/member/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_payment_bot/message/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_payment_bot/payment/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_payment_bot/translator/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include pyproject_legacy.toml 2 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | mypy>=0.900 2 | ruff>=0.1 3 | -------------------------------------------------------------------------------- /telegram_payment_bot/_version.py: -------------------------------------------------------------------------------- 1 | __version__: str = "0.7.3" 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pygsheets 2 | xlrd 3 | pyrotgfork 4 | tgcrypto 5 | apscheduler 6 | defusedxml 7 | -------------------------------------------------------------------------------- /app/conf/email_body.txt: -------------------------------------------------------------------------------- 1 | Hi, your payment for the Telegram group GROUP_NAME is about to expire. 2 | Please pay within the end of the month or you'll removed from the group. 3 | -------------------------------------------------------------------------------- /telegram_payment_bot/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Imports 3 | # 4 | from telegram_payment_bot._version import __version__ 5 | from telegram_payment_bot.payment_bot import PaymentBot 6 | -------------------------------------------------------------------------------- /app/conf/email_body.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group Name 5 | 6 | 7 | 8 | EMAIL BODY... 9 | 10 | 11 | -------------------------------------------------------------------------------- /.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_payment_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 | *.xls 39 | *.xlsx 40 | *.log 41 | *.json 42 | *.pickle 43 | *.session 44 | *.session-journal 45 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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_payment_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_payment_bot/payment/payment_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 Enum, auto, unique 25 | 26 | 27 | # 28 | # Enumerations 29 | # 30 | 31 | # Payment types 32 | @unique 33 | class PaymentTypes(Enum): 34 | EXCEL_FILE = auto() 35 | GOOGLE_SHEET = auto() 36 | -------------------------------------------------------------------------------- /telegram_payment_bot/google/google_cred_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 Enum, auto, unique 25 | 26 | 27 | # 28 | # Enumeratives 29 | # 30 | 31 | # Google credential types 32 | @unique 33 | class GoogleCredTypes(Enum): 34 | OAUTH2 = auto() 35 | SERVICE_ACCOUNT = auto() 36 | -------------------------------------------------------------------------------- /telegram_payment_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 | -------------------------------------------------------------------------------- /telegram_payment_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_payment_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 | return True 33 | if s in ["false", "off", "no", "n"]: 34 | return False 35 | raise ValueError("Invalid string") 36 | 37 | # Convert string to float 38 | @staticmethod 39 | def StrToFloat(s: str) -> float: 40 | return float(s) 41 | 42 | # Convert string to integer 43 | @staticmethod 44 | def StrToInt(s: str) -> int: 45 | return int(s) 46 | -------------------------------------------------------------------------------- /telegram_payment_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_payment_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_payment_bot.config.config_object import ConfigObject 27 | from telegram_payment_bot.config.config_sections_loader import ConfigSectionsLoader 28 | from telegram_payment_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 | -------------------------------------------------------------------------------- /app/conf/config.ini: -------------------------------------------------------------------------------- 1 | # Configuration for Pyrogram 2 | [pyrogram] 3 | session_name = payment_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 | # Configuration for users 15 | [users] 16 | authorized_users = username1,username2,username3 17 | 18 | # Configuration for support 19 | [support] 20 | support_email = info@mywebsite.com 21 | support_telegram = MySupportUsername 22 | 23 | # Configuration for payment 24 | [payment] 25 | payment_website = https://mywebsite.com 26 | payment_check_on_join = False 27 | payment_check_dup_email = True 28 | payment_type = GOOGLE_SHEET 29 | payment_google_sheet_id = 0000000000000000-AAAAAAAAAAAAAAAAAAA_0000000 30 | payment_google_cred_type = OAUTH2 31 | payment_google_cred = credentials.json 32 | payment_google_cred_path = 33 | payment_use_user_id = False 34 | payment_worksheet_idx = 0 35 | payment_email_col = A 36 | payment_user_col = B 37 | payment_expiration_col = C 38 | payment_date_format = %%d/%%m/%%Y 39 | 40 | # Example configuration for using an Excel file 41 | #payment_type = EXCEL_FILE 42 | #payment_excel_file = data/Payments.xlsx 43 | 44 | # Configuration for email 45 | [email] 46 | email_enabled = True 47 | email_from = My Website 48 | email_reply_to = info@mywebsite.com 49 | email_host = myhost.com 50 | email_user = user@myhost.com 51 | email_password = password 52 | email_subject = Payment Expiring - GROUP_NAME 53 | email_alt_body = conf/email_body.txt 54 | email_html_body = conf/email_body.html 55 | 56 | # Configuration for logging 57 | [logging] 58 | log_level = INFO 59 | log_console_enabled = True 60 | log_file_enabled = True 61 | log_file_name = logs/payment_bot.log 62 | log_file_use_rotating = True 63 | log_file_max_bytes = 5242880 64 | log_file_backup_cnt = 10 65 | 66 | # Only if log file rotating is not used 67 | #log_file_append = False 68 | -------------------------------------------------------------------------------- /telegram_payment_bot/google/google_sheet_rows_getter.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 | from telegram_payment_bot.config.config_object import ConfigObject 27 | from telegram_payment_bot.google.google_sheet_opener import GoogleSheetOpener 28 | from telegram_payment_bot.logger.logger import Logger 29 | 30 | 31 | # 32 | # Classes 33 | # 34 | 35 | # Google Sheet rows getter class 36 | class GoogleSheetRowsGetter: 37 | 38 | google_sheet_opener: GoogleSheetOpener 39 | 40 | # Constructor 41 | def __init__(self, 42 | config: ConfigObject, 43 | logger: Logger) -> None: 44 | self.google_sheet_opener = GoogleSheetOpener(config, logger) 45 | 46 | # Open worksheet 47 | def GetRows(self, 48 | worksheet_idx: int) -> List[List[str]]: 49 | worksheet = self.google_sheet_opener.OpenWorksheet(worksheet_idx) 50 | return worksheet.get_all_values( 51 | include_tailing_empty_rows=False, 52 | include_tailing_empty=False, 53 | returnas="matrix" 54 | ) 55 | -------------------------------------------------------------------------------- /telegram_payment_bot/payment_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_payment_bot.bot.bot_base import BotBase 25 | from telegram_payment_bot.bot.bot_config import BotConfig 26 | from telegram_payment_bot.bot.bot_handlers_config import BotHandlersConfig 27 | from telegram_payment_bot.payment.payments_check_scheduler import PaymentsCheckScheduler 28 | 29 | 30 | # 31 | # Classes 32 | # 33 | 34 | # Payment bot class 35 | class PaymentBot(BotBase): 36 | 37 | payments_check_scheduler: PaymentsCheckScheduler 38 | 39 | # Constructor 40 | def __init__(self, 41 | config_file: str) -> None: 42 | super().__init__(config_file, 43 | BotConfig, 44 | BotHandlersConfig) 45 | # Initialize payment check scheduler 46 | self.payments_check_scheduler = PaymentsCheckScheduler(self.client, 47 | self.config, 48 | self.logger, 49 | self.translator) 50 | -------------------------------------------------------------------------------- /telegram_payment_bot/auth_user/authorized_users_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 pyrogram 25 | 26 | from telegram_payment_bot.bot.bot_config_types import BotConfigTypes 27 | from telegram_payment_bot.config.config_object import ConfigObject 28 | from telegram_payment_bot.utils.wrapped_list import WrappedList 29 | 30 | 31 | # 32 | # Classes 33 | # 34 | 35 | # Authorized users list class 36 | class AuthorizedUsersList(WrappedList): 37 | # Constructor 38 | def __init__(self, 39 | config: ConfigObject) -> None: 40 | super().__init__() 41 | self.AddMultiple(config.GetValue(BotConfigTypes.AUTHORIZED_USERS)) 42 | 43 | # Get if a user is present 44 | def IsUserPresent(self, 45 | user: pyrogram.types.User) -> bool: 46 | return user.username is not None and user.username in self.list_elements 47 | 48 | # Convert to string 49 | def ToString(self) -> str: 50 | return "\n".join([f"- @{username}" for username in self.list_elements]) 51 | 52 | # Convert to string 53 | def __str__(self) -> str: 54 | return self.ToString() 55 | -------------------------------------------------------------------------------- /telegram_payment_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_payment_bot.config.config_object import ConfigObject 27 | from telegram_payment_bot.config.config_section_loader import ConfigSectionLoader 28 | from telegram_payment_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_payment_bot/auth_user/authorized_users_getter.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_payment_bot.bot.bot_config_types import BotConfigTypes 27 | from telegram_payment_bot.config.config_object import ConfigObject 28 | from telegram_payment_bot.misc.chat_members import ChatMembersGetter, ChatMembersList 29 | 30 | 31 | # 32 | # Classes 33 | # 34 | 35 | # Authorized users getter class 36 | class AuthorizedUsersGetter: 37 | 38 | config: ConfigObject 39 | chat_members_getter: ChatMembersGetter 40 | 41 | # Constructor 42 | def __init__(self, 43 | client: pyrogram.Client, 44 | config: ConfigObject) -> None: 45 | self.config = config 46 | self.chat_members_getter = ChatMembersGetter(client) 47 | 48 | # Get authorized users 49 | def GetUsers(self, 50 | chat: pyrogram.types.Chat) -> ChatMembersList: 51 | return self.chat_members_getter.FilterMembers( 52 | chat, 53 | lambda member: ( 54 | member.user is not None and 55 | member.user.username is not None and 56 | member.user.username in self.config.GetValue(BotConfigTypes.AUTHORIZED_USERS) 57 | ) 58 | ) 59 | -------------------------------------------------------------------------------- /telegram_payment_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_payment_bot/payment/payments_loader_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 Optional 26 | 27 | from telegram_payment_bot.config.config_object import ConfigObject 28 | from telegram_payment_bot.logger.logger import Logger 29 | from telegram_payment_bot.misc.user import User 30 | from telegram_payment_bot.payment.payments_data import PaymentsData, PaymentsDataErrors, SinglePayment 31 | 32 | 33 | # 34 | # Classes 35 | # 36 | 37 | # Payments loader base class 38 | class PaymentsLoaderBase(ABC): 39 | 40 | config: ConfigObject 41 | logger: Logger 42 | 43 | # Constructor 44 | def __init__(self, 45 | config: ConfigObject, 46 | logger: Logger) -> None: 47 | self.config = config 48 | self.logger = logger 49 | 50 | # Load all payments 51 | @abstractmethod 52 | def LoadAll(self) -> PaymentsData: 53 | pass 54 | 55 | @abstractmethod 56 | # Load single payment by user 57 | def LoadSingleByUser(self, 58 | user: User) -> Optional[SinglePayment]: 59 | pass 60 | 61 | # Check for errors 62 | @abstractmethod 63 | def CheckForErrors(self) -> PaymentsDataErrors: 64 | pass 65 | 66 | # Convert column string to index 67 | @staticmethod 68 | def _ColumnToIndex(col: str) -> int: 69 | return ord(col) - ord("A") 70 | -------------------------------------------------------------------------------- /telegram_payment_bot/misc/ban_helper.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_payment_bot.utils.pyrogram_wrapper import PyrogramWrapper 27 | 28 | 29 | # 30 | # Classes 31 | # 32 | 33 | # Constants for ban helper class 34 | class BanHelperConst: 35 | # Ban time in seconds 36 | BAN_TIME_SEC: int = 60 37 | 38 | 39 | # Ban helper class 40 | class BanHelper: 41 | 42 | client: pyrogram.Client 43 | 44 | # Constructor 45 | def __init__(self, 46 | client: pyrogram.Client) -> None: 47 | self.client = client 48 | 49 | # Ban user 50 | def BanUser(self, 51 | chat: pyrogram.types.Chat, 52 | user: pyrogram.types.User) -> None: 53 | PyrogramWrapper.BanChatMember(self.client, chat, user) 54 | 55 | # Kick user 56 | def KickUser(self, 57 | chat: pyrogram.types.Chat, 58 | user: pyrogram.types.User) -> None: 59 | # Ban only for 1 minute, so they can join again with an invite link if necessary 60 | # (otherwise they cannot join anymore, unless manually added to the group) 61 | PyrogramWrapper.BanChatMember(self.client, chat, user, BanHelperConst.BAN_TIME_SEC) 62 | 63 | # Unban user 64 | def UnbanUser(self, 65 | chat: pyrogram.types.Chat, 66 | user: pyrogram.types.User) -> None: 67 | self.client.unban_chat_member(chat.id, user.id) 68 | -------------------------------------------------------------------------------- /telegram_payment_bot/payment/payments_loader_factory.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_payment_bot.bot.bot_config_types import BotConfigTypes 25 | from telegram_payment_bot.config.config_object import ConfigObject 26 | from telegram_payment_bot.logger.logger import Logger 27 | from telegram_payment_bot.payment.payment_types import PaymentTypes 28 | from telegram_payment_bot.payment.payments_excel_loader import PaymentsExcelLoader 29 | from telegram_payment_bot.payment.payments_google_sheet_loader import PaymentsGoogleSheetLoader 30 | from telegram_payment_bot.payment.payments_loader_base import PaymentsLoaderBase 31 | 32 | 33 | # 34 | # Classes 35 | # 36 | 37 | # Exception in case of payment type error 38 | class PaymentTypeError(Exception): 39 | pass 40 | 41 | 42 | # Payments loader factory class 43 | class PaymentsLoaderFactory: 44 | 45 | config: ConfigObject 46 | logger: Logger 47 | 48 | # Constructor 49 | def __init__(self, 50 | config: ConfigObject, 51 | logger: Logger) -> None: 52 | self.config = config 53 | self.logger = logger 54 | 55 | # Create loader 56 | def CreateLoader(self) -> PaymentsLoaderBase: 57 | payment_type = self.config.GetValue(BotConfigTypes.PAYMENT_TYPE) 58 | if payment_type == PaymentTypes.EXCEL_FILE: 59 | return PaymentsExcelLoader(self.config, self.logger) 60 | if payment_type == PaymentTypes.GOOGLE_SHEET: 61 | return PaymentsGoogleSheetLoader(self.config, self.logger) 62 | 63 | raise PaymentTypeError(f"Invalid payment type {payment_type}") 64 | -------------------------------------------------------------------------------- /telegram_payment_bot/member/members_username_getter.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_payment_bot.config.config_object import ConfigObject 27 | from telegram_payment_bot.misc.chat_members import ChatMembersGetter, ChatMembersList 28 | from telegram_payment_bot.misc.helpers import MemberHelper 29 | 30 | 31 | # 32 | # Classes 33 | # 34 | 35 | # Members username getter class 36 | class MembersUsernameGetter: 37 | 38 | client: pyrogram.Client 39 | config: ConfigObject 40 | 41 | # Constructor 42 | def __init__(self, 43 | client: pyrogram.Client, 44 | config: ConfigObject) -> None: 45 | self.client = client 46 | self.config = config 47 | 48 | # Get all with username 49 | def GetAllWithUsername(self, 50 | chat: pyrogram.types.Chat) -> ChatMembersList: 51 | # Filter chat members 52 | return ChatMembersGetter(self.client).FilterMembers( 53 | chat, 54 | lambda member: ( 55 | MemberHelper.IsValidMember(member) and 56 | member.user is not None and 57 | member.user.username is not None 58 | ) 59 | ) 60 | 61 | # Get all with no username 62 | def GetAllWithNoUsername(self, 63 | chat: pyrogram.types.Chat) -> ChatMembersList: 64 | # Filter chat members 65 | return ChatMembersGetter(self.client).FilterMembers( 66 | chat, 67 | lambda member: ( 68 | MemberHelper.IsValidMember(member) and 69 | member.user is not None and 70 | member.user.username is None 71 | ) 72 | ) 73 | -------------------------------------------------------------------------------- /telegram_payment_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_payment_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 | # Users 44 | AUTHORIZED_USERS = auto() 45 | # Support 46 | SUPPORT_EMAIL = auto() 47 | SUPPORT_TELEGRAM = auto() 48 | # Payment 49 | PAYMENT_WEBSITE = auto() 50 | PAYMENT_CHECK_ON_JOIN = auto() 51 | PAYMENT_CHECK_DUP_EMAIL = auto() 52 | PAYMENT_TYPE = auto() 53 | PAYMENT_EXCEL_FILE = auto() 54 | PAYMENT_GOOGLE_SHEET_ID = auto() 55 | PAYMENT_GOOGLE_CRED_TYPE = auto() 56 | PAYMENT_GOOGLE_CRED = auto() 57 | PAYMENT_GOOGLE_CRED_PATH = auto() 58 | PAYMENT_USE_USER_ID = auto() 59 | PAYMENT_WORKSHEET_IDX = auto() 60 | PAYMENT_EMAIL_COL = auto() 61 | PAYMENT_USER_COL = auto() 62 | PAYMENT_EXPIRATION_COL = auto() 63 | PAYMENT_DATE_FORMAT = auto() 64 | # Email 65 | EMAIL_ENABLED = auto() 66 | EMAIL_FROM = auto() 67 | EMAIL_REPLY_TO = auto() 68 | EMAIL_HOST = auto() 69 | EMAIL_USER = auto() 70 | EMAIL_PASSWORD = auto() 71 | EMAIL_SUBJECT = auto() 72 | EMAIL_ALT_BODY = auto() 73 | EMAIL_HTML_BODY = auto() 74 | # Logging 75 | LOG_LEVEL = auto() 76 | LOG_CONSOLE_ENABLED = auto() 77 | LOG_FILE_ENABLED = auto() 78 | LOG_FILE_NAME = auto() 79 | LOG_FILE_USE_ROTATING = auto() 80 | LOG_FILE_APPEND = auto() 81 | LOG_FILE_MAX_BYTES = auto() 82 | LOG_FILE_BACKUP_CNT = auto() 83 | -------------------------------------------------------------------------------- /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_payment_bot import PaymentBot, __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 Payment 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 = PaymentBot(args.config) 95 | bot.Run() 96 | -------------------------------------------------------------------------------- /telegram_payment_bot/email/subscription_emailer.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_payment_bot.bot.bot_config_types import BotConfigTypes 25 | from telegram_payment_bot.config.config_object import ConfigObject 26 | from telegram_payment_bot.email.smtp_emailer import SmtpEmailer 27 | 28 | 29 | # 30 | # Classes 31 | # 32 | 33 | # Subscription emailer class 34 | class SubscriptionEmailer: 35 | 36 | smtp_emailer: SmtpEmailer 37 | 38 | # Constructor 39 | def __init__(self, 40 | config: ConfigObject): 41 | self.smtp_emailer = SmtpEmailer() 42 | self.smtp_emailer.From = config.GetValue(BotConfigTypes.EMAIL_FROM) 43 | self.smtp_emailer.ReplyTo = config.GetValue(BotConfigTypes.EMAIL_REPLY_TO) 44 | self.smtp_emailer.Subject = config.GetValue(BotConfigTypes.EMAIL_SUBJECT) 45 | self.smtp_emailer.HtmlMsg = config.GetValue(BotConfigTypes.EMAIL_HTML_BODY) 46 | self.smtp_emailer.PlainMsg = config.GetValue(BotConfigTypes.EMAIL_ALT_BODY) 47 | self.smtp_emailer.Host = config.GetValue(BotConfigTypes.EMAIL_HOST) 48 | self.smtp_emailer.User = config.GetValue(BotConfigTypes.EMAIL_USER) 49 | self.smtp_emailer.Password = config.GetValue(BotConfigTypes.EMAIL_PASSWORD) 50 | 51 | # Prepare message 52 | def PrepareMsg(self, 53 | recipient: str) -> None: 54 | self.smtp_emailer.To = recipient 55 | self.smtp_emailer.PrepareMsg() 56 | 57 | # Connect 58 | def Connect(self) -> None: 59 | self.smtp_emailer.Connect() 60 | 61 | # Disconnect 62 | def Disconnect(self) -> None: 63 | self.smtp_emailer.Disconnect() 64 | 65 | # Send email 66 | def Send(self) -> None: 67 | self.smtp_emailer.Send() 68 | 69 | # Quick send email 70 | def QuickSend(self, 71 | recipient: str) -> None: 72 | self.smtp_emailer.To = recipient 73 | self.smtp_emailer.QuickSend() 74 | -------------------------------------------------------------------------------- /telegram_payment_bot/auth_user/authorized_users_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 pyrogram 25 | import pyrogram.errors.exceptions as pyrogram_ex 26 | 27 | from telegram_payment_bot.auth_user.authorized_users_getter import AuthorizedUsersGetter 28 | from telegram_payment_bot.config.config_object import ConfigObject 29 | from telegram_payment_bot.logger.logger import Logger 30 | from telegram_payment_bot.message.message_sender import MessageSender 31 | from telegram_payment_bot.misc.helpers import UserHelper 32 | 33 | 34 | # 35 | # Classes 36 | # 37 | 38 | # Authorized users message sender class 39 | class AuthorizedUsersMessageSender: 40 | 41 | logger: Logger 42 | auth_users_getter: AuthorizedUsersGetter 43 | message_sender: MessageSender 44 | 45 | # Constructor 46 | def __init__(self, 47 | client: pyrogram.Client, 48 | config: ConfigObject, 49 | logger: Logger) -> None: 50 | self.logger = logger 51 | self.auth_users_getter = AuthorizedUsersGetter(client, config) 52 | self.message_sender = MessageSender(client, logger) 53 | 54 | # Send message 55 | def SendMessage(self, 56 | chat: pyrogram.types.Chat, 57 | msg: str, 58 | **kwargs) -> None: 59 | # Send to authorized users 60 | for auth_member in self.auth_users_getter.GetUsers(chat): 61 | try: 62 | self.message_sender.SendMessage(auth_member.user, msg, **kwargs) 63 | self.logger.GetLogger().info( 64 | f"Message sent to authorized user: {UserHelper.GetNameOrId(auth_member.user)}" 65 | ) 66 | # It may happen if the user has never talked to the bot or blocked it 67 | except (pyrogram_ex.bad_request_400.PeerIdInvalid, 68 | pyrogram_ex.bad_request_400.UserIsBlocked): 69 | self.logger.GetLogger().error( 70 | f"Unable to send message to authorized user: {UserHelper.GetNameOrId(auth_member.user)}" 71 | ) 72 | -------------------------------------------------------------------------------- /.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_payment_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_payment_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 | # Member helper class 62 | class MemberHelper: 63 | # Get if valid member 64 | @staticmethod 65 | def IsValidMember(member: pyrogram.types.ChatMember) -> bool: 66 | return (PyrogramWrapper.MemberIsStatus(member, "member") and 67 | member.user is not None and 68 | (member.user.is_self is None or not member.user.is_self) and 69 | (member.user.is_bot is None or not member.user.is_bot)) 70 | 71 | 72 | # User helper class 73 | class UserHelper: 74 | # Get user name or ID 75 | @staticmethod 76 | def GetNameOrId(user: Optional[pyrogram.types.User]) -> str: 77 | if user is None: 78 | return "Anonymous user" 79 | 80 | if user.username is not None: 81 | return f"@{user.username} ({UserHelper.GetName(user)} - ID: {user.id})" 82 | 83 | name = UserHelper.GetName(user) 84 | return f"{name} (ID: {user.id})" if name is not None else f"ID: {user.id}" 85 | 86 | # Get user name 87 | @staticmethod 88 | def GetName(user: Optional[pyrogram.types.User]) -> str: 89 | if user is None: 90 | return "Anonymous user" 91 | 92 | if user.first_name is not None: 93 | return f"{user.first_name} {user.last_name}" if user.last_name is not None else f"{user.first_name}" 94 | return user.last_name if user.last_name is not None else "" 95 | -------------------------------------------------------------------------------- /telegram_payment_bot/google/google_sheet_opener.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 pygsheets 27 | 28 | from telegram_payment_bot.bot.bot_config_types import BotConfigTypes 29 | from telegram_payment_bot.config.config_object import ConfigObject 30 | from telegram_payment_bot.google.google_cred_types import GoogleCredTypes 31 | from telegram_payment_bot.logger.logger import Logger 32 | 33 | 34 | # 35 | # Classes 36 | # 37 | 38 | # Google Sheet opener class 39 | class GoogleSheetOpener: 40 | 41 | config: ConfigObject 42 | logger: Logger 43 | gsheet: ConfigObject 44 | worksheet: Optional[pygsheets.Spreadsheet] 45 | 46 | # Constructor 47 | def __init__(self, 48 | config: ConfigObject, 49 | logger: Logger) -> None: 50 | self.config = config 51 | self.logger = logger 52 | self.google_sheet = None 53 | 54 | # Open worksheet 55 | def OpenWorksheet(self, 56 | worksheet_idx: int) -> pygsheets.Worksheet: 57 | self.__OpenGoogleSheet() 58 | assert self.google_sheet is not None 59 | return self.google_sheet[worksheet_idx] 60 | 61 | # Open Google Sheet 62 | def __OpenGoogleSheet(self) -> None: 63 | if self.google_sheet is not None: 64 | return 65 | 66 | # Get configuration 67 | sheet_id = self.config.GetValue(BotConfigTypes.PAYMENT_GOOGLE_SHEET_ID) 68 | cred_file = self.config.GetValue(BotConfigTypes.PAYMENT_GOOGLE_CRED) 69 | cred_type = self.config.GetValue(BotConfigTypes.PAYMENT_GOOGLE_CRED_TYPE) 70 | 71 | # Log 72 | self.logger.GetLogger().info(f"Credential file: {cred_file}") 73 | self.logger.GetLogger().info(f"Credential type: {cred_type}") 74 | self.logger.GetLogger().info(f"Opening Google Sheet ID \"{sheet_id}\"...") 75 | 76 | # Authorize and open Google Sheet 77 | if cred_type == GoogleCredTypes.OAUTH2: 78 | cred_path = self.config.GetValue(BotConfigTypes.PAYMENT_GOOGLE_CRED_PATH) 79 | self.logger.GetLogger().info(f"Credential path: {cred_path}") 80 | 81 | google_client = pygsheets.authorize(client_secret=cred_file, 82 | credentials_directory=cred_path, 83 | local=True) 84 | elif cred_type == GoogleCredTypes.SERVICE_ACCOUNT: 85 | google_client = pygsheets.authorize(service_file=cred_file) 86 | else: 87 | raise ValueError("Invalid credential type") 88 | 89 | self.google_sheet = google_client.open_by_key(sheet_id) 90 | -------------------------------------------------------------------------------- /telegram_payment_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 | from __future__ import annotations 25 | 26 | import typing 27 | from abc import ABC 28 | from typing import Callable, Iterator, List, Optional, Union 29 | 30 | 31 | # 32 | # Classes 33 | # 34 | 35 | # Wrapped list class 36 | class WrappedList(ABC): 37 | 38 | list_elements: List[typing.Any] 39 | 40 | # Constructor 41 | def __init__(self) -> None: 42 | self.list_elements = [] 43 | 44 | # Add single element 45 | def AddSingle(self, 46 | element: typing.Any) -> None: 47 | self.list_elements.append(element) 48 | 49 | # Add multiple elements 50 | def AddMultiple(self, 51 | elements: Union[List[typing.Any], WrappedList]) -> None: 52 | if isinstance(elements, WrappedList): 53 | self.list_elements.extend(elements.GetList()) 54 | else: 55 | self.list_elements.extend(elements) 56 | 57 | # Remove single element 58 | def RemoveSingle(self, 59 | element: typing.Any) -> None: 60 | self.list_elements.remove(element) 61 | 62 | # Get if element is present 63 | def IsElem(self, 64 | element: typing.Any) -> bool: 65 | return element in self.list_elements 66 | 67 | # Clear element 68 | def Clear(self) -> None: 69 | self.list_elements.clear() 70 | 71 | # Get elements count 72 | def Count(self) -> int: 73 | return len(self.list_elements) 74 | 75 | # Get if any 76 | def Any(self) -> bool: 77 | return self.Count() > 0 78 | 79 | # Get if empty 80 | def Empty(self) -> bool: 81 | return self.Count() == 0 82 | 83 | # Sort 84 | def Sort(self, 85 | key: Optional[Callable[[typing.Any], typing.Any]] = None, 86 | reverse: bool = False) -> None: 87 | self.list_elements.sort(key=key, reverse=reverse) 88 | 89 | # Get list 90 | def GetList(self) -> List[typing.Any]: 91 | return self.list_elements 92 | 93 | # Get item 94 | def __getitem__(self, 95 | key: int): 96 | return self.list_elements[key] 97 | 98 | # Delete item 99 | def __delitem__(self, 100 | key: int): 101 | del self.list_elements[key] 102 | 103 | # Set item 104 | def __setitem__(self, 105 | key: int, 106 | value: typing.Any): 107 | self.list_elements[key] = value 108 | 109 | # Get iterator 110 | def __iter__(self) -> Iterator[typing.Any]: 111 | yield from self.list_elements 112 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.7.3 2 | 3 | - Use `pyrotgfork`, since `pyrogram` was archived 4 | 5 | # 0.7.2 6 | 7 | - Update Python versions 8 | 9 | # 0.7.1 10 | 11 | - Fix replying to commands in topics 12 | 13 | # 0.7.0 14 | 15 | - Add support for service accounts (`payment_google_cred_type`) 16 | - Add possibility to specify the worksheet index in config (`payment_worksheet_idx`) 17 | 18 | # 0.6.3 19 | 20 | - Minor bug fix (passing not-existent variable as parameter) 21 | 22 | # 0.6.2 23 | 24 | - Migrate Google OAuth flow after deprecation 25 | 26 | # 0.6.1 27 | 28 | - Fix some _mypy_ and _prospector_ warnings 29 | - Add configuration for _isort_ and run it on project 30 | 31 | # 0.6.0 32 | 33 | - Add support for _pyrogram_ version 2 (version 1 still supported) 34 | 35 | # 0.5.3 36 | 37 | - Members without username or that haven't paid are kicked until no one left (useful in channels with more than 200 members) 38 | 39 | # 0.5.2 40 | 41 | - Fix usage in channels 42 | - Fix ban method name for _pyrogram_ 1.4 43 | 44 | # 0.5.1 45 | 46 | - Add command for showing bot version 47 | 48 | # 0.5.0 49 | 50 | - Add possibility to disable duplicated email check 51 | - Project re-organized into folders 52 | 53 | # 0.4.0 54 | 55 | - Add the possibility to use either user ID or username in payment file 56 | - Bot works also in channels in addition to supergroups 57 | - Fix group only restriction to `/paybot_task_remove_all_chats` command 58 | - Add single handlers for message updates, to avoid being notified of each single message sent in groups 59 | 60 | # 0.3.7 61 | 62 | - Fix bug when checking the correctness or payments data 63 | 64 | # 0.3.6 65 | 66 | - Rename commands by adding the `paybot_` prefix, to avoid conflicts with other bots 67 | - Email is checked for duplication in payment file 68 | 69 | # 0.3.5 70 | 71 | - Use _pygsheets_ library for reading Google Sheets 72 | - File columns specified using the letter instead of the index 73 | 74 | # 0.3.4 75 | 76 | - Add configuration files for _flake8_ and _prospector_ 77 | - Fix all _flake8_ warnings 78 | - Fix the vast majority of _prospector_ warnings 79 | - Remove all star imports (`import *`) 80 | 81 | # 0.3.3 82 | 83 | - Fix wrong imports 84 | - Add typing to class members 85 | - Fix _mypy_ errors 86 | 87 | # 0.3.2 88 | 89 | - Minor bug fixes 90 | 91 | # 0.3.1 92 | 93 | - Fix sentences sent during periodic payment check 94 | - Fix some sentence names 95 | - Exclude bots and self from username/payment check 96 | 97 | # 0.3.0 98 | 99 | - Payment check task is not statically configured anymore, but now it can be configured dynamically with specific commands 100 | - Payment check on member join can be configured dynamically with specific commands 101 | - Possibility to usa a "normal" log file handler in addition to the rotating file handler 102 | - Add placeholders to translation sentences (in this way, they can be moved to different positions depending on the language) 103 | 104 | # 0.2.0 105 | 106 | - Add possibility to translate bot messages in different languages using a custom xml file (`app_lang_file`) 107 | - Add possibility to leave some configuration fields empty (`support_email`, `support_telegram`, `payment_website`) 108 | - Add possibility to check and change test mode without restarting the bot using commands `set_test_mode` and `is_test_mode` 109 | - Add possibility to check payments data using command `check_payments_data` 110 | - Add possibility to set the date format for payments data (`payment_date_format`) 111 | - Add possibility to disable emails (`email_enabled`) and payment check when members join (`payment_check_on_join`) 112 | - Payments data is checked for duplicated usernames and invalid dates 113 | - Change periodical payment check period from seconds to minutes (`payment_check_period_sec` to `payment_check_period_min`) 114 | 115 | # 0.1.0 116 | 117 | First release 118 | -------------------------------------------------------------------------------- /telegram_payment_bot/misc/user.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 __future__ import annotations 25 | 26 | from typing import Union 27 | 28 | import pyrogram 29 | 30 | from telegram_payment_bot.bot.bot_config_types import BotConfigTypes 31 | from telegram_payment_bot.config.config_object import ConfigObject 32 | from telegram_payment_bot.utils.utils import Utils 33 | 34 | 35 | # 36 | # Classes 37 | # 38 | 39 | # User class 40 | class User: 41 | 42 | user: Union[int, str] 43 | 44 | # Construct from string 45 | @classmethod 46 | def FromString(cls, 47 | config: ConfigObject, 48 | user_str: str) -> User: 49 | if config.GetValue(BotConfigTypes.PAYMENT_USE_USER_ID): 50 | try: 51 | user = Utils.StrToInt(user_str) 52 | except ValueError: 53 | # Try also from float (it may happen in Excel files) 54 | try: 55 | user = int(Utils.StrToFloat(user_str)) 56 | except ValueError: 57 | user = 0 58 | return cls(user) 59 | return cls(user_str[1:] if user_str.startswith("@") else user_str) 60 | 61 | # Construct from user object 62 | @classmethod 63 | def FromUserObject(cls, 64 | config: ConfigObject, 65 | user: pyrogram.types.User) -> User: 66 | if config.GetValue(BotConfigTypes.PAYMENT_USE_USER_ID): 67 | return cls(user.id) 68 | 69 | if user.username is None: 70 | raise ValueError("Class cannot be created from a None username") 71 | return cls(user.username) 72 | 73 | # Constructor 74 | def __init__(self, 75 | user: Union[int, str]) -> None: 76 | self.user = user 77 | 78 | # Get if user ID 79 | def IsUserId(self) -> bool: 80 | return isinstance(self.user, int) 81 | 82 | # Get if username 83 | def IsUsername(self) -> bool: 84 | return isinstance(self.user, str) or self.user is None 85 | 86 | # Get if valid 87 | def IsValid(self) -> bool: 88 | return self.user != "" if self.IsUsername() else self.user != 0 89 | 90 | # Get value 91 | def Get(self) -> Union[int, str]: 92 | return self.user 93 | 94 | # Get as key 95 | def GetAsKey(self) -> Union[int, str]: 96 | if not self.IsValid(): 97 | raise KeyError("An invalid user cannot be used as a key") 98 | 99 | return self.user if self.IsUserId() else self.user.lower() # type: ignore 100 | 101 | # Convert to string 102 | def ToString(self) -> str: 103 | return f"{self.user}" 104 | 105 | # Convert to string 106 | def __str__(self) -> str: 107 | return self.ToString() 108 | -------------------------------------------------------------------------------- /telegram_payment_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_payment_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_payment_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_payment_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().debug(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_payment_bot/utils/wrapped_dict.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 __future__ import annotations 25 | 26 | import typing 27 | from abc import ABC 28 | from collections.abc import ItemsView, KeysView, ValuesView 29 | from typing import Dict, Iterator, Union 30 | 31 | 32 | # 33 | # Classes 34 | # 35 | 36 | # Wrapped dict class 37 | class WrappedDict(ABC): 38 | 39 | dict_elements: Dict[typing.Any, typing.Any] 40 | 41 | # Constructor 42 | def __init__(self) -> None: 43 | self.dict_elements = {} 44 | 45 | # Add single element 46 | def AddSingle(self, 47 | key: typing.Any, 48 | value: typing.Any) -> None: 49 | self.dict_elements[key] = value 50 | 51 | # Add multiple elements 52 | def AddMultiple(self, 53 | elements: Union[Dict[typing.Any, typing.Any], WrappedDict]) -> None: 54 | if isinstance(elements, WrappedDict): 55 | self.dict_elements = {**self.dict_elements, **elements.GetDict()} 56 | else: 57 | self.dict_elements = {**self.dict_elements, **elements} 58 | 59 | # Remove single element 60 | def RemoveSingle(self, 61 | key: typing.Any) -> None: 62 | self.dict_elements.pop(key, None) 63 | 64 | # Get if key is present 65 | def IsKey(self, 66 | key: typing.Any) -> bool: 67 | return key in self.dict_elements 68 | 69 | # Get if value is present 70 | def IsValue(self, 71 | value: typing.Any) -> bool: 72 | return value in self.dict_elements.values() 73 | 74 | # Get keys 75 | def Keys(self) -> KeysView: 76 | return self.dict_elements.keys() 77 | 78 | # Get values 79 | def Values(self) -> ValuesView: 80 | return self.dict_elements.values() 81 | 82 | # Get items 83 | def Items(self) -> ItemsView: 84 | return self.dict_elements.items() 85 | 86 | # Clear element 87 | def Clear(self) -> None: 88 | self.dict_elements.clear() 89 | 90 | # Get elements count 91 | def Count(self) -> int: 92 | return len(self.dict_elements) 93 | 94 | # Get if any 95 | def Any(self) -> bool: 96 | return self.Count() > 0 97 | 98 | # Get if empty 99 | def Empty(self) -> bool: 100 | return self.Count() == 0 101 | 102 | # Get dict 103 | def GetDict(self) -> Dict[typing.Any, typing.Any]: 104 | return self.dict_elements 105 | 106 | # Get item 107 | def __getitem__(self, 108 | key: typing.Any): 109 | return self.dict_elements[key] 110 | 111 | # Delete item 112 | def __delitem__(self, 113 | key: typing.Any): 114 | del self.dict_elements[key] 115 | 116 | # Set item 117 | def __setitem__(self, 118 | key: typing.Any, 119 | value: typing.Any): 120 | self.dict_elements[key] = value 121 | 122 | # Get iterator 123 | def __iter__(self) -> Iterator[typing.Any]: 124 | yield from self.dict_elements 125 | -------------------------------------------------------------------------------- /telegram_payment_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_payment_bot.config.config_loader_ex import ConfigFieldNotExistentError, ConfigFieldValueError 27 | from telegram_payment_bot.config.config_object import ConfigObject 28 | from telegram_payment_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_payment_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_payment_bot.utils.utils import Utils 29 | from telegram_payment_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_payment_bot/member/joined_members_checker.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 | 28 | from telegram_payment_bot.auth_user.authorized_users_message_sender import AuthorizedUsersMessageSender 29 | from telegram_payment_bot.config.config_object import ConfigObject 30 | from telegram_payment_bot.logger.logger import Logger 31 | from telegram_payment_bot.member.members_kicker import MembersKicker 32 | from telegram_payment_bot.misc.helpers import UserHelper 33 | from telegram_payment_bot.translator.translation_loader import TranslationLoader 34 | 35 | 36 | # 37 | # Classes 38 | # 39 | 40 | # 41 | # Joined members checker class 42 | # 43 | class JoinedMembersChecker: 44 | 45 | client: pyrogram.Client 46 | config: ConfigObject 47 | logger: Logger 48 | translator: TranslationLoader 49 | auth_users_msg_sender: AuthorizedUsersMessageSender 50 | member_kicker: MembersKicker 51 | 52 | # Constructor 53 | def __init__(self, 54 | client: pyrogram.Client, 55 | config: ConfigObject, 56 | logger: Logger, 57 | translator: TranslationLoader) -> None: 58 | self.client = client 59 | self.config = config 60 | self.logger = logger 61 | self.translator = translator 62 | self.auth_users_msg_sender = AuthorizedUsersMessageSender(client, config, logger) 63 | self.member_kicker = MembersKicker(client, config, logger) 64 | 65 | # Check new users 66 | def CheckNewUsers(self, 67 | chat: pyrogram.types.Chat, 68 | new_users: List[pyrogram.types.User]) -> None: 69 | # Check all the new users 70 | for user in new_users: 71 | # Skip bots 72 | if not user.is_self and not user.is_bot: 73 | self.__CheckSingleUser(chat, user) 74 | 75 | # Check single user 76 | def __CheckSingleUser(self, 77 | chat: pyrogram.types.Chat, 78 | user: pyrogram.types.User) -> None: 79 | # Kick if no username 80 | if self.member_kicker.KickSingleIfNoUsername(chat, user): 81 | self.logger.GetLogger().info( 82 | f"New user {UserHelper.GetNameOrId(user)} kicked (joined with no username)" 83 | ) 84 | self.auth_users_msg_sender.SendMessage( 85 | chat, 86 | self.translator.GetSentence("JOINED_MEMBER_KICKED_FOR_USERNAME_MSG", 87 | name=UserHelper.GetNameOrId(user)) 88 | ) 89 | # Kick if no payment 90 | elif self.member_kicker.KickSingleIfExpiredPayment(chat, user): 91 | self.logger.GetLogger().info( 92 | f"New user {UserHelper.GetNameOrId(user)} kicked (joined with no payment)" 93 | ) 94 | self.auth_users_msg_sender.SendMessage( 95 | chat, 96 | self.translator.GetSentence("JOINED_MEMBER_KICKED_FOR_PAYMENT_MSG", 97 | name=UserHelper.GetNameOrId(user)) 98 | ) 99 | # Everything ok 100 | else: 101 | self.logger.GetLogger().info( 102 | f"New user {UserHelper.GetNameOrId(user)} joined, username and payment ok" 103 | ) 104 | -------------------------------------------------------------------------------- /telegram_payment_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_payment_bot.misc.helpers import UserHelper 29 | from telegram_payment_bot.utils.pyrogram_wrapper import PyrogramWrapper 30 | from telegram_payment_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 single 109 | def GetSingle(self, 110 | chat: pyrogram.types.Chat, 111 | user: pyrogram.types.User) -> ChatMembersList: 112 | return self.FilterMembers(chat, lambda member: member.user is not None and user.id == member.user.id) 113 | 114 | # Get admins 115 | def GetAdmins(self, 116 | chat: pyrogram.types.Chat) -> ChatMembersList: 117 | return self.FilterMembers(chat, 118 | lambda member: True, 119 | "administrators") 120 | -------------------------------------------------------------------------------- /telegram_payment_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_payment_bot.bot.bot_config_types import BotConfigTypes 30 | from telegram_payment_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 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=77", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "telegram_payment_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 managing payments" 15 | readme = "README.md" 16 | license = "MIT" 17 | license-files = [ 18 | "LICENSE", 19 | ] 20 | requires-python = ">=3.7" 21 | keywords = ["telegram", "bot", "telegram bot", "payments", "payments check"] 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_payment_bot" 37 | Changelog = "https://github.com/ebellocchia/telegram_payment_bot/blob/master/CHANGELOG.md" 38 | Repository = "https://github.com/ebellocchia/telegram_payment_bot" 39 | Download = "https://github.com/ebellocchia/telegram_payment_bot/archive/v{version}.tar.gz" 40 | 41 | [tool.setuptools] 42 | packages = {find = {}} 43 | 44 | [tool.setuptools.package-data] 45 | telegram_payment_bot = ["lang/lang_en.xml"] 46 | 47 | [tool.setuptools.dynamic] 48 | version = {attr = "telegram_payment_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_payment_bot/payment/payments_emailer.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 | 26 | import pyrogram 27 | 28 | from telegram_payment_bot.bot.bot_config_types import BotConfigTypes 29 | from telegram_payment_bot.config.config_object import ConfigObject 30 | from telegram_payment_bot.email.subscription_emailer import SubscriptionEmailer 31 | from telegram_payment_bot.logger.logger import Logger 32 | from telegram_payment_bot.member.members_payment_getter import MembersPaymentGetter 33 | from telegram_payment_bot.payment.payments_data import PaymentsData 34 | 35 | 36 | # 37 | # Classes 38 | # 39 | 40 | # Constants for payments emailer class 41 | class PaymentsEmailerConst: 42 | # Sleep time for sending emails 43 | SEND_EMAIL_SLEEP_TIME_SEC: float = 0.05 44 | 45 | 46 | # Payments emailer class 47 | class PaymentsEmailer: 48 | 49 | client: pyrogram.Client 50 | config: ConfigObject 51 | logger: Logger 52 | emailer: SubscriptionEmailer 53 | members_payment_getter: MembersPaymentGetter 54 | 55 | # Constructor 56 | def __init__(self, 57 | client: pyrogram.Client, 58 | config: ConfigObject, 59 | logger: Logger) -> None: 60 | self.client = client 61 | self.config = config 62 | self.logger = logger 63 | self.emailer = SubscriptionEmailer(config) 64 | self.members_payment_getter = MembersPaymentGetter(client, config, logger) 65 | 66 | # Email all users with expired payment 67 | def EmailAllWithExpiredPayment(self) -> PaymentsData: 68 | # Get expired members 69 | expired_payments = self.members_payment_getter.GetAllEmailsWithExpiredPayment() 70 | 71 | # Send emails 72 | self.__SendEmails(expired_payments) 73 | 74 | return expired_payments 75 | 76 | # Email all users with expiring payment in the specified number of days 77 | def EmailAllWithExpiringPayment(self, 78 | days: int) -> PaymentsData: 79 | # Get expired members 80 | expired_payments = self.members_payment_getter.GetAllEmailsWithExpiringPayment(days) 81 | 82 | # Send emails 83 | self.__SendEmails(expired_payments) 84 | 85 | return expired_payments 86 | 87 | # Send emails to expired payments 88 | def __SendEmails(self, 89 | expired_payments: PaymentsData) -> None: 90 | # Do not send emails if test mode 91 | if self.config.GetValue(BotConfigTypes.APP_TEST_MODE): 92 | self.logger.GetLogger().info("Test mode ON: no email was sent") 93 | return 94 | 95 | # Email members if any 96 | if expired_payments.Any(): 97 | emails = set() 98 | 99 | # Connect 100 | self.emailer.Connect() 101 | 102 | for payment in expired_payments.Values(): 103 | pay_email = payment.Email() 104 | 105 | # Check empty email 106 | if pay_email == "": 107 | self.logger.GetLogger().warning(f"No email set for user {payment.User()}, skipped") 108 | continue 109 | # Check duplicated emails 110 | if pay_email in emails: 111 | self.logger.GetLogger().warning(f"Email {pay_email} is present more than one time, skipped") 112 | continue 113 | 114 | # Prepare and send message 115 | self.emailer.PrepareMsg(pay_email) 116 | # Send email 117 | self.emailer.Send() 118 | self.logger.GetLogger().info( 119 | f"Email successfully sent to: {pay_email} ({payment.User()})" 120 | ) 121 | # Add to set 122 | emails.add(payment.Email()) 123 | # Sleep 124 | time.sleep(PaymentsEmailerConst.SEND_EMAIL_SLEEP_TIME_SEC) 125 | 126 | # Disconnect 127 | self.emailer.Disconnect() 128 | -------------------------------------------------------------------------------- /telegram_payment_bot/member/members_kicker.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 | 26 | import pyrogram 27 | 28 | from telegram_payment_bot.bot.bot_config_types import BotConfigTypes 29 | from telegram_payment_bot.config.config_object import ConfigObject 30 | from telegram_payment_bot.logger.logger import Logger 31 | from telegram_payment_bot.member.members_payment_getter import MembersPaymentGetter 32 | from telegram_payment_bot.member.members_username_getter import MembersUsernameGetter 33 | from telegram_payment_bot.misc.ban_helper import BanHelper 34 | from telegram_payment_bot.misc.chat_members import ChatMembersList 35 | 36 | 37 | # 38 | # Classes 39 | # 40 | 41 | # Constants for members kicker class 42 | class MembersKickerConst: 43 | # Sleep time 44 | SLEEP_TIME_SEC: float = 0.01 45 | 46 | 47 | # Members kicker class 48 | class MembersKicker: 49 | 50 | client: pyrogram.Client 51 | config: ConfigObject 52 | logger: Logger 53 | ban_helper: BanHelper 54 | members_payment_getter: MembersPaymentGetter 55 | members_username_getter: MembersUsernameGetter 56 | 57 | # Constructor 58 | def __init__(self, 59 | client: pyrogram.Client, 60 | config: ConfigObject, 61 | logger: Logger) -> None: 62 | self.client = client 63 | self.config = config 64 | self.logger = logger 65 | self.ban_helper = BanHelper(client) 66 | self.members_payment_getter = MembersPaymentGetter(client, config, logger) 67 | self.members_username_getter = MembersUsernameGetter(client, config) 68 | 69 | # Kick all members with expired payment 70 | def KickAllWithExpiredPayment(self, 71 | chat: pyrogram.types.Chat) -> ChatMembersList: 72 | no_payment_members = self.members_payment_getter.GetAllMembersWithExpiredPayment(chat) 73 | if no_payment_members.Any(): 74 | self.__KickMultiple(chat, no_payment_members) 75 | return no_payment_members 76 | 77 | # Kick single member if expired payment 78 | def KickSingleIfExpiredPayment(self, 79 | chat: pyrogram.types.Chat, 80 | user: pyrogram.types.User) -> bool: 81 | payment_expired = self.members_payment_getter.IsSingleMemberExpired(chat, user) 82 | if payment_expired: 83 | self.__KickSingle(chat, user) 84 | return payment_expired 85 | 86 | # Kick all members with no username 87 | def KickAllWithNoUsername(self, 88 | chat: pyrogram.types.Chat) -> ChatMembersList: 89 | no_username_members = self.members_username_getter.GetAllWithNoUsername(chat) 90 | if no_username_members.Any(): 91 | self.__KickMultiple(chat, no_username_members) 92 | return no_username_members 93 | 94 | # Kick single member if no username 95 | def KickSingleIfNoUsername(self, 96 | chat: pyrogram.types.Chat, 97 | user: pyrogram.types.User) -> bool: 98 | no_username = user.username is None 99 | if no_username: 100 | self.__KickSingle(chat, user) 101 | return no_username 102 | 103 | # Kick single 104 | def __KickSingle(self, 105 | chat: pyrogram.types.Chat, 106 | user: pyrogram.types.User) -> None: 107 | if not self.config.GetValue(BotConfigTypes.APP_TEST_MODE): 108 | self.ban_helper.KickUser(chat, user) 109 | else: 110 | self.logger.GetLogger().info("Test mode ON: no member was kicked") 111 | 112 | # Kick multiple 113 | def __KickMultiple(self, 114 | chat: pyrogram.types.Chat, 115 | members: ChatMembersList) -> None: 116 | if not self.config.GetValue(BotConfigTypes.APP_TEST_MODE): 117 | for member in members: 118 | self.ban_helper.KickUser(chat, member.user) 119 | time.sleep(MembersKickerConst.SLEEP_TIME_SEC) 120 | else: 121 | self.logger.GetLogger().info("Test mode ON: no member was kicked") 122 | -------------------------------------------------------------------------------- /telegram_payment_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_payment_bot.bot.bot_config_types import BotConfigTypes 30 | from telegram_payment_bot.bot.bot_handlers_config_typing import BotHandlersConfigType 31 | from telegram_payment_bot.command.command_dispatcher import CommandDispatcher, CommandTypes 32 | from telegram_payment_bot.config.config_file_sections_loader import ConfigFileSectionsLoader 33 | from telegram_payment_bot.config.config_object import ConfigObject 34 | from telegram_payment_bot.config.config_typing import ConfigSectionsType 35 | from telegram_payment_bot.logger.logger import Logger 36 | from telegram_payment_bot.message.message_dispatcher import MessageDispatcher, MessageTypes 37 | from telegram_payment_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_payment_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_payment_bot.bot.bot_config_types import BotConfigTypes 30 | from telegram_payment_bot.config.config_object import ConfigObject 31 | from telegram_payment_bot.logger.logger import Logger 32 | from telegram_payment_bot.member.joined_members_checker import JoinedMembersChecker 33 | from telegram_payment_bot.message.message_sender import MessageSender 34 | from telegram_payment_bot.translator.translation_loader import TranslationLoader 35 | 36 | 37 | # 38 | # Enumerations 39 | # 40 | 41 | # Message types 42 | @unique 43 | class MessageTypes(Enum): 44 | GROUP_CHAT_CREATED = auto() 45 | LEFT_CHAT_MEMBER = auto() 46 | NEW_CHAT_MEMBERS = auto() 47 | 48 | 49 | # 50 | # Classes 51 | # 52 | 53 | # Message dispatcher class 54 | class MessageDispatcher: 55 | 56 | config: ConfigObject 57 | logger: Logger 58 | translator: TranslationLoader 59 | 60 | # Constructor 61 | def __init__(self, 62 | config: ConfigObject, 63 | logger: Logger, 64 | translator: TranslationLoader) -> None: 65 | self.config = config 66 | self.logger = logger 67 | self.translator = translator 68 | 69 | # Dispatch command 70 | def Dispatch(self, 71 | client: pyrogram.Client, 72 | message: pyrogram.types.Message, 73 | msg_type: MessageTypes, 74 | **kwargs: Any) -> None: 75 | if not isinstance(msg_type, MessageTypes): 76 | raise TypeError("Message type is not an enumerative of MessageTypes") 77 | 78 | # Log 79 | self.logger.GetLogger().info(f"Dispatching message type: {msg_type}") 80 | 81 | # New chat created 82 | if msg_type == MessageTypes.GROUP_CHAT_CREATED: 83 | self.__OnCreatedChat(client, message, **kwargs) 84 | # A member left the chat 85 | elif msg_type == MessageTypes.LEFT_CHAT_MEMBER: 86 | self.__OnLeftMember(client, message, **kwargs) 87 | # A member joined the chat 88 | elif msg_type == MessageTypes.NEW_CHAT_MEMBERS: 89 | self.__OnJoinedMember(client, message, **kwargs) 90 | 91 | # Function called when a new chat is created 92 | def __OnCreatedChat(self, 93 | client, 94 | message: pyrogram.types.Message, 95 | **kwargs: Any) -> None: 96 | if message.chat is None: 97 | return 98 | 99 | # Send the welcome message 100 | MessageSender(client, self.logger).SendMessage( 101 | message.chat, 102 | self.translator.GetSentence("BOT_WELCOME_MSG") 103 | ) 104 | 105 | # Function called when a member left the chat 106 | def __OnLeftMember(self, 107 | client, 108 | message: pyrogram.types.Message, 109 | **kwargs: Any) -> None: 110 | # If the member is the bot itself, remove the chat from the scheduler 111 | if message.left_chat_member is not None and message.left_chat_member.is_self: 112 | kwargs["payments_check_scheduler"].ChatLeft(message.chat) 113 | 114 | # Function called when a member joined the chat 115 | def __OnJoinedMember(self, 116 | client, 117 | message: pyrogram.types.Message, 118 | **kwargs: Any) -> None: 119 | if message.new_chat_members is None or message.chat is None: 120 | return 121 | 122 | # If one of the members is the bot itself, send the welcome message 123 | for member in message.new_chat_members: 124 | if member.is_self: 125 | MessageSender(client, self.logger).SendMessage( 126 | message.chat, 127 | self.translator.GetSentence("BOT_WELCOME_MSG") 128 | ) 129 | break 130 | 131 | # Check joined members for payment in any case 132 | if self.config.GetValue(BotConfigTypes.PAYMENT_CHECK_ON_JOIN): 133 | JoinedMembersChecker(client, 134 | self.config, 135 | self.logger, 136 | self.translator).CheckNewUsers(message.chat, message.new_chat_members) 137 | -------------------------------------------------------------------------------- /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_payment_bot) 10 | # 11 | [build-system] 12 | requires = ["setuptools>=61", "wheel"] 13 | build-backend = "setuptools.build_meta" 14 | 15 | [project] 16 | name = "telegram_payment_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 managing payments" 25 | readme = "README.md" 26 | license = {text = "MIT"} 27 | requires-python = ">=3.7" 28 | keywords = ["telegram", "bot", "telegram bot", "payments", "payments check"] 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_payment_bot" 45 | Changelog = "https://github.com/ebellocchia/telegram_payment_bot/blob/master/CHANGELOG.md" 46 | Repository = "https://github.com/ebellocchia/telegram_payment_bot" 47 | Download = "https://github.com/ebellocchia/telegram_payment_bot/archive/v{version}.tar.gz" 48 | 49 | [tool.setuptools] 50 | packages = {find = {}} 51 | 52 | [tool.setuptools.package-data] 53 | telegram_payment_bot = ["lang/lang_en.xml"] 54 | 55 | [tool.setuptools.dynamic] 56 | version = {attr = "telegram_payment_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_payment_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 | import time 27 | from datetime import datetime, timedelta 28 | from typing import Iterator 29 | 30 | import pyrogram 31 | 32 | from telegram_payment_bot.utils.utils import Utils 33 | 34 | 35 | if int(pyrogram.__version__[0]) == 2: 36 | from pyrogram.enums import ChatMembersFilter, ChatMemberStatus, ChatType 37 | else: 38 | from enum import Enum 39 | 40 | # Fake enums 41 | class ChatMembersFilter(Enum): # type: ignore 42 | pass 43 | class ChatMemberStatus(Enum): # type: ignore 44 | pass 45 | 46 | 47 | # 48 | # Classes 49 | # 50 | 51 | # Wrapper for pyrogram for handling different versions 52 | class PyrogramWrapper: 53 | # Ban chat member 54 | @staticmethod 55 | def BanChatMember(client: pyrogram.Client, 56 | chat: pyrogram.types.Chat, 57 | user: pyrogram.types.User, 58 | time_sec: int = 0) -> None: 59 | if PyrogramWrapper.__MajorVersion() == 2: 60 | client.ban_chat_member(chat.id, 61 | user.id, 62 | until_date=datetime.now() + timedelta(seconds=time_sec)) 63 | elif PyrogramWrapper.__MajorVersion() == 1: 64 | if PyrogramWrapper.__MinorVersion() >= 3: 65 | client.ban_chat_member(chat.id, 66 | user.id, 67 | until_date=int(time.time() + time_sec)) 68 | else: 69 | client.kick_chat_member(chat.id, 70 | user.id, 71 | until_date=int(time.time() + time_sec)) 72 | 73 | # Get if member is status 74 | @staticmethod 75 | def MemberIsStatus(member: pyrogram.types.ChatMember, 76 | status_str: str) -> bool: 77 | if PyrogramWrapper.__MajorVersion() == 2: 78 | return member.status == PyrogramWrapper.__StrToChatMemberStatus(status_str) 79 | if PyrogramWrapper.__MajorVersion() == 1: 80 | return member.status == status_str 81 | raise RuntimeError("Unsupported pyrogram version") 82 | 83 | # Get message id 84 | @staticmethod 85 | def MessageId(message: pyrogram.types.Message) -> int: 86 | if PyrogramWrapper.__MajorVersion() == 2: 87 | return message.id 88 | if PyrogramWrapper.__MajorVersion() == 1: 89 | return message.message_id 90 | raise RuntimeError("Unsupported pyrogram version") 91 | 92 | # Get if channel 93 | @staticmethod 94 | def IsChannel(chat: pyrogram.types.Chat) -> bool: 95 | if PyrogramWrapper.__MajorVersion() == 2: 96 | return chat.type == ChatType.CHANNEL 97 | if PyrogramWrapper.__MajorVersion() == 1: 98 | return chat["type"] == "channel" 99 | raise RuntimeError("Unsupported pyrogram version") 100 | 101 | # Get if channel 102 | @staticmethod 103 | def GetChatMembers(client: pyrogram.Client, 104 | chat: pyrogram.types.Chat, 105 | filter_str: str) -> Iterator[pyrogram.types.ChatMember]: 106 | if PyrogramWrapper.__MajorVersion() == 2: 107 | return client.get_chat_members(chat.id, filter=PyrogramWrapper.__StrToChatMembersFilter(filter_str)) 108 | if PyrogramWrapper.__MajorVersion() == 1: 109 | return client.iter_chat_members(chat.id, filter=filter_str) 110 | raise RuntimeError("Unsupported pyrogram version") 111 | 112 | @staticmethod 113 | def __StrToChatMembersFilter(filter_str: str) -> ChatMembersFilter: 114 | str_to_enum = { 115 | "all": ChatMembersFilter.SEARCH, 116 | "banned": ChatMembersFilter.BANNED, 117 | "bots": ChatMembersFilter.BOTS, 118 | "restricted": ChatMembersFilter.RESTRICTED, 119 | "administrators": ChatMembersFilter.ADMINISTRATORS, 120 | } 121 | 122 | return str_to_enum[filter_str] 123 | 124 | @staticmethod 125 | def __StrToChatMemberStatus(status_str: str) -> ChatMemberStatus: 126 | str_to_enum = { 127 | "owner": ChatMemberStatus.OWNER, 128 | "administrator": ChatMemberStatus.ADMINISTRATOR, 129 | "member": ChatMemberStatus.MEMBER, 130 | "restricted": ChatMemberStatus.RESTRICTED, 131 | "left": ChatMemberStatus.LEFT, 132 | "banned": ChatMemberStatus.BANNED, 133 | } 134 | 135 | return str_to_enum[status_str] 136 | 137 | # Get major version 138 | @staticmethod 139 | def __MajorVersion() -> int: 140 | return Utils.StrToInt(pyrogram.__version__[0]) 141 | 142 | # Get minor version 143 | @staticmethod 144 | def __MinorVersion() -> int: 145 | return Utils.StrToInt(pyrogram.__version__[2]) 146 | -------------------------------------------------------------------------------- /telegram_payment_bot/payment/payments_check_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_payment_bot.auth_user.authorized_users_message_sender import AuthorizedUsersMessageSender 29 | from telegram_payment_bot.config.config_object import ConfigObject 30 | from telegram_payment_bot.logger.logger import Logger 31 | from telegram_payment_bot.member.members_kicker import MembersKicker 32 | from telegram_payment_bot.misc.helpers import ChatHelper 33 | from telegram_payment_bot.translator.translation_loader import TranslationLoader 34 | from telegram_payment_bot.utils.wrapped_dict import WrappedDict 35 | 36 | 37 | # 38 | # Classes 39 | # 40 | 41 | # Payments check job chats class 42 | class PaymentsCheckJobChats(WrappedDict): 43 | # Convert to string 44 | def ToString(self) -> str: 45 | return "\n".join( 46 | [f"- {ChatHelper.GetTitle(chat)}" for _, chat in self.dict_elements.items()] 47 | ) 48 | 49 | # Convert to string 50 | def __str__(self) -> str: 51 | return self.ToString() 52 | 53 | 54 | # Payments check job class 55 | class PaymentsCheckJob: 56 | 57 | client: pyrogram.Client 58 | config: ConfigObject 59 | logger: Logger 60 | translator: TranslationLoader 61 | job_chats_lock: Lock 62 | period: int 63 | auth_users_msg_sender: AuthorizedUsersMessageSender 64 | job_chats: PaymentsCheckJobChats 65 | 66 | # Constructor 67 | def __init__(self, 68 | client: pyrogram.Client, 69 | config: ConfigObject, 70 | logger: Logger, 71 | translator: TranslationLoader) -> None: 72 | self.client = client 73 | self.config = config 74 | self.logger = logger 75 | self.translator = translator 76 | self.job_chats_lock = Lock() 77 | self.period = 0 78 | self.auth_users_msg_sender = AuthorizedUsersMessageSender(client, config, logger) 79 | self.job_chats = PaymentsCheckJobChats() 80 | 81 | # Get period 82 | def GetPeriod(self) -> int: 83 | return self.period 84 | 85 | # Set period 86 | def SetPeriod(self, 87 | period: int) -> None: 88 | self.period = period 89 | 90 | # Add chat 91 | def AddChat(self, 92 | chat: pyrogram.types.Chat) -> bool: 93 | # Prevent accidental modifications while job is executing 94 | with self.job_chats_lock: 95 | if self.job_chats.IsKey(chat.id): 96 | return False 97 | 98 | self.job_chats.AddSingle(chat.id, chat) 99 | return True 100 | 101 | # Remove chat 102 | def RemoveChat(self, 103 | chat: pyrogram.types.Chat) -> bool: 104 | # Prevent accidental modifications while job is executing 105 | with self.job_chats_lock: 106 | if not self.job_chats.IsKey(chat.id): 107 | return False 108 | 109 | self.job_chats.RemoveSingle(chat.id) 110 | return True 111 | 112 | # Remove all chats 113 | def RemoveAllChats(self) -> None: 114 | # Prevent accidental modifications while job is executing 115 | with self.job_chats_lock: 116 | self.job_chats.Clear() 117 | 118 | # Get chat list 119 | def GetChats(self) -> PaymentsCheckJobChats: 120 | return self.job_chats 121 | 122 | # Do job 123 | def DoJob(self) -> None: 124 | # Log 125 | self.logger.GetLogger().info("Payments check job started") 126 | 127 | # Lock 128 | with self.job_chats_lock: 129 | # Exit if no chats 130 | if self.job_chats.Empty(): 131 | self.logger.GetLogger().info("No chat to check, exiting...") 132 | return 133 | 134 | # Kick members for each chat 135 | members_kicker = MembersKicker(self.client, self.config, self.logger) 136 | for chat in self.job_chats.Values(): 137 | self.__KickMembersInChat(chat, members_kicker) 138 | 139 | # Kick members in chat 140 | def __KickMembersInChat(self, 141 | chat: pyrogram.types.Chat, 142 | members_kicker: MembersKicker) -> None: 143 | # Kick all members 144 | self.logger.GetLogger().info(f"Checking payments for chat {ChatHelper.GetTitleOrId(chat)}...") 145 | kicked_members = members_kicker.KickAllWithExpiredPayment(chat) 146 | 147 | # Log kicked members 148 | self.logger.GetLogger().info( 149 | f"Kicked members for chat {ChatHelper.GetTitleOrId(chat)}: {kicked_members.Count()}" 150 | ) 151 | if kicked_members.Any(): 152 | self.logger.GetLogger().info(str(kicked_members)) 153 | 154 | # Inform authorized users 155 | msg = self.translator.GetSentence("REMOVE_NO_PAYMENT_NOTICE_CMD", 156 | chat_title=ChatHelper.GetTitle(chat)) 157 | msg += "\n\n" 158 | msg += self.translator.GetSentence("REMOVE_NO_PAYMENT_COMPLETED_CMD", 159 | members_count=kicked_members.Count()) 160 | msg += self.translator.GetSentence("REMOVE_NO_PAYMENT_LIST_CMD", 161 | members_list=str(kicked_members)) 162 | 163 | self.auth_users_msg_sender.SendMessage(chat, msg) 164 | -------------------------------------------------------------------------------- /telegram_payment_bot/email/smtp_emailer.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 smtplib 25 | from email.mime.multipart import MIMEMultipart 26 | from email.mime.text import MIMEText 27 | from typing import Optional 28 | 29 | 30 | # 31 | # Classes 32 | # 33 | 34 | # SMTP emailer error class 35 | class SmtpEmailerError(Exception): 36 | pass 37 | 38 | 39 | # SMTP emailer class 40 | class SmtpEmailer: 41 | 42 | html_msg: str 43 | plain_msg: str 44 | subject: str 45 | sender: str 46 | recipient: str 47 | reply_to: str 48 | host: str 49 | user: str 50 | password: str 51 | msg: Optional[MIMEMultipart] 52 | smtp: Optional[smtplib.SMTP] 53 | 54 | # Constructor 55 | def __init__(self): 56 | self.html_msg = "" 57 | self.plain_msg = "" 58 | self.subject = "" 59 | self.sender = "" 60 | self.recipient = "" 61 | self.reply_to = "" 62 | self.host = "" 63 | self.user = "" 64 | self.password = "" 65 | self.msg = None 66 | self.smtp = None 67 | 68 | # HTML message getter 69 | @property 70 | def HtmlMsg(self) -> str: 71 | return self.html_msg 72 | 73 | # HTML message setter 74 | @HtmlMsg.setter 75 | def HtmlMsg(self, 76 | html_msg: str) -> None: 77 | self.html_msg = html_msg 78 | 79 | # Plain message getter 80 | @property 81 | def PlainMsg(self) -> str: 82 | return self.plain_msg 83 | 84 | # Plain message setter 85 | @PlainMsg.setter 86 | def PlainMsg(self, 87 | plain_msg: str) -> None: 88 | self.plain_msg = plain_msg 89 | 90 | # Sender getter 91 | @property 92 | def From(self) -> str: 93 | return self.sender 94 | 95 | # Sender setter 96 | @From.setter 97 | def From(self, 98 | sender: str) -> None: 99 | self.sender = sender 100 | 101 | # Recipient getter 102 | @property 103 | def To(self) -> str: 104 | return self.recipient 105 | 106 | # Recipient setter 107 | @To.setter 108 | def To(self, 109 | recipient: str) -> None: 110 | self.recipient = recipient 111 | 112 | # Reply-to getter 113 | @property 114 | def ReplyTo(self) -> str: 115 | return self.reply_to 116 | 117 | # Reply-to setter 118 | @ReplyTo.setter 119 | def ReplyTo(self, 120 | reply_to: str) -> None: 121 | self.reply_to = reply_to 122 | 123 | # Subject getter 124 | @property 125 | def Subject(self) -> str: 126 | return self.subject 127 | 128 | # Subject setter 129 | @Subject.setter 130 | def Subject(self, 131 | subject: str) -> None: 132 | self.subject = subject 133 | 134 | # Host getter 135 | @property 136 | def Host(self) -> str: 137 | return self.host 138 | 139 | # Host setter 140 | @Host.setter 141 | def Host(self, 142 | host: str) -> None: 143 | self.host = host 144 | 145 | # User getter 146 | @property 147 | def User(self) -> str: 148 | return self.user 149 | 150 | # User setter 151 | @User.setter 152 | def User(self, 153 | user: str) -> None: 154 | self.user = user 155 | 156 | # Password getter 157 | @property 158 | def Password(self) -> str: 159 | return self.password 160 | 161 | # Password setter 162 | @Password.setter 163 | def Password(self, 164 | password: str) -> None: 165 | self.password = password 166 | 167 | # Prepare message 168 | def PrepareMsg(self) -> None: 169 | self.msg = MIMEMultipart("alternative") 170 | # Set header 171 | self.msg["From"] = self.sender 172 | self.msg["To"] = self.recipient 173 | self.msg["Subject"] = self.subject 174 | self.msg["Reply-To"] = self.reply_to 175 | # Set message body 176 | self.msg.attach(MIMEText(self.plain_msg, "plain")) 177 | self.msg.attach(MIMEText(self.html_msg, "html")) 178 | 179 | # Connect 180 | def Connect(self) -> None: 181 | try: 182 | self.smtp = smtplib.SMTP(self.host) 183 | if self.user != "": 184 | self.smtp.login(self.user, self.password) 185 | except smtplib.SMTPException as ex: 186 | raise SmtpEmailerError("Error while connecting") from ex 187 | 188 | # Disconnect 189 | def Disconnect(self) -> None: 190 | if self.smtp is None: 191 | raise SmtpEmailerError("Disconnect called before connecting") 192 | 193 | try: 194 | self.smtp.quit() 195 | self.smtp = None 196 | except smtplib.SMTPException as ex: 197 | raise SmtpEmailerError("Error while disconnecting") from ex 198 | 199 | # Send email 200 | def Send(self) -> None: 201 | if self.msg is None: 202 | raise SmtpEmailerError("Send called before preparing message") 203 | if self.smtp is None: 204 | raise SmtpEmailerError("Send called before connecting") 205 | 206 | try: 207 | self.smtp.sendmail(self.sender, self.recipient, self.msg.as_string()) 208 | except smtplib.SMTPException as ex: 209 | raise SmtpEmailerError("Error while sending email") from ex 210 | 211 | # Quick send email 212 | def QuickSend(self) -> None: 213 | self.PrepareMsg() 214 | self.Connect() 215 | self.Send() 216 | self.Disconnect() 217 | -------------------------------------------------------------------------------- /telegram_payment_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_payment_bot.command.command_base import CommandBase 30 | from telegram_payment_bot.command.commands import ( 31 | AliveCmd, 32 | AuthUsersCmd, 33 | ChatInfoCmd, 34 | CheckNoPaymentCmd, 35 | CheckNoUsernameCmd, 36 | CheckPaymentsDataCmd, 37 | EmailNoPaymentCmd, 38 | HelpCmd, 39 | InviteLinkCmd, 40 | IsCheckPaymentsOnJoinCmd, 41 | IsTestModeCmd, 42 | PaymentTaskAddChatCmd, 43 | PaymentTaskInfoCmd, 44 | PaymentTaskRemoveAllChatsCmd, 45 | PaymentTaskRemoveChatCmd, 46 | PaymentTaskStartCmd, 47 | PaymentTaskStopCmd, 48 | RemoveNoPaymentCmd, 49 | RemoveNoUsernameCmd, 50 | SetCheckPaymentsOnJoinCmd, 51 | SetTestModeCmd, 52 | UsersListCmd, 53 | VersionCmd, 54 | ) 55 | from telegram_payment_bot.config.config_object import ConfigObject 56 | from telegram_payment_bot.logger.logger import Logger 57 | from telegram_payment_bot.translator.translation_loader import TranslationLoader 58 | 59 | 60 | # 61 | # Enumerations 62 | # 63 | 64 | # Command types 65 | @unique 66 | class CommandTypes(Enum): 67 | # Generic 68 | START_CMD = auto() 69 | HELP_CMD = auto() 70 | ALIVE_CMD = auto() 71 | SET_TEST_MODE_CMD = auto() 72 | IS_TEST_MODE_CMD = auto() 73 | AUTH_USERS_CMD = auto() 74 | CHAT_INFO_CMD = auto() 75 | USERS_LIST_CMD = auto() 76 | INVITE_LINKS_CMD = auto() 77 | VERSION_CMD = auto() 78 | # Username 79 | CHECK_NO_USERNAME_CMD = auto() 80 | REMOVE_NO_USERNAME_CMD = auto() 81 | # Payment 82 | SET_CHECK_PAYMENT_ON_JOIN = auto() 83 | IS_CHECK_PAYMENT_ON_JOIN = auto() 84 | CHECK_PAYMENTS_DATA_CMD = auto() 85 | EMAIL_NO_PAYMENT_CMD = auto() 86 | CHECK_NO_PAYMENT_CMD = auto() 87 | REMOVE_NO_PAYMENT_CMD = auto() 88 | # Payment check task 89 | PAYMENT_TASK_START_CMD = auto() 90 | PAYMENT_TASK_STOP_CMD = auto() 91 | PAYMENT_TASK_ADD_CHAT_CMD = auto() 92 | PAYMENT_TASK_REMOVE_CHAT_CMD = auto() 93 | PAYMENT_TASK_REMOVE_ALL_CHATS_CMD = auto() 94 | PAYMENT_TASK_INFO_CMD = auto() 95 | 96 | 97 | # 98 | # Classes 99 | # 100 | 101 | # Comstant for command dispatcher class 102 | class CommandDispatcherConst: 103 | # Command to class map 104 | CMD_TYPE_TO_CLASS: Dict[CommandTypes, Type[CommandBase]] = { 105 | # Generic 106 | CommandTypes.START_CMD: HelpCmd, 107 | CommandTypes.HELP_CMD: HelpCmd, 108 | CommandTypes.ALIVE_CMD: AliveCmd, 109 | CommandTypes.SET_TEST_MODE_CMD: SetTestModeCmd, 110 | CommandTypes.IS_TEST_MODE_CMD: IsTestModeCmd, 111 | CommandTypes.AUTH_USERS_CMD: AuthUsersCmd, 112 | CommandTypes.CHAT_INFO_CMD: ChatInfoCmd, 113 | CommandTypes.USERS_LIST_CMD: UsersListCmd, 114 | CommandTypes.INVITE_LINKS_CMD: InviteLinkCmd, 115 | CommandTypes.VERSION_CMD: VersionCmd, 116 | # Username 117 | CommandTypes.CHECK_NO_USERNAME_CMD: CheckNoUsernameCmd, 118 | CommandTypes.REMOVE_NO_USERNAME_CMD: RemoveNoUsernameCmd, 119 | # Payment 120 | CommandTypes.SET_CHECK_PAYMENT_ON_JOIN: SetCheckPaymentsOnJoinCmd, 121 | CommandTypes.IS_CHECK_PAYMENT_ON_JOIN: IsCheckPaymentsOnJoinCmd, 122 | CommandTypes.CHECK_PAYMENTS_DATA_CMD: CheckPaymentsDataCmd, 123 | CommandTypes.EMAIL_NO_PAYMENT_CMD: EmailNoPaymentCmd, 124 | CommandTypes.CHECK_NO_PAYMENT_CMD: CheckNoPaymentCmd, 125 | CommandTypes.REMOVE_NO_PAYMENT_CMD: RemoveNoPaymentCmd, 126 | # Payment check task 127 | CommandTypes.PAYMENT_TASK_START_CMD: PaymentTaskStartCmd, 128 | CommandTypes.PAYMENT_TASK_STOP_CMD: PaymentTaskStopCmd, 129 | CommandTypes.PAYMENT_TASK_INFO_CMD: PaymentTaskInfoCmd, 130 | CommandTypes.PAYMENT_TASK_ADD_CHAT_CMD: PaymentTaskAddChatCmd, 131 | CommandTypes.PAYMENT_TASK_REMOVE_CHAT_CMD: PaymentTaskRemoveChatCmd, 132 | CommandTypes.PAYMENT_TASK_REMOVE_ALL_CHATS_CMD: PaymentTaskRemoveAllChatsCmd, 133 | } 134 | 135 | 136 | # Command dispatcher class 137 | class CommandDispatcher: 138 | 139 | config: ConfigObject 140 | logger: Logger 141 | translator: TranslationLoader 142 | 143 | # Constructor 144 | def __init__(self, 145 | config: ConfigObject, 146 | logger: Logger, 147 | translator: TranslationLoader) -> None: 148 | self.config = config 149 | self.logger = logger 150 | self.translator = translator 151 | 152 | # Dispatch command 153 | def Dispatch(self, 154 | client: pyrogram.Client, 155 | message: pyrogram.types.Message, 156 | cmd_type: CommandTypes, 157 | **kwargs: Any) -> None: 158 | if not isinstance(cmd_type, CommandTypes): 159 | raise TypeError("Command type is not an enumerative of CommandTypes") 160 | 161 | # Log 162 | self.logger.GetLogger().info(f"Dispatching command type: {cmd_type}") 163 | 164 | # Create and execute command if existent 165 | if cmd_type in CommandDispatcherConst.CMD_TYPE_TO_CLASS: 166 | cmd_class = CommandDispatcherConst.CMD_TYPE_TO_CLASS[cmd_type](client, 167 | self.config, 168 | self.logger, 169 | self.translator) 170 | cmd_class.Execute(message, **kwargs) 171 | -------------------------------------------------------------------------------- /telegram_payment_bot/payment/payments_google_sheet_loader.py: -------------------------------------------------------------------------------- 1 | # 2 | # Permission is hereby granted, free of charge, to any person obtaining a copy 3 | # of this software and associated documentation files (the "Software"), to deal 4 | # in the Software without restriction, including without limitation the rights 5 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | # copies of the Software, and to permit persons to whom the Software is 7 | # furnished to do so, subject to the following conditions: 8 | # 9 | # The above copyright notice and this permission notice shall be included in 10 | # all copies or substantial portions of the Software. 11 | # 12 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 17 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 18 | # THE SOFTWARE. 19 | 20 | # 21 | # Imports 22 | # 23 | from datetime import datetime 24 | from typing import Optional, Tuple 25 | 26 | from telegram_payment_bot.bot.bot_config_types import BotConfigTypes 27 | from telegram_payment_bot.config.config_object import ConfigObject 28 | from telegram_payment_bot.google.google_sheet_rows_getter import GoogleSheetRowsGetter 29 | from telegram_payment_bot.logger.logger import Logger 30 | from telegram_payment_bot.misc.user import User 31 | from telegram_payment_bot.payment.payments_data import PaymentErrorTypes, PaymentsData, PaymentsDataErrors, SinglePayment 32 | from telegram_payment_bot.payment.payments_loader_base import PaymentsLoaderBase 33 | 34 | 35 | # 36 | # Classes 37 | # 38 | 39 | # Payments Google Sheet loader class 40 | class PaymentsGoogleSheetLoader(PaymentsLoaderBase): 41 | 42 | google_sheet_rows_getter: GoogleSheetRowsGetter 43 | 44 | # Constructor 45 | def __init__(self, 46 | config: ConfigObject, 47 | logger: Logger) -> None: 48 | super().__init__(config, logger) 49 | self.google_sheet_rows_getter = GoogleSheetRowsGetter(config, logger) 50 | 51 | # Load all payments 52 | def LoadAll(self) -> PaymentsData: 53 | return self.__LoadAndCheckAll()[0] 54 | 55 | # Load single payment by user 56 | def LoadSingleByUser(self, 57 | user: User) -> Optional[SinglePayment]: 58 | return self.LoadAll().GetByUser(user) 59 | 60 | # Check for errors 61 | def CheckForErrors(self) -> PaymentsDataErrors: 62 | return self.__LoadAndCheckAll()[1] 63 | 64 | # Load and check all payments 65 | def __LoadAndCheckAll(self) -> Tuple[PaymentsData, PaymentsDataErrors]: 66 | try: 67 | # Load worksheet 68 | payments_data, payments_data_err = self.__LoadWorkSheet() 69 | # Log 70 | self.logger.GetLogger().info( 71 | f"Google Sheet successfully loaded, number of rows: {payments_data.Count()}" 72 | ) 73 | return payments_data, payments_data_err 74 | 75 | except Exception: 76 | self.logger.GetLogger().exception("An error occurred while loading Google Sheet") 77 | raise 78 | 79 | # Load worksheet 80 | def __LoadWorkSheet(self) -> Tuple[PaymentsData, PaymentsDataErrors]: 81 | payments_data = PaymentsData(self.config) 82 | payments_data_err = PaymentsDataErrors() 83 | 84 | # Get column indexes 85 | email_col_idx = self._ColumnToIndex(self.config.GetValue(BotConfigTypes.PAYMENT_EMAIL_COL)) 86 | user_col_idx = self._ColumnToIndex(self.config.GetValue(BotConfigTypes.PAYMENT_USER_COL)) 87 | expiration_col_idx = self._ColumnToIndex(self.config.GetValue(BotConfigTypes.PAYMENT_EXPIRATION_COL)) 88 | 89 | # Get all rows 90 | rows = self.google_sheet_rows_getter.GetRows( 91 | self.config.GetValue(BotConfigTypes.PAYMENT_WORKSHEET_IDX) 92 | ) 93 | 94 | # Read each row 95 | for i, row in enumerate(rows): 96 | # Skip header (first row) 97 | if i == 0: 98 | continue 99 | 100 | try: 101 | # Get cell values 102 | email = row[email_col_idx].strip() 103 | user = User.FromString(self.config, row[user_col_idx].strip()) 104 | expiration = row[expiration_col_idx].strip() 105 | except IndexError: 106 | self.logger.GetLogger().warning( 107 | f"Row index {i + 1} is not valid (some fields are missing), skipping it..." 108 | ) 109 | else: 110 | # Skip invalid users 111 | if user.IsValid(): 112 | self.__AddPayment(i + 1, payments_data, payments_data_err, email, user, expiration) 113 | 114 | return payments_data, payments_data_err 115 | 116 | # Add payment 117 | def __AddPayment(self, 118 | row_idx: int, 119 | payments_data: PaymentsData, 120 | payments_data_err: PaymentsDataErrors, 121 | email: str, 122 | user: User, 123 | expiration: str) -> None: 124 | # Convert date to datetime object 125 | try: 126 | expiration_datetime = datetime.strptime(expiration, 127 | self.config.GetValue(BotConfigTypes.PAYMENT_DATE_FORMAT)).date() 128 | except ValueError: 129 | self.logger.GetLogger().warning( 130 | f"Expiration date for user {user} at row {row_idx} is not valid ({expiration}), skipped" 131 | ) 132 | # Add error 133 | payments_data_err.AddPaymentError(PaymentErrorTypes.INVALID_DATE_ERR, 134 | row_idx, 135 | user, 136 | expiration) 137 | return 138 | 139 | # Add data 140 | if payments_data.AddPayment(email, user, expiration_datetime): 141 | self.logger.GetLogger().debug( 142 | f"{payments_data.Count():4d} - Row {row_idx:4d} | {email} | {user} | {expiration_datetime}" 143 | ) 144 | else: 145 | self.logger.GetLogger().warning( 146 | f"Row {row_idx} contains duplicated data, skipped" 147 | ) 148 | # Add error 149 | payments_data_err.AddPaymentError(PaymentErrorTypes.DUPLICATED_DATA_ERR, 150 | row_idx, 151 | user) 152 | -------------------------------------------------------------------------------- /telegram_payment_bot/payment/payments_excel_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 | from datetime import datetime 25 | from typing import Any, Optional, Tuple 26 | 27 | import xlrd 28 | 29 | from telegram_payment_bot.bot.bot_config_types import BotConfigTypes 30 | from telegram_payment_bot.misc.user import User 31 | from telegram_payment_bot.payment.payments_data import PaymentErrorTypes, PaymentsData, PaymentsDataErrors, SinglePayment 32 | from telegram_payment_bot.payment.payments_loader_base import PaymentsLoaderBase 33 | 34 | 35 | # 36 | # Classes 37 | # 38 | 39 | # Constants for payment Excel loader class 40 | class PaymentsExcelLoaderConst: 41 | # Sheet index 42 | SHEET_IDX: int = 0 43 | 44 | 45 | # Payments Excel loader class 46 | class PaymentsExcelLoader(PaymentsLoaderBase): 47 | # Load all payments 48 | def LoadAll(self) -> PaymentsData: 49 | return self.__LoadAndCheckAll()[0] 50 | 51 | # Load single payment by user 52 | def LoadSingleByUser(self, 53 | user: User) -> Optional[SinglePayment]: 54 | return self.LoadAll().GetByUser(user) 55 | 56 | # Check for errors 57 | def CheckForErrors(self) -> PaymentsDataErrors: 58 | return self.__LoadAndCheckAll()[1] 59 | 60 | # Load and check all payments 61 | def __LoadAndCheckAll(self) -> Tuple[PaymentsData, PaymentsDataErrors]: 62 | # Get payment file 63 | payment_file = self.config.GetValue(BotConfigTypes.PAYMENT_EXCEL_FILE) 64 | 65 | try: 66 | # Log 67 | self.logger.GetLogger().info(f"Loading file \"{payment_file}\"...") 68 | 69 | # Get sheet 70 | sheet = self.__GetSheet(payment_file) 71 | # Load sheet 72 | payments_data, payments_data_err = self.__LoadSheet(sheet) 73 | 74 | # Log 75 | self.logger.GetLogger().info( 76 | f"File \"{payment_file}\" successfully loaded, number of rows: {payments_data.Count()}" 77 | ) 78 | 79 | return payments_data, payments_data_err 80 | 81 | # Catch everything and log exception 82 | except Exception: 83 | self.logger.GetLogger().exception(f"An error occurred while loading file \"{payment_file}\"") 84 | raise 85 | 86 | # Load sheet 87 | def __LoadSheet(self, 88 | sheet: xlrd.sheet.Sheet) -> Tuple[PaymentsData, PaymentsDataErrors]: 89 | payments_data = PaymentsData(self.config) 90 | payments_data_err = PaymentsDataErrors() 91 | 92 | # Get column indexes 93 | email_col_idx = self._ColumnToIndex(self.config.GetValue(BotConfigTypes.PAYMENT_EMAIL_COL)) 94 | user_col_idx = self._ColumnToIndex(self.config.GetValue(BotConfigTypes.PAYMENT_USER_COL)) 95 | expiration_col_idx = self._ColumnToIndex(self.config.GetValue(BotConfigTypes.PAYMENT_EXPIRATION_COL)) 96 | 97 | # Read each row 98 | for i in range(sheet.nrows): 99 | # Skip header (first row) 100 | if i > 0: 101 | # Get cell values 102 | email = str(sheet.cell_value(i, email_col_idx)).strip() 103 | user = User.FromString(self.config, str(sheet.cell_value(i, user_col_idx)).strip()) 104 | expiration = sheet.cell_value(i, expiration_col_idx) 105 | 106 | # Skip invalid users 107 | if user.IsValid(): 108 | self.__AddPayment(i + 1, payments_data, payments_data_err, email, user, expiration) 109 | 110 | return payments_data, payments_data_err 111 | 112 | # Add payment 113 | def __AddPayment(self, 114 | row_idx: int, 115 | payments_data: PaymentsData, 116 | payments_data_err: PaymentsDataErrors, 117 | email: str, 118 | user: User, 119 | expiration: Any) -> None: 120 | # In Excel, a date can be a number or a string 121 | try: 122 | expiration_datetime = xlrd.xldate_as_datetime(expiration, 0).date() 123 | except TypeError: 124 | try: 125 | expiration_datetime = datetime.strptime(expiration.strip(), 126 | self.config.GetValue(BotConfigTypes.PAYMENT_DATE_FORMAT)).date() 127 | except ValueError: 128 | self.logger.GetLogger().warning( 129 | f"Expiration date for user {user} at row {row_idx} is not valid ({expiration}), skipped" 130 | ) 131 | # Add error 132 | payments_data_err.AddPaymentError(PaymentErrorTypes.INVALID_DATE_ERR, 133 | row_idx, 134 | user, 135 | expiration) 136 | return 137 | 138 | # Add data 139 | if payments_data.AddPayment(email, user, expiration_datetime): 140 | self.logger.GetLogger().debug( 141 | f"{payments_data.Count():4d} - Row {row_idx:4d} | {email} | {user} | {expiration_datetime}" 142 | ) 143 | else: 144 | self.logger.GetLogger().warning( 145 | f"Row {row_idx} contains duplicated data, skipped" 146 | ) 147 | # Add error 148 | payments_data_err.AddPaymentError(PaymentErrorTypes.DUPLICATED_DATA_ERR, 149 | row_idx, 150 | user) 151 | 152 | # Get sheet 153 | def __GetSheet(self, 154 | payment_file: str) -> xlrd.sheet.Sheet: 155 | # Open file 156 | wb = xlrd.open_workbook(payment_file) 157 | return wb.sheet_by_index(self.config.GetValue(BotConfigTypes.PAYMENT_WORKSHEET_IDX)) 158 | -------------------------------------------------------------------------------- /telegram_payment_bot/member/members_payment_getter.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, Optional 25 | 26 | import pyrogram 27 | 28 | from telegram_payment_bot.config.config_object import ConfigObject 29 | from telegram_payment_bot.logger.logger import Logger 30 | from telegram_payment_bot.misc.chat_members import ChatMembersGetter, ChatMembersList 31 | from telegram_payment_bot.misc.helpers import MemberHelper 32 | from telegram_payment_bot.misc.user import User 33 | from telegram_payment_bot.payment.payments_data import PaymentsData, SinglePayment 34 | from telegram_payment_bot.payment.payments_loader_base import PaymentsLoaderBase 35 | from telegram_payment_bot.payment.payments_loader_factory import PaymentsLoaderFactory 36 | 37 | 38 | # 39 | # Classes 40 | # 41 | 42 | # Members payment getter class 43 | class MembersPaymentGetter: 44 | 45 | client: pyrogram.Client 46 | config: ConfigObject 47 | logger: Logger 48 | payments_loader: PaymentsLoaderBase 49 | payments_cache: Optional[PaymentsData] 50 | single_payment_cache: Optional[Dict[str, Any]] 51 | 52 | # Constructor 53 | def __init__(self, 54 | client: pyrogram.Client, 55 | config: ConfigObject, 56 | logger: Logger) -> None: 57 | self.client = client 58 | self.config = config 59 | self.logger = logger 60 | self.payments_loader = PaymentsLoaderFactory(config, logger).CreateLoader() 61 | self.payments_cache = None 62 | self.single_payment_cache = None 63 | 64 | self.ReloadPayment() 65 | 66 | # Reload payment 67 | def ReloadPayment(self): 68 | self.payments_cache = None 69 | self.single_payment_cache = None 70 | 71 | # Get all members with OK payment 72 | def GetAllMembersWithOkPayment(self, 73 | chat: pyrogram.types.Chat) -> ChatMembersList: 74 | # Get all payments 75 | payments = self.__GetAllPayments() 76 | 77 | # Filter chat members 78 | return ChatMembersGetter(self.client).FilterMembers( 79 | chat, 80 | lambda member: ( 81 | MemberHelper.IsValidMember(member) and 82 | member.user is not None and 83 | member.user.username is not None and 84 | not payments.IsExpiredByUser(User.FromUserObject(self.config, member.user)) 85 | ) 86 | ) 87 | 88 | # Get all members with expired payment 89 | def GetAllMembersWithExpiredPayment(self, 90 | chat: pyrogram.types.Chat) -> ChatMembersList: 91 | # Get all payments 92 | payments = self.__GetAllPayments() 93 | 94 | # For safety: if no data was loaded, no user is expired 95 | if payments.Empty(): 96 | return ChatMembersList() 97 | 98 | # Filter chat members 99 | return ChatMembersGetter(self.client).FilterMembers( 100 | chat, 101 | lambda member: ( 102 | MemberHelper.IsValidMember(member) and 103 | member.user is not None and 104 | (member.user.username is None or 105 | payments.IsExpiredByUser(User.FromUserObject(self.config, member.user))) 106 | ) 107 | ) 108 | 109 | # Get all members with expiring payment 110 | def GetAllMembersWithExpiringPayment(self, 111 | chat: pyrogram.types.Chat, 112 | days: int) -> ChatMembersList: 113 | # Get all payments 114 | payments = self.__GetAllPayments() 115 | 116 | # For safety: if no data was loaded, no user is expired 117 | if payments.Empty(): 118 | return ChatMembersList() 119 | 120 | # Filter chat members 121 | return ChatMembersGetter(self.client).FilterMembers( 122 | chat, 123 | lambda member: ( 124 | MemberHelper.IsValidMember(member) and 125 | member.user is not None and 126 | (member.user.username is None or 127 | payments.IsExpiringInDaysByUser(User.FromUserObject(self.config, member.user), days)) 128 | ) 129 | ) 130 | 131 | # Get all emails with expired payment 132 | def GetAllEmailsWithExpiredPayment(self) -> PaymentsData: 133 | return self.__GetAllPayments().FilterExpired() 134 | 135 | # Get all emails with expiring payment in the specified number of days 136 | def GetAllEmailsWithExpiringPayment(self, 137 | days: int) -> PaymentsData: 138 | return self.__GetAllPayments().FilterExpiringInDays(days) 139 | 140 | # Get if single member is expired 141 | def IsSingleMemberExpired(self, 142 | chat: pyrogram.types.Chat, 143 | user: pyrogram.types.User) -> bool: 144 | # If the user is not in the chat, consider payment as not expired 145 | chat_members = ChatMembersGetter(self.client).GetSingle(chat, user) 146 | if chat_members is None: 147 | return False 148 | 149 | # Get single payment 150 | single_payment = self.__GetSinglePayment(user) 151 | # If the user is not in the payment data, consider payment as expired 152 | return single_payment.IsExpired() if single_payment is not None else True 153 | 154 | # Get all payments 155 | def __GetAllPayments(self) -> PaymentsData: 156 | # Load only the first time 157 | if self.payments_cache is None: 158 | self.payments_cache = self.payments_loader.LoadAll() 159 | 160 | return self.payments_cache 161 | 162 | # Get single payment 163 | def __GetSinglePayment(self, 164 | user: pyrogram.types.User) -> Optional[SinglePayment]: 165 | # Load only the first time 166 | if self.single_payment_cache is None or self.single_payment_cache["user_id"] != user.id: 167 | self.single_payment_cache = { 168 | "payment": self.payments_loader.LoadSingleByUser(User.FromUserObject(self.config, user)), 169 | "user_id": user.id, 170 | } 171 | 172 | return self.single_payment_cache["payment"] 173 | -------------------------------------------------------------------------------- /telegram_payment_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_payment_bot.auth_user.authorized_users_list import AuthorizedUsersList 32 | from telegram_payment_bot.auth_user.authorized_users_message_sender import AuthorizedUsersMessageSender 33 | from telegram_payment_bot.command.command_data import CommandData 34 | from telegram_payment_bot.config.config_object import ConfigObject 35 | from telegram_payment_bot.logger.logger import Logger 36 | from telegram_payment_bot.message.message_sender import MessageSender 37 | from telegram_payment_bot.misc.helpers import ChatHelper, UserHelper 38 | from telegram_payment_bot.translator.translation_loader import TranslationLoader 39 | 40 | 41 | # 42 | # Classes 43 | # 44 | 45 | # 46 | # Generic command base class 47 | # 48 | class CommandBase(ABC): 49 | 50 | client: pyrogram.Client 51 | config: ConfigObject 52 | logger: Logger 53 | translator: TranslationLoader 54 | message: pyrogram.types.Message 55 | cmd_data: CommandData 56 | message_sender: MessageSender 57 | 58 | # Constructor 59 | def __init__(self, 60 | client: pyrogram.Client, 61 | config: ConfigObject, 62 | logger: Logger, 63 | translator: TranslationLoader) -> None: 64 | self.client = client 65 | self.config = config 66 | self.logger = logger 67 | self.translator = translator 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 | # Set members 75 | self.message = message 76 | self.cmd_data = CommandData(message) 77 | 78 | # Log command 79 | self.__LogCommand() 80 | 81 | # Check if user is anonymous 82 | if self._IsUserAnonymous() and not self._IsChannel(): 83 | self.logger.GetLogger().warning("An anonymous user tried to execute the command, exiting") 84 | return 85 | 86 | # Check if user is authorized 87 | if not self._IsUserAuthorized(): 88 | if self._IsPrivateChat(): 89 | self._SendMessage(self.translator.GetSentence("AUTH_ONLY_ERR_MSG")) 90 | 91 | self.logger.GetLogger().warning( 92 | f"User {UserHelper.GetNameOrId(self.cmd_data.User())} tried to execute the command but it's not authorized" 93 | ) 94 | return 95 | 96 | # Try to execute command 97 | try: 98 | self._ExecuteCommand(**kwargs) 99 | except RPCError: 100 | self._SendMessage(self.translator.GetSentence("GENERIC_ERR_MSG")) 101 | self.logger.GetLogger().exception( 102 | f"An error occurred while executing command {self.cmd_data.Name()}" 103 | ) 104 | 105 | # Send message 106 | def _SendMessage(self, 107 | msg: str) -> None: 108 | if self._IsQuietMode(): 109 | cmd_user = self.cmd_data.User() 110 | if not self._IsChannel() and cmd_user is not None: 111 | self.message_sender.SendMessage(cmd_user, msg) 112 | else: 113 | self._SendMessageToAuthUsers(msg) 114 | else: 115 | try: 116 | self.message_sender.SendMessage( 117 | self.cmd_data.Chat(), 118 | msg, 119 | reply_to_message_id=self.message.reply_to_message_id 120 | ) 121 | # Send message privately if topic is closed 122 | except BadRequest: 123 | self.message_sender.SendMessage(self.cmd_data.User(), msg) 124 | 125 | # Send message to authorized users 126 | def _SendMessageToAuthUsers(self, 127 | msg: str) -> None: 128 | AuthorizedUsersMessageSender(self.client, 129 | self.config, 130 | self.logger).SendMessage(self.cmd_data.Chat(), msg) 131 | 132 | # Get if channel 133 | def _IsChannel(self) -> bool: 134 | return ChatHelper.IsChannel(self.cmd_data.Chat()) 135 | 136 | # Get if user is anonymous 137 | def _IsUserAnonymous(self) -> bool: 138 | return self.cmd_data.User() is None 139 | 140 | # Get if user is authorized 141 | def _IsUserAuthorized(self) -> bool: 142 | if not self._IsChannel(): 143 | user = self.cmd_data.User() 144 | return user is not None and AuthorizedUsersList(self.config).IsUserPresent(user) 145 | # In channels only admins can write, so we consider the user authorized since there is no way to know the specific user 146 | # This is a limitation for channels only 147 | return True 148 | 149 | # Get if chat is private 150 | def _IsPrivateChat(self) -> bool: 151 | cmd_user = self.cmd_data.User() 152 | if cmd_user is None: 153 | return False 154 | return ChatHelper.IsPrivateChat(self.cmd_data.Chat(), cmd_user) 155 | 156 | # Get if quiet mode 157 | def _IsQuietMode(self) -> bool: 158 | return self.cmd_data.Params().IsLast("q") or self.cmd_data.Params().IsLast("quiet") 159 | 160 | # Generate new invite link 161 | def _NewInviteLink(self) -> None: 162 | # Generate new invite link 163 | invite_link = self.client.export_chat_invite_link(self.cmd_data.Chat().id) 164 | # Send messages 165 | self._SendMessage(self.translator.GetSentence("INVITE_LINK_ALL_CMD")) 166 | self._SendMessageToAuthUsers( 167 | self.translator.GetSentence("INVITE_LINK_AUTH_CMD", 168 | chat_title=ChatHelper.GetTitle(self.cmd_data.Chat()), 169 | invite_link=invite_link) 170 | ) 171 | 172 | # Log command 173 | def __LogCommand(self) -> None: 174 | self.logger.GetLogger().info(f"Command: {self.cmd_data.Name()}") 175 | self.logger.GetLogger().info(f"Executed by user: {UserHelper.GetNameOrId(self.cmd_data.User())}") 176 | self.logger.GetLogger().debug(f"Received message: {self.message}") 177 | 178 | # Execute command - Abstract method 179 | @abstractmethod 180 | def _ExecuteCommand(self, 181 | **kwargs: Any) -> None: 182 | pass 183 | -------------------------------------------------------------------------------- /telegram_payment_bot/payment/payments_check_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 | import pyrogram 25 | from apscheduler.schedulers.background import BackgroundScheduler 26 | 27 | from telegram_payment_bot.bot.bot_config_types import BotConfigTypes 28 | from telegram_payment_bot.config.config_object import ConfigObject 29 | from telegram_payment_bot.logger.logger import Logger 30 | from telegram_payment_bot.misc.helpers import ChatHelper 31 | from telegram_payment_bot.payment.payments_check_job import PaymentsCheckJob, PaymentsCheckJobChats 32 | from telegram_payment_bot.translator.translation_loader import TranslationLoader 33 | 34 | 35 | # 36 | # Classes 37 | # 38 | 39 | # Job already running error 40 | class PaymentsCheckJobAlreadyRunningError(Exception): 41 | pass 42 | 43 | 44 | # Job not running error 45 | class PaymentsCheckJobNotRunningError(Exception): 46 | pass 47 | 48 | 49 | # Job invalid period error 50 | class PaymentsCheckJobInvalidPeriodError(Exception): 51 | pass 52 | 53 | 54 | # Job chat already present 55 | class PaymentsCheckJobChatAlreadyPresentError(Exception): 56 | pass 57 | 58 | 59 | # Job chat not present 60 | class PaymentsCheckJobChatNotPresentError(Exception): 61 | pass 62 | 63 | 64 | # Constants for payments check scheduler class 65 | class PaymentsCheckSchedulerConst: 66 | # Minimum/Maximum periods 67 | MIN_PERIOD_HOURS: int = 1 68 | MAX_PERIOD_HOURS: int = 24 69 | # Job ID 70 | JOB_ID: str = "payment_check_job" 71 | 72 | 73 | # Payments check scheduler class 74 | class PaymentsCheckScheduler: 75 | 76 | config: ConfigObject 77 | logger: Logger 78 | payments_checker_job: PaymentsCheckJob 79 | scheduler: BackgroundScheduler 80 | 81 | # Constructor 82 | def __init__(self, 83 | client: pyrogram.Client, 84 | config: ConfigObject, 85 | logger: Logger, 86 | translator: TranslationLoader) -> None: 87 | self.config = config 88 | self.logger = logger 89 | self.payments_checker_job = PaymentsCheckJob(client, config, logger, translator) 90 | self.scheduler = BackgroundScheduler() 91 | self.scheduler.start() 92 | 93 | # Get chats 94 | def GetChats(self) -> PaymentsCheckJobChats: 95 | return self.payments_checker_job.GetChats() 96 | 97 | # Get period 98 | def GetPeriod(self) -> int: 99 | return self.payments_checker_job.GetPeriod() 100 | 101 | # Start 102 | def Start(self, 103 | period_hours: int) -> None: 104 | # Check if running 105 | if self.IsRunning(): 106 | self.logger.GetLogger().error("Payments check job already running, cannot start it") 107 | raise PaymentsCheckJobAlreadyRunningError() 108 | 109 | # Check period 110 | if (period_hours < PaymentsCheckSchedulerConst.MIN_PERIOD_HOURS or 111 | period_hours > PaymentsCheckSchedulerConst.MAX_PERIOD_HOURS): 112 | self.logger.GetLogger().error( 113 | f"Invalid period {period_hours} for payments check job, cannot start it" 114 | ) 115 | raise PaymentsCheckJobInvalidPeriodError() 116 | 117 | # Add job 118 | self.__AddJob(period_hours) 119 | 120 | # Stop 121 | def Stop(self) -> None: 122 | if not self.IsRunning(): 123 | self.logger.GetLogger().error("Payments check job not running, cannot stop it") 124 | raise PaymentsCheckJobNotRunningError() 125 | 126 | self.scheduler.remove_job(PaymentsCheckSchedulerConst.JOB_ID) 127 | self.logger.GetLogger().info("Stopped payments check job") 128 | 129 | # Add chat 130 | def AddChat(self, 131 | chat: pyrogram.types.Chat) -> None: 132 | if not self.payments_checker_job.AddChat(chat): 133 | self.logger.GetLogger().error( 134 | f"Chat {ChatHelper.GetTitleOrId(chat)} already present in payments check job, cannot add it" 135 | ) 136 | raise PaymentsCheckJobChatAlreadyPresentError() 137 | 138 | self.logger.GetLogger().info( 139 | f"Added chat {ChatHelper.GetTitleOrId(chat)} to payments check job" 140 | ) 141 | 142 | # Remove chat 143 | def RemoveChat(self, 144 | chat: pyrogram.types.Chat) -> None: 145 | if not self.payments_checker_job.RemoveChat(chat): 146 | self.logger.GetLogger().error( 147 | f"Chat {ChatHelper.GetTitleOrId(chat)} not present in payments check job, cannot remove it" 148 | ) 149 | raise PaymentsCheckJobChatNotPresentError() 150 | 151 | self.logger.GetLogger().info( 152 | f"Removed chat {ChatHelper.GetTitleOrId(chat)} from payments check job" 153 | ) 154 | 155 | # Called when chat is left by the bot 156 | def ChatLeft(self, 157 | chat: pyrogram.types.Chat) -> None: 158 | self.payments_checker_job.RemoveChat(chat) 159 | self.logger.GetLogger().info(f"Left chat {ChatHelper.GetTitleOrId(chat)}") 160 | 161 | # Remove all chats 162 | def RemoveAllChats(self) -> None: 163 | self.payments_checker_job.RemoveAllChats() 164 | self.logger.GetLogger().info("Removed all chats from payments check job") 165 | 166 | # Get if running 167 | def IsRunning(self) -> bool: 168 | return self.scheduler.get_job(PaymentsCheckSchedulerConst.JOB_ID) is not None 169 | 170 | # Add job 171 | def __AddJob(self, 172 | period: int) -> None: 173 | # Set period 174 | self.payments_checker_job.SetPeriod(period) 175 | # Add job 176 | is_test_mode = self.config.GetValue(BotConfigTypes.APP_TEST_MODE) 177 | cron_str = self.__BuildCronString(period, is_test_mode) 178 | if is_test_mode: 179 | self.scheduler.add_job(self.payments_checker_job.DoJob, 180 | "cron", 181 | minute=cron_str, 182 | id=PaymentsCheckSchedulerConst.JOB_ID) 183 | else: 184 | self.scheduler.add_job(self.payments_checker_job.DoJob, 185 | "cron", 186 | hour=cron_str, 187 | id=PaymentsCheckSchedulerConst.JOB_ID) 188 | # Log 189 | per_sym = "minute(s)" if is_test_mode else "hour(s)" 190 | self.logger.GetLogger().info( 191 | f"Started payments check job (period: {period} {per_sym}, cron: {cron_str})" 192 | ) 193 | 194 | # Build cron string 195 | @staticmethod 196 | def __BuildCronString(period: int, 197 | is_test_mode: bool) -> str: 198 | max_val = 24 if not is_test_mode else 60 199 | return ",".join([str(i) for i in range(0, max_val, period)]) 200 | -------------------------------------------------------------------------------- /telegram_payment_bot/payment/payments_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 __future__ import annotations 25 | 26 | import datetime 27 | from enum import Enum, auto, unique 28 | from typing import Optional 29 | 30 | from telegram_payment_bot.bot.bot_config_types import BotConfigTypes 31 | from telegram_payment_bot.config.config_object import ConfigObject 32 | from telegram_payment_bot.misc.user import User 33 | from telegram_payment_bot.utils.wrapped_dict import WrappedDict 34 | from telegram_payment_bot.utils.wrapped_list import WrappedList 35 | 36 | 37 | # 38 | # Enumerations 39 | # 40 | 41 | # Payment error types 42 | @unique 43 | class PaymentErrorTypes(Enum): 44 | DUPLICATED_DATA_ERR = auto() 45 | INVALID_DATE_ERR = auto() 46 | 47 | 48 | # 49 | # Classes 50 | # 51 | 52 | # Single payment class 53 | class SinglePayment: 54 | 55 | email: str 56 | user: User 57 | expiration_date: datetime.date 58 | 59 | # Constructor 60 | def __init__(self, 61 | email: str, 62 | user: User, 63 | expiration_date: datetime.date): 64 | self.email = email 65 | self.user = user 66 | self.expiration_date = expiration_date 67 | 68 | # Get email 69 | def Email(self) -> str: 70 | return self.email 71 | 72 | # Get user 73 | def User(self) -> User: 74 | return self.user 75 | 76 | # Get expiration date 77 | def ExpirationDate(self) -> datetime.date: 78 | return self.expiration_date 79 | 80 | # Get days left until expiration 81 | def DaysLeft(self) -> int: 82 | return (self.expiration_date - datetime.date.today()).days 83 | 84 | # Get if expired 85 | def IsExpired(self) -> bool: 86 | return self.expiration_date < datetime.date.today() 87 | 88 | # Get if expiring in the specified number of days 89 | def IsExpiringInDays(self, 90 | days: int) -> bool: 91 | return self.DaysLeft() < days 92 | 93 | # Convert to string 94 | def ToString(self) -> str: 95 | return f"{self.email} ({self.user}): {self.expiration_date.strftime('%Y-%m-%d')}" 96 | 97 | # Convert to string 98 | def __str__(self) -> str: 99 | return self.ToString() 100 | 101 | 102 | # Payment error class 103 | class PaymentError: 104 | 105 | err_type: PaymentErrorTypes 106 | row: int 107 | user: User 108 | expiration_date: Optional[str] 109 | 110 | # Constructor 111 | def __init__(self, 112 | err_type: PaymentErrorTypes, 113 | row: int, 114 | user: User, 115 | expiration_data: Optional[str]): 116 | self.err_type = err_type 117 | self.row = row 118 | self.user = user 119 | self.expiration_date = expiration_data 120 | 121 | # Get type 122 | def Type(self) -> PaymentErrorTypes: 123 | return self.err_type 124 | 125 | # Get row 126 | def Row(self) -> int: 127 | return self.row 128 | 129 | # Get user 130 | def User(self) -> User: 131 | return self.user 132 | 133 | # Get expiration date 134 | def ExpirationDate(self) -> Optional[str]: 135 | return self.expiration_date 136 | 137 | 138 | # Payments data error class 139 | class PaymentsDataErrors(WrappedList): 140 | # Add payment error 141 | def AddPaymentError(self, 142 | err_type: PaymentErrorTypes, 143 | row: int, 144 | user: User, 145 | expiration: Optional[str] = None) -> None: 146 | self.AddSingle(PaymentError(err_type, row, user, expiration)) 147 | 148 | 149 | # Payments data class 150 | class PaymentsData(WrappedDict): 151 | 152 | config: ConfigObject 153 | 154 | # Constructor 155 | def __init__(self, 156 | config: ConfigObject) -> None: 157 | super().__init__() 158 | self.config = config 159 | 160 | # Add payment 161 | def AddPayment(self, 162 | email: str, 163 | user: User, 164 | expiration: datetime.date) -> bool: 165 | # User shall not be existent 166 | if not self.IsUserExistent(user): 167 | # Check for duplicated email if configured 168 | if self.config.GetValue(BotConfigTypes.PAYMENT_CHECK_DUP_EMAIL): 169 | if email != "" and self.IsEmailExistent(email): 170 | return False 171 | self.AddSingle(user.GetAsKey(), SinglePayment(email, user, expiration)) 172 | return True 173 | 174 | return False 175 | 176 | # Get by email 177 | def GetByEmail(self, 178 | email: str) -> Optional[SinglePayment]: 179 | for _, payment in self.dict_elements.items(): 180 | if email == payment.Email(): 181 | return payment 182 | return None 183 | 184 | # Get by user 185 | def GetByUser(self, 186 | user: User) -> Optional[SinglePayment]: 187 | if not user.IsValid() or user.GetAsKey() not in self.dict_elements: 188 | return None 189 | return self.dict_elements[user.GetAsKey()] 190 | 191 | # Get if email is existent 192 | def IsEmailExistent(self, 193 | email: str) -> bool: 194 | return self.GetByEmail(email) is not None 195 | 196 | # Get if user is existent 197 | def IsUserExistent(self, 198 | user: User) -> bool: 199 | return self.GetByUser(user) is not None 200 | 201 | # Get if the payment associated to the user is expired 202 | def IsExpiredByUser(self, 203 | user: User) -> bool: 204 | # Get user payment 205 | payment = self.GetByUser(user) 206 | # If user is not in the file, consider it as expired 207 | return payment.IsExpired() if payment is not None else True 208 | 209 | # Get if the payment associated to the user is expiring payments in the specified number of days 210 | def IsExpiringInDaysByUser(self, 211 | user: User, 212 | days: int) -> bool: 213 | # Get user payment 214 | payment = self.GetByUser(user) 215 | # If user is not in the file, consider it as expired 216 | return payment.IsExpiringInDays(days) if payment is not None else True 217 | 218 | # Filter expired payments 219 | def FilterExpired(self) -> PaymentsData: 220 | expired_payments = {user: payment for (user, payment) 221 | in self.dict_elements.items() 222 | if payment.IsExpired()} 223 | 224 | payments = PaymentsData(self.config) 225 | payments.AddMultiple(expired_payments) 226 | 227 | return payments 228 | 229 | # Filter expiring payments in the specified number of days 230 | def FilterExpiringInDays(self, 231 | days: int) -> PaymentsData: 232 | expiring_payments = {user: payment for (user, payment) 233 | in self.dict_elements.items() 234 | if payment.IsExpiringInDays(days)} 235 | 236 | payments = PaymentsData(self.config) 237 | payments.AddMultiple(expiring_payments) 238 | 239 | return payments 240 | 241 | # Convert to string 242 | def ToString(self) -> str: 243 | return "\n".join( 244 | [f"- {str(payment)}" for _, payment in self.dict_elements.items()] 245 | ) 246 | 247 | # Convert to string 248 | def __str__(self) -> str: 249 | return self.ToString() 250 | --------------------------------------------------------------------------------