Browse Source

Update dependencies, add more configuration env vars

- Migrate to python-telegram-bot 13.0
- Replace dateutil with pytz because the latter is the transitive
dependency now
- Improve README
- Remove hardcoded daily title assignment job start time
archive/feature/daily-titles-log
Vladislav Glinsky 5 years ago
parent
commit
abe8bd6f1e
Signed by: cl0ne GPG Key ID: 9D058DD29491782E
  1. 32
      README.md
  2. 3
      devpotato_bot/commands/__init__.py
  3. 3
      devpotato_bot/commands/fortune.py
  4. 3
      devpotato_bot/commands/help_/__init__.py
  5. 2
      devpotato_bot/commands/me.py
  6. 5
      devpotato_bot/commands/roll.py
  7. 98
      devpotato_bot/runner.py
  8. 5
      docker-compose.yaml
  9. 8
      requirements.txt
  10. 2
      setup.py

32
README.md

@ -17,21 +17,35 @@ Use `init_db.py` script to create new database, it requires `DB_URL` to be set (
To apply schema changes to existing database, [use Alembic](https://alembic.sqlalchemy.org/en/latest/tutorial.html#running-our-first-migration). E.g., to get latest version of the schema use command `alembic upgrade head`. Details on specifying database URL can be found [here](alembic/README.md).
### Running bot
You're required to provide following environment variables:
You're required to provide the following environment variables:
- `BOT_TOKEN`: bot's authorization token, you can get one for your bot from [BotFather](https://telegram.me/botfather);
- `DB_URL`: [SQLAlchemy database URL](https://docs.sqlalchemy.org/en/13/core/engines.html#engine-configuration).
If you want bot send error reports directly to your private messages, set `DEVELOPER_IDS` environment variable to a comma-separated list of corresponding user ids. You can get yours, for example, from [userinfobot](https://t.me/userinfobot) (it supports retrieving ids from forwarded messages too, but it works I suppose only when message's author has enabled linking back to their account in forwarded messages privacy settings). [This question on SO](https://stackoverflow.com/questions/32683992/find-out-my-own-user-id-for-sending-a-message-with-telegram-api) has more options.
Below you can find a list of optional environment variables recognized by the bot.
Daily titles assignment job is enabled by default; this can be changed by setting `TITLES_DAILY_JOB_ENABLED` environment variable to 0 or 1 (disable and enable, respectively).
- `DEVELOPER_IDS`
N.B. To receive error reports from bot you have to initiate a conversation with the bot first. For example, by issuing `/start` command to it in direct messages or unblocking the bot if you blocked it before.
If you want bot to send error reports directly to your private messages, set `DEVELOPER_IDS` to a comma-separated list of recipient user ids. You can get yours, for example, from [userinfobot](https://t.me/userinfobot) (it supports retrieving ids from forwarded messages too, but it works I suppose only when message's author has enabled linking back to their account in forwarded messages privacy settings). [This question on SO](https://stackoverflow.com/questions/32683992/find-out-my-own-user-id-for-sending-a-message-with-telegram-api) has more options.
To start bot you have to run specify package name with [`-m` option](https://docs.python.org/3/using/cmdline.html#cmdoption-m) to Python interpreter:
N.B. In order to receive error reports from bot you have to initiate a conversation with the bot first. For example, by issuing `/start` command to it in direct messages or by unblocking the bot if you have blocked it before.
- `DAILY_TITLES_JOB_ENABLED`
Daily titles assignment job is enabled by default, to explicitly disable or enable it set `DAILY_TITLES_JOB_ENABLED` to 0 or 1, respectively.
- `DAILY_TITLES_JOB_TIME`
By default, daily titles assignment job is scheduled to run at midnight (timezone depends on `BOT_TIMEZONE` variable), set `DAILY_TITLES_JOB_TIME` to a string in the format `HH[:MM]` to specify a different time. For example, specifying either `09:00` or `9` changes the job's start time to 9 AM.
- `BOT_TIMEZONE`
Bot's local timezone is UTC by default; to choose a different one, set `BOT_TIMEZONE` to one of the timezone names supported by [`pytz`](https://pypi.org/project/pytz/). For example, `Europe/Kiev`, `US/Pacific`, `Asia/Shanghai`, etc.
To start bot you have to pass its package name with [`-m` option](https://docs.python.org/3/using/cmdline.html#cmdoption-m) to Python interpreter:
```
python -m devpotato_bot
```
Make sure package `devpotato_bot` is present in the `sys.path` (e.g., by adding its parent directory to `PYTHONPATH` environment variable or by setting working directory to it).
Make sure package `devpotato_bot` is present in the `sys.path` (e.g., by adding its parent directory to `PYTHONPATH` environment variable or by changing working directory to it).
## Current Features
@ -53,7 +67,7 @@ Dice can be rolled with `/roll` (or `/r` for short) command followed by formula
The basic formula in the dice notation is `AdB`. `A` is a number of dice to be rolled (can be omitted if 1). `B` specifies the number of sides the die has, you can use `%` for percentile dice (i.e. `d100`). The maximum number of rolls is 100, the biggest allowed dice has 120 sides.
The basic formula can be extended with modifiers:
Modifier to keep/discard the lowest/highest `k` results. Keep and discard is indicated by modifier's sign: `+` and `-`, omitted sign is equivalent to `+` (keep). It's followed by letter `L` or `H` and a positive number to specify which results to be kept/discarded and how many. For example, `10d6-L6` discards 6 lowest results, `10d6+L4` and `10d6L4` keep 4 lowest, `10d6+H5` and `10d6H5` keep 5 highest, `10d6-H3` discards 3 highest.
Additive modifier, a number with a sign that is added to (or subtracted from) total roll result. For example, `d6+5` adds 5 to a single roll result and `5d20L3-2` will subtract 2 from the sum of the 3 lowest results.
@ -73,7 +87,9 @@ The inevitable titles are always assigned first and are assigned every day in th
The activity has to be activated first for the chat by chat administrator with `/daily_titles_start` command (and can be deactivated any time with `/daily_titles_stop`). To participate in the activity, chat member can use `/daily_titles_join` command, command `/daily_titles_leave` allows to cease participation (there are also join/leave buttons under messages with assigned titles). Any user who leaves the chat will be automatically removed from participation.
Every day at 9AM (Kyiv time) bot will post message with assigned titles in chats where activity is active. You can get today's assigned titles with `/daily_titles` command. It will make attempt to assign titles if there were no titles assigned today (activity was enabled after 9AM or there were no participants at that time).
Every day at a specified time bot will post messages with assigned titles in chats where the activity is active. You can tune this behavior with environment variables described in the [Running bot](#running-bot) section.
Today's assigned titles can be shown on demand with `/daily_titles` command. Which will also make attempt to assign titles if there were no titles assigned today in the current chat. This occurs when the automatic title assignment was disabled, the activity was enabled after the job was running or there were no participants at that time.
Of course, some titles appropriate for members of one chat can be inappropriate for another (they can be seen as not funny or even offensive). That means that there has to be some sort of customization of available titles for every chat. There also has to be a global list of title templates so that every chat can have "default" set of titles.

3
devpotato_bot/commands/__init__.py

@ -7,7 +7,8 @@ def register_handlers(runner):
dispatcher.add_handler(CommandHandler(
'fortune', fortune.command_callback,
filters=~Filters.update.edited_message
filters=~Filters.update.edited_message,
run_async=True
))
runner.add_command_description('fortune', fortune.COMMAND_DESCRIPTION)

3
devpotato_bot/commands/fortune.py

@ -2,7 +2,7 @@ import logging
import subprocess
from telegram import Update, ChatAction, Bot, ParseMode, Chat
from telegram.ext import CallbackContext, run_async
from telegram.ext import CallbackContext
from ..helpers import deletes_caller_message
@ -12,7 +12,6 @@ _logger = logging.getLogger(__name__)
COMMAND_DESCRIPTION = 'Get yourself a random epigram'
@run_async
@deletes_caller_message
def command_callback(update: Update, context: CallbackContext):
"""Get random epigram from `fortune`."""

3
devpotato_bot/commands/help_/__init__.py

@ -65,8 +65,7 @@ class HelpPages:
]
return dict(text=current_page.page_contents,
reply_markup=InlineKeyboardMarkup(buttons),
parse_mode=ParseMode.MARKDOWN_V2,
disable_web_page_preview=True)
parse_mode=ParseMode.MARKDOWN_V2)
def _get_page_button(self, p: Page, disabled_page: Page):
if p == disabled_page:

2
devpotato_bot/commands/me.py

@ -15,4 +15,4 @@ def command_callback(update: Update, context: CallbackContext):
name = '<b>***{}</b>'.format(update.effective_user.mention_html())
text = '{} {}'.format(name, status)
chat: Chat = update.effective_chat
context.bot.send_message(chat.id, text, disable_web_page_preview=True, parse_mode=ParseMode.HTML)
context.bot.send_message(chat.id, text, parse_mode=ParseMode.HTML)

5
devpotato_bot/commands/roll.py

@ -39,8 +39,7 @@ def command_callback(update: Update, context: CallbackContext):
message.reply_markdown_v2(
"Oops, couldn't decide what kind of roll you want to make\\.\n"
"\n"
"See /help for the detailed description of the supported notation",
disable_web_page_preview=True
"See /help for the detailed description of the supported notation"
)
return
except ValueRangeError as e:
@ -72,4 +71,4 @@ def command_callback(update: Update, context: CallbackContext):
lines.append(' \\+ ⋯ ')
lines.extend(('\\) \\= ', str(roll_total)))
text = ''.join(lines)
message.reply_markdown_v2(text, quote=False, disable_web_page_preview=True)
message.reply_markdown_v2(text, quote=False)

98
devpotato_bot/runner.py

@ -1,41 +1,94 @@
import datetime
import logging
import os
import sys
from typing import Set, Dict, Optional
import pytz
import telegram
from dateutil import tz
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from telegram import Bot
from telegram.ext import Updater
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_POST_TIME = datetime.time(hour=9, tzinfo=tz.gettz('Europe/Kiev'))
DAILY_TITLES_JOB_NAME = 'assign titles'
def __init__(self, token: str, db_url: str, titles_daily_job_enabled: bool = True, developer_ids=None):
def __init__(self, config: RunnerConfig):
self.logger = logging.getLogger(__name__)
self.updater = Updater(token=token, use_context=True)
self.engine = create_engine(db_url)
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 = set() if developer_ids is None else developer_ids
self.developer_ids = config.developer_ids
self._command_list = dict()
from . import commands
commands.register_handlers(self)
self.updater.bot.set_my_commands(self._command_list.items())
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 titles_daily_job_enabled:
job_time = Runner.DAILY_TITLES_POST_TIME
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
@ -64,23 +117,16 @@ def main():
level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info('Cryptopotato bot started')
bot_token = os.getenv('BOT_TOKEN')
db_url = os.getenv('DB_URL')
developer_ids_str = os.getenv('DEVELOPER_IDS')
titles_daily_job_enabled = int(os.getenv('TITLES_DAILY_JOB_ENABLED', 1))
developer_ids = None
if developer_ids_str:
try:
developer_ids = set(map(int, developer_ids_str.split(',')))
except ValueError:
logger.error(
'DEVELOPER_IDS value must be a comma-separated integers list, not "%s"!',
developer_ids_str
)
config = RunnerConfig.from_env(os.environ)
if config is None:
logger.error('Failed to create runner config')
sys.exit(-1)
try:
Runner(bot_token, db_url, titles_daily_job_enabled, developer_ids).run()
Runner(config).run()
except telegram.error.InvalidToken as e:
logger.error('Bot token "%s" is not valid', bot_token, exc_info=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)

5
docker-compose.yaml

@ -30,10 +30,13 @@ services:
image: "sevoid/cryptopotato-bot:latest"
restart: always
environment:
- WAIT_BEFORE_START=5
- DB_URL=sqlite:////data/db.sqlite
- BOT_TOKEN=
- DEVELOPER_IDS=
- WAIT_BEFORE_START=5
- DAILY_TITLES_JOB_ENABLED=
- DAILY_TITLES_JOB_TIME=
- BOT_TIMEZONE=
volumes:
- data:/data
networks:

8
requirements.txt

@ -1,6 +1,6 @@
python-telegram-bot==12.8
ujson==3.2.0
python-telegram-bot==13.0
ujson==4.0.1
pytz>=2020.4
cachetools==4.1.1
python-dateutil==2.8.1
SQLAlchemy==1.3.19
SQLAlchemy==1.3.20
alembic==1.4.3

2
setup.py

@ -11,7 +11,7 @@ setup(
'python-telegram-bot>=12.6',
'ujson>=3.0.0',
'cachetools>=4',
'python-dateutil>=2.8',
'pytz>=2020.4',
'SQLAlchemy>=1.3',
'alembic>=1.4'
],

Loading…
Cancel
Save