├── 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 |
--------------------------------------------------------------------------------