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
132 lines
5.0 KiB
import datetime
|
|
import logging
|
|
import os
|
|
import sys
|
|
from typing import Set, Dict, Optional
|
|
|
|
import pytz
|
|
import telegram
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
from telegram import Bot
|
|
from telegram.ext import Updater, Defaults
|
|
from dataclasses import dataclass, field
|
|
|
|
# noinspection PyUnresolvedReferences
|
|
from . import sqlite_fk # enable foreign key support for SQLite
|
|
|
|
|
|
@dataclass
|
|
class RunnerConfig:
|
|
bot_token: str
|
|
db_url: str
|
|
daily_titles_job_enabled: bool = True
|
|
developer_ids: Set[int] = field(default_factory=set)
|
|
bot_timezone: pytz.BaseTzInfo = pytz.utc
|
|
daily_titles_job_time: datetime.time = datetime.time()
|
|
|
|
@staticmethod
|
|
def from_env(env: Dict[str, str]) -> Optional['RunnerConfig']:
|
|
logger = logging.getLogger(__name__)
|
|
config = RunnerConfig(env.get('BOT_TOKEN'), env.get('DB_URL'))
|
|
fail = False
|
|
|
|
if tz_name := env.get('BOT_TIMEZONE'):
|
|
try:
|
|
config.bot_timezone = pytz.timezone(tz_name)
|
|
except pytz.UnknownTimeZoneError:
|
|
logger.error('BOT_TIMEZONE value "%s" is not a valid timezone name', tz_name)
|
|
fail = True
|
|
if enable_job_str := env.get('DAILY_TITLES_JOB_ENABLED'):
|
|
try:
|
|
config.daily_titles_job_enabled = bool(int(enable_job_str))
|
|
except ValueError:
|
|
logger.error('DAILY_TITLES_JOB_ENABLED value "%s" does not correspond to 0 or 1', enable_job_str)
|
|
fail = True
|
|
if time_str := env.get('DAILY_TITLES_JOB_TIME'):
|
|
try:
|
|
time_args = map(int, time_str.split(':'))
|
|
config.daily_titles_job_time = datetime.time(*time_args)
|
|
except ValueError as e:
|
|
logger.error(
|
|
'DAILY_TITLES_JOB_TIME value "%s" does not represent a valid time: %s',
|
|
time_str, e.args[0]
|
|
)
|
|
fail = True
|
|
if developer_ids_str := env.get('DEVELOPER_IDS'):
|
|
try:
|
|
config.developer_ids = set(map(int, developer_ids_str.split(',')))
|
|
except ValueError:
|
|
logger.error(
|
|
'DEVELOPER_IDS value "%s" is not a valid comma-separated list of integers',
|
|
developer_ids_str
|
|
)
|
|
fail = True
|
|
return None if fail else config
|
|
|
|
|
|
class Runner:
|
|
DAILY_TITLES_JOB_NAME = 'assign titles'
|
|
|
|
def __init__(self, config: RunnerConfig):
|
|
self.logger = logging.getLogger(__name__)
|
|
bot_defaults = Defaults(disable_web_page_preview=True, tzinfo=config.bot_timezone)
|
|
self.updater = Updater(token=config.bot_token, defaults=bot_defaults)
|
|
self.engine = create_engine(config.db_url)
|
|
self.session_factory = sessionmaker(bind=self.engine)
|
|
self.developer_ids = config.developer_ids
|
|
|
|
self._command_list = dict()
|
|
from . import commands
|
|
commands.register_handlers(self)
|
|
bot: Bot = self.updater.bot
|
|
bot.set_my_commands(self._command_list.items())
|
|
|
|
dispatcher = self.updater.dispatcher
|
|
from devpotato_bot.error_handler import create_callback
|
|
dispatcher.add_error_handler(create_callback(self.developer_ids))
|
|
|
|
logger = logging.getLogger(__name__)
|
|
if config.daily_titles_job_enabled:
|
|
job_time = config.daily_titles_job_time
|
|
logger.info('Scheduling daily titles assignment job @ %s', job_time)
|
|
from .commands.daily_titles.daily_job import job_callback
|
|
job_queue = self.updater.job_queue
|
|
job_queue.run_daily(job_callback, job_time,
|
|
context=self.session_factory,
|
|
name=Runner.DAILY_TITLES_JOB_NAME)
|
|
else:
|
|
logger.info('Daily titles assignment job was disabled')
|
|
|
|
def add_command_description(self, command, description):
|
|
"""Add command to the list of commands to be shown to Telegram clients"""
|
|
self._command_list[command] = description
|
|
|
|
def run(self):
|
|
self.updater.start_polling()
|
|
|
|
# Run the bot until you press Ctrl-C or the process receives SIGINT,
|
|
# SIGTERM or SIGABRT. This should be used most of the time, since
|
|
# start_polling() is non-blocking and will stop the bot gracefully.
|
|
self.updater.idle()
|
|
|
|
|
|
def main():
|
|
"""Start the bot."""
|
|
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
logger.info('Cryptopotato bot started')
|
|
config = RunnerConfig.from_env(os.environ)
|
|
if config is None:
|
|
logger.error('Failed to create runner config')
|
|
sys.exit(-1)
|
|
try:
|
|
Runner(config).run()
|
|
except telegram.error.InvalidToken as e:
|
|
logger.error('Bot token "%s" is not valid', config.bot_token, exc_info=e)
|
|
except Exception as e:
|
|
logger.error('Unhandled exception', exc_info=e)
|
|
else:
|
|
return
|
|
sys.exit(-1)
|