Sources of Telegram bot for cryptopotato chat. https://t.me/devpotato_bot
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

132 lines
5.0 KiB

  1. import datetime
  2. import logging
  3. import os
  4. import sys
  5. from typing import Set, Dict, Optional
  6. import pytz
  7. import telegram
  8. from sqlalchemy import create_engine
  9. from sqlalchemy.orm import sessionmaker
  10. from telegram import Bot
  11. from telegram.ext import Updater, Defaults
  12. from dataclasses import dataclass, field
  13. # noinspection PyUnresolvedReferences
  14. from . import sqlite_fk # enable foreign key support for SQLite
  15. @dataclass
  16. class RunnerConfig:
  17. bot_token: str
  18. db_url: str
  19. daily_titles_job_enabled: bool = True
  20. developer_ids: Set[int] = field(default_factory=set)
  21. bot_timezone: pytz.BaseTzInfo = pytz.utc
  22. daily_titles_job_time: datetime.time = datetime.time()
  23. @staticmethod
  24. def from_env(env: Dict[str, str]) -> Optional['RunnerConfig']:
  25. logger = logging.getLogger(__name__)
  26. config = RunnerConfig(env.get('BOT_TOKEN'), env.get('DB_URL'))
  27. fail = False
  28. if tz_name := env.get('BOT_TIMEZONE'):
  29. try:
  30. config.bot_timezone = pytz.timezone(tz_name)
  31. except pytz.UnknownTimeZoneError:
  32. logger.error('BOT_TIMEZONE value "%s" is not a valid timezone name', tz_name)
  33. fail = True
  34. if enable_job_str := env.get('DAILY_TITLES_JOB_ENABLED'):
  35. try:
  36. config.daily_titles_job_enabled = bool(int(enable_job_str))
  37. except ValueError:
  38. logger.error('DAILY_TITLES_JOB_ENABLED value "%s" does not correspond to 0 or 1', enable_job_str)
  39. fail = True
  40. if time_str := env.get('DAILY_TITLES_JOB_TIME'):
  41. try:
  42. time_args = map(int, time_str.split(':'))
  43. config.daily_titles_job_time = datetime.time(*time_args)
  44. except ValueError as e:
  45. logger.error(
  46. 'DAILY_TITLES_JOB_TIME value "%s" does not represent a valid time: %s',
  47. time_str, e.args[0]
  48. )
  49. fail = True
  50. if developer_ids_str := env.get('DEVELOPER_IDS'):
  51. try:
  52. config.developer_ids = set(map(int, developer_ids_str.split(',')))
  53. except ValueError:
  54. logger.error(
  55. 'DEVELOPER_IDS value "%s" is not a valid comma-separated list of integers',
  56. developer_ids_str
  57. )
  58. fail = True
  59. return None if fail else config
  60. class Runner:
  61. DAILY_TITLES_JOB_NAME = 'assign titles'
  62. def __init__(self, config: RunnerConfig):
  63. self.logger = logging.getLogger(__name__)
  64. bot_defaults = Defaults(disable_web_page_preview=True, tzinfo=config.bot_timezone)
  65. self.updater = Updater(token=config.bot_token, defaults=bot_defaults)
  66. self.engine = create_engine(config.db_url)
  67. self.session_factory = sessionmaker(bind=self.engine)
  68. self.developer_ids = config.developer_ids
  69. self._command_list = dict()
  70. from . import commands
  71. commands.register_handlers(self)
  72. bot: Bot = self.updater.bot
  73. bot.set_my_commands(self._command_list.items())
  74. dispatcher = self.updater.dispatcher
  75. from devpotato_bot.error_handler import create_callback
  76. dispatcher.add_error_handler(create_callback(self.developer_ids))
  77. logger = logging.getLogger(__name__)
  78. if config.daily_titles_job_enabled:
  79. job_time = config.daily_titles_job_time
  80. logger.info('Scheduling daily titles assignment job @ %s', job_time)
  81. from .commands.daily_titles.daily_job import job_callback
  82. job_queue = self.updater.job_queue
  83. job_queue.run_daily(job_callback, job_time,
  84. context=self.session_factory,
  85. name=Runner.DAILY_TITLES_JOB_NAME)
  86. else:
  87. logger.info('Daily titles assignment job was disabled')
  88. def add_command_description(self, command, description):
  89. """Add command to the list of commands to be shown to Telegram clients"""
  90. self._command_list[command] = description
  91. def run(self):
  92. self.updater.start_polling()
  93. # Run the bot until you press Ctrl-C or the process receives SIGINT,
  94. # SIGTERM or SIGABRT. This should be used most of the time, since
  95. # start_polling() is non-blocking and will stop the bot gracefully.
  96. self.updater.idle()
  97. def main():
  98. """Start the bot."""
  99. logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
  100. level=logging.INFO)
  101. logger = logging.getLogger(__name__)
  102. logger.info('Cryptopotato bot started')
  103. config = RunnerConfig.from_env(os.environ)
  104. if config is None:
  105. logger.error('Failed to create runner config')
  106. sys.exit(-1)
  107. try:
  108. Runner(config).run()
  109. except telegram.error.InvalidToken as e:
  110. logger.error('Bot token "%s" is not valid', config.bot_token, exc_info=e)
  111. except Exception as e:
  112. logger.error('Unhandled exception', exc_info=e)
  113. else:
  114. return
  115. sys.exit(-1)