├── telegram_crypto_price_bot ├── bot │ ├── __init__.py │ ├── bot_handlers_config_typing.py │ ├── bot_config_types.py │ ├── bot_base.py │ ├── bot_handlers_config.py │ └── bot_config.py ├── misc │ ├── __init__.py │ ├── helpers.py │ ├── formatters.py │ └── chat_members.py ├── utils │ ├── __init__.py │ ├── utils.py │ ├── wrapped_list.py │ └── pyrogram_wrapper.py ├── chart_info │ ├── __init__.py │ ├── chart_info.py │ └── chart_info_file_saver.py ├── coin_info │ ├── __init__.py │ └── coin_info_job.py ├── coingecko │ ├── __init__.py │ └── coingecko_price_api.py ├── command │ ├── __init__.py │ ├── command_data.py │ ├── command_dispatcher.py │ ├── command_base.py │ └── commands.py ├── config │ ├── __init__.py │ ├── config_typing.py │ ├── config_loader_ex.py │ ├── config_file_sections_loader.py │ ├── config_sections_loader.py │ ├── config_object.py │ └── config_section_loader.py ├── logger │ ├── __init__.py │ └── logger.py ├── message │ ├── __init__.py │ ├── message_deleter.py │ ├── message_sender.py │ └── message_dispatcher.py ├── price_info │ ├── __init__.py │ ├── price_info.py │ └── price_info_builder.py ├── translation │ ├── __init__.py │ └── translation_loader.py ├── info_message_sender │ ├── __init__.py │ ├── price_info_message_sender.py │ ├── chart_info_message_sender.py │ ├── info_message_sender_base.py │ ├── chart_price_info_message_sender.py │ └── coin_info_message_sender.py ├── _version.py ├── __init__.py ├── price_bot.py └── lang │ └── lang_en.xml ├── MANIFEST.in ├── requirements-dev.txt ├── requirements.txt ├── img ├── example_diff_msg.png └── example_same_msg.png ├── .gitignore ├── .github └── workflows │ ├── code-analysis.yml │ └── build.yml ├── LICENSE ├── app ├── conf │ └── config.ini ├── bot.py └── lang │ └── lang_it.xml ├── CHANGELOG.md ├── pyproject.toml ├── pyproject_legacy.toml └── README.md /telegram_crypto_price_bot/bot/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/misc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include pyproject_legacy.toml 2 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/chart_info/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/coin_info/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/coingecko/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/command/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/logger/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/message/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/price_info/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | mypy>=0.900 2 | ruff>=0.1 3 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/translation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/info_message_sender/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/_version.py: -------------------------------------------------------------------------------- 1 | __version__: str = "0.4.1" 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pycoingecko 2 | matplotlib 3 | pyrotgfork 4 | tgcrypto 5 | apscheduler 6 | defusedxml 7 | -------------------------------------------------------------------------------- /img/example_diff_msg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ebellocchia/telegram_crypto_price_bot/HEAD/img/example_diff_msg.png -------------------------------------------------------------------------------- /img/example_same_msg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ebellocchia/telegram_crypto_price_bot/HEAD/img/example_same_msg.png -------------------------------------------------------------------------------- /telegram_crypto_price_bot/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Imports 3 | # 4 | from telegram_crypto_price_bot._version import __version__ 5 | from telegram_crypto_price_bot.price_bot import PriceBot 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled 2 | *~ 3 | *.pyc 4 | *.pyo 5 | __pycache__/ 6 | 7 | # Distribution 8 | build/ 9 | dist/ 10 | eggs/ 11 | .eggs/ 12 | sdist/ 13 | *.egg-info/ 14 | *.egg 15 | telegram_crypto_price_bot-*/ 16 | 17 | # Unit test / Coverage reports 18 | htmlcov 19 | .coverage 20 | .coverage.* 21 | .pytest_cache/ 22 | .tox/ 23 | 24 | # Environments 25 | .idea 26 | .env 27 | .venv 28 | env/ 29 | venv/ 30 | 31 | # mypy 32 | .mypy_cache/ 33 | 34 | # Ruff 35 | .ruff_cache/ 36 | 37 | # Additional 38 | *.log 39 | *.session 40 | *.session-journal 41 | -------------------------------------------------------------------------------- /.github/workflows/code-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies and run code analysis tools 2 | 3 | name: Code Analysis 4 | 5 | on: 6 | push: 7 | branches: [ "master" ] 8 | paths-ignore: 9 | - '*.md' 10 | pull_request: 11 | branches: [ "master" ] 12 | paths-ignore: 13 | - '*.md' 14 | 15 | jobs: 16 | code_analysis: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Set up Python 3.13 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: "3.13" 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -r requirements.txt 28 | pip install -r requirements-dev.txt 29 | - name: Run code analysis 30 | run: | 31 | # Run mypy 32 | mypy . 33 | # Run ruff 34 | ruff check . 35 | -------------------------------------------------------------------------------- /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_crypto_price_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_crypto_price_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 | -------------------------------------------------------------------------------- /app/conf/config.ini: -------------------------------------------------------------------------------- 1 | # Configuration for Pyrogram 2 | [pyrogram] 3 | session_name = crypto_price_bot_client 4 | api_id = 0000000 5 | api_hash = 00000000000000000000000000000000 6 | bot_token = 0000000000:AAAAAAAAAAAA-0000000000000000000000 7 | 8 | # App configuration 9 | [app] 10 | app_test_mode = False 11 | # Example with custom translation 12 | #app_lang_file = lang/lang_it.xml 13 | 14 | # Task configuration 15 | [task] 16 | tasks_max_num = 10 17 | 18 | # Coingecko configuration (optional) 19 | #[coingecko] 20 | #coingecko_api_key = 21 | 22 | # Chart configuration 23 | [chart] 24 | chart_display = True 25 | chart_date_format = %%d/%%m/%%Y %%H:00 26 | chart_background_color = white 27 | chart_title_color = black 28 | chart_frame_color = black 29 | chart_axes_color = black 30 | chart_line_color = #3475AB 31 | chart_line_style = - 32 | chart_line_width = 1 33 | chart_display_grid = True 34 | chart_grid_max_size = 4 35 | chart_grid_color = #DFDFDF 36 | chart_grid_line_style = -- 37 | chart_grid_line_width = 1 38 | 39 | [price] 40 | price_display_market_cap = True 41 | price_display_market_cap_rank = False 42 | 43 | # Configuration for logging 44 | [logging] 45 | log_level = INFO 46 | log_console_enabled = True 47 | log_file_enabled = True 48 | log_file_name = logs/payment_bot.log 49 | log_file_use_rotating = True 50 | log_file_max_bytes = 5242880 51 | log_file_backup_cnt = 10 52 | 53 | # Only if log file rotating is not used 54 | #log_file_append = False 55 | -------------------------------------------------------------------------------- /telegram_crypto_price_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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.4.1 2 | 3 | - Use `pyrotgfork`, since `pyrogram` was archived 4 | 5 | # 0.4.0 6 | 7 | - Add possibility to specificy a Coingecko API key, in case paid APIs are used. If no API key is specified, free APIs will be used. 8 | 9 | # 0.3.5 10 | 11 | - Fix replying to commands in topics 12 | 13 | # 0.3.4 14 | 15 | - Fix usage in channels 16 | 17 | # 0.3.3 18 | 19 | - Add new line for markdown delimiters 20 | - Prevent crash if some data is not available 21 | 22 | # 0.3.2 23 | 24 | - Prevent crash if cannot connect to an X display (MacOS/Linux) 25 | 26 | # 0.3.1 27 | 28 | - Fix some _mypy_ and _prospector_ warnings 29 | - Add configuration for _isort_ and run it on project 30 | 31 | # 0.3.0 32 | 33 | - Add support for _pyrogram_ version 2 (version 1 still supported) 34 | 35 | # 0.2.3 36 | 37 | - Bot can now work in channels 38 | 39 | # 0.2.2 40 | 41 | - Handle anonymous user case when executing a command 42 | 43 | # 0.2.1 44 | 45 | - Project re-organized into folders 46 | - Add command for showing bot version 47 | 48 | # 0.2.0 49 | 50 | - Add possibility to specify a starting hour for price tasks 51 | 52 | # 0.1.5 53 | 54 | - Add single handlers for message updates, to avoid being notified of each single message sent in groups 55 | 56 | # 0.1.4 57 | 58 | - Rename commands by adding the `pricebot_` prefix, to avoid conflicts with other bots 59 | 60 | # 0.1.3 61 | 62 | - Add configuration files for _flake8_ and prospector 63 | - Fix all _flake8_ warnings 64 | - Fix the vast majority of _prospector_ warnings 65 | - Remove all star imports (`import *`) 66 | 67 | # 0.1.2 68 | 69 | - Add typing to class members 70 | - Fix _mypy_ errors 71 | 72 | # 0.1.1 73 | 74 | - Minor bug fixes 75 | 76 | # 0.1.0 77 | 78 | First release 79 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/price_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_crypto_price_bot.bot.bot_base import BotBase 25 | from telegram_crypto_price_bot.bot.bot_config import BotConfig 26 | from telegram_crypto_price_bot.bot.bot_handlers_config import BotHandlersConfig 27 | from telegram_crypto_price_bot.coin_info.coin_info_scheduler import CoinInfoScheduler 28 | 29 | 30 | # 31 | # Classes 32 | # 33 | 34 | # Price bot class 35 | class PriceBot(BotBase): 36 | 37 | coin_info_scheduler: CoinInfoScheduler 38 | 39 | # Constructor 40 | def __init__(self, 41 | config_file: str) -> None: 42 | super().__init__(config_file, BotConfig, BotHandlersConfig) 43 | # Initialize coin info scheduler 44 | self.coin_info_scheduler = CoinInfoScheduler( 45 | self.client, 46 | self.config, 47 | self.logger, 48 | self.translator 49 | ) 50 | -------------------------------------------------------------------------------- /telegram_crypto_price_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_crypto_price_bot.config.config_object import ConfigObject 27 | from telegram_crypto_price_bot.config.config_sections_loader import ConfigSectionsLoader 28 | from telegram_crypto_price_bot.config.config_typing import ConfigSectionsType 29 | 30 | 31 | # 32 | # Classes 33 | # 34 | 35 | # Configuration file sections loader class 36 | class ConfigFileSectionsLoader: 37 | # Load 38 | @staticmethod 39 | def Load(file_name: str, 40 | sections: ConfigSectionsType) -> ConfigObject: 41 | print(f"\nLoading configuration file {file_name}...\n") 42 | 43 | # Read file 44 | config_parser = configparser.ConfigParser() 45 | config_parser.read(file_name, encoding="utf-8") 46 | 47 | # Load sections 48 | return ConfigSectionsLoader(config_parser).LoadSections(sections) 49 | -------------------------------------------------------------------------------- /telegram_crypto_price_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_crypto_price_bot.config.config_object import ConfigObject 27 | from telegram_crypto_price_bot.config.config_section_loader import ConfigSectionLoader 28 | from telegram_crypto_price_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_crypto_price_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_crypto_price_bot/chart_info/chart_info.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | from typing import Dict, List, Union 25 | 26 | 27 | # 28 | # Classes 29 | # 30 | 31 | # Chart info class 32 | class ChartInfo: 33 | 34 | coin_id: str 35 | coin_vs: str 36 | last_days: int 37 | x: List[int] 38 | y: List[float] 39 | 40 | # Constructor 41 | def __init__(self, 42 | chart_info: Dict[str, List[List[Union[int, float]]]], 43 | coin_id: str, 44 | coin_vs: str, 45 | last_days: int) -> None: 46 | self.coin_id = coin_id 47 | self.coin_vs = coin_vs 48 | self.last_days = last_days 49 | self.x = [] 50 | self.y = [] 51 | 52 | for price in chart_info["prices"]: 53 | self.x.append(int(price[0] / 1000)) # Convert timestamp from milliseconds to seconds 54 | self.y.append(price[1]) 55 | 56 | # Get coin ID 57 | def CoinId(self) -> str: 58 | return self.coin_id 59 | 60 | # Get coin VS 61 | def CoinVs(self) -> str: 62 | return self.coin_vs 63 | 64 | # Get last days 65 | def LastDays(self) -> int: 66 | return self.last_days 67 | 68 | # Get x coordinates 69 | def X(self) -> List[int]: 70 | return self.x 71 | 72 | # Get y coordinates 73 | def Y(self) -> List[float]: 74 | return self.y 75 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/message/message_deleter.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | from typing import List 25 | 26 | import pyrogram 27 | import pyrogram.errors.exceptions as pyrogram_ex 28 | 29 | from telegram_crypto_price_bot.logger.logger import Logger 30 | from telegram_crypto_price_bot.utils.pyrogram_wrapper import PyrogramWrapper 31 | 32 | 33 | # 34 | # Classes 35 | # 36 | 37 | # Message deleter class 38 | class MessageDeleter: 39 | 40 | client: pyrogram.Client 41 | logger: Logger 42 | 43 | # Constructor 44 | def __init__(self, 45 | client: pyrogram.Client, 46 | logger: Logger) -> None: 47 | self.client = client 48 | self.logger = logger 49 | 50 | # Delete message 51 | def DeleteMessage(self, 52 | message: pyrogram.types.Message) -> bool: 53 | try: 54 | if message.chat is not None: 55 | self.client.delete_messages(message.chat.id, PyrogramWrapper.MessageId(message)) 56 | return True 57 | except pyrogram_ex.forbidden_403.MessageDeleteForbidden: 58 | self.logger.GetLogger().exception(f"Unable to delete message {PyrogramWrapper.MessageId(message)}") 59 | return False 60 | 61 | # Delete messages 62 | def DeleteMessages(self, 63 | messages: List[pyrogram.types.Message]) -> None: 64 | for message in messages: 65 | self.DeleteMessage(message) 66 | -------------------------------------------------------------------------------- /telegram_crypto_price_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 | # Imports 23 | # 24 | import functools 25 | from threading import Lock 26 | from typing import Any, Callable, Optional 27 | 28 | 29 | # 30 | # Decorators 31 | # 32 | 33 | # Decorator for synchronized functions or methods 34 | def Synchronized(lock: Lock): 35 | def _decorator(wrapped: Callable[..., Any]): 36 | @functools.wraps(wrapped) 37 | def _wrapper(*args: Any, 38 | **kwargs: Any): 39 | with lock: 40 | return wrapped(*args, **kwargs) 41 | return _wrapper 42 | return _decorator 43 | 44 | 45 | # 46 | # Classes 47 | # 48 | 49 | # Wrapper for utility functions 50 | class Utils: 51 | # Convert string to bool 52 | @staticmethod 53 | def StrToBool(s: str) -> bool: 54 | s = s.lower() 55 | if s in ["true", "on", "yes", "y"]: 56 | res = True 57 | elif s in ["false", "off", "no", "n"]: 58 | res = False 59 | else: 60 | raise ValueError(f"Invalid boolean string: {s}") 61 | return res 62 | 63 | # Convert string to integer 64 | @staticmethod 65 | def StrToInt(s: Optional[str]) -> int: 66 | if s is None: 67 | return 0 68 | return int(s) 69 | 70 | # Convert string to float 71 | @staticmethod 72 | def StrToFloat(s: Optional[str]) -> float: 73 | if s is None: 74 | return 0.0 75 | return float(s) 76 | -------------------------------------------------------------------------------- /telegram_crypto_price_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_crypto_price_bot.config.config_object import ConfigTypes 27 | 28 | 29 | # 30 | # Enumerations 31 | # 32 | 33 | # Bot configuration types 34 | @unique 35 | class BotConfigTypes(ConfigTypes): 36 | API_ID = auto() 37 | API_HASH = auto() 38 | BOT_TOKEN = auto() 39 | SESSION_NAME = auto() 40 | # App 41 | APP_TEST_MODE = auto() 42 | APP_LANG_FILE = auto() 43 | # Task 44 | TASKS_MAX_NUM = auto() 45 | # Coingecko 46 | COINGECKO_API_KEY = auto() 47 | # Chart 48 | CHART_DISPLAY = auto() 49 | CHART_DATE_FORMAT = auto() 50 | CHART_BACKGROUND_COLOR = auto() 51 | CHART_TITLE_COLOR = auto() 52 | CHART_FRAME_COLOR = auto() 53 | CHART_AXES_COLOR = auto() 54 | CHART_LINE_COLOR = auto() 55 | CHART_LINE_STYLE = auto() 56 | CHART_LINE_WIDTH = auto() 57 | CHART_DISPLAY_GRID = auto() 58 | CHART_GRID_MAX_SIZE = auto() 59 | CHART_GRID_COLOR = auto() 60 | CHART_GRID_LINE_STYLE = auto() 61 | CHART_GRID_LINE_WIDTH = auto() 62 | # Price 63 | PRICE_DISPLAY_MARKET_CAP = auto() 64 | PRICE_DISPLAY_MARKET_CAP_RANK = auto() 65 | # Logging 66 | LOG_LEVEL = auto() 67 | LOG_CONSOLE_ENABLED = auto() 68 | LOG_FILE_ENABLED = auto() 69 | LOG_FILE_NAME = auto() 70 | LOG_FILE_USE_ROTATING = auto() 71 | LOG_FILE_APPEND = auto() 72 | LOG_FILE_MAX_BYTES = auto() 73 | LOG_FILE_BACKUP_CNT = auto() 74 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/info_message_sender/price_info_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 | from typing import Any 25 | 26 | import pyrogram 27 | 28 | from telegram_crypto_price_bot.config.config_object import ConfigObject 29 | from telegram_crypto_price_bot.info_message_sender.info_message_sender_base import InfoMessageSenderBase 30 | from telegram_crypto_price_bot.logger.logger import Logger 31 | from telegram_crypto_price_bot.price_info.price_info_builder import PriceInfoBuilder 32 | from telegram_crypto_price_bot.translation.translation_loader import TranslationLoader 33 | 34 | 35 | # 36 | # Classes 37 | # 38 | 39 | # Price info message sender class (price in a single message) 40 | class PriceInfoMessageSender(InfoMessageSenderBase): 41 | 42 | price_info_builder: PriceInfoBuilder 43 | 44 | # Constructor 45 | def __init__(self, 46 | client: pyrogram.Client, 47 | config: ConfigObject, 48 | logger: Logger, 49 | translator: TranslationLoader) -> None: 50 | super().__init__(client, config, logger) 51 | self.price_info_builder = PriceInfoBuilder(config, translator) 52 | 53 | # Send message 54 | def _SendMessage(self, 55 | chat: pyrogram.types.Chat, 56 | *args: Any, 57 | **kwargs: Any) -> pyrogram.types.Message: 58 | # Get price information 59 | price_info = self._CoinGeckoPriceApi().GetPriceInfo(args[0], args[1]) 60 | # Build price information string 61 | price_info_str = self.price_info_builder.Build(price_info) 62 | 63 | return self._MessageSender().SendMessage(chat, price_info_str)[0] 64 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/utils/wrapped_list.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | import typing 25 | from abc import ABC 26 | from typing import Iterator, List 27 | 28 | 29 | # 30 | # Classes 31 | # 32 | 33 | # Wrapped list class 34 | class WrappedList(ABC): 35 | 36 | list_elements: List[typing.Any] 37 | 38 | # Constructor 39 | def __init__(self) -> None: 40 | self.list_elements = [] 41 | 42 | # Add single element 43 | def AddSingle(self, 44 | element: typing.Any) -> None: 45 | self.list_elements.append(element) 46 | 47 | # Add multiple elements 48 | def AddMultiple(self, 49 | elements: List[typing.Any]) -> None: 50 | self.list_elements.extend(elements) 51 | 52 | # Remove single element 53 | def RemoveSingle(self, 54 | element: typing.Any) -> None: 55 | self.list_elements.remove(element) 56 | 57 | # Get if element is present 58 | def IsElem(self, 59 | element: typing.Any) -> bool: 60 | return element in self.list_elements 61 | 62 | # Clear element 63 | def Clear(self) -> None: 64 | self.list_elements.clear() 65 | 66 | # Get elements count 67 | def Count(self) -> int: 68 | return len(self.list_elements) 69 | 70 | # Get if any 71 | def Any(self) -> bool: 72 | return self.Count() > 0 73 | 74 | # Get if empty 75 | def Empty(self) -> bool: 76 | return self.Count() == 0 77 | 78 | # Get list 79 | def GetList(self) -> List[typing.Any]: 80 | return self.list_elements 81 | 82 | # Get iterator 83 | def __iter__(self) -> Iterator[typing.Any]: 84 | yield from self.list_elements 85 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/coingecko/coingecko_price_api.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 pycoingecko import CoinGeckoAPI 25 | 26 | from telegram_crypto_price_bot.bot.bot_config_types import BotConfigTypes 27 | from telegram_crypto_price_bot.chart_info.chart_info import ChartInfo 28 | from telegram_crypto_price_bot.config.config_object import ConfigObject 29 | from telegram_crypto_price_bot.price_info.price_info import PriceInfo 30 | 31 | 32 | # 33 | # Classes 34 | # 35 | 36 | # Error for coingecko price API class 37 | class CoinGeckoPriceApiError(Exception): 38 | pass 39 | 40 | 41 | # Coingecko price API class 42 | class CoinGeckoPriceApi: 43 | 44 | api: CoinGeckoAPI 45 | 46 | # Constructor 47 | def __init__(self, 48 | config: ConfigObject) -> None: 49 | self.api = CoinGeckoAPI(api_key=config.GetValue(BotConfigTypes.COINGECKO_API_KEY)) 50 | 51 | # Get price info 52 | def GetPriceInfo(self, 53 | coin_id: str, 54 | coin_vs: str) -> PriceInfo: 55 | try: 56 | coin_info = self.api.get_coin_by_id(id=coin_id) 57 | except ValueError as ex: 58 | raise CoinGeckoPriceApiError() from ex 59 | 60 | return PriceInfo(coin_info, coin_vs) 61 | 62 | # Get chart info 63 | def GetChartInfo(self, 64 | coin_id: str, 65 | coin_vs: str, 66 | last_days: int) -> ChartInfo: 67 | try: 68 | chart_info = self.api.get_coin_market_chart_by_id(id=coin_id, vs_currency=coin_vs, days=last_days) 69 | except ValueError as ex: 70 | raise CoinGeckoPriceApiError() from ex 71 | 72 | return ChartInfo(chart_info, coin_id, coin_vs, last_days) 73 | -------------------------------------------------------------------------------- /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_crypto_price_bot import PriceBot, __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 Crypto Price 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 = PriceBot(args.config) 95 | bot.Run() 96 | -------------------------------------------------------------------------------- /telegram_crypto_price_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_crypto_price_bot.utils.pyrogram_wrapper import PyrogramWrapper 29 | 30 | 31 | # 32 | # Classes 33 | # 34 | 35 | # Chat helper class 36 | class ChatHelper: 37 | # Get if channel 38 | @staticmethod 39 | def IsChannel(chat: pyrogram.types.Chat) -> bool: 40 | return PyrogramWrapper.IsChannel(chat) 41 | 42 | # Get title 43 | @staticmethod 44 | def GetTitle(chat: pyrogram.types.Chat) -> str: 45 | return chat.title if chat.title is not None else "" 46 | 47 | # Get title or ID 48 | @staticmethod 49 | def GetTitleOrId(chat: pyrogram.types.Chat) -> str: 50 | return f"'{chat.title}' (ID: {chat.id})" if chat.title is not None else f"{chat.id}" 51 | 52 | # Get if private chat 53 | @staticmethod 54 | def IsPrivateChat(chat: pyrogram.types.Chat, 55 | user: pyrogram.types.User): 56 | if ChatHelper.IsChannel(chat): 57 | return False 58 | return chat.id == user.id 59 | 60 | 61 | # User helper class 62 | class UserHelper: 63 | # Get user name or ID 64 | @staticmethod 65 | def GetNameOrId(user: Optional[pyrogram.types.User]) -> str: 66 | if user is None: 67 | return "Anonymous user" 68 | 69 | if user.username is not None: 70 | return f"@{user.username} ({UserHelper.GetName(user)} - ID: {user.id})" 71 | 72 | name = UserHelper.GetName(user) 73 | return f"{name} (ID: {user.id})" if name is not None else f"ID: {user.id}" 74 | 75 | # Get user name 76 | @staticmethod 77 | def GetName(user: Optional[pyrogram.types.User]) -> str: 78 | if user is None: 79 | return "Anonymous user" 80 | 81 | if user.first_name is not None: 82 | return f"{user.first_name} {user.last_name}" if user.last_name is not None else f"{user.first_name}" 83 | return user.last_name if user.last_name is not None else "" 84 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/info_message_sender/chart_info_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 | from typing import Any 25 | 26 | import pyrogram 27 | 28 | from telegram_crypto_price_bot.chart_info.chart_info_file_saver import ChartInfoTmpFileSaver 29 | from telegram_crypto_price_bot.config.config_object import ConfigObject 30 | from telegram_crypto_price_bot.info_message_sender.info_message_sender_base import InfoMessageSenderBase 31 | from telegram_crypto_price_bot.logger.logger import Logger 32 | from telegram_crypto_price_bot.translation.translation_loader import TranslationLoader 33 | 34 | 35 | # 36 | # Classes 37 | # 38 | 39 | # Chart info message sender class (chart in a single message) 40 | class ChartInfoMessageSender(InfoMessageSenderBase): 41 | 42 | config: ConfigObject 43 | logger: Logger 44 | translator: TranslationLoader 45 | 46 | # Constructor 47 | def __init__(self, 48 | client: pyrogram.Client, 49 | config: ConfigObject, 50 | logger: Logger, 51 | translator: TranslationLoader) -> None: 52 | super().__init__(client, config, logger) 53 | self.config = config 54 | self.logger = logger 55 | self.translator = translator 56 | 57 | # Send message 58 | def _SendMessage(self, 59 | chat: pyrogram.types.Chat, 60 | *args: Any, 61 | **kwargs: Any) -> pyrogram.types.Message: 62 | # Get chart information 63 | chart_info = self._CoinGeckoPriceApi().GetChartInfo(args[0], args[1], args[2]) 64 | # Save chart image 65 | chart_info_saver = ChartInfoTmpFileSaver(self.config, self.logger, self.translator) 66 | chart_info_saver.SaveToTmpFile(chart_info) 67 | # Get temporary file name 68 | tmp_file_name = chart_info_saver.TmpFileName() 69 | if tmp_file_name is None: 70 | raise RuntimeError("Unable to save chart to file") 71 | 72 | # Send chart image 73 | return self._MessageSender().SendPhoto(chat, 74 | tmp_file_name, 75 | **kwargs) 76 | -------------------------------------------------------------------------------- /.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_crypto_price_bot/info_message_sender/info_message_sender_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, Optional 26 | 27 | import pyrogram 28 | 29 | from telegram_crypto_price_bot.coingecko.coingecko_price_api import CoinGeckoPriceApi 30 | from telegram_crypto_price_bot.config.config_object import ConfigObject 31 | from telegram_crypto_price_bot.logger.logger import Logger 32 | from telegram_crypto_price_bot.message.message_deleter import MessageDeleter 33 | from telegram_crypto_price_bot.message.message_sender import MessageSender 34 | 35 | 36 | # 37 | # Classes 38 | # 39 | 40 | # Info message sender base class 41 | class InfoMessageSenderBase(ABC): 42 | 43 | last_sent_msg: Optional[pyrogram.types.Message] 44 | coingecko_api: CoinGeckoPriceApi 45 | message_deleter: MessageDeleter 46 | message_sender: MessageSender 47 | 48 | # Constructor 49 | def __init__(self, 50 | client: pyrogram.Client, 51 | config: ConfigObject, 52 | logger: Logger) -> None: 53 | self.last_sent_msg = None 54 | self.coingecko_api = CoinGeckoPriceApi(config) 55 | self.message_deleter = MessageDeleter(client, logger) 56 | self.message_sender = MessageSender(client, logger) 57 | 58 | # Send message 59 | def SendMessage(self, 60 | chat: pyrogram.types.Chat, 61 | *args: Any, 62 | **kwargs: Any) -> None: 63 | self.last_sent_msg = self._SendMessage(chat, *args, **kwargs) 64 | 65 | # Delete last sent message 66 | def DeleteLastSentMessage(self) -> None: 67 | if self.last_sent_msg is not None: 68 | self.message_deleter.DeleteMessage(self.last_sent_msg) 69 | 70 | self.last_sent_msg = None 71 | 72 | # Get CoinGecko API 73 | def _CoinGeckoPriceApi(self) -> CoinGeckoPriceApi: 74 | return self.coingecko_api 75 | 76 | # Get message sender 77 | def _MessageSender(self) -> MessageSender: 78 | return self.message_sender 79 | 80 | # Send message (to be implemented by children classes) 81 | @abstractmethod 82 | def _SendMessage(self, 83 | chat: pyrogram.types.Chat, 84 | *args: Any, 85 | **kwargs: Any) -> pyrogram.types.Message: 86 | pass 87 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/utils/pyrogram_wrapper.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # Old wrapper, not needed anymore with pyrotgfork 22 | 23 | # 24 | # Imports 25 | # 26 | from typing import Iterator 27 | 28 | import pyrogram 29 | 30 | from telegram_crypto_price_bot.utils.utils import Utils 31 | 32 | 33 | if int(pyrogram.__version__[0]) == 2: 34 | from pyrogram.enums import ChatMembersFilter, ChatType 35 | else: 36 | from enum import Enum 37 | 38 | # Fake enum 39 | class ChatMembersFilter(Enum): # type: ignore 40 | pass 41 | 42 | 43 | # 44 | # Classes 45 | # 46 | 47 | # Wrapper for pyrogram for handling different versions 48 | class PyrogramWrapper: 49 | # Get message id 50 | @staticmethod 51 | def MessageId(message: pyrogram.types.Message) -> int: 52 | if PyrogramWrapper.__Version() == 2: 53 | return message.id 54 | if PyrogramWrapper.__Version() == 1: 55 | return message.message_id 56 | raise RuntimeError("Unsupported pyrogram version") 57 | 58 | # Get if channel 59 | @staticmethod 60 | def IsChannel(chat: pyrogram.types.Chat) -> bool: 61 | if PyrogramWrapper.__Version() == 2: 62 | return chat.type == ChatType.CHANNEL 63 | if PyrogramWrapper.__Version() == 1: 64 | return chat["type"] == "channel" 65 | raise RuntimeError("Unsupported pyrogram version") 66 | 67 | # Get if channel 68 | @staticmethod 69 | def GetChatMembers(client: pyrogram.Client, 70 | chat: pyrogram.types.Chat, 71 | filter_str: str) -> Iterator[pyrogram.types.ChatMember]: 72 | if PyrogramWrapper.__Version() == 2: 73 | return client.get_chat_members(chat.id, filter=PyrogramWrapper.__StrToChatMembersFilter(filter_str)) 74 | if PyrogramWrapper.__Version() == 1: 75 | return client.iter_chat_members(chat.id, filter=filter_str) 76 | raise RuntimeError("Unsupported pyrogram version") 77 | 78 | @staticmethod 79 | def __StrToChatMembersFilter(filter_str: str) -> ChatMembersFilter: 80 | str_to_enum = { 81 | "all": ChatMembersFilter.SEARCH, 82 | "banned": ChatMembersFilter.BANNED, 83 | "bots": ChatMembersFilter.BOTS, 84 | "restricted": ChatMembersFilter.RESTRICTED, 85 | "administrators": ChatMembersFilter.ADMINISTRATORS, 86 | } 87 | 88 | return str_to_enum[filter_str] 89 | 90 | # Get version 91 | @staticmethod 92 | def __Version() -> int: 93 | return Utils.StrToInt(pyrogram.__version__[0]) 94 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/info_message_sender/chart_price_info_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 | from typing import Any 25 | 26 | import pyrogram 27 | 28 | from telegram_crypto_price_bot.chart_info.chart_info_file_saver import ChartInfoTmpFileSaver 29 | from telegram_crypto_price_bot.config.config_object import ConfigObject 30 | from telegram_crypto_price_bot.info_message_sender.info_message_sender_base import InfoMessageSenderBase 31 | from telegram_crypto_price_bot.logger.logger import Logger 32 | from telegram_crypto_price_bot.price_info.price_info_builder import PriceInfoBuilder 33 | from telegram_crypto_price_bot.translation.translation_loader import TranslationLoader 34 | 35 | 36 | # 37 | # Classes 38 | # 39 | 40 | # Chart price info message sender class (chart and price in the same message) 41 | class ChartPriceInfoMessageSender(InfoMessageSenderBase): 42 | 43 | config: ConfigObject 44 | logger: Logger 45 | translator: TranslationLoader 46 | price_info_builder: PriceInfoBuilder 47 | 48 | # Constructor 49 | def __init__(self, 50 | client: pyrogram.Client, 51 | config: ConfigObject, 52 | logger: Logger, 53 | translator: TranslationLoader) -> None: 54 | super().__init__(client, config, logger) 55 | self.config = config 56 | self.logger = logger 57 | self.translator = translator 58 | self.price_info_builder = PriceInfoBuilder(config, translator) 59 | 60 | # Send message 61 | def _SendMessage(self, 62 | chat: pyrogram.types.Chat, 63 | *args: Any, 64 | **kwargs: Any) -> pyrogram.types.Message: 65 | # Get chart and price information 66 | chart_info = self._CoinGeckoPriceApi().GetChartInfo(args[0], args[1], args[2]) 67 | price_info = self._CoinGeckoPriceApi().GetPriceInfo(args[0], args[1]) 68 | 69 | # Build price information string 70 | price_info_str = self.price_info_builder.Build(price_info) 71 | # Save chart image 72 | chart_info_saver = ChartInfoTmpFileSaver(self.config, self.logger, self.translator) 73 | chart_info_saver.SaveToTmpFile(chart_info) 74 | # Get temporary file name 75 | tmp_file_name = chart_info_saver.TmpFileName() 76 | if tmp_file_name is None: 77 | raise RuntimeError("Unable to save chart to file") 78 | 79 | # Send chart image with price information as caption 80 | return self._MessageSender().SendPhoto(chat, 81 | tmp_file_name, 82 | caption=price_info_str, 83 | **kwargs) 84 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/translation/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_crypto_price_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_crypto_price_bot/price_info/price_info.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 26 | 27 | from telegram_crypto_price_bot.utils.utils import Utils 28 | 29 | 30 | # 31 | # Enumerations 32 | # 33 | 34 | # Price data types 35 | @unique 36 | class PriceInfoTypes(Enum): 37 | COIN_NAME = auto() 38 | COIN_SYMBOL = auto() 39 | COIN_VS = auto() 40 | COIN_VS_SYMBOL = auto() 41 | CURR_PRICE = auto() 42 | MARKET_CAP = auto() 43 | MARKET_CAP_RANK = auto() 44 | HIGH_24H = auto() 45 | LOW_24H = auto() 46 | TOTAL_VOLUME = auto() 47 | PRICE_CHANGE_PERC_24H = auto() 48 | PRICE_CHANGE_PERC_7D = auto() 49 | PRICE_CHANGE_PERC_14D = auto() 50 | PRICE_CHANGE_PERC_30D = auto() 51 | 52 | 53 | # 54 | # Classes 55 | # 56 | 57 | # Price info class 58 | class PriceInfo: 59 | 60 | info: Dict[PriceInfoTypes, Any] 61 | 62 | # Constructor 63 | def __init__(self, 64 | coin_data: Dict[str, Any], 65 | coin_vs: str) -> None: 66 | self.info = { 67 | PriceInfoTypes.COIN_NAME: coin_data["name"], 68 | PriceInfoTypes.COIN_SYMBOL: coin_data["symbol"].upper(), 69 | PriceInfoTypes.COIN_VS: coin_vs.upper(), 70 | PriceInfoTypes.COIN_VS_SYMBOL: "$" if coin_vs == "usd" else ("€" if coin_vs == "eur" else coin_vs.upper()), 71 | PriceInfoTypes.CURR_PRICE: Utils.StrToFloat(coin_data["market_data"]["current_price"][coin_vs]), 72 | PriceInfoTypes.MARKET_CAP: Utils.StrToInt(coin_data["market_data"]["market_cap"][coin_vs]), 73 | PriceInfoTypes.MARKET_CAP_RANK: Utils.StrToInt(coin_data["market_data"]["market_cap_rank"]), 74 | PriceInfoTypes.HIGH_24H: Utils.StrToFloat(coin_data["market_data"]["high_24h"][coin_vs]), 75 | PriceInfoTypes.LOW_24H: Utils.StrToFloat(coin_data["market_data"]["low_24h"][coin_vs]), 76 | PriceInfoTypes.TOTAL_VOLUME: Utils.StrToInt(coin_data["market_data"]["total_volume"][coin_vs]), 77 | # Price change percentage 78 | PriceInfoTypes.PRICE_CHANGE_PERC_24H: Utils.StrToFloat( 79 | coin_data["market_data"]["price_change_percentage_24h"] 80 | ), 81 | PriceInfoTypes.PRICE_CHANGE_PERC_7D: Utils.StrToFloat( 82 | coin_data["market_data"]["price_change_percentage_7d"] 83 | ), 84 | PriceInfoTypes.PRICE_CHANGE_PERC_14D: Utils.StrToFloat( 85 | coin_data["market_data"]["price_change_percentage_14d"] 86 | ), 87 | PriceInfoTypes.PRICE_CHANGE_PERC_30D: Utils.StrToFloat( 88 | coin_data["market_data"]["price_change_percentage_30d"] 89 | ), 90 | } 91 | 92 | # Get info 93 | def GetData(self, 94 | data_type: PriceInfoTypes) -> Any: 95 | if not isinstance(data_type, PriceInfoTypes): 96 | raise TypeError("Invalid info type") 97 | 98 | return self.info[data_type] 99 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/misc/formatters.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 | 27 | # 28 | # Classes 29 | # 30 | 31 | # Coin ID formatter class 32 | class CoinIdFormatter: 33 | # Format coin ID 34 | @staticmethod 35 | def Format(coin_id: str) -> str: 36 | return coin_id.title().replace("-", " ") 37 | 38 | 39 | # Coin pair formatter class 40 | class CoinPairFormatter: 41 | # Format coin pair 42 | @staticmethod 43 | def Format(coin_sym: str, 44 | coin_vs: str) -> str: 45 | return f"{coin_sym}/{coin_vs}" 46 | 47 | 48 | # Market cap formatter class 49 | class MarketCapFormatter: 50 | # Format market cap 51 | @staticmethod 52 | def Format(market_cap: int, 53 | coin_vs: Optional[str] = None) -> str: 54 | coin_vs = coin_vs or "" 55 | space = " " if coin_vs not in ("$", "€") else "" 56 | 57 | if market_cap > 1e9: 58 | formatted_str = f"{market_cap / 1e9:.2f}B {coin_vs}" 59 | elif market_cap > 1e6: 60 | formatted_str = f"{market_cap / 1e6:.2f}M {coin_vs}" 61 | else: 62 | formatted_str = f"{market_cap:,}{space}{coin_vs}" 63 | return formatted_str 64 | 65 | 66 | # Price formatter class 67 | class PriceFormatter: 68 | # Format price 69 | @staticmethod 70 | def Format(price: float, 71 | coin_vs: Optional[str] = None) -> str: 72 | coin_vs = coin_vs or "" 73 | space = " " if coin_vs not in ("$", "€") else "" 74 | 75 | if price >= 1000: 76 | formatted_str = f"{price:.0f}{space}{coin_vs}" 77 | elif price >= 100: 78 | formatted_str = f"{price:.1f}{space}{coin_vs}" 79 | elif price >= 1: 80 | formatted_str = f"{price:.2f}{space}{coin_vs}" 81 | elif price >= 0.0001: 82 | formatted_str = f"{price:.4f}{space}{coin_vs}" 83 | else: 84 | formatted_str = f"{price:f}{space}{coin_vs}" 85 | return formatted_str 86 | 87 | 88 | # Price change percentage formatter class 89 | class PriceChangePercFormatter: 90 | # Format price change percentage 91 | @staticmethod 92 | def Format(price_change: float) -> str: 93 | return f"{'🔴' if price_change < 0 else '🟢'} {price_change:+.2f}%" 94 | 95 | 96 | # Volume formatter class 97 | class VolumeFormatter: 98 | # Format volume 99 | @staticmethod 100 | def Format(volume: int, 101 | coin_vs: Optional[str] = None) -> str: 102 | coin_vs = coin_vs or "" 103 | space = " " if coin_vs not in ("$", "€") else "" 104 | 105 | if volume > 1e9: 106 | formatted_str = f"{volume / 1e9:.2f}B {coin_vs}" 107 | elif volume > 1e6: 108 | formatted_str = f"{volume / 1e6:.2f}M {coin_vs}" 109 | else: 110 | formatted_str = f"{volume:,}{space}{coin_vs}" 111 | return formatted_str 112 | -------------------------------------------------------------------------------- /telegram_crypto_price_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_crypto_price_bot.config.config_loader_ex import ConfigFieldNotExistentError, ConfigFieldValueError 27 | from telegram_crypto_price_bot.config.config_object import ConfigObject 28 | from telegram_crypto_price_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_crypto_price_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_crypto_price_bot.logger.logger import Logger 30 | 31 | 32 | # 33 | # Classes 34 | # 35 | 36 | # Constants for message sender class 37 | class MessageSenderConst: 38 | # Maximum message length 39 | MSG_MAX_LEN: int = 4096 40 | # Sleep time for sending messages 41 | SEND_MSG_SLEEP_TIME_SEC: float = 0.1 42 | 43 | 44 | # Message sender class 45 | class MessageSender: 46 | 47 | client: pyrogram.Client 48 | logger: Logger 49 | 50 | # Constructor 51 | def __init__(self, 52 | client: pyrogram.Client, 53 | logger: Logger) -> None: 54 | self.client = client 55 | self.logger = logger 56 | 57 | # Send message 58 | def SendMessage(self, 59 | receiver: Union[pyrogram.types.Chat, pyrogram.types.User], 60 | msg: str, 61 | **kwargs: Any) -> List[pyrogram.types.Message]: 62 | # Log 63 | self.logger.GetLogger().info(f"Sending message (length: {len(msg)}):\n{msg}") 64 | # Split and send message 65 | return self.__SendSplitMessage(receiver, self.__SplitMessage(msg), **kwargs) 66 | 67 | # Send photo 68 | def SendPhoto(self, 69 | receiver: Union[pyrogram.types.Chat, pyrogram.types.User], 70 | photo: str, 71 | **kwargs: Any) -> pyrogram.types.Message: 72 | return self.client.send_photo(receiver.id, photo, **kwargs) # type: ignore 73 | 74 | # Send split message 75 | def __SendSplitMessage(self, 76 | receiver: Union[pyrogram.types.Chat, pyrogram.types.User], 77 | split_msg: List[str], 78 | **kwargs) -> List[pyrogram.types.Message]: 79 | sent_msgs = [] 80 | 81 | # Send message 82 | for msg_part in split_msg: 83 | sent_msgs.append(self.client.send_message(receiver.id, msg_part, **kwargs)) 84 | time.sleep(MessageSenderConst.SEND_MSG_SLEEP_TIME_SEC) 85 | 86 | return sent_msgs # type: ignore 87 | 88 | # Split message 89 | def __SplitMessage(self, 90 | msg: str) -> List[str]: 91 | msg_parts = [] 92 | 93 | while len(msg) > 0: 94 | # If length is less than maximum, the operation is completed 95 | if len(msg) <= MessageSenderConst.MSG_MAX_LEN: 96 | msg_parts.append(msg) 97 | break 98 | 99 | # Take the current part 100 | curr_part = msg[:MessageSenderConst.MSG_MAX_LEN] 101 | # Get the last occurrence of a new line 102 | idx = curr_part.rfind("\n") 103 | 104 | # Split with respect to the found occurrence 105 | if idx != -1: 106 | msg_parts.append(curr_part[:idx]) 107 | msg = msg[idx + 1:] 108 | else: 109 | msg_parts.append(curr_part) 110 | msg = msg[MessageSenderConst.MSG_MAX_LEN + 1:] 111 | 112 | # Log 113 | self.logger.GetLogger().info(f"Message split into {len(msg_parts)} part(s)") 114 | 115 | return msg_parts 116 | -------------------------------------------------------------------------------- /telegram_crypto_price_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_crypto_price_bot.utils.utils import Utils 29 | from telegram_crypto_price_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_crypto_price_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_crypto_price_bot.misc.helpers import UserHelper 29 | from telegram_crypto_price_bot.utils.pyrogram_wrapper import PyrogramWrapper 30 | from telegram_crypto_price_bot.utils.wrapped_list import WrappedList 31 | 32 | 33 | # 34 | # Classes 35 | # 36 | 37 | # Chat members list class 38 | class ChatMembersList(WrappedList): 39 | # Get by user ID 40 | def GetByUserId(self, 41 | user_id: int) -> Optional[pyrogram.types.ChatMember]: 42 | res = list(filter(lambda member: user_id == member.user.id, self.list_elements)) 43 | return None if len(res) == 0 else res[0] 44 | 45 | # Get by username 46 | def GetByUsername(self, 47 | username: str) -> Optional[pyrogram.types.ChatMember]: 48 | res = list(filter(lambda member: username == member.user.username, self.list_elements)) 49 | return None if len(res) == 0 else res[0] 50 | 51 | # Get if ID is present 52 | def IsUserIdPresent(self, 53 | user_id: int) -> bool: 54 | return self.GetByUserId(user_id) is not None 55 | 56 | # Get if username is present 57 | def IsUsernamePresent(self, 58 | username: str) -> bool: 59 | return self.GetByUsername(username) is not None 60 | 61 | # Convert to string 62 | def ToString(self) -> str: 63 | return "\n".join( 64 | [f"- {UserHelper.GetNameOrId(member.user)}" for member in self.list_elements] 65 | ) 66 | 67 | # Convert to string 68 | def __str__(self) -> str: 69 | return self.ToString() 70 | 71 | 72 | # Chat members getter class 73 | class ChatMembersGetter: 74 | 75 | client: pyrogram.Client 76 | 77 | # Constructor 78 | def __init__(self, 79 | client: pyrogram.Client) -> None: 80 | self.client = client 81 | 82 | # Get the list of chat members by applying the specified filter 83 | def FilterMembers(self, 84 | chat: pyrogram.types.Chat, 85 | filter_fct: Optional[Callable[[pyrogram.types.ChatMember], bool]] = None, 86 | filter_str: str = "all") -> ChatMembersList: 87 | # Get members 88 | filtered_members = list(PyrogramWrapper.GetChatMembers(self.client, chat, filter_str)) 89 | # Filter them if necessary 90 | if filter_fct is not None: 91 | filtered_members = list(filter(filter_fct, filtered_members)) # type: ignore 92 | # Order filtered members 93 | filtered_members.sort( # type: ignore 94 | key=lambda member: member.user.username.lower() if member.user.username is not None else str(member.user.id) 95 | ) 96 | 97 | # Build chat members 98 | chat_members = ChatMembersList() 99 | chat_members.AddMultiple(filtered_members) # type: ignore 100 | 101 | return chat_members 102 | 103 | # Get all 104 | def GetAll(self, 105 | chat: pyrogram.types.Chat) -> ChatMembersList: 106 | return self.FilterMembers(chat) 107 | 108 | # Get admins 109 | def GetAdmins(self, 110 | chat: pyrogram.types.Chat) -> ChatMembersList: 111 | return self.FilterMembers(chat, 112 | lambda member: True, 113 | "administrators") 114 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/coin_info/coin_info_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 | import pyrogram 25 | 26 | from telegram_crypto_price_bot.config.config_object import ConfigObject 27 | from telegram_crypto_price_bot.info_message_sender.coin_info_message_sender import CoinInfoMessageSender 28 | from telegram_crypto_price_bot.logger.logger import Logger 29 | from telegram_crypto_price_bot.misc.helpers import ChatHelper 30 | from telegram_crypto_price_bot.translation.translation_loader import TranslationLoader 31 | 32 | 33 | # 34 | # Classes 35 | # 36 | 37 | # Coin info job class 38 | class CoinInfoJobData: 39 | 40 | chat: pyrogram.types.Chat 41 | period_hours: int 42 | start_hour: int 43 | coin_id: str 44 | coin_vs: str 45 | last_days: int 46 | running: bool 47 | 48 | # Constructor 49 | def __init__(self, 50 | chat: pyrogram.types.Chat, 51 | period_hours: int, 52 | start_hour: int, 53 | coin_id: str, 54 | coin_vs: str, 55 | last_days: int) -> None: 56 | self.chat = chat 57 | self.period_hours = period_hours 58 | self.start_hour = start_hour 59 | self.coin_id = coin_id 60 | self.coin_vs = coin_vs 61 | self.last_days = last_days 62 | self.running = True 63 | 64 | # Get chat 65 | def Chat(self) -> pyrogram.types.Chat: 66 | return self.chat 67 | 68 | # Get period hours 69 | def PeriodHours(self) -> int: 70 | return self.period_hours 71 | 72 | # Get start hour 73 | def StartHour(self) -> int: 74 | return self.start_hour 75 | 76 | # Get coin ID 77 | def CoinId(self) -> str: 78 | return self.coin_id 79 | 80 | # Get coin VS 81 | def CoinVs(self) -> str: 82 | return self.coin_vs 83 | 84 | # Get last days 85 | def LastDays(self) -> int: 86 | return self.last_days 87 | 88 | # Set if running 89 | def SetRunning(self, 90 | flag: bool) -> None: 91 | self.running = flag 92 | 93 | # Get if running 94 | def IsRunning(self) -> bool: 95 | return self.running 96 | 97 | 98 | # Coin info job class 99 | class CoinInfoJob: 100 | 101 | data: CoinInfoJobData 102 | logger: Logger 103 | coin_info_msg_sender: CoinInfoMessageSender 104 | 105 | # Constructor 106 | def __init__(self, 107 | client: pyrogram.Client, 108 | config: ConfigObject, 109 | logger: Logger, 110 | translator: TranslationLoader, 111 | data: CoinInfoJobData) -> None: 112 | self.data = data 113 | self.logger = logger 114 | self.coin_info_msg_sender = CoinInfoMessageSender(client, config, logger, translator) 115 | 116 | # Get data 117 | def Data(self) -> CoinInfoJobData: 118 | return self.data 119 | 120 | # Set if running 121 | def SetRunning(self, 122 | flag: bool) -> None: 123 | self.data.SetRunning(flag) 124 | 125 | # Set delete last sent message 126 | def DeleteLastSentMessage(self, 127 | flag: bool) -> None: 128 | self.coin_info_msg_sender.DeleteLastSentMessage(flag) 129 | 130 | # Set send in same message 131 | def SendInSameMessage(self, 132 | flag: bool) -> None: 133 | self.coin_info_msg_sender.SendInSameMessage(flag) 134 | 135 | # Do job 136 | def DoJob(self, 137 | chat: pyrogram.types.Chat, 138 | coin_id: str, 139 | coin_vs: str, 140 | last_days: int) -> None: 141 | self.logger.GetLogger().info(f"Coin job started in chat {ChatHelper.GetTitleOrId(chat)}") 142 | self.coin_info_msg_sender.SendMessage(chat, coin_id, coin_vs, last_days) 143 | -------------------------------------------------------------------------------- /telegram_crypto_price_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_crypto_price_bot.bot.bot_config_types import BotConfigTypes 30 | from telegram_crypto_price_bot.config.config_object import ConfigObject 31 | 32 | 33 | # 34 | # Classes 35 | # 36 | 37 | # Constants for logger class 38 | class LoggerConst: 39 | # Logger name 40 | LOGGER_NAME: str = "" 41 | # Log formats 42 | LOG_CONSOLE_FORMAT: str = "%(asctime)-15s %(levelname)s - %(message)s" 43 | LOG_FILE_FORMAT: str = "%(asctime)-15s %(levelname)s - [%(name)s.%(funcName)s:%(lineno)d] %(message)s" 44 | 45 | 46 | # Logger class 47 | class Logger: 48 | 49 | config: ConfigObject 50 | logger: logging.Logger 51 | 52 | # Constructor 53 | def __init__(self, 54 | config: ConfigObject) -> None: 55 | self.config = config 56 | self.logger = logging.getLogger(LoggerConst.LOGGER_NAME) 57 | self.__Init() 58 | 59 | # Get logger 60 | def GetLogger(self) -> logging.Logger: 61 | return self.logger 62 | 63 | # Initialize 64 | def __Init(self) -> None: 65 | # Configure loggers 66 | self.__ConfigureRootLogger() 67 | self.__ConfigureConsoleLogger() 68 | self.__ConfigureFileLogger() 69 | # Log 70 | self.logger.info("Logger initialized") 71 | 72 | # Configure root logger 73 | def __ConfigureRootLogger(self) -> None: 74 | self.logger.setLevel(self.config.GetValue(BotConfigTypes.LOG_LEVEL)) 75 | 76 | # Configure console logger 77 | def __ConfigureConsoleLogger(self) -> None: 78 | # Configure console handler if required 79 | if self.config.GetValue(BotConfigTypes.LOG_CONSOLE_ENABLED): 80 | # Create handler 81 | ch = logging.StreamHandler() 82 | ch.setLevel(self.config.GetValue(BotConfigTypes.LOG_LEVEL)) 83 | ch.setFormatter(logging.Formatter(LoggerConst.LOG_CONSOLE_FORMAT)) 84 | # Add handler 85 | self.logger.addHandler(ch) 86 | 87 | # Configure file logger 88 | def __ConfigureFileLogger(self) -> None: 89 | # Configure file handler if required 90 | if self.config.GetValue(BotConfigTypes.LOG_FILE_ENABLED): 91 | # Get file name 92 | log_file_name = self.config.GetValue(BotConfigTypes.LOG_FILE_NAME) 93 | # Create log directories if needed 94 | self.__MakeLogDir(log_file_name) 95 | 96 | # Create file handler 97 | fh: Union[logging.handlers.RotatingFileHandler, logging.FileHandler] 98 | if self.config.GetValue(BotConfigTypes.LOG_FILE_USE_ROTATING): 99 | fh = logging.handlers.RotatingFileHandler(log_file_name, 100 | maxBytes=self.config.GetValue(BotConfigTypes.LOG_FILE_MAX_BYTES), 101 | backupCount=self.config.GetValue(BotConfigTypes.LOG_FILE_BACKUP_CNT), 102 | encoding="utf-8") 103 | else: 104 | fh = logging.FileHandler(log_file_name, 105 | mode="a" if self.config.GetValue(BotConfigTypes.LOG_FILE_APPEND) else "w", 106 | encoding="utf-8") 107 | 108 | fh.setLevel(self.config.GetValue(BotConfigTypes.LOG_LEVEL)) 109 | fh.setFormatter(logging.Formatter(LoggerConst.LOG_FILE_FORMAT)) 110 | # Add handler 111 | self.logger.addHandler(fh) 112 | 113 | # Make log directories 114 | @staticmethod 115 | def __MakeLogDir(file_name: str) -> None: 116 | try: 117 | os.makedirs(os.path.dirname(file_name)) 118 | except FileExistsError: 119 | pass 120 | -------------------------------------------------------------------------------- /telegram_crypto_price_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_crypto_price_bot.config.config_object import ConfigObject 30 | from telegram_crypto_price_bot.logger.logger import Logger 31 | from telegram_crypto_price_bot.message.message_sender import MessageSender 32 | from telegram_crypto_price_bot.translation.translation_loader import TranslationLoader 33 | 34 | 35 | # 36 | # Enumerations 37 | # 38 | 39 | # Message types 40 | @unique 41 | class MessageTypes(Enum): 42 | GROUP_CHAT_CREATED = auto() 43 | LEFT_CHAT_MEMBER = auto() 44 | NEW_CHAT_MEMBERS = auto() 45 | 46 | 47 | # 48 | # Classes 49 | # 50 | 51 | # Message dispatcher class 52 | class MessageDispatcher: 53 | 54 | config: ConfigObject 55 | logger: Logger 56 | translator: TranslationLoader 57 | 58 | # Constructor 59 | def __init__(self, 60 | config: ConfigObject, 61 | logger: Logger, 62 | translator: TranslationLoader) -> None: 63 | self.config = config 64 | self.logger = logger 65 | self.translator = translator 66 | 67 | # Dispatch command 68 | def Dispatch(self, 69 | client: pyrogram.Client, 70 | message: pyrogram.types.Message, 71 | msg_type: MessageTypes, 72 | **kwargs: Any) -> None: 73 | if not isinstance(msg_type, MessageTypes): 74 | raise TypeError("Message type is not an enumerative of MessageTypes") 75 | 76 | # Log 77 | self.logger.GetLogger().info(f"Dispatching message type: {msg_type}") 78 | 79 | # New chat created 80 | if msg_type == MessageTypes.GROUP_CHAT_CREATED: 81 | self.__OnCreatedChat(client, message, **kwargs) 82 | # A member left the chat 83 | elif msg_type == MessageTypes.LEFT_CHAT_MEMBER: 84 | self.__OnLeftMember(client, message, **kwargs) 85 | # A member joined the chat 86 | elif msg_type == MessageTypes.NEW_CHAT_MEMBERS: 87 | self.__OnJoinedMember(client, message, **kwargs) 88 | 89 | # Function called when a new chat is created 90 | def __OnCreatedChat(self, 91 | client, 92 | message: pyrogram.types.Message, 93 | **kwargs: Any) -> None: 94 | if message.chat is None: 95 | return 96 | 97 | # Send the welcome message 98 | MessageSender(client, self.logger).SendMessage( 99 | message.chat, 100 | self.translator.GetSentence("BOT_WELCOME_MSG") 101 | ) 102 | 103 | # Function called when a member left the chat 104 | def __OnLeftMember(self, 105 | client, 106 | message: pyrogram.types.Message, 107 | **kwargs: Any) -> None: 108 | # If the member is the bot itself, remove the chat from the scheduler 109 | if message.left_chat_member is not None and message.left_chat_member.is_self: 110 | kwargs["coin_info_scheduler"].ChatLeft(message.chat) 111 | 112 | # Function called when a member joined the chat 113 | def __OnJoinedMember(self, 114 | client, 115 | message: pyrogram.types.Message, 116 | **kwargs: Any) -> None: 117 | if message.new_chat_members is None or message.chat is None: 118 | return 119 | 120 | # If the member is the bot itself, send the welcome message 121 | for member in message.new_chat_members: 122 | if member.is_self: 123 | MessageSender(client, self.logger).SendMessage( 124 | message.chat, 125 | self.translator.GetSentence("BOT_WELCOME_MSG") 126 | ) 127 | break 128 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=77", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "telegram_crypto_price_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 displaying cryptocurrencies price" 15 | readme = "README.md" 16 | license = "MIT" 17 | license-files = [ 18 | "LICENSE", 19 | ] 20 | requires-python = ">=3.7" 21 | keywords = [ 22 | "telegram", 23 | "bot", 24 | "telegram bot", 25 | "crypto", 26 | "crypto prices", 27 | "cryptocurrency", 28 | "cryptocurrency prices" 29 | ] 30 | classifiers = [ 31 | "Programming Language :: Python :: 3.7", 32 | "Programming Language :: Python :: 3.8", 33 | "Programming Language :: Python :: 3.9", 34 | "Programming Language :: Python :: 3.10", 35 | "Programming Language :: Python :: 3.11", 36 | "Programming Language :: Python :: 3.12", 37 | "Programming Language :: Python :: 3.13", 38 | "Development Status :: 5 - Production/Stable", 39 | "Operating System :: OS Independent", 40 | "Intended Audience :: Developers", 41 | ] 42 | 43 | [project.urls] 44 | Homepage = "https://github.com/ebellocchia/telegram_crypto_price_bot" 45 | Changelog = "https://github.com/ebellocchia/telegram_crypto_price_bot/blob/master/CHANGELOG.md" 46 | Repository = "https://github.com/ebellocchia/telegram_crypto_price_bot" 47 | Download = "https://github.com/ebellocchia/telegram_crypto_price_bot/archive/v{version}.tar.gz" 48 | 49 | [tool.setuptools] 50 | packages = {find = {}} 51 | 52 | [tool.setuptools.package-data] 53 | telegram_crypto_price_bot = ["lang/lang_en.xml"] 54 | 55 | [tool.setuptools.dynamic] 56 | version = {attr = "telegram_crypto_price_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_crypto_price_bot/info_message_sender/coin_info_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 | 26 | from telegram_crypto_price_bot.bot.bot_config_types import BotConfigTypes 27 | from telegram_crypto_price_bot.coingecko.coingecko_price_api import CoinGeckoPriceApiError 28 | from telegram_crypto_price_bot.config.config_object import ConfigObject 29 | from telegram_crypto_price_bot.info_message_sender.chart_info_message_sender import ChartInfoMessageSender 30 | from telegram_crypto_price_bot.info_message_sender.chart_price_info_message_sender import ChartPriceInfoMessageSender 31 | from telegram_crypto_price_bot.info_message_sender.price_info_message_sender import PriceInfoMessageSender 32 | from telegram_crypto_price_bot.logger.logger import Logger 33 | from telegram_crypto_price_bot.message.message_sender import MessageSender 34 | from telegram_crypto_price_bot.translation.translation_loader import TranslationLoader 35 | 36 | 37 | # 38 | # Classes 39 | # 40 | 41 | # Coin info message sender class 42 | class CoinInfoMessageSender: 43 | 44 | config: ConfigObject 45 | logger: Logger 46 | translator: TranslationLoader 47 | delete_last_sent_msg: bool 48 | send_in_same_msg: bool 49 | chart_price_info_msg_sender: ChartPriceInfoMessageSender 50 | chart_info_msg_sender: ChartInfoMessageSender 51 | price_info_msg_sender: PriceInfoMessageSender 52 | msg_sender: MessageSender 53 | 54 | # Constructor 55 | def __init__(self, 56 | client: pyrogram.Client, 57 | config: ConfigObject, 58 | logger: Logger, 59 | translator: TranslationLoader) -> None: 60 | self.config = config 61 | self.logger = logger 62 | self.translator = translator 63 | self.delete_last_sent_msg = True 64 | self.send_in_same_msg = True 65 | self.chart_price_info_msg_sender = ChartPriceInfoMessageSender(client, config, logger, translator) 66 | self.chart_info_msg_sender = ChartInfoMessageSender(client, config, logger, translator) 67 | self.price_info_msg_sender = PriceInfoMessageSender(client, config, logger, translator) 68 | self.msg_sender = MessageSender(client, logger) 69 | 70 | # Set delete last sent message 71 | def DeleteLastSentMessage(self, 72 | flag: bool) -> None: 73 | self.delete_last_sent_msg = flag 74 | 75 | # Set send in same message 76 | def SendInSameMessage(self, 77 | flag: bool) -> None: 78 | self.send_in_same_msg = flag 79 | 80 | # Send message 81 | def SendMessage(self, 82 | chat: pyrogram.types.Chat, 83 | coin_id: str, 84 | coin_vs: str, 85 | last_days: int) -> None: 86 | if self.delete_last_sent_msg: 87 | self.chart_info_msg_sender.DeleteLastSentMessage() 88 | self.chart_price_info_msg_sender.DeleteLastSentMessage() 89 | self.price_info_msg_sender.DeleteLastSentMessage() 90 | 91 | try: 92 | # Chart and price information in the same message 93 | if self.send_in_same_msg and self.config.GetValue(BotConfigTypes.CHART_DISPLAY): 94 | self.chart_price_info_msg_sender.SendMessage(chat, coin_id, coin_vs, last_days) 95 | # Chart and price information in the different messages 96 | else: 97 | self.price_info_msg_sender.SendMessage(chat, coin_id, coin_vs) 98 | 99 | if self.config.GetValue(BotConfigTypes.CHART_DISPLAY): 100 | self.chart_info_msg_sender.SendMessage(chat, coin_id, coin_vs, last_days) 101 | except CoinGeckoPriceApiError: 102 | self.logger.GetLogger().exception( 103 | f"Coingecko API error when retrieving data for coin {coin_id}/{coin_vs}" 104 | ) 105 | self.msg_sender.SendMessage( 106 | chat, 107 | self.translator.GetSentence("API_ERR_MSG", 108 | coin_id=coin_id, 109 | coin_vs=coin_vs) 110 | ) 111 | -------------------------------------------------------------------------------- /telegram_crypto_price_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_crypto_price_bot.bot.bot_config_types import BotConfigTypes 30 | from telegram_crypto_price_bot.bot.bot_handlers_config_typing import BotHandlersConfigType 31 | from telegram_crypto_price_bot.command.command_dispatcher import CommandDispatcher, CommandTypes 32 | from telegram_crypto_price_bot.config.config_file_sections_loader import ConfigFileSectionsLoader 33 | from telegram_crypto_price_bot.config.config_object import ConfigObject 34 | from telegram_crypto_price_bot.config.config_typing import ConfigSectionsType 35 | from telegram_crypto_price_bot.logger.logger import Logger 36 | from telegram_crypto_price_bot.message.message_dispatcher import MessageDispatcher, MessageTypes 37 | from telegram_crypto_price_bot.translation.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_crypto_price_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_crypto_price_bot.command.command_base import CommandBase 30 | from telegram_crypto_price_bot.command.commands import ( 31 | AliveCmd, 32 | HelpCmd, 33 | IsTestModeCmd, 34 | PriceGetSingleCmd, 35 | PriceTaskDeleteLastMsgCmd, 36 | PriceTaskInfoCmd, 37 | PriceTaskPauseCmd, 38 | PriceTaskResumeCmd, 39 | PriceTaskSendInSameMsgCmd, 40 | PriceTaskStartCmd, 41 | PriceTaskStopAllCmd, 42 | PriceTaskStopCmd, 43 | SetTestModeCmd, 44 | VersionCmd, 45 | ) 46 | from telegram_crypto_price_bot.config.config_object import ConfigObject 47 | from telegram_crypto_price_bot.logger.logger import Logger 48 | from telegram_crypto_price_bot.translation.translation_loader import TranslationLoader 49 | 50 | 51 | # 52 | # Enumerations 53 | # 54 | 55 | # Command types 56 | @unique 57 | class CommandTypes(Enum): 58 | START_CMD = auto() 59 | HELP_CMD = auto() 60 | ALIVE_CMD = auto() 61 | SET_TEST_MODE_CMD = auto() 62 | IS_TEST_MODE_CMD = auto() 63 | VERSION_CMD = auto() 64 | PRICE_GET_SINGLE_CMD = auto() 65 | PRICE_TASK_START_CMD = auto() 66 | PRICE_TASK_STOP_CMD = auto() 67 | PRICE_TASK_STOP_ALL_CMD = auto() 68 | PRICE_TASK_PAUSE_CMD = auto() 69 | PRICE_TASK_RESUME_CMD = auto() 70 | PRICE_TASK_SEND_IN_SAME_MSG_CMD = auto() 71 | PRICE_TASK_DELETE_LAST_MSG_CMD = auto() 72 | PRICE_TASK_INFO_CMD = auto() 73 | 74 | 75 | # 76 | # Classes 77 | # 78 | 79 | # Comstant for command dispatcher class 80 | class CommandDispatcherConst: 81 | # Command to class map 82 | CMD_TYPE_TO_CLASS: Dict[CommandTypes, Type[CommandBase]] = { 83 | CommandTypes.START_CMD: HelpCmd, 84 | CommandTypes.HELP_CMD: HelpCmd, 85 | CommandTypes.ALIVE_CMD: AliveCmd, 86 | CommandTypes.SET_TEST_MODE_CMD: SetTestModeCmd, 87 | CommandTypes.IS_TEST_MODE_CMD: IsTestModeCmd, 88 | CommandTypes.VERSION_CMD: VersionCmd, 89 | CommandTypes.PRICE_GET_SINGLE_CMD: PriceGetSingleCmd, 90 | CommandTypes.PRICE_TASK_START_CMD: PriceTaskStartCmd, 91 | CommandTypes.PRICE_TASK_STOP_CMD: PriceTaskStopCmd, 92 | CommandTypes.PRICE_TASK_STOP_ALL_CMD: PriceTaskStopAllCmd, 93 | CommandTypes.PRICE_TASK_PAUSE_CMD: PriceTaskPauseCmd, 94 | CommandTypes.PRICE_TASK_RESUME_CMD: PriceTaskResumeCmd, 95 | CommandTypes.PRICE_TASK_SEND_IN_SAME_MSG_CMD: PriceTaskSendInSameMsgCmd, 96 | CommandTypes.PRICE_TASK_DELETE_LAST_MSG_CMD: PriceTaskDeleteLastMsgCmd, 97 | CommandTypes.PRICE_TASK_INFO_CMD: PriceTaskInfoCmd, 98 | } 99 | 100 | 101 | # Command dispatcher class 102 | class CommandDispatcher: 103 | 104 | config: ConfigObject 105 | logger: Logger 106 | translator: TranslationLoader 107 | 108 | # Constructor 109 | def __init__(self, 110 | config: ConfigObject, 111 | logger: Logger, 112 | translator: TranslationLoader) -> None: 113 | self.config = config 114 | self.logger = logger 115 | self.translator = translator 116 | 117 | # Dispatch command 118 | def Dispatch(self, 119 | client: pyrogram.Client, 120 | message: pyrogram.types.Message, 121 | cmd_type: CommandTypes, 122 | **kwargs: Any) -> None: 123 | if not isinstance(cmd_type, CommandTypes): 124 | raise TypeError("Command type is not an enumerative of CommandTypes") 125 | 126 | # Log 127 | self.logger.GetLogger().info(f"Dispatching command type: {cmd_type}") 128 | 129 | # Create and execute command if existent 130 | if cmd_type in CommandDispatcherConst.CMD_TYPE_TO_CLASS: 131 | cmd_class = CommandDispatcherConst.CMD_TYPE_TO_CLASS[cmd_type](client, 132 | self.config, 133 | self.logger, 134 | self.translator) 135 | cmd_class.Execute(message, **kwargs) 136 | -------------------------------------------------------------------------------- /pyproject_legacy.toml: -------------------------------------------------------------------------------- 1 | # 2 | # pyproject.toml with the old license syntax, compatible with older versions of setuptools 3 | # Use this to install the package from the archive or repo for Python 3.7 and 3.8 4 | # Just rename this file to `pyproject.toml`, overwriting the existent one, and install as usual with pip: 5 | # 6 | # pip install . 7 | # 8 | # NOTE: This file is only for installing the package from the folder. 9 | # You DON'T need it if you install the package from PyPi (i.e. pip install telegram_crypto_price_bot) 10 | # 11 | [build-system] 12 | requires = ["setuptools>=61", "wheel"] 13 | build-backend = "setuptools.build_meta" 14 | 15 | [project] 16 | name = "telegram_crypto_price_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 displaying cryptocurrencies price" 25 | readme = "README.md" 26 | license = {text = "MIT"} 27 | requires-python = ">=3.7" 28 | keywords = [ 29 | "telegram", 30 | "bot", 31 | "telegram bot", 32 | "crypto", 33 | "crypto prices", 34 | "cryptocurrency", 35 | "cryptocurrency prices" 36 | ] 37 | classifiers = [ 38 | "Programming Language :: Python :: 3.7", 39 | "Programming Language :: Python :: 3.8", 40 | "Programming Language :: Python :: 3.9", 41 | "Programming Language :: Python :: 3.10", 42 | "Programming Language :: Python :: 3.11", 43 | "Programming Language :: Python :: 3.12", 44 | "Programming Language :: Python :: 3.13", 45 | "Development Status :: 5 - Production/Stable", 46 | "License :: OSI Approved :: MIT License", 47 | "Operating System :: OS Independent", 48 | "Intended Audience :: Developers", 49 | ] 50 | 51 | [project.urls] 52 | Homepage = "https://github.com/ebellocchia/telegram_crypto_price_bot" 53 | Changelog = "https://github.com/ebellocchia/telegram_crypto_price_bot/blob/master/CHANGELOG.md" 54 | Repository = "https://github.com/ebellocchia/telegram_crypto_price_bot" 55 | Download = "https://github.com/ebellocchia/telegram_crypto_price_bot/archive/v{version}.tar.gz" 56 | 57 | [tool.setuptools] 58 | packages = {find = {}} 59 | 60 | [tool.setuptools.package-data] 61 | telegram_crypto_price_bot = ["lang/lang_en.xml"] 62 | 63 | [tool.setuptools.dynamic] 64 | version = {attr = "telegram_crypto_price_bot._version.__version__"} 65 | dependencies = {file = ["requirements.txt"]} 66 | optional-dependencies.develop = {file = ["requirements-dev.txt"]} 67 | 68 | # 69 | # Tools configuration 70 | # 71 | 72 | [tool.ruff] 73 | target-version = "py37" 74 | line-length = 140 75 | exclude = [ 76 | ".github", 77 | ".eggs", 78 | ".egg-info", 79 | ".idea", 80 | ".mypy_cache", 81 | ".tox", 82 | "build", 83 | "dist", 84 | "venv", 85 | ] 86 | 87 | [tool.ruff.lint] 88 | select = [ 89 | "E", # pycodestyle errors 90 | "W", # pycodestyle warnings 91 | "F", # pyflakes 92 | "I", # pyflakes 93 | "N", # pep8-naming 94 | "D", # pydocstyle 95 | "UP", # pyupgrade 96 | "C90", # mccabe complexity 97 | "PL", # pylint 98 | ] 99 | ignore = [ 100 | "N802", # Function name should be lowercase 101 | "E231", # Missing whitespace after ':' 102 | "F821", # Undefined name (Literal import for Python 3.7 compatibility) 103 | "UP006", # Use `type` instead of `Type` for type annotation (Python <3.9 compatibility) 104 | "UP007", # Use `X | Y` for type annotations (Python <3.10 compatibility) 105 | "UP037", # Remove quotes from type annotation (Literal import for Python 3.7 compatibility) 106 | "UP045", # Use `X | None` for type annotations (Python <3.10 compatibility) 107 | # pydocstyle 108 | "D100", # Missing docstring 109 | "D101", # Missing docstring 110 | "D102", # Missing docstring 111 | "D103", # Missing docstring 112 | "D104", # Missing docstring 113 | "D105", # Missing docstring 114 | "D107", # Missing docstring 115 | "D202", # No blank lines allowed after function docstring 116 | "D203", # 1 blank line required before class docstring 117 | "D205", # 1 blank line required between summary line and description 118 | "D212", # Multi-line docstring summary should start at the first line 119 | "D406", # Section name should end with a newline 120 | "D407", # Missing dashed underline after section 121 | "D413", # Missing blank line after last section 122 | "D415", # First line should end with a period, question mark, or exclamation point 123 | "D417", # Missing argument description in the docstring: **kwargs 124 | # pylint 125 | "PLR0911", # Too many return statements 126 | "PLR0912", # Too many branches 127 | "PLR0913", # Too many arguments 128 | "PLR0915", # Too many statements 129 | "PLR2004", # Magic value used in comparison 130 | ] 131 | 132 | [tool.ruff.lint.per-file-ignores] 133 | "__init__.py" = ["F401", "D104"] # Imported but unused, missing docstring 134 | "app/bot.py" = ["UP031"] # Use format specifiers instead of percent format 135 | 136 | [tool.ruff.lint.isort] 137 | known-first-party = [] 138 | lines-after-imports = 2 139 | combine-as-imports = false 140 | force-single-line = false 141 | 142 | [tool.ruff.lint.pydocstyle] 143 | convention = "google" 144 | 145 | [tool.ruff.lint.mccabe] 146 | max-complexity = 10 147 | 148 | [tool.mypy] 149 | python_version = "3.7" 150 | ignore_missing_imports = true 151 | follow_imports = "skip" 152 | exclude = [ 153 | "\\.github", 154 | "\\.eggs", 155 | "\\.egg-info", 156 | "\\.idea", 157 | "\\.ruff_cache", 158 | "\\.tox", 159 | "build", 160 | "dist", 161 | "venv", 162 | ] 163 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/price_info/price_info_builder.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_crypto_price_bot.bot.bot_config_types import BotConfigTypes 25 | from telegram_crypto_price_bot.config.config_object import ConfigObject 26 | from telegram_crypto_price_bot.misc.formatters import ( 27 | CoinPairFormatter, 28 | MarketCapFormatter, 29 | PriceChangePercFormatter, 30 | PriceFormatter, 31 | VolumeFormatter, 32 | ) 33 | from telegram_crypto_price_bot.price_info.price_info import PriceInfo, PriceInfoTypes 34 | from telegram_crypto_price_bot.translation.translation_loader import TranslationLoader 35 | 36 | 37 | # 38 | # Classes 39 | # 40 | 41 | # Constants for price info builder class 42 | class PriceInfoBuilderConst: 43 | # Alignment size 44 | ALIGN_LEN: int = 16 45 | # Delimiter for Markdown code 46 | MARKDOWN_CODE_DELIM: str = "```" 47 | 48 | 49 | # Price info builder class 50 | class PriceInfoBuilder: 51 | 52 | config: ConfigObject 53 | translator: TranslationLoader 54 | 55 | # Constructor 56 | def __init__(self, 57 | config: ConfigObject, 58 | translator: TranslationLoader) -> None: 59 | self.config = config 60 | self.translator = translator 61 | 62 | # Build price info 63 | def Build(self, 64 | price_info: PriceInfo) -> str: 65 | coin_vs = price_info.GetData(PriceInfoTypes.COIN_VS) 66 | coin_vs_sym = price_info.GetData(PriceInfoTypes.COIN_VS_SYMBOL) 67 | 68 | # Get and format data 69 | coin_pair = CoinPairFormatter.Format(price_info.GetData(PriceInfoTypes.COIN_SYMBOL), coin_vs) 70 | price = PriceFormatter.Format(price_info.GetData(PriceInfoTypes.CURR_PRICE), coin_vs_sym) 71 | 72 | high_24h = PriceFormatter.Format(price_info.GetData(PriceInfoTypes.HIGH_24H), coin_vs_sym) 73 | low_24h = PriceFormatter.Format(price_info.GetData(PriceInfoTypes.LOW_24H), coin_vs_sym) 74 | volume = VolumeFormatter.Format(price_info.GetData(PriceInfoTypes.TOTAL_VOLUME), coin_vs_sym) 75 | market_cap = MarketCapFormatter.Format(price_info.GetData(PriceInfoTypes.MARKET_CAP), coin_vs_sym) 76 | market_cap_rank = price_info.GetData(PriceInfoTypes.MARKET_CAP_RANK) 77 | 78 | change_perc_24h = PriceChangePercFormatter.Format(price_info.GetData(PriceInfoTypes.PRICE_CHANGE_PERC_24H)) 79 | change_perc_7d = PriceChangePercFormatter.Format(price_info.GetData(PriceInfoTypes.PRICE_CHANGE_PERC_7D)) 80 | change_perc_14d = PriceChangePercFormatter.Format(price_info.GetData(PriceInfoTypes.PRICE_CHANGE_PERC_14D)) 81 | change_perc_30d = PriceChangePercFormatter.Format(price_info.GetData(PriceInfoTypes.PRICE_CHANGE_PERC_30D)) 82 | 83 | # 84 | # Build message string 85 | # 86 | 87 | # Title 88 | msg = self.translator.GetSentence("PRICE_INFO_TITLE_MSG", 89 | coin_name=price_info.GetData(PriceInfoTypes.COIN_NAME)) 90 | 91 | # Begin code 92 | msg += PriceInfoBuilderConst.MARKDOWN_CODE_DELIM 93 | msg += "\n" 94 | 95 | # Price info 96 | msg += self.__PrintAligned(f"💵 {coin_pair}", price) 97 | msg += self.__PrintAligned("📈 High 24h", high_24h) 98 | msg += self.__PrintAligned("📈 Low 24h", low_24h) 99 | msg += self.__PrintAligned("📊 Volume 24h", volume) 100 | 101 | # Market cap 102 | if self.config.GetValue(BotConfigTypes.PRICE_DISPLAY_MARKET_CAP): 103 | msg += self.__PrintAligned("💎 Market Cap", market_cap) 104 | # Market cap rank 105 | if self.config.GetValue(BotConfigTypes.PRICE_DISPLAY_MARKET_CAP_RANK): 106 | msg += self.__PrintAligned("🏆 Rank", f"{market_cap_rank:d}") 107 | 108 | # Price differences 109 | msg += "\n⚖ Diff.\n" 110 | msg += self.__PrintAligned(" 24h", change_perc_24h, 1) 111 | msg += self.__PrintAligned(" 7d", change_perc_7d, 1) 112 | msg += self.__PrintAligned(" 14d", change_perc_14d, 1) 113 | msg += self.__PrintAligned(" 30d", change_perc_30d, 1, False) 114 | 115 | # End code 116 | msg += "\n" 117 | msg += PriceInfoBuilderConst.MARKDOWN_CODE_DELIM 118 | 119 | return msg 120 | 121 | # Print string aligned 122 | @staticmethod 123 | def __PrintAligned(header: str, 124 | value: str, 125 | correction: int = 0, 126 | add_new_line: bool = True) -> str: 127 | alignment = " " * (PriceInfoBuilderConst.ALIGN_LEN + correction - len(header)) 128 | new_line = "\n" if add_new_line else "" 129 | 130 | return f"{header}{alignment}{value}{new_line}" 131 | -------------------------------------------------------------------------------- /telegram_crypto_price_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_crypto_price_bot.command.command_data import CommandData 32 | from telegram_crypto_price_bot.config.config_object import ConfigObject 33 | from telegram_crypto_price_bot.logger.logger import Logger 34 | from telegram_crypto_price_bot.message.message_sender import MessageSender 35 | from telegram_crypto_price_bot.misc.chat_members import ChatMembersGetter 36 | from telegram_crypto_price_bot.misc.helpers import ChatHelper, UserHelper 37 | from telegram_crypto_price_bot.translation.translation_loader import TranslationLoader 38 | 39 | 40 | # 41 | # Classes 42 | # 43 | 44 | # 45 | # Generic command base class 46 | # 47 | class CommandBase(ABC): 48 | 49 | client: pyrogram.Client 50 | config: ConfigObject 51 | logger: Logger 52 | translator: TranslationLoader 53 | message: pyrogram.types.Message 54 | cmd_data: CommandData 55 | message_sender: MessageSender 56 | 57 | # Constructor 58 | def __init__(self, 59 | client: pyrogram.Client, 60 | config: ConfigObject, 61 | logger: Logger, 62 | translator: TranslationLoader) -> None: 63 | self.client = client 64 | self.config = config 65 | self.logger = logger 66 | self.translator = translator 67 | # Helper classes 68 | self.message_sender = MessageSender(client, logger) 69 | 70 | # Execute command 71 | def Execute(self, 72 | message: pyrogram.types.Message, 73 | **kwargs: Any) -> None: 74 | self.message = message 75 | self.cmd_data = CommandData(message) 76 | 77 | # Log command 78 | self.__LogCommand() 79 | 80 | # Check if user is anonymous 81 | if self._IsUserAnonymous() and not self._IsChannel(): 82 | self.logger.GetLogger().warning("An anonymous user tried to execute the command, exiting") 83 | return 84 | 85 | # Check if user is authorized 86 | if not self._IsUserAuthorized(): 87 | if self._IsPrivateChat(): 88 | self._SendMessage(self.translator.GetSentence("AUTH_ONLY_ERR_MSG")) 89 | 90 | self.logger.GetLogger().warning( 91 | f"User {UserHelper.GetNameOrId(self.cmd_data.User())} tried to execute the command but it's not authorized" 92 | ) 93 | return 94 | 95 | # Try to execute command 96 | try: 97 | self._ExecuteCommand(**kwargs) 98 | except RPCError: 99 | self._SendMessage(self.translator.GetSentence("GENERIC_ERR_MSG")) 100 | self.logger.GetLogger().exception( 101 | f"An error occurred while executing command {self.cmd_data.Name()}" 102 | ) 103 | 104 | # Send message 105 | def _SendMessage(self, 106 | msg: str) -> None: 107 | try: 108 | self.message_sender.SendMessage( 109 | self.cmd_data.Chat(), 110 | msg, 111 | reply_to_message_id=self.message.reply_to_message_id 112 | ) 113 | # Send message privately if topic is closed 114 | except BadRequest: 115 | self.message_sender.SendMessage(self.cmd_data.User(), msg) 116 | 117 | # Get if channel 118 | def _IsChannel(self) -> bool: 119 | return ChatHelper.IsChannel(self.cmd_data.Chat()) 120 | 121 | # Get if user is anonymous 122 | def _IsUserAnonymous(self) -> bool: 123 | return self.cmd_data.User() is None 124 | 125 | # Get if user is authorized 126 | def _IsUserAuthorized(self) -> bool: 127 | # In channels only admins can write, so we consider the user authorized since there is no way to know the specific user 128 | # This is a limitation for channels only 129 | if self._IsChannel(): 130 | return True 131 | 132 | # Anonymous user 133 | cmd_user = self.cmd_data.User() 134 | if cmd_user is None: 135 | return False 136 | # Private chat is always authorized 137 | if ChatHelper.IsPrivateChat(self.cmd_data.Chat(), cmd_user): 138 | return True 139 | # Check if admin 140 | admin_members = ChatMembersGetter(self.client).GetAdmins(self.cmd_data.Chat()) 141 | return any(cmd_user.id == member.user.id for member in admin_members if member.user is not None) 142 | 143 | # Get if chat is private 144 | def _IsPrivateChat(self) -> bool: 145 | cmd_user = self.cmd_data.User() 146 | if cmd_user is None: 147 | return False 148 | return ChatHelper.IsPrivateChat(self.cmd_data.Chat(), cmd_user) 149 | 150 | # Log command 151 | def __LogCommand(self) -> None: 152 | self.logger.GetLogger().info(f"Command: {self.cmd_data.Name()}") 153 | self.logger.GetLogger().info(f"Executed by user: {UserHelper.GetNameOrId(self.cmd_data.User())}") 154 | self.logger.GetLogger().debug(f"Received message: {self.message}") 155 | 156 | # Execute command - Abstract method 157 | @abstractmethod 158 | def _ExecuteCommand(self, 159 | **kwargs: Any) -> None: 160 | pass 161 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/lang/lang_en.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | **HELP** 5 | Hi {name}, 6 | welcome to the Telegram Price Bot. 7 | 8 | ℹ️ Here is the list of supported commands. 9 | 10 | 🔘 **/help** : show this message 11 | 🔘 **/alive** : show if bot is active 12 | 🔘 **/pricebot_set_test_mode** __true/false__ : enable/disable test mode 13 | 🔘 **/pricebot_is_test_mode** : show if test mode is enabled 14 | 🔘 **/pricebot_version** : show bot version 15 | 🔘 **/pricebot_get_single** __COIN_ID COIN_VS LAST_DAYS [SAME_MSG]__ : show chart and price information of the specified pair (single call) 16 | 🔘 **/pricebot_task_start** __PERIOD_HOURS START_HOUR COIN_ID COIN_VS LAST_DAYS__ : start a price task in the current chat 17 | 🔘 **/pricebot_task_stop** __COIN_ID COIN_VS__ : stop the specified price task in the current chat 18 | 🔘 **/pricebot_task_stop_all** : stop all price tasks in the current chat 19 | 🔘 **/pricebot_task_pause** __COIN_ID COIN_VS__ : pause the specified price task in the current chat 20 | 🔘 **/pricebot_task_resume** __COIN_ID COIN_VS__ : resume the specified price task in the current chat 21 | 🔘 **/pricebot_task_send_in_same_msg** __COIN_ID COIN_VS true/false__ : enable/disable sending chart and price information in the same message for the specified price task in the current chat 22 | 🔘 **/pricebot_task_delete_last_msg** __COIN_ID COIN_VS true/false__ : enable/disable the deletion of last messages for the specified price task in the current chat 23 | 🔘 **/pricebot_task_info** : show the list of active price tasks in the current chat 24 | 25 | Parameters in square brakets are optional. 26 | 27 | **STATUS** 28 | ✅ The bot is alive. 29 | 30 | 31 | **TEST MODE** 32 | ✅ Test mode enabled. 33 | 34 | **TEST MODE** 35 | ✅ Test mode disabled. 36 | 37 | 38 | **TEST MODE** 39 | ℹ️ Test mode is currently enabled. 40 | 41 | **TEST MODE** 42 | ℹ️ Test mode is currently disabled. 43 | 44 | 45 | **Telegram Crypto Price Bot** 46 | 47 | Author: Emanuele Bellocchia (ebellocchia@gmail.com) 48 | Version: **{version}** 49 | 50 | 51 | **TASK CONTROL** 52 | ✅ Price task successfully started. 53 | 54 | Parameters: 55 | - Period: __{period}h__ 56 | - Start: __{start:02d}:00__ 57 | - Coin ID: __{coin_id}__ 58 | - Coin VS: __{coin_vs}__ 59 | - Last days: __{last_days}__ 60 | 61 | 62 | **TASK CONTROL** 63 | ✅ Price task [{coin_id}, {coin_vs}] successfully stopped. 64 | 65 | 66 | **TASK CONTROL** 67 | ✅ All price tasks successfully stopped. 68 | 69 | 70 | **TASK CONTROL** 71 | ✅ Price task [{coin_id}, {coin_vs}] successfully paused. 72 | 73 | 74 | **TASK CONTROL** 75 | ✅ Price task [{coin_id}, {coin_vs}] successfully resumed. 76 | 77 | 78 | **TASK CONTROL** 79 | ✅ Price task [{coin_id}, {coin_vs}] send chart/price in the same message set to: {flag}. 80 | 81 | 82 | **TASK CONTROL** 83 | ✅ Price task [{coin_id}, {coin_vs}] delete last message set to: {flag}. 84 | 85 | 86 | **TASKS INFO** 87 | Number of active tasks in this chat: **{tasks_num}** 88 | Tasks list: 89 | {tasks_list} 90 | 91 | **TASKS INFO** 92 | No task is active in this chat. 93 | 94 | 95 | Hi! 96 | Thanks for choosing the **Telegram Crypto Price Bot**. 97 | Use __/help__ to see the list of supported commands. 98 | Do not forget to **make me administrator of the group** or I won't work properly. 99 | 100 | 101 | **ERROR** 102 | ❌ An error occurred while executing command. 103 | 104 | **ERROR** 105 | ❌ You are not authorized to use the command. 106 | 107 | **ERROR** 108 | ❌ This command can be executed only in the chat group. 109 | 110 | **ERROR** 111 | ❌ API error for coin {coin_id}/{coin_vs}, check network or coin ID. 112 | 113 | **ERROR** 114 | ❌ Invalid parameters. 115 | 116 | **ERROR** 117 | ❌ Price task [{coin_id}, {coin_vs}] is already active in this chat. Stop the task to start a new one with the same coins again. 118 | 119 | **ERROR** 120 | ❌ Price task [{coin_id}, {coin_vs}] is not active in this chat. 121 | 122 | **ERROR** 123 | ❌ Period shall be between 1 and 24. 124 | **ERROR** 125 | ❌ Start hour shall be between 0 and 23. 126 | 127 | **ERROR** 128 | ❌ Maximum number of tasks reached. Stop some tasks to start new ones. 129 | 130 | - Coin: __{coin_id}/{coin_vs}__, period: __{period}h__, start: __{start:02d}:00__, last days: __{last_days}__, state: __{state}__ 131 | 132 | runnnig 133 | 134 | paused 135 | 136 | **{coin_name} - Price Info**\n 137 | 138 | {coin_id}/{coin_vs} - Price of last {last_days} day(s) 139 | 140 | -------------------------------------------------------------------------------- /app/lang/lang_it.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | **MENU DI AIUTO** 5 | Ciao {name}, 6 | benvenuto nel Telegram Price Bot. 7 | 8 | ℹ️ Qui trovi la lista dei comandi supportati. 9 | 10 | 🔘 **/help** : mostra questo messaggio 11 | 🔘 **/alive** : mostra se il bot è attivo 12 | 🔘 **/pricebot_set_test_mode** __true/false__ : attiva/disattiva la modalità di test 13 | 🔘 **/pricebot_is_test_mode** : mostra se la modalità di test è attiva 14 | 🔘 **/pricebot_version** : mostra la versione del bot 15 | 🔘 **/pricebot_get_single** __COIN_ID COIN_VS LAST_DAYS [SAME_MSG]__ : mostra i dati e grafico del prezzo (chiamata singola) 16 | 🔘 **/pricebot_task_start** __PERIOD_HOURS START_HOUR COIN_ID COIN_VS LAST_DAYS__ : avvia un task di avviso prezzo nella chat corrente 17 | 🔘 **/pricebot_task_stop** __COIN_ID COIN_VS__ : ferma il task specificato nella chat corrente 18 | 🔘 **/pricebot_task_stop_all** : ferma tutti i task nella chat corrente 19 | 🔘 **/pricebot_task_pause** __COIN_ID COIN_VS__ : mette in pausa il task specificato nella chat corrente 20 | 🔘 **/pricebot_task_resume** __COIN_ID COIN_VS__ : riavvia il task specificato nella chat corrente 21 | 🔘 **/pricebot_task_send_in_same_msg** __COIN_ID COIN_VS true/false__ : attiva/disattiva l'invio del grafico e informazioni prezzo nello stesso messaggio per il task specificato nella chat corrente 22 | 🔘 **/pricebot_task_delete_last_msg** __COIN_ID COIN_VS true/false__ : attiva/disattiva la rimozione degli ultimi messaggi inviati per il task specificato nella chat corrente 23 | 🔘 **/pricebot_task_info** : mostra la lista di tutti i task attivi nella chat corrente 24 | 25 | I parametri tra parentesi quadre sono opzionali. 26 | 27 | **STATO** 28 | ✅ Il bot è attivo. 29 | 30 | 31 | **MODALITÀ TEST** 32 | ✅ Modalità di test attivata. 33 | 34 | **MODALITÀ TEST** 35 | ✅ Modalità di test disattivata. 36 | 37 | 38 | **MODALITÀ TEST** 39 | ℹ️ La modalità di test è attualmente attivata. 40 | 41 | **MODALITÀ TEST** 42 | ℹ️ La modalità di test è attualmente disattivata. 43 | 44 | 45 | **Telegram Crypto Price Bot** 46 | 47 | Autore: Emanuele Bellocchia (ebellocchia@gmail.com) 48 | Versione: **{version}** 49 | 50 | 51 | **CONTROLLO TASK** 52 | ✅ Task di avviso prezzo avviato con successo. 53 | 54 | Parametri: 55 | - Periodo: __{period}h__ 56 | - Inizio: __{start:02d}:00__ 57 | - Coin ID: __{coin_id}__ 58 | - Coin VS: __{coin_vs}__ 59 | - Ultimi giorni: __{last_days}__ 60 | 61 | 62 | **CONTROLLO TASK** 63 | ✅ Task di avviso prezzo [{coin_id}, {coin_vs}] fermato con successo. 64 | 65 | 66 | **CONTROLLO TASK** 67 | ✅ Tutti i task di avviso prezzo fermati con successo. 68 | 69 | 70 | **CONTROLLO TASK** 71 | ✅ Task di avviso prezzo [{coin_id}, {coin_vs}] messo in pausa con successo. 72 | 73 | 74 | **CONTROLLO TASK** 75 | ✅ Task di avviso prezzo [{coin_id}, {coin_vs}] riavviato con successo. 76 | 77 | 78 | **CONTROLLO TASK** 79 | ✅ Task di avviso prezzo [{coin_id}, {coin_vs}] invio grafico/prezzo nello stesso messaggio impostato a: {flag}. 80 | 81 | 82 | **CONTROLLO TASK** 83 | ✅ Task di avviso prezzo [{coin_id}, {coin_vs}] cancella ultimo messaggio impostato a: {flag}. 84 | 85 | 86 | **INFORMAZIONI TASK** 87 | Numero di task attivi in questa chat: **{tasks_num}** 88 | Lista dei task: 89 | {tasks_list} 90 | 91 | **INFORMAZIONI TASK** 92 | Nessun task attivo in questa chat. 93 | 94 | 95 | Ciao! 96 | Grazie per aver scelto il **Telegram Crypto Price Bot**. 97 | Usa __/help__ per vedere la lista dei comandi supportati. 98 | Non dimenticarti di **farmi amministratore del gruppo** o non potrò funzionare correttamente. 99 | 100 | 101 | **ERRORE** 102 | ❌ Si è verificato un errore durante l'esecuzione del comando. 103 | 104 | **ERRORE** 105 | ❌ Non sei autorizzato a utilizzare il comando. 106 | 107 | **ERRORE** 108 | ❌ Questo comando può essere eseguito solo nel gruppo. 109 | 110 | **ERRORE** 111 | ❌ Errore API per la coin {coin_id}/{coin_vs}, controllare la connessione o il simbolo della coin. 112 | 113 | **ERRORE** 114 | ❌ Parametri non validi. 115 | 116 | **ERRORE** 117 | ❌ Task di avviso prezzo [{coin_id}, {coin_vs}] già attivo in questa chat. Ferma il task per avviarne un altro con le stesse coin. 118 | 119 | **ERRORE** 120 | ❌ Task di avviso prezzo [{coin_id}, {coin_vs}] non attivo in questa chat. 121 | 122 | **ERRORE** 123 | ❌ Il periodo deve essere compreso tra 1 e 24. 124 | **ERRORE** 125 | ❌ L'ora di inizio deve essere compresa tra 0 e 23. 126 | 127 | **ERRORE** 128 | ❌ Massimo numero di task raggiunto. Ferma qualche task per avviarne dei nuovi. 129 | 130 | - Coin: __{coin_id}/{coin_vs}__, periodo: __{period}h__, inizio: __{start:02d}:00__, ultimi giorni: __{last_days}__, stato: __{state}__ 131 | 132 | attivo 133 | 134 | in pausa 135 | 136 | **{coin_name} - Informazioni Prezzo**\n 137 | 138 | {coin_id}/{coin_vs} - Prezzo degli ultimi {last_days} giorni 139 | 140 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/chart_info/chart_info_file_saver.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 | import tempfile 26 | from datetime import datetime 27 | from threading import Lock 28 | from typing import Optional 29 | 30 | import matplotlib 31 | from matplotlib import pyplot as plt 32 | 33 | from telegram_crypto_price_bot.bot.bot_config_types import BotConfigTypes 34 | from telegram_crypto_price_bot.chart_info.chart_info import ChartInfo 35 | from telegram_crypto_price_bot.config.config_object import ConfigObject 36 | from telegram_crypto_price_bot.logger.logger import Logger 37 | from telegram_crypto_price_bot.misc.formatters import CoinIdFormatter, PriceFormatter 38 | from telegram_crypto_price_bot.translation.translation_loader import TranslationLoader 39 | from telegram_crypto_price_bot.utils.utils import Synchronized 40 | 41 | 42 | # Prevent crash if it cannot connect to an X display 43 | matplotlib.use("Agg") 44 | 45 | 46 | # 47 | # Variables 48 | # 49 | 50 | # Lock for plotting charts, since matplotlib works only in single thread 51 | plot_lock: Lock = Lock() 52 | 53 | 54 | # 55 | # Classes 56 | # 57 | 58 | # Constants for chart info file saver class 59 | class ChartInfoFileSaverConst: 60 | # Chart image extension 61 | CHART_IMG_EXT: str = ".png" 62 | 63 | 64 | # Chart info file saver class 65 | class ChartInfoFileSaver: 66 | 67 | config: ConfigObject 68 | translator: TranslationLoader 69 | 70 | # Constructor 71 | def __init__(self, 72 | config: ConfigObject, 73 | translator: TranslationLoader) -> None: 74 | self.config = config 75 | self.translator = translator 76 | 77 | # Save chart to file 78 | @Synchronized(plot_lock) 79 | def SaveToFile(self, 80 | chart_info: ChartInfo, 81 | file_name: str) -> None: 82 | # Create subplot 83 | fig, ax = plt.subplots() 84 | 85 | # Configure plot 86 | self.__SetAxesFormatter(ax) 87 | self.__SetBackgroundColor(fig, ax) 88 | self.__SetAxesColor(ax) 89 | self.__SetFrameColor(ax) 90 | self.__SetGrid(ax) 91 | self.__SetTitle(chart_info) 92 | # Plot 93 | self.__Plot(chart_info, fig, ax) 94 | # Save to file and close figure 95 | self.__SaveAndClose(fig, file_name) 96 | 97 | # Set axes formatter 98 | def __SetAxesFormatter(self, 99 | ax: plt.axes) -> None: 100 | date_format = self.config.GetValue(BotConfigTypes.CHART_DATE_FORMAT) 101 | grid_max_size = self.config.GetValue(BotConfigTypes.CHART_GRID_MAX_SIZE) 102 | 103 | ax.xaxis.set_major_locator(plt.MaxNLocator(grid_max_size)) 104 | ax.xaxis.set_major_formatter( 105 | matplotlib.ticker.FuncFormatter( 106 | lambda x, p: datetime.fromtimestamp(int(x)).strftime(date_format) 107 | ) 108 | ) 109 | ax.yaxis.set_major_formatter( 110 | matplotlib.ticker.FuncFormatter(lambda x, p: PriceFormatter.Format(x)) 111 | ) 112 | 113 | # Set background color 114 | def __SetBackgroundColor(self, 115 | fig: plt.figure, 116 | ax: plt.axes) -> None: 117 | bckg_color = self.config.GetValue(BotConfigTypes.CHART_BACKGROUND_COLOR) 118 | 119 | fig.patch.set_facecolor(bckg_color) 120 | ax.set_facecolor(bckg_color) 121 | 122 | # Set axes color 123 | def __SetAxesColor(self, 124 | ax: plt.axes) -> None: 125 | axes_color = self.config.GetValue(BotConfigTypes.CHART_AXES_COLOR) 126 | 127 | ax.tick_params(color=axes_color, 128 | labelcolor=axes_color) 129 | 130 | # Set frame color 131 | def __SetFrameColor(self, 132 | ax: plt.axes) -> None: 133 | frame_color = self.config.GetValue(BotConfigTypes.CHART_FRAME_COLOR) 134 | 135 | for spine in ax.spines.values(): 136 | spine.set_edgecolor(frame_color) 137 | 138 | # Set grid 139 | def __SetGrid(self, 140 | ax: plt.axes) -> None: 141 | display_grid = self.config.GetValue(BotConfigTypes.CHART_DISPLAY_GRID) 142 | grid_color = self.config.GetValue(BotConfigTypes.CHART_GRID_COLOR) 143 | grid_line_style = self.config.GetValue(BotConfigTypes.CHART_GRID_LINE_STYLE) 144 | grid_line_width = self.config.GetValue(BotConfigTypes.CHART_GRID_LINE_WIDTH) 145 | 146 | ax.grid(display_grid, 147 | color=grid_color, 148 | linestyle=grid_line_style, 149 | linewidth=grid_line_width) 150 | 151 | # Set title 152 | def __SetTitle(self, 153 | chart_info: ChartInfo) -> None: 154 | title_color = self.config.GetValue(BotConfigTypes.CHART_TITLE_COLOR) 155 | 156 | plt.title( 157 | self.translator.GetSentence("CHART_INFO_TITLE_MSG", 158 | coin_id=CoinIdFormatter.Format(chart_info.CoinId()), 159 | coin_vs=chart_info.CoinVs().upper(), 160 | last_days=chart_info.LastDays()), 161 | color=title_color 162 | ) 163 | 164 | # Plot 165 | def __Plot(self, 166 | chart_info: ChartInfo, 167 | fig: plt.figure, 168 | ax: plt.axes) -> None: 169 | line_color = self.config.GetValue(BotConfigTypes.CHART_LINE_COLOR) 170 | line_style = self.config.GetValue(BotConfigTypes.CHART_LINE_STYLE) 171 | line_width = self.config.GetValue(BotConfigTypes.CHART_LINE_WIDTH) 172 | 173 | # Plot 174 | ax.plot(chart_info.X(), 175 | chart_info.Y(), 176 | color=line_color, 177 | linestyle=line_style, 178 | linewidth=line_width) 179 | # Rotate and align dates 180 | fig.autofmt_xdate() 181 | 182 | # Save to file and close 183 | @staticmethod 184 | def __SaveAndClose(fig: plt.figure, 185 | file_name: str) -> None: 186 | plt.savefig(file_name, bbox_inches="tight") 187 | plt.close(fig) 188 | 189 | 190 | # Chart info temporary file saver class 191 | class ChartInfoTmpFileSaver: 192 | 193 | logger: Logger 194 | tmp_file_name: Optional[str] 195 | chart_info_file_saver: ChartInfoFileSaver 196 | 197 | # Constructor 198 | def __init__(self, 199 | config: ConfigObject, 200 | logger: Logger, 201 | translator: TranslationLoader) -> None: 202 | self.logger = logger 203 | self.tmp_file_name = None 204 | self.chart_info_file_saver = ChartInfoFileSaver(config, translator) 205 | 206 | # Destructor 207 | def __del__(self): 208 | self.DeleteTmpFile() 209 | 210 | # Save chart to temporary file 211 | def SaveToTmpFile(self, 212 | chart_info: ChartInfo) -> None: 213 | # Delete old file 214 | self.DeleteTmpFile() 215 | # Save new file 216 | self.tmp_file_name = self.__NewTmpFileName() 217 | self.chart_info_file_saver.SaveToFile(chart_info, 218 | self.tmp_file_name) 219 | # Log 220 | self.logger.GetLogger().info( 221 | f"Saved chart information for coin {chart_info.CoinId()}/{chart_info.CoinVs()}, " 222 | f"last days {chart_info.LastDays()}, number of points ({len(chart_info.X())}, {len(chart_info.Y())}), " 223 | f"file name: \"{self.tmp_file_name}\"" 224 | ) 225 | 226 | # Get temporary file name 227 | def TmpFileName(self) -> Optional[str]: 228 | return self.tmp_file_name 229 | 230 | # Delete temporary file 231 | def DeleteTmpFile(self) -> None: 232 | if self.tmp_file_name is not None: 233 | try: 234 | os.remove(self.tmp_file_name) 235 | self.logger.GetLogger().info(f"Deleted chart file \"{self.tmp_file_name}\"") 236 | except FileNotFoundError: 237 | pass 238 | finally: 239 | self.tmp_file_name = None 240 | 241 | # Get new temporary file name 242 | @staticmethod 243 | def __NewTmpFileName() -> str: 244 | return f"{next(tempfile._get_candidate_names())}{ChartInfoFileSaverConst.CHART_IMG_EXT}" # type: ignore # noqa 245 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/bot/bot_handlers_config.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | from pyrogram import filters 25 | from pyrogram.handlers import MessageHandler 26 | 27 | from telegram_crypto_price_bot.bot.bot_handlers_config_typing import BotHandlersConfigType 28 | from telegram_crypto_price_bot.command.command_dispatcher import CommandTypes 29 | from telegram_crypto_price_bot.message.message_dispatcher import MessageTypes 30 | 31 | 32 | # 33 | # Variables 34 | # 35 | 36 | # Bot handlers configuration 37 | BotHandlersConfig: BotHandlersConfigType = { 38 | # Handlers for MessageHandler 39 | MessageHandler: [ 40 | 41 | # 42 | # Generic commands 43 | # 44 | 45 | { 46 | "callback": lambda self, client, message: self.DispatchCommand(client, 47 | message, 48 | CommandTypes.START_CMD), 49 | "filters": filters.private & filters.command(["start"]), 50 | }, 51 | { 52 | "callback": lambda self, client, message: self.DispatchCommand(client, 53 | message, 54 | CommandTypes.HELP_CMD), 55 | "filters": filters.command(["help"]), 56 | }, 57 | { 58 | "callback": lambda self, client, message: self.DispatchCommand(client, 59 | message, 60 | CommandTypes.ALIVE_CMD), 61 | "filters": filters.command(["alive"]), 62 | }, 63 | { 64 | "callback": lambda self, client, message: self.DispatchCommand(client, 65 | message, 66 | CommandTypes.SET_TEST_MODE_CMD), 67 | "filters": filters.command(["pricebot_set_test_mode"]), 68 | }, 69 | { 70 | "callback": lambda self, client, message: self.DispatchCommand(client, 71 | message, 72 | CommandTypes.IS_TEST_MODE_CMD), 73 | "filters": filters.command(["pricebot_is_test_mode"]), 74 | }, 75 | { 76 | "callback": lambda self, client, message: self.DispatchCommand(client, 77 | message, 78 | CommandTypes.VERSION_CMD), 79 | "filters": filters.command(["pricebot_version"]), 80 | }, 81 | 82 | # 83 | # Price commands (single call) 84 | # 85 | 86 | { 87 | "callback": lambda self, client, message: self.DispatchCommand(client, 88 | message, 89 | CommandTypes.PRICE_GET_SINGLE_CMD), 90 | "filters": filters.command(["pricebot_get_single"]), 91 | }, 92 | 93 | # 94 | # Price commands (task) 95 | # 96 | 97 | { 98 | "callback": (lambda self, client, message: self.DispatchCommand(client, 99 | message, 100 | CommandTypes.PRICE_TASK_START_CMD, 101 | coin_info_scheduler=self.coin_info_scheduler)), 102 | "filters": filters.command(["pricebot_task_start"]), 103 | }, 104 | { 105 | "callback": (lambda self, client, message: self.DispatchCommand(client, 106 | message, 107 | CommandTypes.PRICE_TASK_STOP_CMD, 108 | coin_info_scheduler=self.coin_info_scheduler)), 109 | "filters": filters.command(["pricebot_task_stop"]), 110 | }, 111 | { 112 | "callback": (lambda self, client, message: self.DispatchCommand(client, 113 | message, 114 | CommandTypes.PRICE_TASK_STOP_ALL_CMD, 115 | coin_info_scheduler=self.coin_info_scheduler)), 116 | "filters": filters.command(["pricebot_task_stop_all"]), 117 | }, 118 | { 119 | "callback": (lambda self, client, message: self.DispatchCommand(client, 120 | message, 121 | CommandTypes.PRICE_TASK_PAUSE_CMD, 122 | coin_info_scheduler=self.coin_info_scheduler)), 123 | "filters": filters.command(["pricebot_task_pause"]), 124 | }, 125 | { 126 | "callback": (lambda self, client, message: self.DispatchCommand(client, 127 | message, 128 | CommandTypes.PRICE_TASK_RESUME_CMD, 129 | coin_info_scheduler=self.coin_info_scheduler)), 130 | "filters": filters.command(["pricebot_task_resume"]), 131 | }, 132 | { 133 | "callback": (lambda self, client, message: self.DispatchCommand(client, 134 | message, 135 | CommandTypes.PRICE_TASK_SEND_IN_SAME_MSG_CMD, 136 | coin_info_scheduler=self.coin_info_scheduler)), 137 | "filters": filters.command(["pricebot_task_send_in_same_msg"]), 138 | }, 139 | { 140 | "callback": (lambda self, client, message: self.DispatchCommand(client, 141 | message, 142 | CommandTypes.PRICE_TASK_DELETE_LAST_MSG_CMD, 143 | coin_info_scheduler=self.coin_info_scheduler)), 144 | "filters": filters.command(["pricebot_task_delete_last_msg"]), 145 | }, 146 | { 147 | "callback": (lambda self, client, message: self.DispatchCommand(client, 148 | message, 149 | CommandTypes.PRICE_TASK_INFO_CMD, 150 | coin_info_scheduler=self.coin_info_scheduler)), 151 | "filters": filters.command(["pricebot_task_info"]), 152 | }, 153 | 154 | # 155 | # Update status messages 156 | # 157 | 158 | { 159 | "callback": (lambda self, client, message: self.HandleMessage(client, 160 | message, 161 | MessageTypes.GROUP_CHAT_CREATED)), 162 | "filters": filters.group_chat_created, 163 | }, 164 | { 165 | "callback": (lambda self, client, message: self.HandleMessage(client, 166 | message, 167 | MessageTypes.NEW_CHAT_MEMBERS)), 168 | "filters": filters.new_chat_members, 169 | }, 170 | { 171 | "callback": (lambda self, client, message: self.HandleMessage(client, 172 | message, 173 | MessageTypes.LEFT_CHAT_MEMBER, 174 | coin_info_scheduler=self.coin_info_scheduler)), 175 | "filters": filters.left_chat_member, 176 | }, 177 | ], 178 | } 179 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/bot/bot_config.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | import logging 25 | from typing import Dict, Tuple 26 | 27 | from telegram_crypto_price_bot.bot.bot_config_types import BotConfigTypes 28 | from telegram_crypto_price_bot.config.config_typing import ConfigSectionsType 29 | from telegram_crypto_price_bot.utils.utils import Utils 30 | 31 | 32 | # 33 | # Classes 34 | # 35 | 36 | # Configuration type converter class 37 | class _ConfigTypeConverter: 38 | # String to log level 39 | STR_TO_LOG_LEVEL: Dict[str, int] = { 40 | "DEBUG": logging.DEBUG, 41 | "INFO": logging.INFO, 42 | "WARNING": logging.WARNING, 43 | "ERROR": logging.ERROR, 44 | "CRITICAL": logging.CRITICAL, 45 | } 46 | 47 | # Convert string to log level 48 | @staticmethod 49 | def StrToLogLevel(log_level: str) -> int: 50 | return (_ConfigTypeConverter.STR_TO_LOG_LEVEL[log_level] 51 | if log_level in _ConfigTypeConverter.STR_TO_LOG_LEVEL 52 | else logging.INFO) 53 | 54 | # Convert log level to string 55 | @staticmethod 56 | def LogLevelToStr(log_level: int) -> str: 57 | idx = list(_ConfigTypeConverter.STR_TO_LOG_LEVEL.values()).index(log_level) 58 | return list(_ConfigTypeConverter.STR_TO_LOG_LEVEL.keys())[idx] 59 | 60 | 61 | # Constants for price bot configuration 62 | class PriceBotConfigConst: 63 | LINE_STYLES: Tuple[str, ...] = ("-", "--", "-.", ":", " ", "") 64 | 65 | 66 | # 67 | # Variables 68 | # 69 | 70 | # Bot configuration 71 | BotConfig: ConfigSectionsType = { 72 | # Pyrogram 73 | "pyrogram": [ 74 | { 75 | "type": BotConfigTypes.API_ID, 76 | "name": "api_id", 77 | }, 78 | { 79 | "type": BotConfigTypes.API_HASH, 80 | "name": "api_hash", 81 | }, 82 | { 83 | "type": BotConfigTypes.BOT_TOKEN, 84 | "name": "bot_token", 85 | }, 86 | { 87 | "type": BotConfigTypes.SESSION_NAME, 88 | "name": "session_name", 89 | }, 90 | ], 91 | # App 92 | "app": [ 93 | { 94 | "type": BotConfigTypes.APP_TEST_MODE, 95 | "name": "app_test_mode", 96 | "conv_fct": Utils.StrToBool, 97 | }, 98 | { 99 | "type": BotConfigTypes.APP_LANG_FILE, 100 | "name": "app_lang_file", 101 | "def_val": None, 102 | }, 103 | ], 104 | # Task 105 | "task": [ 106 | { 107 | "type": BotConfigTypes.TASKS_MAX_NUM, 108 | "name": "tasks_max_num", 109 | "conv_fct": Utils.StrToInt, 110 | "def_val": 20, 111 | "valid_if": lambda cfg, val: val > 0, 112 | }, 113 | ], 114 | # Coingecko 115 | "coingecko": [ 116 | { 117 | "type": BotConfigTypes.COINGECKO_API_KEY, 118 | "name": "coingecko_api_key", 119 | "def_val": "", 120 | }, 121 | ], 122 | # Chart 123 | "chart": [ 124 | { 125 | "type": BotConfigTypes.CHART_DISPLAY, 126 | "name": "chart_display", 127 | "conv_fct": Utils.StrToBool, 128 | "def_val": True, 129 | }, 130 | { 131 | "type": BotConfigTypes.CHART_DATE_FORMAT, 132 | "name": "chart_date_format", 133 | "def_val": "%d/%m/%Y %H:00", 134 | "load_if": lambda cfg: cfg.GetValue(BotConfigTypes.CHART_DISPLAY), 135 | }, 136 | { 137 | "type": BotConfigTypes.CHART_BACKGROUND_COLOR, 138 | "name": "chart_background_color", 139 | "def_val": "white", 140 | "load_if": lambda cfg: cfg.GetValue(BotConfigTypes.CHART_DISPLAY), 141 | }, 142 | { 143 | "type": BotConfigTypes.CHART_TITLE_COLOR, 144 | "name": "chart_title_color", 145 | "def_val": "black", 146 | "load_if": lambda cfg: cfg.GetValue(BotConfigTypes.CHART_DISPLAY), 147 | }, 148 | { 149 | "type": BotConfigTypes.CHART_FRAME_COLOR, 150 | "name": "chart_frame_color", 151 | "def_val": "black", 152 | "load_if": lambda cfg: cfg.GetValue(BotConfigTypes.CHART_DISPLAY), 153 | }, 154 | { 155 | "type": BotConfigTypes.CHART_AXES_COLOR, 156 | "name": "chart_axes_color", 157 | "def_val": "black", 158 | "load_if": lambda cfg: cfg.GetValue(BotConfigTypes.CHART_DISPLAY), 159 | }, 160 | { 161 | "type": BotConfigTypes.CHART_LINE_COLOR, 162 | "name": "chart_line_color", 163 | "def_val": "#3475AB", 164 | "load_if": lambda cfg: cfg.GetValue(BotConfigTypes.CHART_DISPLAY), 165 | }, 166 | { 167 | "type": BotConfigTypes.CHART_LINE_STYLE, 168 | "name": "chart_line_style", 169 | "def_val": "-", 170 | "load_if": lambda cfg: cfg.GetValue(BotConfigTypes.CHART_DISPLAY), 171 | "valid_if": lambda cfg, val: val in PriceBotConfigConst.LINE_STYLES, 172 | }, 173 | { 174 | "type": BotConfigTypes.CHART_LINE_WIDTH, 175 | "name": "chart_line_width", 176 | "conv_fct": Utils.StrToInt, 177 | "def_val": 1, 178 | "load_if": lambda cfg: cfg.GetValue(BotConfigTypes.CHART_DISPLAY), 179 | "valid_if": lambda cfg, val: val > 0, 180 | }, 181 | { 182 | "type": BotConfigTypes.CHART_DISPLAY_GRID, 183 | "name": "chart_display_grid", 184 | "conv_fct": Utils.StrToBool, 185 | "def_val": True, 186 | "load_if": lambda cfg: cfg.GetValue(BotConfigTypes.CHART_DISPLAY), 187 | }, 188 | { 189 | "type": BotConfigTypes.CHART_GRID_MAX_SIZE, 190 | "name": "chart_grid_max_size", 191 | "conv_fct": Utils.StrToInt, 192 | "def_val": 4, 193 | "load_if": lambda cfg: (cfg.GetValue(BotConfigTypes.CHART_DISPLAY) and 194 | cfg.GetValue(BotConfigTypes.CHART_DISPLAY_GRID)), 195 | "valid_if": lambda cfg, val: val > 0, 196 | }, 197 | { 198 | "type": BotConfigTypes.CHART_GRID_COLOR, 199 | "name": "chart_grid_color", 200 | "def_val": "#DFDFDF", 201 | "load_if": lambda cfg: (cfg.GetValue(BotConfigTypes.CHART_DISPLAY) and 202 | cfg.GetValue(BotConfigTypes.CHART_DISPLAY_GRID)), 203 | }, 204 | { 205 | "type": BotConfigTypes.CHART_GRID_LINE_STYLE, 206 | "name": "chart_grid_line_style", 207 | "def_val": "--", 208 | "load_if": lambda cfg: (cfg.GetValue(BotConfigTypes.CHART_DISPLAY) and 209 | cfg.GetValue(BotConfigTypes.CHART_DISPLAY_GRID)), 210 | "valid_if": lambda cfg, val: val in PriceBotConfigConst.LINE_STYLES, 211 | }, 212 | { 213 | "type": BotConfigTypes.CHART_GRID_LINE_WIDTH, 214 | "name": "chart_grid_line_width", 215 | "conv_fct": Utils.StrToInt, 216 | "def_val": 1, 217 | "load_if": lambda cfg: (cfg.GetValue(BotConfigTypes.CHART_DISPLAY) and 218 | cfg.GetValue(BotConfigTypes.CHART_DISPLAY_GRID)), 219 | "valid_if": lambda cfg, val: val > 0, 220 | }, 221 | ], 222 | # Price 223 | "price": [ 224 | { 225 | "type": BotConfigTypes.PRICE_DISPLAY_MARKET_CAP, 226 | "name": "price_display_market_cap", 227 | "conv_fct": Utils.StrToBool, 228 | "def_val": True, 229 | }, 230 | { 231 | "type": BotConfigTypes.PRICE_DISPLAY_MARKET_CAP_RANK, 232 | "name": "price_display_market_cap_rank", 233 | "conv_fct": Utils.StrToBool, 234 | "def_val": False, 235 | }, 236 | ], 237 | # Logging 238 | "logging": [ 239 | { 240 | "type": BotConfigTypes.LOG_LEVEL, 241 | "name": "log_level", 242 | "conv_fct": _ConfigTypeConverter.StrToLogLevel, 243 | "print_fct": _ConfigTypeConverter.LogLevelToStr, 244 | "def_val": logging.INFO, 245 | }, 246 | { 247 | "type": BotConfigTypes.LOG_CONSOLE_ENABLED, 248 | "name": "log_console_enabled", 249 | "conv_fct": Utils.StrToBool, 250 | "def_val": True, 251 | }, 252 | { 253 | "type": BotConfigTypes.LOG_FILE_ENABLED, 254 | "name": "log_file_enabled", 255 | "conv_fct": Utils.StrToBool, 256 | "def_val": False, 257 | }, 258 | { 259 | "type": BotConfigTypes.LOG_FILE_NAME, 260 | "name": "log_file_name", 261 | "load_if": lambda cfg: cfg.GetValue(BotConfigTypes.LOG_FILE_ENABLED), 262 | }, 263 | { 264 | "type": BotConfigTypes.LOG_FILE_USE_ROTATING, 265 | "name": "log_file_use_rotating", 266 | "conv_fct": Utils.StrToBool, 267 | "load_if": lambda cfg: cfg.GetValue(BotConfigTypes.LOG_FILE_ENABLED), 268 | }, 269 | { 270 | "type": BotConfigTypes.LOG_FILE_APPEND, 271 | "name": "log_file_append", 272 | "conv_fct": Utils.StrToBool, 273 | "load_if": lambda cfg: (cfg.GetValue(BotConfigTypes.LOG_FILE_ENABLED) and 274 | not cfg.GetValue(BotConfigTypes.LOG_FILE_USE_ROTATING)), 275 | }, 276 | { 277 | "type": BotConfigTypes.LOG_FILE_MAX_BYTES, 278 | "name": "log_file_max_bytes", 279 | "conv_fct": Utils.StrToInt, 280 | "load_if": lambda cfg: (cfg.GetValue(BotConfigTypes.LOG_FILE_ENABLED) and 281 | cfg.GetValue(BotConfigTypes.LOG_FILE_USE_ROTATING)), 282 | }, 283 | { 284 | "type": BotConfigTypes.LOG_FILE_BACKUP_CNT, 285 | "name": "log_file_backup_cnt", 286 | "conv_fct": Utils.StrToInt, 287 | "load_if": lambda cfg: (cfg.GetValue(BotConfigTypes.LOG_FILE_ENABLED) and 288 | cfg.GetValue(BotConfigTypes.LOG_FILE_USE_ROTATING)), 289 | }, 290 | ], 291 | } 292 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telegram Crypto Price Bot 2 | 3 | | | 4 | |---| 5 | | [![PyPI - Version](https://img.shields.io/pypi/v/telegram_crypto_price_bot.svg?logo=pypi&label=PyPI&logoColor=gold)](https://pypi.org/project/telegram_crypto_price_bot/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/telegram_crypto_price_bot.svg?logo=python&label=Python&logoColor=gold)](https://pypi.org/project/telegram_crypto_price_bot/) [![GitHub License](https://img.shields.io/github/license/ebellocchia/telegram_crypto_price_bot?label=License)](https://github.com/ebellocchia/telegram_crypto_price_bot?tab=MIT-1-ov-file) | 6 | | [![Build](https://github.com/ebellocchia/telegram_crypto_price_bot/actions/workflows/build.yml/badge.svg)](https://github.com/ebellocchia/telegram_crypto_price_bot/actions/workflows/build.yml) [![Code Analysis](https://github.com/ebellocchia/telegram_crypto_price_bot/actions/workflows/code-analysis.yml/badge.svg)](https://github.com/ebellocchia/telegram_crypto_price_bot/actions/workflows/code-analysis.yml) | 7 | | [![Codacy grade](https://img.shields.io/codacy/grade/e494ae8a0df847ca85dc72305bdb3ffa?label=Codacy%20Grade)](https://app.codacy.com/gh/ebellocchia/telegram_crypto_price_bot/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/ebellocchia/telegram_crypto_price_bot?label=CodeFactor%20Grade)](https://www.codefactor.io/repository/github/ebellocchia/telegram_crypto_price_bot) | 8 | | | 9 | 10 | ## Introduction 11 | 12 | Telegram bot for displaying cryptocurrencies prices and charts based on *pyrogram* and *matplotlib* libraries.\ 13 | Data is retrieved using CoinGecko APIs.\ 14 | It is possible to show coin information either on demand (by manually calling a command) or periodically using background tasks.\ 15 | A single bot instance can be used with multiple coins and in multiple groups.\ 16 | The usage of the bot is restricted to admins, in order to avoid users to flood the chat with price requests. 17 | 18 | ## Setup 19 | 20 | ### Create Telegram app 21 | 22 | In order to use the bot, in addition to the bot token you also need an APP ID and hash.\ 23 | To get them, create an app using the following website: [https://my.telegram.org/apps](https://my.telegram.org/apps). 24 | 25 | ### Installation 26 | 27 | The package requires Python >= 3.7.\ 28 | To install it: 29 | 30 | pip install telegram_crypto_price_bot 31 | 32 | To run the bot, edit the configuration file by specifying the API ID/hash and bot token. Then, move to the *app* folder and run the *bot.py* script: 33 | 34 | cd app 35 | python bot.py 36 | 37 | When run with no parameter, *conf/config.ini* will be the default configuration file (in this way it can be used for different groups).\ 38 | To specify a different configuration file: 39 | 40 | python bot.py -c another_conf.ini 41 | python bot.py --config another_conf.ini 42 | 43 | Of course, the *app* folder can be moved elsewhere if needed. 44 | 45 | To run code analysis: 46 | 47 | mypy . 48 | ruff check . 49 | 50 | ## Configuration 51 | 52 | An example of configuration file is provided in the *app/conf* folder.\ 53 | The list of all possible fields that can be set is shown below. 54 | 55 | |Name|Description| 56 | |---|---| 57 | |**[pyrogram]**|Configuration for pyrogram| 58 | |`session_name`|Name of the file used to store the session| 59 | |`api_id`|API ID from [https://my.telegram.org/apps](https://my.telegram.org/apps)| 60 | |`api_hash`|API hash from [https://my.telegram.org/apps](https://my.telegram.org/apps)| 61 | |`bot_token`|Bot token from BotFather| 62 | |**[app]**|Configuration for app| 63 | |`app_is_test_mode`|True to activate test mode false otherwise| 64 | |`app_lang_file`|Language file in XML format (default: English)| 65 | |**[task]**|Configuration for tasks| 66 | |`tasks_max_num`|Maximum number of running tasks (totally, in all groups). Default: `20`.| 67 | |**[coingecko]**|Configuration for Coingecko| 68 | |`coingecko_api_key`|Key for using Coingecko APIs. If not specified, the free APIs will be used. Default: `empty string`.| 69 | |**[chart]**|Configuration for price chart| 70 | |`chart_display`|True to display price chart, false otherwise (default: true). If false, all the next fields will be skipped.| 71 | |`chart_date_format`|Date format for price chart (default: `%%d/%%m/%%Y %%H:00`)| 72 | |`chart_background_color`|Background color for price chart (default: `white`)| 73 | |`chart_title_color`|Title color for price chart (default: `black`)| 74 | |`chart_frame_color`|Frame color for price chart (default: `black`)| 75 | |`chart_axes_color`|Axes color for price chart (default: `black`)| 76 | |`chart_line_color`|Line color for price chart (default: `#3475AB`)| 77 | |`chart_line_style`|Line style for price chart (default: `-`). Same as matplotlib line styles: `-` `--` `-.` `:`| 78 | |`chart_line_width`|Line width for price chart (default: `1`)| 79 | |`chart_display_grid`|True to display price chart grid, false otherwise (default: `true`). If false, all the next fields will be skipped.| 80 | |`chart_grid_max_size`|Maximum size for price chart grid (default: `4`)| 81 | |`chart_grid_color`|Line color for price chart grid (default: `#DFDFDF`)| 82 | |`chart_grid_line_style`|Line style for price chart grid (default: `--`). Same as matplotlib line styles: `-` `--` `-.` `:`| 83 | |`chart_grid_line_width`|Line width for price chart grid (default: `1`)| 84 | |**[price]**|Configuration for price info| 85 | |`price_display_market_cap`|True to display market cap, false otherwise (default: `true`)| 86 | |`price_display_market_cap_rank`|True to display market cap rank, false otherwise (default: `false`)| 87 | |**[logging]**|Configuration for logging| 88 | |`log_level`|Log level, same of python logging (`DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`). Default: `INFO`.| 89 | |`log_console_enabled`|True to enable logging to console, false otherwise (default: `true`)| 90 | |`log_file_enabled`|True to enable logging to file, false otherwise (default: `false`). If false, all the next fields will be skipped.| 91 | |`log_file_name`|Log file name| 92 | |`log_file_use_rotating`|True for using a rotating log file, false otherwise| 93 | |`log_file_max_bytes`|Maximum size in bytes for a log file. When reached, a new log file is created up to `log_file_backup_cnt`. Valid only if `log_file_use_rotating` is true.| 94 | |`log_file_backup_cnt`|Maximum number of log files. Valid only if `log_file_use_rotating` is true.| 95 | |`log_file_append`|True to append to log file, false to start from a new file each time. Valid only if `log_file_use_rotating` is false.| 96 | 97 | All the colors can be either a name or a RGB color in format `#RRGGBB` (same as matplotlib colors).\ 98 | Chart and price configurations will be applied to all coin information in all groups. It's not possible to configure a single coin. 99 | 100 | ## Supported Commands 101 | 102 | List of supported commands: 103 | - `/help`: show this message 104 | - `/alive`: show if bot is active 105 | - `/pricebot_set_test_mode true/false`: enable/disable test mode 106 | - `/pricebot_is_test_mode`: show if test mode is enabled 107 | - `/pricebot_version`: show bot version 108 | - `/pricebot_get_single COIN_ID COIN_VS LAST_DAYS [SAME_MSG]`: show chart and price information of the specified pair (single call).\ 109 | Parameters: 110 | - `COIN_ID`: CoinGecko *ID* 111 | - `COIN_VS`: CoinGecko *vs_currency* 112 | - `LAST_DAYS`: Last number of days to show price chart 113 | - `SAME_MSG` (optional): true for sending chart and price information in the same message (price information will be a caption of the chart image), false to send them in separate messages. Default value: true. 114 | - `/pricebot_task_start PERIOD_HOURS START_HOUR COIN_ID COIN_VS LAST_DAYS`: start a price task in the current chat. If the task `COIN_ID/COIN_VS` already exists in the current chat, an error message will be shown. To start it again, it shall be stopped with the `pricebot_task_stop` command.\ 115 | Parameters: 116 | - `PERIOD_HOURS`: Task period in hours, it shall be between 1 and 24 117 | - `START_HOUR`: Task start hour, it shall be between 0 and 23 118 | - `COIN_ID`: CoinGecko *ID* 119 | - `COIN_VS`: CoinGecko *vs_currency* 120 | - `LAST_DAYS`: Last number of days to show price chart 121 | - `/pricebot_task_stop COIN_ID COIN_VS`: stop the specified price task in the current chat. If the task `COIN_ID/COIN_VS` does not exist in the current chat, an error message will be shown.\ 122 | Parameters: 123 | - `COIN_ID`: CoinGecko *ID* 124 | - `COIN_VS`: CoinGecko *vs_currency* 125 | - `/pricebot_task_stop_all`: stop all price tasks in the current chat 126 | - `/pricebot_task_pause COIN_ID COIN_VS`: pause the specified price task in the current chat. If the task `COIN_ID/COIN_VS` does not exist in the current chat, an error message will be shown.\ 127 | Parameters: 128 | - `COIN_ID`: CoinGecko *ID* 129 | - `COIN_VS`: CoinGecko *vs_currency* 130 | - `/pricebot_task_resume COIN_ID COIN_VS`: resume the specified price task in the current chat. If the task `COIN_ID/COIN_VS` does not exist in the current chat, an error message will be shown.\ 131 | Parameters: 132 | - `COIN_ID`: CoinGecko *ID* 133 | - `COIN_VS`: CoinGecko *vs_currency* 134 | - `/pricebot_task_send_in_same_msg COIN_ID COIN_VS true/false`: enable/disable the sending of chart and price information in the same message. If the task `COIN_ID/COIN_VS` does not exist in the current chat, an error message will be shown.\ 135 | Parameters: 136 | - `COIN_ID`: CoinGecko *ID* 137 | - `COIN_VS`: CoinGecko *vs_currency* 138 | - `flag`: true for sending chart and price information in the same message (price information will be a caption of the chart image), false to send them in separate messages 139 | - `/pricebot_task_delete_last_msg COIN_ID COIN_VS true/false`: enable/disable the deletion of last messages for the specified price task in the current chat. If the task `COIN_ID/COIN_VS` does not exist in the current chat, an error message will be shown.\ 140 | Parameters: 141 | - `COIN_ID`: CoinGecko *ID* 142 | - `COIN_VS`: CoinGecko *vs_currency* 143 | - `flag`: true or false 144 | - `/pricebot_task_info`: show the list of active price tasks in the current chat 145 | 146 | By default: 147 | - a price task will send chart and price information in the same message. This can be enabled/disabled with the `pricebot_task_send_in_same_msg` command. 148 | - a price task will delete the last sent message when sending a new one. This can be enabled/disabled with the `pricebot_task_delete_last_msg` command. 149 | 150 | The task period starts from the specified starting hour (be sure to set the correct time on the VPS), for example: 151 | - A task period of 8 hours starting from 00:00 will send the message at: 00:00, 08:00 and 16:00 152 | - A task period of 6 hours starting from 08:00 will send the message at: 08:00, 14:00, 20:00 and 02:00 153 | 154 | In case of API errors (e.g. network issues or invalid coin ID) an error message will be shown. 155 | 156 | **Examples** 157 | 158 | Show the price of BTC/USD of the last 14 days in the current chat (single call): 159 | 160 | /pricebot_get_single bitcoin usd 14 161 | 162 | Show the price of ETH/BTC of the last 30 days periodically every 8 hours starting from 10:00 in the current chat: 163 | 164 | /pricebot_task_start 8 10 ethereum btc 30 165 | 166 | Pause/Resume/Stop the previous task: 167 | 168 | /pricebot_task_pause ethereum btc 169 | /pricebot_task_resume ethereum btc 170 | /pricebot_task_stop ethereum btc 171 | 172 | Set task so that it sends chart and price information in the same message: 173 | 174 | /pricebot_task_send_in_same_msg ethereum btc true 175 | 176 | Set task so that it doesn't delete the last sent message: 177 | 178 | /pricebot_task_delete_last_msg ethereum btc false 179 | 180 | ## Run the Bot 181 | 182 | 183 | Since the bot deletes the last sent messages, it'd be better if it's an administrator of the group (otherwise the last messages cannot be deleted).\ 184 | In order to display prices periodically, the bot shall run 24h/24h. So, it's suggested to run it on a VPS (there is no performance requirements, so a cheap VPS will suffice). 185 | 186 | ## Test Mode 187 | 188 | During test mode, the bot will work as usual but the task period will be applied in minutes instead of hours. This allows to quickly check if it is working. 189 | 190 | ## Translation 191 | 192 | The messages sent by the bot on Telegram can be translated into different languages (the default language is English) by providing a custom XML file.\ 193 | The XML file path is specified in the configuration file (`app_lang_file` field).\ 194 | An example XML file in italian is provided in the folder *app/lang*. 195 | 196 | ## Image Examples 197 | 198 | Example with chart and price information on different messages: 199 | 200 | 201 | 202 | Example with chart and price information on the same message: 203 | 204 | 205 | 206 | # License 207 | 208 | This software is available under the MIT license. 209 | -------------------------------------------------------------------------------- /telegram_crypto_price_bot/command/commands.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Emanuele Bellocchia 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # 22 | # Imports 23 | # 24 | from typing import Any, Callable 25 | 26 | from telegram_crypto_price_bot._version import __version__ 27 | from telegram_crypto_price_bot.bot.bot_config_types import BotConfigTypes 28 | from telegram_crypto_price_bot.coin_info.coin_info_scheduler import ( 29 | CoinInfoJobAlreadyExistentError, 30 | CoinInfoJobInvalidPeriodError, 31 | CoinInfoJobInvalidStartError, 32 | CoinInfoJobMaxNumError, 33 | CoinInfoJobNotExistentError, 34 | ) 35 | from telegram_crypto_price_bot.command.command_base import CommandBase 36 | from telegram_crypto_price_bot.command.command_data import CommandParameterError 37 | from telegram_crypto_price_bot.info_message_sender.coin_info_message_sender import CoinInfoMessageSender 38 | from telegram_crypto_price_bot.misc.helpers import UserHelper 39 | 40 | 41 | # 42 | # Decorators 43 | # 44 | 45 | # Decorator for group-only commands 46 | def GroupChatOnly(exec_cmd_fct: Callable[..., None]) -> Callable[..., None]: 47 | def decorated(self, 48 | **kwargs: Any): 49 | # Check if private chat 50 | if self._IsPrivateChat(): 51 | self._SendMessage(self.translator.GetSentence("GROUP_ONLY_ERR_MSG")) 52 | else: 53 | exec_cmd_fct(self, **kwargs) 54 | 55 | return decorated 56 | 57 | 58 | # 59 | # Classes 60 | # 61 | 62 | # 63 | # Command for getting help 64 | # 65 | class HelpCmd(CommandBase): 66 | # Execute command 67 | def _ExecuteCommand(self, 68 | **kwargs: Any) -> None: 69 | self._SendMessage( 70 | self.translator.GetSentence("HELP_CMD", 71 | name=UserHelper.GetName(self.cmd_data.User())) 72 | ) 73 | 74 | 75 | # 76 | # Command for checking if bot is alive 77 | # 78 | class AliveCmd(CommandBase): 79 | # Execute command 80 | def _ExecuteCommand(self, 81 | **kwargs: Any) -> None: 82 | self._SendMessage(self.translator.GetSentence("ALIVE_CMD")) 83 | 84 | 85 | # 86 | # Command for setting test mode 87 | # 88 | class SetTestModeCmd(CommandBase): 89 | # Execute command 90 | @GroupChatOnly 91 | def _ExecuteCommand(self, 92 | **kwargs: Any) -> None: 93 | try: 94 | # Get parameters 95 | flag = self.cmd_data.Params().GetAsBool(0) 96 | except CommandParameterError: 97 | self._SendMessage(self.translator.GetSentence("PARAM_ERR_MSG")) 98 | else: 99 | # Set test mode 100 | self.config.SetValue(BotConfigTypes.APP_TEST_MODE, flag) 101 | 102 | # Send message 103 | if self.config.GetValue(BotConfigTypes.APP_TEST_MODE): 104 | self._SendMessage(self.translator.GetSentence("SET_TEST_MODE_EN_CMD")) 105 | else: 106 | self._SendMessage(self.translator.GetSentence("SET_TEST_MODE_DIS_CMD")) 107 | 108 | 109 | # 110 | # Command for checking if test mode 111 | # 112 | class IsTestModeCmd(CommandBase): 113 | # Execute command 114 | def _ExecuteCommand(self, 115 | **kwargs: Any) -> None: 116 | if self.config.GetValue(BotConfigTypes.APP_TEST_MODE): 117 | self._SendMessage(self.translator.GetSentence("IS_TEST_MODE_EN_CMD")) 118 | else: 119 | self._SendMessage(self.translator.GetSentence("IS_TEST_MODE_DIS_CMD")) 120 | 121 | 122 | # 123 | # Command for showing bot version 124 | # 125 | class VersionCmd(CommandBase): 126 | # Execute command 127 | def _ExecuteCommand(self, 128 | **kwargs: Any) -> None: 129 | self._SendMessage( 130 | self.translator.GetSentence("VERSION_CMD", 131 | version=__version__) 132 | ) 133 | 134 | 135 | # 136 | # Get single price command 137 | # 138 | class PriceGetSingleCmd(CommandBase): 139 | # Execute command 140 | def _ExecuteCommand(self, 141 | **kwargs: Any) -> None: 142 | # Get parameters 143 | try: 144 | coin_id = self.cmd_data.Params().GetAsString(0) 145 | coin_vs = self.cmd_data.Params().GetAsString(1) 146 | last_days = self.cmd_data.Params().GetAsInt(2) 147 | same_msg = self.cmd_data.Params().GetAsBool(3, True) 148 | except CommandParameterError: 149 | self._SendMessage(self.translator.GetSentence("PARAM_ERR_MSG")) 150 | else: 151 | coin_info_sender = CoinInfoMessageSender(self.client, self.config, self.logger, self.translator) 152 | coin_info_sender.SendInSameMessage(same_msg) 153 | coin_info_sender.SendMessage(self.cmd_data.Chat(), coin_id, coin_vs, last_days) 154 | 155 | 156 | # 157 | # Price task start command 158 | # 159 | class PriceTaskStartCmd(CommandBase): 160 | # Execute command 161 | @GroupChatOnly 162 | def _ExecuteCommand(self, 163 | **kwargs: Any) -> None: 164 | # Get parameters 165 | try: 166 | period_hours = self.cmd_data.Params().GetAsInt(0) 167 | start_hour = self.cmd_data.Params().GetAsInt(1) 168 | coin_id = self.cmd_data.Params().GetAsString(2) 169 | coin_vs = self.cmd_data.Params().GetAsString(3) 170 | last_days = self.cmd_data.Params().GetAsInt(4) 171 | except CommandParameterError: 172 | self._SendMessage(self.translator.GetSentence("PARAM_ERR_MSG")) 173 | else: 174 | try: 175 | kwargs["coin_info_scheduler"].Start(self.cmd_data.Chat(), 176 | period_hours, 177 | start_hour, 178 | coin_id, 179 | coin_vs, 180 | last_days) 181 | self._SendMessage( 182 | self.translator.GetSentence("PRICE_TASK_START_OK_CMD", 183 | period=period_hours, 184 | start=start_hour, 185 | coin_id=coin_id, 186 | coin_vs=coin_vs, 187 | last_days=last_days) 188 | ) 189 | except CoinInfoJobInvalidPeriodError: 190 | self._SendMessage(self.translator.GetSentence("TASK_PERIOD_ERR_MSG")) 191 | except CoinInfoJobInvalidStartError: 192 | self._SendMessage(self.translator.GetSentence("TASK_START_ERR_MSG")) 193 | except CoinInfoJobMaxNumError: 194 | self._SendMessage(self.translator.GetSentence("MAX_TASK_ERR_MSG")) 195 | except CoinInfoJobAlreadyExistentError: 196 | self._SendMessage( 197 | self.translator.GetSentence("TASK_EXISTENT_ERR_MSG", 198 | coin_id=coin_id, 199 | coin_vs=coin_vs) 200 | ) 201 | 202 | 203 | # 204 | # Price task stop command 205 | # 206 | class PriceTaskStopCmd(CommandBase): 207 | # Execute command 208 | @GroupChatOnly 209 | def _ExecuteCommand(self, 210 | **kwargs: Any) -> None: 211 | # Get parameters 212 | try: 213 | coin_id = self.cmd_data.Params().GetAsString(0) 214 | coin_vs = self.cmd_data.Params().GetAsString(1) 215 | except CommandParameterError: 216 | self._SendMessage(self.translator.GetSentence("PARAM_ERR_MSG")) 217 | else: 218 | try: 219 | kwargs["coin_info_scheduler"].Stop(self.cmd_data.Chat(), coin_id, coin_vs) 220 | self._SendMessage( 221 | self.translator.GetSentence("PRICE_TASK_STOP_OK_CMD", 222 | coin_id=coin_id, 223 | coin_vs=coin_vs) 224 | ) 225 | except CoinInfoJobNotExistentError: 226 | self._SendMessage( 227 | self.translator.GetSentence("TASK_NOT_EXISTENT_ERR_MSG", 228 | coin_id=coin_id, 229 | coin_vs=coin_vs) 230 | ) 231 | 232 | 233 | # 234 | # Price task stop all command 235 | # 236 | class PriceTaskStopAllCmd(CommandBase): 237 | # Execute command 238 | @GroupChatOnly 239 | def _ExecuteCommand(self, 240 | **kwargs: Any) -> None: 241 | kwargs["coin_info_scheduler"].StopAll(self.cmd_data.Chat()) 242 | self._SendMessage( 243 | self.translator.GetSentence("PRICE_TASK_STOP_ALL_CMD") 244 | ) 245 | 246 | 247 | # 248 | # Price task pause command 249 | # 250 | class PriceTaskPauseCmd(CommandBase): 251 | # Execute command 252 | @GroupChatOnly 253 | def _ExecuteCommand(self, 254 | **kwargs: Any) -> None: 255 | # Get parameters 256 | try: 257 | coin_id = self.cmd_data.Params().GetAsString(0) 258 | coin_vs = self.cmd_data.Params().GetAsString(1) 259 | except CommandParameterError: 260 | self._SendMessage(self.translator.GetSentence("PARAM_ERR_MSG")) 261 | else: 262 | try: 263 | kwargs["coin_info_scheduler"].Pause(self.cmd_data.Chat(), coin_id, coin_vs) 264 | self._SendMessage( 265 | self.translator.GetSentence("PRICE_TASK_PAUSE_OK_CMD", 266 | coin_id=coin_id, 267 | coin_vs=coin_vs) 268 | ) 269 | except CoinInfoJobNotExistentError: 270 | self._SendMessage( 271 | self.translator.GetSentence("TASK_NOT_EXISTENT_ERR_MSG", 272 | coin_id=coin_id, 273 | coin_vs=coin_vs) 274 | ) 275 | 276 | 277 | # 278 | # Price task resume command 279 | # 280 | class PriceTaskResumeCmd(CommandBase): 281 | # Execute command 282 | @GroupChatOnly 283 | def _ExecuteCommand(self, 284 | **kwargs: Any) -> None: 285 | # Get parameters 286 | try: 287 | coin_id = self.cmd_data.Params().GetAsString(0) 288 | coin_vs = self.cmd_data.Params().GetAsString(1) 289 | except CommandParameterError: 290 | self._SendMessage(self.translator.GetSentence("PARAM_ERR_MSG")) 291 | else: 292 | try: 293 | kwargs["coin_info_scheduler"].Resume(self.cmd_data.Chat(), coin_id, coin_vs) 294 | self._SendMessage( 295 | self.translator.GetSentence("PRICE_TASK_RESUME_OK_CMD", 296 | coin_id=coin_id, 297 | coin_vs=coin_vs) 298 | ) 299 | except CoinInfoJobNotExistentError: 300 | self._SendMessage( 301 | self.translator.GetSentence("TASK_NOT_EXISTENT_ERR_MSG", 302 | coin_id=coin_id, 303 | coin_vs=coin_vs) 304 | ) 305 | 306 | 307 | # 308 | # Price task send in same message command 309 | # 310 | class PriceTaskSendInSameMsgCmd(CommandBase): 311 | # Execute command 312 | @GroupChatOnly 313 | def _ExecuteCommand(self, 314 | **kwargs: Any) -> None: 315 | # Get parameters 316 | try: 317 | coin_id = self.cmd_data.Params().GetAsString(0) 318 | coin_vs = self.cmd_data.Params().GetAsString(1) 319 | flag = self.cmd_data.Params().GetAsBool(2) 320 | except CommandParameterError: 321 | self._SendMessage(self.translator.GetSentence("PARAM_ERR_MSG")) 322 | else: 323 | try: 324 | kwargs["coin_info_scheduler"].SendInSameMessage(self.cmd_data.Chat(), coin_id, coin_vs, flag) 325 | self._SendMessage( 326 | self.translator.GetSentence("PRICE_TASK_SEND_IN_SAME_MSG_OK_CMD", 327 | coin_id=coin_id, 328 | coin_vs=coin_vs, 329 | flag=flag) 330 | ) 331 | except CoinInfoJobNotExistentError: 332 | self._SendMessage( 333 | self.translator.GetSentence("TASK_NOT_EXISTENT_ERR_MSG", 334 | coin_id=coin_id, 335 | coin_vs=coin_vs) 336 | ) 337 | 338 | 339 | # 340 | # Price task delete last message command 341 | # 342 | class PriceTaskDeleteLastMsgCmd(CommandBase): 343 | # Execute command 344 | @GroupChatOnly 345 | def _ExecuteCommand(self, 346 | **kwargs: Any) -> None: 347 | # Get parameters 348 | try: 349 | coin_id = self.cmd_data.Params().GetAsString(0) 350 | coin_vs = self.cmd_data.Params().GetAsString(1) 351 | flag = self.cmd_data.Params().GetAsBool(2) 352 | except CommandParameterError: 353 | self._SendMessage(self.translator.GetSentence("PARAM_ERR_MSG")) 354 | else: 355 | try: 356 | kwargs["coin_info_scheduler"].DeleteLastSentMessage(self.cmd_data.Chat(), coin_id, coin_vs, flag) 357 | self._SendMessage( 358 | self.translator.GetSentence("PRICE_TASK_DELETE_LAST_MSG_OK_CMD", 359 | coin_id=coin_id, 360 | coin_vs=coin_vs, 361 | flag=flag) 362 | ) 363 | except CoinInfoJobNotExistentError: 364 | self._SendMessage( 365 | self.translator.GetSentence("TASK_NOT_EXISTENT_ERR_MSG", 366 | coin_id=coin_id, 367 | coin_vs=coin_vs) 368 | ) 369 | 370 | 371 | # 372 | # Price task info command 373 | # 374 | class PriceTaskInfoCmd(CommandBase): 375 | # Execute command 376 | @GroupChatOnly 377 | def _ExecuteCommand(self, 378 | **kwargs: Any) -> None: 379 | jobs_list = kwargs["coin_info_scheduler"].GetJobsInChat(self.cmd_data.Chat()) 380 | 381 | if jobs_list.Any(): 382 | self._SendMessage( 383 | self.translator.GetSentence("PRICE_TASK_INFO_CMD", 384 | tasks_num=jobs_list.Count(), 385 | tasks_list=str(jobs_list)) 386 | ) 387 | else: 388 | self._SendMessage(self.translator.GetSentence("PRICE_TASK_INFO_NO_TASK_CMD")) 389 | --------------------------------------------------------------------------------