├── 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 | | [](https://pypi.org/project/telegram_crypto_price_bot/) [](https://pypi.org/project/telegram_crypto_price_bot/) [](https://github.com/ebellocchia/telegram_crypto_price_bot?tab=MIT-1-ov-file) |
6 | | [](https://github.com/ebellocchia/telegram_crypto_price_bot/actions/workflows/build.yml) [](https://github.com/ebellocchia/telegram_crypto_price_bot/actions/workflows/code-analysis.yml) |
7 | | [](https://app.codacy.com/gh/ebellocchia/telegram_crypto_price_bot/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [](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 |
--------------------------------------------------------------------------------