Compare commits

..

10 Commits

Author SHA1 Message Date
4a79535321
Compliance with bot API 8.02
Also, closed sessions are now removed from Bot.sessions
2025-02-01 16:46:33 +01:00
425902d2ac
Compliance with bot API 7.10 2024-09-06 16:01:50 +02:00
6759e5c704
Administrators may be set after administration module initialization
Do not check for administrators until bot has got information about itself. Prevent duplicate calls to bot.get_me() async method
2024-09-01 18:27:28 +02:00
db240ce199
Compliance with bot API 7.9
Implemented a system to get administration privileges if running the bot in absence of administrators (the token is provided through the command line).
2024-08-17 21:12:00 +02:00
02386d69da
Get package version from metadata when package has no __version__ attribute 2024-08-04 18:49:41 +02:00
cc4cbbacbd
Compliance with bot API 7.8 2024-08-04 18:29:21 +02:00
fd4374f328
Compliance with bot API 7.7 2024-07-07 14:36:58 +02:00
6b489363de
Compliance with bot API 7.2
- Included python version in `/version` command
- Added the parameter `business_connection_id` to the methods sendMessage, sendPhoto, sendVideo, sendAnimation, sendAudio, sendDocument, sendSticker, sendVideoNote, sendVoice, sendLocation, sendVenue, sendContact, sendPoll, sendDice, sendGame, and sendMediaGroup, sendChatAction.
- Moved the parameter `format` to Sticker instead of StickerSet (and related methods)
2024-04-13 11:15:42 +02:00
a343e095e8
Compliance with bot API 7.1
- Added the class ReplyParameters and replaced parameters reply_to_message_id and allow_sending_without_reply in the methods copyMessage, sendMessage, sendPhoto, sendVideo, sendAnimation, sendAudio, sendDocument, sendSticker, sendVideoNote, sendVoice, sendLocation, sendVenue, sendContact, sendPoll, sendDice, sendInvoice, sendGame, and sendMediaGroup with the field reply_parameters of type ReplyParameters.
- Added the class LinkPreviewOptions and replaced the parameter disable_web_page_preview with link_preview_options in the methods sendMessage and editMessageText.
2024-03-10 20:22:05 +01:00
a3b28bc1d6
Allow to overwrite file when using bot.download_file method 2024-03-05 18:50:03 +01:00
6 changed files with 1091 additions and 222 deletions

1
.gitignore vendored
View File

@ -41,6 +41,7 @@ __pycache__/
# Distribution / packaging # Distribution / packaging
.Python .Python
env/ env/
.venv/
build/ build/
develop-eggs/ develop-eggs/
dist/ dist/

View File

@ -11,7 +11,7 @@ __author__ = "Davide Testa"
__email__ = "davide@davte.it" __email__ = "davide@davte.it"
__credits__ = ["Marco Origlia", "Nick Lee @Nickoala"] __credits__ = ["Marco Origlia", "Nick Lee @Nickoala"]
__license__ = "GNU General Public License v3.0" __license__ = "GNU General Public License v3.0"
__version__ = "2.9.11" __version__ = "2.10.9"
__maintainer__ = "Davide Testa" __maintainer__ = "Davide Testa"
__contact__ = "t.me/davte" __contact__ = "t.me/davte"

View File

@ -13,10 +13,12 @@ import asyncio
import datetime import datetime
import json import json
import logging import logging
import platform
import re import re
import types import types
from collections import OrderedDict from collections import OrderedDict
from importlib.metadata import version as get_package_version_from_metadata
from typing import Union, List, Tuple from typing import Union, List, Tuple
# Third party modules # Third party modules
@ -27,8 +29,8 @@ from davtelepot.messages import default_admin_messages, default_talk_messages
from davtelepot.bot import Bot from davtelepot.bot import Bot
from davtelepot.utilities import ( from davtelepot.utilities import (
async_wrapper, CachedPage, Confirmator, extract, get_cleaned_text, async_wrapper, CachedPage, Confirmator, extract, get_cleaned_text,
get_user, clean_html_string, line_drawing_unordered_list, make_button, get_secure_key, get_user, clean_html_string, line_drawing_unordered_list,
make_inline_keyboard, remove_html_tags, send_part_of_text_file, make_button, make_inline_keyboard, remove_html_tags, send_part_of_text_file,
send_csv_file, make_lines_of_buttons, join_path send_csv_file, make_lines_of_buttons, join_path
) )
@ -43,6 +45,13 @@ variable_regex = re.compile(r"(?P<name>[a-zA-Z]\w*)\s*=\s*"
r"\"[^\"]*\")") r"\"[^\"]*\")")
def get_package_version(package: types.ModuleType):
"""Get version of given package."""
if hasattr(package, '__version__'):
return package.__version__
return get_package_version_from_metadata(package.__name__)
async def _forward_to(update, async def _forward_to(update,
bot: Bot, bot: Bot,
sender, sender,
@ -223,9 +232,9 @@ def get_talk_panel(bot: Bot,
return text, reply_markup return text, reply_markup
async def _talk_command(bot: Bot, async def talk_command(bot: Bot,
update, update,
user_record): user_record):
text = get_cleaned_text( text = get_cleaned_text(
update, update,
bot, bot,
@ -343,17 +352,17 @@ async def end_session(bot: Bot,
return return
async def _talk_button(bot: Bot, async def talk_button(bot: Bot,
update, update,
user_record, user_record,
data): data):
telegram_id = user_record['telegram_id'] telegram_id = user_record['telegram_id']
command, *arguments = data command, *arguments = data
result, text, reply_markup = '', '', None result, text, reply_markup = '', '', None
if command == 'search': if command == 'search':
bot.set_individual_text_message_handler( bot.set_individual_text_message_handler(
await async_wrapper( await async_wrapper(
_talk_command, talk_command,
), ),
update update
) )
@ -426,7 +435,7 @@ async def _talk_button(bot: Bot,
return result return result
async def _restart_command(bot: Bot, async def restart_command(bot: Bot,
update, update,
user_record): user_record):
with bot.db as db: with bot.db as db:
@ -453,7 +462,7 @@ async def _restart_command(bot: Bot,
return return
async def _stop_command(bot: Bot, async def stop_command(bot: Bot,
update, update,
user_record): user_record):
text = bot.get_message( text = bot.get_message(
@ -495,10 +504,10 @@ async def stop_bots(bot: Bot):
return return
async def _stop_button(bot: Bot, async def stop_button(bot: Bot,
update, update,
user_record, user_record,
data: List[Union[int, str]]): data: List[Union[int, str]]):
result, text, reply_markup = '', '', None result, text, reply_markup = '', '', None
telegram_id = user_record['telegram_id'] telegram_id = user_record['telegram_id']
command = data[0] if len(data) > 0 else 'None' command = data[0] if len(data) > 0 else 'None'
@ -535,7 +544,7 @@ async def _stop_button(bot: Bot,
return result return result
async def _send_bot_database(bot: Bot, user_record: OrderedDict, language: str): async def send_bot_database(bot: Bot, user_record: OrderedDict, language: str):
if not all( if not all(
[ [
bot.db_url.endswith('.db'), bot.db_url.endswith('.db'),
@ -562,7 +571,7 @@ async def _send_bot_database(bot: Bot, user_record: OrderedDict, language: str):
) )
async def _query_command(bot, update, user_record): async def query_command(bot, update, user_record):
query = get_cleaned_text( query = get_cleaned_text(
update, update,
bot, bot,
@ -640,7 +649,7 @@ async def _query_command(bot, update, user_record):
) )
async def _query_button(bot, update, user_record, data): async def query_button(bot, update, user_record, data):
result, text, reply_markup = '', '', None result, text, reply_markup = '', '', None
command = data[0] if len(data) else 'default' command = data[0] if len(data) else 'default'
error_message = bot.get_message( error_message = bot.get_message(
@ -677,7 +686,7 @@ async def _query_button(bot, update, user_record, data):
return result return result
async def _log_command(bot, update, user_record): async def log_command(bot, update, user_record):
if bot.log_file_path is None: if bot.log_file_path is None:
return bot.get_message( return bot.get_message(
'admin', 'log_command', 'no_log', 'admin', 'log_command', 'no_log',
@ -730,7 +739,7 @@ async def _log_command(bot, update, user_record):
return return
async def _errors_command(bot, update, user_record): async def errors_command(bot, update, user_record):
# Always send errors log file in private chat # Always send errors log file in private chat
chat_id = update['from']['id'] chat_id = update['from']['id']
if bot.errors_file_path is None: if bot.errors_file_path is None:
@ -774,7 +783,7 @@ async def _errors_command(bot, update, user_record):
return return
async def _maintenance_command(bot, update, user_record): async def maintenance_command(bot, update, user_record):
maintenance_message = get_cleaned_text(update, bot, ['maintenance']) maintenance_message = get_cleaned_text(update, bot, ['maintenance'])
if maintenance_message.startswith('{'): if maintenance_message.startswith('{'):
maintenance_message = json.loads(maintenance_message) maintenance_message = json.loads(maintenance_message)
@ -864,7 +873,12 @@ async def get_new_versions(bot: Bot,
"skipping...") "skipping...")
continue continue
new_version = web_page['info']['version'] new_version = web_page['info']['version']
current_version = package.__version__ try:
current_version = get_package_version(package)
except TypeError:
current_version = "NA"
logging.error("Could not get current version of "
"package %s", package.__name__)
notification_record = bot.db['updates_notifications'].find_one( notification_record = bot.db['updates_notifications'].find_one(
package=package.__name__, package=package.__name__,
order_by=['-id'], order_by=['-id'],
@ -883,17 +897,18 @@ async def get_new_versions(bot: Bot,
return news return news
async def _version_command(bot: Bot, update: dict, async def version_command(bot: Bot, update: dict,
user_record: OrderedDict, language: str): user_record: OrderedDict, language: str):
last_commit = await get_last_commit() last_commit = await get_last_commit()
text = bot.get_message( text = bot.get_message(
'admin', 'version_command', 'header', 'admin', 'version_command', 'header',
last_commit=last_commit, last_commit=last_commit,
update=update, user_record=user_record update=update, user_record=user_record
) + '\n\n' ) + '\n\n'
text += f'<b>Python: </b> <code>{platform.python_version()}</code>\n'
text += '\n'.join( text += '\n'.join(
f"<b>{package.__name__}</b>: " f"<b>{package.__name__}</b>: "
f"<code>{package.__version__}</code>" f"<code>{get_package_version(package)}</code>"
for package in bot.packages for package in bot.packages
) )
temporary_message = await bot.send_message( temporary_message = await bot.send_message(
@ -937,7 +952,7 @@ async def notify_new_version(bot: Bot):
order_by=['-id'] order_by=['-id']
) )
current_versions = { current_versions = {
f"{package.__name__}_version": package.__version__ f"{package.__name__}_version": get_package_version(package)
for package in bot.packages for package in bot.packages
} }
current_versions['last_commit'] = last_commit current_versions['last_commit'] = last_commit
@ -1032,7 +1047,7 @@ async def get_package_updates(bot: Bot,
await asyncio.sleep(monitoring_interval) await asyncio.sleep(monitoring_interval)
async def _send_start_messages(bot: Bot): async def send_start_messages(bot: Bot):
"""Send restart messages at restart.""" """Send restart messages at restart."""
for restart_message in bot.db['restart_messages'].find(sent=None): for restart_message in bot.db['restart_messages'].find(sent=None):
asyncio.ensure_future( asyncio.ensure_future(
@ -1060,7 +1075,7 @@ async def _send_start_messages(bot: Bot):
return return
async def _load_talking_sessions(bot: Bot): async def load_talking_sessions(bot: Bot):
sessions = [] sessions = []
for session in bot.db.query( for session in bot.db.query(
"""SELECT * """SELECT *
@ -1139,7 +1154,7 @@ def get_custom_commands(bot: Bot, language: str = None) -> List[dict]:
) )
async def _father_command(bot, language): async def father_command(bot, language):
modes = [ modes = [
{ {
key: ( key: (
@ -1443,12 +1458,12 @@ async def edit_bot_father_settings_via_message(bot: Bot,
return result, text, reply_markup return result, text, reply_markup
async def _father_button(bot: Bot, user_record: OrderedDict, async def father_button(bot: Bot, user_record: OrderedDict,
language: str, data: list): language: str, data: list):
"""Handle BotFather button. """Handle BotFather button.
Operational modes Operational modes
- main: back to main page (see _father_command) - main: back to main page (see `father_command`)
- get: show commands stored by @BotFather - get: show commands stored by @BotFather
- set: edit commands stored by @BotFather - set: edit commands stored by @BotFather
""" """
@ -1542,7 +1557,7 @@ async def _father_button(bot: Bot, user_record: OrderedDict,
elif command == 'main': elif command == 'main':
return dict( return dict(
text='', text='',
edit=(await _father_command(bot=bot, language=language)) edit=(await father_command(bot=bot, language=language))
) )
elif command == 'set': elif command == 'set':
stored_commands = await bot.getMyCommands() stored_commands = await bot.getMyCommands()
@ -1810,8 +1825,8 @@ async def _father_button(bot: Bot, user_record: OrderedDict,
return result return result
async def _config_command(bot: Bot, update: dict, async def config_command(bot: Bot, update: dict,
user_record: dict, language: str): user_record: dict, language: str):
text = get_cleaned_text( text = get_cleaned_text(
update, update,
bot, bot,
@ -1844,6 +1859,52 @@ async def _config_command(bot: Bot, update: dict,
language=language) language=language)
async def become_administrator(bot: Bot, update: dict,
user_record: dict, language: str):
"""When the bot has no administrator, become one providing a token.
The token will be printed to the stdout on the machine running the bot.
"""
if len(bot.administrators) > 0:
return
def _get_message(*args):
return bot.get_message('admin', 'become_admin', *args,
update=update, user_record=user_record,
language=language)
token = get_cleaned_text(update=update, bot=bot,
replace=['become_administrator',
'00become_administrator'],
strip='/ @_')
if token != bot.administration_token:
return _get_message('wrong_token')
with bot.db as db:
db['users'].update({**user_record, 'privileges': 1},
['id'])
return _get_message('success')
async def create_promotion_command(bot: Bot):
"""If bot has no administrators, users can elevate themselves.
To do so, they need to provide a token, that will be printed to the stdout
of the machine running the bot.
"""
await bot.get_me()
if len(bot.administrators) > 0:
return
bot.administration_token = get_secure_key(length=10)
print(f"To become administrator click "
f"https://t.me/{bot.name}?start="
f"00become_administrator_{bot.administration_token}")
@bot.command(command='become_administrator',
authorization_level='everybody',
aliases=['00become_administrator'])
async def _become_administrator(bot, update, user_record, language):
return await become_administrator(bot=bot, update=update,
user_record=user_record,
language=language)
def init(telegram_bot: Bot, def init(telegram_bot: Bot,
talk_messages: dict = None, talk_messages: dict = None,
admin_messages: dict = None, admin_messages: dict = None,
@ -1907,16 +1968,16 @@ def init(telegram_bot: Bot,
# Tasks to complete before starting bot # Tasks to complete before starting bot
@telegram_bot.additional_task(when='BEFORE') @telegram_bot.additional_task(when='BEFORE')
async def load_talking_sessions(): async def _load_talking_sessions():
return await _load_talking_sessions(bot=telegram_bot) return await load_talking_sessions(bot=telegram_bot)
@telegram_bot.additional_task(when='BEFORE', bot=telegram_bot) @telegram_bot.additional_task(when='BEFORE', bot=telegram_bot)
async def notify_version(bot): async def notify_version(bot):
return await notify_new_version(bot=bot) return await notify_new_version(bot=bot)
@telegram_bot.additional_task('BEFORE') @telegram_bot.additional_task('BEFORE')
async def send_restart_messages(): async def _send_start_messages():
return await _send_start_messages(bot=telegram_bot) return await send_start_messages(bot=telegram_bot)
# Administration commands # Administration commands
@telegram_bot.command(command='/db', @telegram_bot.command(command='/db',
@ -1925,10 +1986,10 @@ def init(telegram_bot: Bot,
description=admin_messages[ description=admin_messages[
'db_command']['description'], 'db_command']['description'],
authorization_level='admin') authorization_level='admin')
async def send_bot_database(bot, user_record, language): async def _send_bot_database(bot, user_record, language):
return await _send_bot_database(bot=bot, return await send_bot_database(bot=bot,
user_record=user_record, user_record=user_record,
language=language) language=language)
@telegram_bot.command(command='/errors', @telegram_bot.command(command='/errors',
aliases=[], aliases=[],
@ -1936,8 +1997,8 @@ def init(telegram_bot: Bot,
description=admin_messages[ description=admin_messages[
'errors_command']['description'], 'errors_command']['description'],
authorization_level='admin') authorization_level='admin')
async def errors_command(bot, update, user_record): async def _errors_command(bot, update, user_record):
return await _errors_command(bot, update, user_record) return await errors_command(bot, update, user_record)
@telegram_bot.command(command='/father', @telegram_bot.command(command='/father',
aliases=[], aliases=[],
@ -1948,14 +2009,14 @@ def init(telegram_bot: Bot,
if key in ('description', ) if key in ('description', )
}, },
authorization_level='admin') authorization_level='admin')
async def father_command(bot, language): async def _father_command(bot, language):
return await _father_command(bot=bot, language=language) return await father_command(bot=bot, language=language)
@telegram_bot.button(prefix='father:///', @telegram_bot.button(prefix='father:///',
separator='|', separator='|',
authorization_level='admin') authorization_level='admin')
async def query_button(bot, user_record, language, data): async def _father_button(bot, user_record, language, data):
return await _father_button(bot=bot, return await father_button(bot=bot,
user_record=user_record, user_record=user_record,
language=language, language=language,
data=data) data=data)
@ -1966,16 +2027,16 @@ def init(telegram_bot: Bot,
description=admin_messages[ description=admin_messages[
'log_command']['description'], 'log_command']['description'],
authorization_level='admin') authorization_level='admin')
async def log_command(bot, update, user_record): async def _log_command(bot, update, user_record):
return await _log_command(bot, update, user_record) return await log_command(bot, update, user_record)
@telegram_bot.command(command='/maintenance', aliases=[], @telegram_bot.command(command='/maintenance', aliases=[],
show_in_keyboard=False, show_in_keyboard=False,
description=admin_messages[ description=admin_messages[
'maintenance_command']['description'], 'maintenance_command']['description'],
authorization_level='admin') authorization_level='admin')
async def maintenance_command(bot, update, user_record): async def _maintenance_command(bot, update, user_record):
return await _maintenance_command(bot, update, user_record) return await maintenance_command(bot, update, user_record)
@telegram_bot.command(command='/query', @telegram_bot.command(command='/query',
aliases=[], aliases=[],
@ -1983,16 +2044,16 @@ def init(telegram_bot: Bot,
description=admin_messages[ description=admin_messages[
'query_command']['description'], 'query_command']['description'],
authorization_level='admin') authorization_level='admin')
async def query_command(bot, update, user_record): async def _query_command(bot, update, user_record):
return await _query_command(bot, update, user_record) return await query_command(bot, update, user_record)
@telegram_bot.button(prefix='db_query:///', @telegram_bot.button(prefix='db_query:///',
separator='|', separator='|',
description=admin_messages[ description=admin_messages[
'query_command']['description'], 'query_command']['description'],
authorization_level='admin') authorization_level='admin')
async def query_button(bot, update, user_record, data): async def _query_button(bot, update, user_record, data):
return await _query_button(bot, update, user_record, data) return await query_button(bot, update, user_record, data)
@telegram_bot.command(command='/restart', @telegram_bot.command(command='/restart',
aliases=[], aliases=[],
@ -2000,8 +2061,8 @@ def init(telegram_bot: Bot,
description=admin_messages[ description=admin_messages[
'restart_command']['description'], 'restart_command']['description'],
authorization_level='admin') authorization_level='admin')
async def restart_command(bot, update, user_record): async def _restart_command(bot, update, user_record):
return await _restart_command(bot, update, user_record) return await restart_command(bot, update, user_record)
@telegram_bot.command(command='/select', @telegram_bot.command(command='/select',
aliases=[], aliases=[],
@ -2009,8 +2070,8 @@ def init(telegram_bot: Bot,
description=admin_messages[ description=admin_messages[
'select_command']['description'], 'select_command']['description'],
authorization_level='admin') authorization_level='admin')
async def select_command(bot, update, user_record): async def _select_command(bot, update, user_record):
return await _query_command(bot, update, user_record) return await query_command(bot, update, user_record)
@telegram_bot.command(command='/stop', @telegram_bot.command(command='/stop',
aliases=[], aliases=[],
@ -2018,16 +2079,16 @@ def init(telegram_bot: Bot,
description=admin_messages[ description=admin_messages[
'stop_command']['description'], 'stop_command']['description'],
authorization_level='admin') authorization_level='admin')
async def stop_command(bot, update, user_record): async def _stop_command(bot, update, user_record):
return await _stop_command(bot, update, user_record) return await stop_command(bot, update, user_record)
@telegram_bot.button(prefix='stop:///', @telegram_bot.button(prefix='stop:///',
separator='|', separator='|',
description=admin_messages[ description=admin_messages[
'stop_command']['description'], 'stop_command']['description'],
authorization_level='admin') authorization_level='admin')
async def stop_button(bot, update, user_record, data): async def _stop_button(bot, update, user_record, data):
return await _stop_button(bot, update, user_record, data) return await stop_button(bot, update, user_record, data)
@telegram_bot.command(command='/talk', @telegram_bot.command(command='/talk',
aliases=[], aliases=[],
@ -2035,14 +2096,14 @@ def init(telegram_bot: Bot,
description=admin_messages[ description=admin_messages[
'talk_command']['description'], 'talk_command']['description'],
authorization_level='admin') authorization_level='admin')
async def talk_command(bot, update, user_record): async def _talk_command(bot, update, user_record):
return await _talk_command(bot, update, user_record) return await talk_command(bot, update, user_record)
@telegram_bot.button(prefix='talk:///', @telegram_bot.button(prefix='talk:///',
separator='|', separator='|',
authorization_level='admin') authorization_level='admin')
async def talk_button(bot, update, user_record, data): async def _talk_button(bot, update, user_record, data):
return await _talk_button(bot, update, user_record, data) return await talk_button(bot, update, user_record, data)
@telegram_bot.command(command='/version', @telegram_bot.command(command='/version',
aliases=[], aliases=[],
@ -2053,11 +2114,11 @@ def init(telegram_bot: Bot,
}, },
show_in_keyboard=False, show_in_keyboard=False,
authorization_level='admin') authorization_level='admin')
async def version_command(bot, update, user_record, language): async def _version_command(bot, update, user_record, language):
return await _version_command(bot=bot, return await version_command(bot=bot,
update=update, update=update,
user_record=user_record, user_record=user_record,
language=language) language=language)
@telegram_bot.command(command='/config', @telegram_bot.command(command='/config',
aliases=[], aliases=[],
@ -2068,8 +2129,9 @@ def init(telegram_bot: Bot,
}, },
show_in_keyboard=False, show_in_keyboard=False,
authorization_level='admin') authorization_level='admin')
async def config_command(bot, update, user_record, language): async def _config_command(bot, update, user_record, language):
return await _config_command(bot=bot, return await config_command(bot=bot,
update=update, update=update,
user_record=user_record, user_record=user_record,
language=language) language=language)
asyncio.ensure_future(create_promotion_command(bot=telegram_bot))

File diff suppressed because it is too large Load Diff

View File

@ -2,34 +2,6 @@
camelCase methods mirror API directly, while snake_case ones act as middleware camelCase methods mirror API directly, while snake_case ones act as middleware
someway. someway.
Usage
```
import sys
from davtelepot.bot import Bot
from data.passwords import my_token, my_other_token
long_polling_bot = Bot(token=my_token, database_url='my_db')
webhook_bot = Bot(token=my_other_token, hostname='example.com',
certificate='path/to/certificate.pem',
database_url='my_other_db')
@long_polling_bot.command('/foo')
async def foo_command(bot, update, user_record, language):
return "Bar!"
@webhook_bot.command('/bar')
async def bar_command(bot, update, user_record, language):
return "Foo!"
exit_state = Bot.run(
local_host='127.0.0.5',
port=8552
)
sys.exit(exit_state)
```
""" """
# Standard library modules # Standard library modules
@ -49,7 +21,9 @@ from typing import Callable, List, Union, Dict
import aiohttp.web import aiohttp.web
# Project modules # Project modules
from davtelepot.api import TelegramBot, TelegramError from davtelepot.api import (
LinkPreviewOptions, ReplyParameters, TelegramBot, TelegramError
)
from davtelepot.database import ObjectWithDatabase from davtelepot.database import ObjectWithDatabase
from davtelepot.languages import MultiLanguageObject from davtelepot.languages import MultiLanguageObject
from davtelepot.messages import davtelepot_messages from davtelepot.messages import davtelepot_messages
@ -148,12 +122,47 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
'edited_message': self.edited_message_handler, 'edited_message': self.edited_message_handler,
'channel_post': self.channel_post_handler, 'channel_post': self.channel_post_handler,
'edited_channel_post': self.edited_channel_post_handler, 'edited_channel_post': self.edited_channel_post_handler,
'business_connection': self.get_update_handler(
update_type='business_connection'
),
'business_message': self.get_update_handler(
update_type='business_message'
),
'edited_business_message': self.get_update_handler(
update_type='edited_business_message'
),
'deleted_business_messages': self.get_update_handler(
update_type='deleted_business_messages'
),
'message_reaction': self.get_update_handler(
update_type='message_reaction'
),
'message_reaction_count': self.get_update_handler(
update_type='message_reaction_count'
),
'inline_query': self.inline_query_handler, 'inline_query': self.inline_query_handler,
'chosen_inline_result': self.chosen_inline_result_handler, 'chosen_inline_result': self.chosen_inline_result_handler,
'callback_query': self.callback_query_handler, 'callback_query': self.callback_query_handler,
'shipping_query': self.shipping_query_handler, 'shipping_query': self.shipping_query_handler,
'pre_checkout_query': self.pre_checkout_query_handler, 'pre_checkout_query': self.pre_checkout_query_handler,
'purchased_paid_media': self.get_update_handler(
update_type='purchased_paid_media'
),
'poll': self.poll_handler, 'poll': self.poll_handler,
'poll_answer': self.get_update_handler(
update_type='poll_answer'
),
'my_chat_member': self.get_update_handler(
update_type='my_chat_member'
),
'chat_member': self.get_update_handler( update_type='chat_member' ),
'chat_join_request': self.get_update_handler(
update_type='chat_join_request'
),
'chat_boost': self.get_update_handler( update_type='chat_boost' ),
'removed_chat_boost': self.get_update_handler(
update_type='removed_chat_boost'
),
} }
# Different message update types need different handlers # Different message update types need different handlers
self.message_handlers = { self.message_handlers = {
@ -247,6 +256,8 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
self.Role = None self.Role = None
self.packages = [sys.modules['davtelepot']] self.packages = [sys.modules['davtelepot']]
self._documents_max_dimension = None self._documents_max_dimension = None
self._getting_me = False
self._got_me = False
# Add `users` table with its fields if missing # Add `users` table with its fields if missing
if 'users' not in self.db.tables: if 'users' not in self.db.tables:
table = self.db.create_table( table = self.db.create_table(
@ -706,6 +717,18 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
) )
return return
def get_update_handler(self, update_type: str):
"""Get update handler for `update_type`."""
bot = self
async def handler(update, user_record, language=None):
"""Handle update message of type `update_type`"""
logging.info(
"The following update was received: %s\n"
"However, this `%s` handler does nothing yet.",
update, update_type
)
return handler
async def edited_channel_post_handler(self, update, user_record, language=None): async def edited_channel_post_handler(self, update, user_record, language=None):
"""Handle Telegram `edited_channel_post` update.""" """Handle Telegram `edited_channel_post` update."""
logging.info( logging.info(
@ -1319,19 +1342,21 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
async def send_message(self, chat_id: Union[int, str] = None, async def send_message(self, chat_id: Union[int, str] = None,
text: str = None, text: str = None,
message_thread_id: int = None,
entities: List[dict] = None, entities: List[dict] = None,
parse_mode: str = 'HTML', parse_mode: str = 'HTML',
message_thread_id: int = None, link_preview_options: LinkPreviewOptions = None,
disable_notification: bool = None,
protect_content: bool = None, protect_content: bool = None,
disable_web_page_preview: bool = None, disable_web_page_preview: bool = None,
disable_notification: bool = None,
reply_to_message_id: int = None, reply_to_message_id: int = None,
allow_sending_without_reply: bool = None, allow_sending_without_reply: bool = None,
reply_markup=None,
update: dict = None, update: dict = None,
reply_to_update: bool = False, reply_to_update: bool = False,
send_default_keyboard: bool = True, send_default_keyboard: bool = True,
user_record: OrderedDict = None): user_record: OrderedDict = None,
reply_parameters: ReplyParameters = None,
reply_markup=None):
"""Send text via message(s). """Send text via message(s).
This method wraps lower-level `TelegramBot.sendMessage` method. This method wraps lower-level `TelegramBot.sendMessage` method.
@ -1352,6 +1377,10 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
user_record = self.db['users'].find_one(telegram_id=chat_id) user_record = self.db['users'].find_one(telegram_id=chat_id)
if reply_to_update and 'message_id' in update: if reply_to_update and 'message_id' in update:
reply_to_message_id = update['message_id'] reply_to_message_id = update['message_id']
if disable_web_page_preview:
if link_preview_options is None:
link_preview_options = LinkPreviewOptions()
link_preview_options['is_disabled'] = True
if ( if (
send_default_keyboard send_default_keyboard
and reply_markup is None and reply_markup is None
@ -1395,19 +1424,27 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
limit=self.__class__.TELEGRAM_MESSAGES_MAX_LEN - 100, limit=self.__class__.TELEGRAM_MESSAGES_MAX_LEN - 100,
parse_mode=parse_mode parse_mode=parse_mode
) )
if reply_to_message_id:
if reply_parameters is None:
reply_parameters = ReplyParameters(message_id=reply_to_message_id)
reply_parameters['message_id'] = reply_to_message_id
if allow_sending_without_reply:
reply_parameters['allow_sending_without_reply'] = allow_sending_without_reply
if reply_to_update and 'chat' in update and 'id' in update['chat']:
if update['chat']['id'] != chat_id:
reply_parameters['chat_id'] = update['chat']['id']
for text_chunk, is_last in text_chunks: for text_chunk, is_last in text_chunks:
_reply_markup = (reply_markup if is_last else None) _reply_markup = (reply_markup if is_last else None)
sent_message_update = await self.sendMessage( sent_message_update = await self.sendMessage(
chat_id=chat_id, chat_id=chat_id,
text=text_chunk, text=text_chunk,
message_thread_id=message_thread_id,
parse_mode=parse_mode, parse_mode=parse_mode,
entities=entities, entities=entities,
message_thread_id=message_thread_id, link_preview_options=link_preview_options,
protect_content=protect_content,
disable_web_page_preview=disable_web_page_preview,
disable_notification=disable_notification, disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id, protect_content=protect_content,
allow_sending_without_reply=allow_sending_without_reply, reply_parameters=reply_parameters,
reply_markup=_reply_markup reply_markup=_reply_markup
) )
return sent_message_update return sent_message_update
@ -1431,6 +1468,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
parse_mode: str = 'HTML', parse_mode: str = 'HTML',
entities: List[dict] = None, entities: List[dict] = None,
disable_web_page_preview: bool = None, disable_web_page_preview: bool = None,
link_preview_options: LinkPreviewOptions = None,
allow_sending_without_reply: bool = None, allow_sending_without_reply: bool = None,
reply_markup=None, reply_markup=None,
update: dict = None): update: dict = None):
@ -1463,6 +1501,10 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
) )
): ):
if i == 0: if i == 0:
if disable_web_page_preview:
if link_preview_options is None:
link_preview_options = LinkPreviewOptions()
link_preview_options['is_disabled'] = True
edited_message = await self.editMessageText( edited_message = await self.editMessageText(
text=text_chunk, text=text_chunk,
chat_id=chat_id, chat_id=chat_id,
@ -1470,7 +1512,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
inline_message_id=inline_message_id, inline_message_id=inline_message_id,
parse_mode=parse_mode, parse_mode=parse_mode,
entities=entities, entities=entities,
disable_web_page_preview=disable_web_page_preview, link_preview_options=link_preview_options,
reply_markup=(reply_markup if is_last else None) reply_markup=(reply_markup if is_last else None)
) )
if chat_id is None: if chat_id is None:
@ -1576,6 +1618,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
reply_markup=None, reply_markup=None,
update: dict = None, update: dict = None,
reply_to_update: bool = False, reply_to_update: bool = False,
reply_parameters: ReplyParameters = None,
send_default_keyboard: bool = True, send_default_keyboard: bool = True,
use_stored_file_id: bool = True): use_stored_file_id: bool = True):
"""Send photos. """Send photos.
@ -1599,6 +1642,15 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
chat_id = self.get_chat_id(update) chat_id = self.get_chat_id(update)
if reply_to_update and 'message_id' in update: if reply_to_update and 'message_id' in update:
reply_to_message_id = update['message_id'] reply_to_message_id = update['message_id']
if reply_to_message_id:
if reply_parameters is None:
reply_parameters = ReplyParameters(message_id=reply_to_message_id)
reply_parameters['message_id'] = reply_to_message_id
if allow_sending_without_reply:
reply_parameters['allow_sending_without_reply'] = allow_sending_without_reply
if reply_to_update and 'chat' in update and 'id' in update['chat']:
if update['chat']['id'] != chat_id:
reply_parameters['chat_id'] = update['chat']['id']
if ( if (
send_default_keyboard send_default_keyboard
and reply_markup is None and reply_markup is None
@ -1652,8 +1704,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
parse_mode=parse_mode, parse_mode=parse_mode,
caption_entities=caption_entities, caption_entities=caption_entities,
disable_notification=disable_notification, disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id, reply_parameters=reply_parameters,
allow_sending_without_reply=allow_sending_without_reply,
reply_markup=reply_markup reply_markup=reply_markup
) )
if isinstance(sent_update, Exception): if isinstance(sent_update, Exception):
@ -1701,6 +1752,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
reply_markup=None, reply_markup=None,
update: dict = None, update: dict = None,
reply_to_update: bool = False, reply_to_update: bool = False,
reply_parameters: ReplyParameters = None,
send_default_keyboard: bool = True, send_default_keyboard: bool = True,
use_stored_file_id: bool = True): use_stored_file_id: bool = True):
"""Send audio files. """Send audio files.
@ -1724,6 +1776,15 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
chat_id = self.get_chat_id(update) chat_id = self.get_chat_id(update)
if reply_to_update and 'message_id' in update: if reply_to_update and 'message_id' in update:
reply_to_message_id = update['message_id'] reply_to_message_id = update['message_id']
if reply_to_message_id:
if reply_parameters is None:
reply_parameters = ReplyParameters(message_id=reply_to_message_id)
reply_parameters['message_id'] = reply_to_message_id
if allow_sending_without_reply:
reply_parameters['allow_sending_without_reply'] = allow_sending_without_reply
if reply_to_update and 'chat' in update and 'id' in update['chat']:
if update['chat']['id'] != chat_id:
reply_parameters['chat_id'] = update['chat']['id']
if ( if (
send_default_keyboard send_default_keyboard
and reply_markup is None and reply_markup is None
@ -1781,8 +1842,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
title=title, title=title,
thumbnail=thumbnail, thumbnail=thumbnail,
disable_notification=disable_notification, disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id, reply_parameters=reply_parameters,
allow_sending_without_reply=allow_sending_without_reply,
reply_markup=reply_markup reply_markup=reply_markup
) )
if isinstance(sent_update, Exception): if isinstance(sent_update, Exception):
@ -1826,6 +1886,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
reply_markup=None, reply_markup=None,
update: dict = None, update: dict = None,
reply_to_update: bool = False, reply_to_update: bool = False,
reply_parameters: ReplyParameters = None,
send_default_keyboard: bool = True, send_default_keyboard: bool = True,
use_stored_file_id: bool = True): use_stored_file_id: bool = True):
"""Send voice messages. """Send voice messages.
@ -1849,6 +1910,15 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
chat_id = self.get_chat_id(update) chat_id = self.get_chat_id(update)
if reply_to_update and 'message_id' in update: if reply_to_update and 'message_id' in update:
reply_to_message_id = update['message_id'] reply_to_message_id = update['message_id']
if reply_to_message_id:
if reply_parameters is None:
reply_parameters = ReplyParameters(message_id=reply_to_message_id)
reply_parameters['message_id'] = reply_to_message_id
if allow_sending_without_reply:
reply_parameters['allow_sending_without_reply'] = allow_sending_without_reply
if reply_to_update and 'chat' in update and 'id' in update['chat']:
if update['chat']['id'] != chat_id:
reply_parameters['chat_id'] = update['chat']['id']
if ( if (
send_default_keyboard send_default_keyboard
and reply_markup is None and reply_markup is None
@ -1903,8 +1973,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
caption_entities=caption_entities, caption_entities=caption_entities,
duration=duration, duration=duration,
disable_notification=disable_notification, disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id, reply_parameters=reply_parameters,
allow_sending_without_reply=allow_sending_without_reply,
reply_markup=reply_markup reply_markup=reply_markup
) )
if isinstance(sent_update, Exception): if isinstance(sent_update, Exception):
@ -1951,6 +2020,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
document_name: str = None, document_name: str = None,
update: dict = None, update: dict = None,
reply_to_update: bool = False, reply_to_update: bool = False,
reply_parameters: ReplyParameters = None,
send_default_keyboard: bool = True, send_default_keyboard: bool = True,
use_stored_file_id: bool = False): use_stored_file_id: bool = False):
"""Send a document. """Send a document.
@ -1983,6 +2053,15 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
return return
if reply_to_update and 'message_id' in update: if reply_to_update and 'message_id' in update:
reply_to_message_id = update['message_id'] reply_to_message_id = update['message_id']
if reply_to_message_id:
if reply_parameters is None:
reply_parameters = ReplyParameters(message_id=reply_to_message_id)
reply_parameters['message_id'] = reply_to_message_id
if allow_sending_without_reply:
reply_parameters['allow_sending_without_reply'] = allow_sending_without_reply
if reply_to_update and 'chat' in update and 'id' in update['chat']:
if update['chat']['id'] != chat_id:
reply_parameters['chat_id'] = update['chat']['id']
if chat_id > 0: if chat_id > 0:
user_record = self.db['users'].find_one(telegram_id=chat_id) user_record = self.db['users'].find_one(telegram_id=chat_id)
language = self.get_language(update=update, user_record=user_record) language = self.get_language(update=update, user_record=user_record)
@ -2061,7 +2140,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
caption=caption, caption=caption,
parse_mode=parse_mode, parse_mode=parse_mode,
disable_notification=disable_notification, disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id, reply_parameters=reply_parameters,
reply_markup=reply_markup, reply_markup=reply_markup,
update=update, update=update,
reply_to_update=reply_to_update, reply_to_update=reply_to_update,
@ -2092,8 +2171,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
caption_entities=caption_entities, caption_entities=caption_entities,
disable_content_type_detection=disable_content_type_detection, disable_content_type_detection=disable_content_type_detection,
disable_notification=disable_notification, disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id, reply_parameters=reply_parameters,
allow_sending_without_reply=allow_sending_without_reply,
reply_markup=reply_markup reply_markup=reply_markup
) )
if isinstance(sent_update, Exception): if isinstance(sent_update, Exception):
@ -2127,7 +2205,8 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
return sent_update return sent_update
async def download_file(self, file_id, async def download_file(self, file_id,
file_name=None, path=None): file_name=None, path=None,
prevent_overwriting: bool = False):
"""Given a telegram `file_id`, download the related file. """Given a telegram `file_id`, download the related file.
Telegram may not preserve the original file name and MIME type: the Telegram may not preserve the original file name and MIME type: the
@ -2151,8 +2230,9 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
if file_name is None: if file_name is None:
file_name = get_secure_key(length=10) file_name = get_secure_key(length=10)
file_complete_path = os.path.join(path, file_name) file_complete_path = os.path.join(path, file_name)
while os.path.exists(file_complete_path): if prevent_overwriting:
file_complete_path = file_complete_path + '1' while os.path.exists(file_complete_path):
file_complete_path = file_complete_path + '1'
try: try:
with open(file_complete_path, 'wb') as local_file: with open(file_complete_path, 'wb') as local_file:
local_file.write(file_bytes) local_file.write(file_bytes)
@ -2773,9 +2853,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
""" """
if not callable(condition): if not callable(condition):
raise TypeError( raise TypeError(
'Condition {c} is not a callable'.format( f'Condition {condition.__name__} is not a callable'
c=condition.__name__
)
) )
def query_decorator(handler): def query_decorator(handler):
@ -3116,26 +3194,41 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
Restart bots if bot can't be got. Restart bots if bot can't be got.
""" """
for _ in range(60):
if not self._getting_me:
break
await asyncio.sleep(0.5)
else:
raise TimeoutError("Getting bot information was in progress but "
"did not make it in 30 seconds...")
if self._got_me:
return
self._getting_me = True
try: try:
me = await self.getMe() me = await self.getMe()
if isinstance(me, Exception): if isinstance(me, Exception):
raise me raise me
elif me is None: if me is None:
raise Exception('getMe returned None') raise TypeError('getMe returned None')
self._name = me["username"] self._name = me["username"]
self._telegram_id = me['id'] self._telegram_id = me['id']
except Exception as e: except Exception as e:
logging.error( logging.error(
f"API getMe method failed, information about this bot could " "API getMe method failed, information about this bot could "
f"not be retrieved. Restarting in 5 minutes...\n\n" "not be retrieved. Restarting in 5 minutes...\n\n"
f"Error information:\n{e}" "Error information:\n%s", e
) )
self._getting_me = False
await asyncio.sleep(5*60) await asyncio.sleep(5*60)
if self._got_me:
return
self.__class__.stop( self.__class__.stop(
message="Information about this bot could not be retrieved.\n" message="Information about this bot could not be retrieved.\n"
"Restarting...", "Restarting...",
final_state=65 final_state=65
) )
self._getting_me = False
self._got_me = True
def setup(self): def setup(self):
"""Make bot ask for updates and handle responses.""" """Make bot ask for updates and handle responses."""
@ -3150,9 +3243,11 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
async def close_sessions(self): async def close_sessions(self):
"""Close open sessions.""" """Close open sessions."""
for session_name, session in self.sessions.items(): for session_name in list(self.sessions.keys()):
session = self.sessions[session_name]
if not session.closed: if not session.closed:
await session.close() await session.close()
del self.sessions[session_name]
async def send_one_message(self, *args, **kwargs): async def send_one_message(self, *args, **kwargs):
sent_message = await self.send_message(*args, **kwargs) sent_message = await self.send_message(*args, **kwargs)
@ -3503,7 +3598,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
Each bot will receive updates via long polling or webhook according to Each bot will receive updates via long polling or webhook according to
its initialization parameters. its initialization parameters.
A single aiohttp.web.Application instance will be run (cls.app) on A single aiohttp.web.Application instance will be run (cls.app) on
local_host:port and it may serve custom-defined routes as well. local_host:port, and it may serve custom-defined routes as well.
""" """
if local_host is not None: if local_host is not None:
cls.local_host = local_host cls.local_host = local_host

View File

@ -22,6 +22,16 @@ davtelepot_messages = {
} }
default_admin_messages = { default_admin_messages = {
'become_admin': {
'success': {
'en': "🎉 You are now administrator! 👑",
'it': "🎉 Ora hai diritti di amministrazione! 👑",
},
'wrong_token': {
'en': "❌ Wrong token 🚷",
'it': "❌ Password errata 🚷",
},
},
'cancel': { 'cancel': {
'button': { 'button': {
'en': "↩️ Cancel", 'en': "↩️ Cancel",