Compare commits

127 Commits

Author SHA1 Message Date
53802728a1 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).

(cherry picked from commit db240ce199)
2024-08-17 21:12:47 +02:00
2cdb4e743f Get package version from metadata when package has no __version__ attribute 2024-08-04 18:49:16 +02:00
98fbbf0127 Compliance with bot API 7.8 2024-08-04 18:30:21 +02:00
856d53de1c Compliance with bot API 7.7
(cherry picked from commit fd4374f328)
2024-07-07 14:37:21 +02:00
5e8a2b65f5 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)

(cherry picked from commit 6b489363de)
2024-04-13 11:17:11 +02:00
3b0bfb4880 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.

(cherry picked from commit a343e095e8)
2024-04-13 11:17:07 +02:00
a49e727e31 Allow to overwrite file when using bot.download_file method
(cherry picked from commit a3b28bc1d6)
2024-04-13 11:17:04 +02:00
c78522d46b download_file behaves differently if a local bot API server is used. Wrong default API url was being used
(cherry picked from commit 3969794075)
2024-04-13 11:17:00 +02:00
c4964105b4 Support Local Bot API Server (with custom api_url).
(cherry picked from commit 748ba624a4)
2024-02-17 14:47:28 +01:00
3396bc6f18 download_file method updated to handle exceptions and return information about downloaded file
(cherry picked from commit 41507067be)
2024-02-14 19:31:20 +01:00
c1f4abc541 Specify encoding in send_part_of_text_file function
(cherry picked from commit ec747bef1d)
2024-02-14 19:31:17 +01:00
c4a822e45d Compliance with bot API 6.8 and 6.9
Added the method `unpinAllGeneralForumTopicMessages` (6.8).

Added the new administrator privileges `can_post_stories`, `can_edit_stories` and `can_delete_stories` to the class `ChatAdministratorRights` and to the method `promoteChatMember` (6.9).
2023-11-12 11:55:03 +01:00
6b4a95be36 Method to add table columns if missing 2023-08-23 18:23:32 +02:00
beaf3f8516 Allow non-null standard output and/or error pipe to be passed to aio_subprocess_shell function 2023-08-23 17:17:55 +02:00
fc1f087d4a When answering inline queries, prevent invalid InlineQueryResultsButton instantiation
(cherry picked from commit eab81b065d)
2023-08-07 20:19:50 +02:00
aeac4aea52 Better variable regex: accept only one decimal separator in float numbers
(cherry picked from commit 51a77c695b)
2023-07-25 18:58:45 +02:00
5ab22909cf Whitespace 2023-07-25 16:42:26 +02:00
9094955812 Config command implemented, allowing administrators to add variables to configuration files 2023-07-25 16:25:56 +02:00
173783e154 Moved join_path function to utilities module 2023-07-25 16:24:18 +02:00
92a986d090 Catch up with branch main (v 2.9.1) 2023-07-25 16:23:21 +02:00
3e6e364336 Compliance with Telegram Bot API 6.5 2023-02-03 21:40:09 +01:00
f0a4c914e4 Compliance with Telegram Bot API 6.4 2023-01-06 10:30:25 +01:00
80a1440e75 Make the regex dot match any character at all, including a newline, when cleaning HTML strings (so multiline tag bodies are allowed, as they are in Telegram HTML markdown). 2022-12-26 14:12:23 +01:00
b978022658 HTML parse_mode check upgraded.
All supported tags permitted, malformed tags replaced with escaped characters
2022-12-12 22:45:08 +01:00
50f561e7f9 Method to add a table and its columns to and ObjectWithDatabase 2022-12-10 18:46:29 +01:00
a9b785f49d Compliance with Telegram Bot API 6.3 2022-12-08 16:19:45 +01:00
b7d08ab2ba When admin A requests profile picture of user B, send B's picture and not A's. 2022-12-06 22:18:35 +01:00
d362ec496b Browse users via /auth command 2022-12-05 22:04:59 +01:00
0bfc1e157b Working on users browsing via /auth command 2022-12-04 22:13:48 +01:00
bc40957b27 Syntax-breaking typo 2022-10-12 14:19:46 +02:00
45117610f9 Bug fix: use a unique loop 2022-10-12 14:01:10 +02:00
f91661927e Bug fix: use a unique loop 2022-10-12 13:34:01 +02:00
12f3a2cc73 Bug fix: asyncio.run cannot take asyncio.gather as coroutine 2022-10-12 13:31:23 +02:00
25abb80343 Bug fix: no running loop, need to create a new one. 2022-10-12 13:26:03 +02:00
76ea65a660 asyncio get_event_loop method is being deprecated; use new_event_loop / get_running_loop and asyncio.run instead
Major version change because it may not be backward-compatible
2022-10-12 13:24:40 +02:00
e254dbf42a send_document method should not require a chat_id since it can be passed an update object 2020-12-20 20:30:49 +01:00
b7b91a65ea send_photo method should not require a chat_id since it can be passed an update object 2020-12-11 08:21:57 +01:00
abebff43d9 Avoid relative import statements 2020-11-19 15:18:50 +01:00
a460f71b69 Items in __all__ must be str, not module 2020-11-19 14:37:33 +01:00
6ad5ec2865 Document can be None if document_path is provided 2020-11-19 12:32:50 +01:00
cfae729270 Do not log chardet INFO and DEBUG levels 2020-11-16 23:05:24 +01:00
dc9c2a7b14 Compliance with Telegram Bot API 5.0 and improved type hinting for API methods. 2020-11-16 22:54:25 +01:00
a3cc676432 Identify edited methods (new or deprecated parameters) in addition to new methods. 2020-11-16 20:51:27 +01:00
15a7a58537 Version number 2020-11-15 21:35:45 +01:00
ea30fd39cf Import statements refactored 2020-11-15 21:34:26 +01:00
a5cabdd102 web should not be imported from aiohttp; use aiohttp.web instead 2020-11-15 20:55:22 +01:00
89fa73d1a5 Store user profile pictures in bot database and show them in authorization panel 2020-10-07 16:22:51 +02:00
4b22658bf5 MySQL compatibility: specify VARCHAR length
`db.types.string` becomes now `db.types.string(n)`
`db.types.text` is the wrong type to use (text blobs)
2020-10-07 16:19:03 +02:00
bce8cff3cf Pass all information to command handler when using /start 2020-09-26 19:28:03 +02:00
ea07aa47cf Make general_handler handle photo updates 2020-09-05 10:35:52 +02:00
d49bab54ca Updated examples 2020-08-23 14:21:37 +02:00
83d01e4da9 Remove argument parameter from parser decorator 2020-08-23 14:07:51 +02:00
59dde0d25e In text message handler, make available both lowered and original text and inspect condition parameters 2020-08-23 14:03:42 +02:00
713f4ffd0e Accept language directly 2020-08-20 15:50:53 +02:00
51bc95332f General handler implemented and applied to contact updates 2020-08-17 23:52:16 +02:00
92758d923c Document handlers implemented 2020-08-16 15:17:09 +02:00
946e2a881f Declare packenv variable 2020-07-19 16:17:39 +02:00
Davte
0f4d31892f Prevent globbing and word splitting 2020-07-19 16:11:00 +02:00
Davte
c4eaa0b9db Update default help messages with given ones, instead of replacing them 2020-07-17 09:48:52 +02:00
Davte
21214feb0a In default keyboard, show only buttons for commands the user is authorized to call 2020-07-16 10:05:28 +02:00
Davte
f7ee5b71a1 Prevent decoding None objects 2020-07-15 13:16:21 +02:00
Davte
fb58c0239d Async function to run shell commands 2020-07-15 13:11:04 +02:00
Davte
c848bfc228 Script to merge split files 2020-06-22 22:53:25 +02:00
Davte
f823057cbe Split outgoing documents bigger than 50 MB and send chunks 2020-06-22 20:30:03 +02:00
Davte
d7a1acd23b Typos 2020-06-20 15:44:26 +02:00
Davte
70053c9279 Send error message if database could not be sent - new version 2020-06-20 15:34:39 +02:00
Davte
1c76c7d797 Send error message if database could not be sent 2020-06-20 15:31:42 +02:00
Davte
dfffbff7c6 Merge 2020-06-20 15:20:22 +02:00
Davte
a4b1ed09a2 longest_cooldown_timedelta property defined 2020-06-20 15:18:13 +02:00
Davte
a1bd681cbf Merge 2020-06-20 15:16:46 +02:00
Davte
9c57677320 Accept both updates and raw_updates in maintenance exception criteria 2020-05-21 15:32:04 +02:00
Davte
956e982157 Pass to handlers the parameters they take and preserve update_id 2020-05-21 14:55:32 +02:00
Davte
4bd10894a8 Merge branch 'master' into develop 2020-05-19 15:21:02 +02:00
Davte
89a9863510 Send long text messages as files 2020-05-19 15:12:38 +02:00
Davte
ed36e53a28 Consider _ as a digit when prettifying expressions 2020-05-19 15:11:58 +02:00
Davte
8977e0fbc3 Shorten buffer time for calculation sessions 2020-05-19 14:04:02 +02:00
Davte
cf7e0c740b Redundant escape 2020-05-19 13:59:15 +02:00
Davte
12b18cff58 Prettify expressions removing whitespace around newlines and adjusting whitespace around parentheses 2020-05-19 12:53:20 +02:00
Davte
3160723aa7 TypeError bug fixed 2020-05-18 22:25:30 +02:00
Davte
8e2feb5ac2 Prevent message is not modified exceptions 2020-05-18 22:08:10 +02:00
Davte
cbc7424a97 Prettify expressions 2020-05-18 19:31:26 +02:00
Davte
1e7c384b6d Multilanguage message in version command
(cherry picked from commit a7fd054106)
2020-05-18 18:58:23 +02:00
Davte
7cc2e02f47 Remove whitespace at the end of the expression after del button is pressed 2020-05-18 18:50:21 +02:00
Davte
6315c0dcc2 Track input via message of algebraic expressions as well as input via buttons. 2020-05-18 18:33:40 +02:00
Davte
f06c525f5b Merged branch master 2020-05-18 15:09:05 +02:00
Davte
ea5c63c1f8 Finished working on calc command 2020-05-18 15:04:18 +02:00
Davte
ea549b63c8 Working on calc command 2020-05-18 13:04:19 +02:00
Davte
e63b0f9265 Working on calc command 2020-05-18 12:42:04 +02:00
Davte
cb9310437f Working on calc button 2020-05-17 19:45:39 +02:00
Davte
5bc58dd7d5 Working on calc button 2020-05-16 15:51:49 +02:00
Davte
5d35579e10 Working on /calc command 2020-05-15 19:48:04 +02:00
Davte
b0132e0b30 /when command implemented 2020-05-14 22:18:21 +02:00
Davte
49ecee63d0 /info command implemented 2020-05-14 21:58:38 +02:00
Davte
273eda2b67 /ping command implemented
/length command returns characters in text message even, not only in reply
2020-05-14 20:25:29 +02:00
Davte
b12f653096 Useful tools module added 2020-05-14 18:50:05 +02:00
Davte
05878f3fbd Language-labelled commands are accepted only for selected language. /father uses selected language for commands 2020-05-14 18:49:40 +02:00
Davte
c583c3c3b0 Allow language-labelled commands 2020-05-14 15:32:06 +02:00
Davte
2d40903069 Finished working on /version command 2020-05-13 19:44:04 +02:00
Davte
217a445a65 Working on /version command 2020-05-13 15:54:38 +02:00
Davte
b68dbe7a07 /ban command implemented 2020-05-13 15:31:59 +02:00
Davte
258ca9f6ef /ban command implemented 2020-05-13 15:31:20 +02:00
Davte
3648a36636 Version 2020-05-13 15:22:37 +02:00
Davte
ef99493c7f Finished working on /ban 2020-05-13 15:22:14 +02:00
Davte
841f7f0c8d Working on /ban 2020-05-13 12:47:37 +02:00
Davte
23882e5b3b Implemented button to delete all commands stored by BotFather 2020-05-07 22:52:42 +02:00
Davte
6ba21f0115 Version 2020-05-07 15:29:10 +02:00
Davte
ca6dca3cb7 Finished working on /father command 2020-05-07 15:07:45 +02:00
Davte
9072c3abe5 Working on /father command 2020-05-06 23:18:27 +02:00
Davte
e74e38c6ae Comment 2020-05-06 15:39:45 +02:00
Davte
7a23045479 New parameters of sendPoll method 2020-05-06 15:33:23 +02:00
Davte
5ef449a542 Temp 2020-05-06 00:44:21 +02:00
Davte
a2c83f8d9c Pass language to update handlers 2020-05-06 00:39:23 +02:00
Davte
dddaf20ea2 Temp 2020-05-05 23:44:58 +02:00
Davte
a828bb5d3c Move functions outside init 2020-05-05 12:29:55 +02:00
Davte
25f5514dff Type hinting
(cherry picked from commit 3896776f0e)
2020-04-28 12:08:01 +02:00
Davte
34f802732b Do not notify package updates too often
(cherry picked from commit ab6a0bc0ad)
2020-04-28 09:48:53 +02:00
Davte
f6466b0540 Merge branch 'develop' of ssh://gogs.davte.it:8445/Davte/davtelepot into develop
# Conflicts:
#	davtelepot/__init__.py
2020-04-28 09:48:42 +02:00
Davte
7565df17ba New version
(cherry picked from commit e61d757c1a)
2020-04-28 00:36:28 +02:00
Davte
4be33370b9 Text made more clear
(cherry picked from commit e7274dce07)
2020-04-28 00:27:36 +02:00
Davte
cf3f7a88fb Deprecated admin_tools submodule 2020-04-28 00:26:38 +02:00
Davte
a4d464da58 Text made more clear 2020-04-27 19:07:10 +02:00
Davte
83c0a91919 Missing whitespace
(cherry picked from commit b107cc105d)
2020-04-27 18:03:24 +02:00
Davte
85a5ce3882 Merge branch 'develop' of ssh://gogs.davte.it:8445/Davte/davtelepot into develop 2020-04-27 17:30:05 +02:00
Davte
77405d2dd3 Merge branch 'master' into develop
# Conflicts:
#	davtelepot/__init__.py
#	davtelepot/administration_tools.py
2020-04-27 14:03:58 +02:00
Davte
1e2316e748 Package update notifier completed 2020-04-27 13:45:25 +02:00
Davte
16014c5a42 Working on package update notifier 2020-04-26 23:30:44 +02:00
Davte
f0be5b5ff2 Working on package update notifier 2020-04-26 23:14:10 +02:00
23 changed files with 3938 additions and 827 deletions

View File

@@ -11,12 +11,13 @@ __author__ = "Davide Testa"
__email__ = "davide@davte.it"
__credits__ = ["Marco Origlia", "Nick Lee @Nickoala"]
__license__ = "GNU General Public License v3.0"
__version__ = "2.6.2"
__version__ = "2.10.6"
__maintainer__ = "Davide Testa"
__contact__ = "t.me/davte"
from . import (administration_tools, authorization, bot, helper, languages,
suggestions, useful_tools, utilities)
from davtelepot import (administration_tools, api, authorization,
bot, helper, languages, messages, suggestions,
useful_tools, utilities)
__all__ = [administration_tools, authorization, bot, helper, languages,
suggestions, useful_tools, utilities]
__all__ = ['administration_tools', 'api', 'authorization', 'bot', 'helper',
'languages', 'messages', 'suggestions', 'useful_tools', 'utilities']

5
davtelepot/__main__.py Normal file
View File

@@ -0,0 +1,5 @@
from davtelepot.cli import run_from_command_line
if __name__ == '__main__':
run_from_command_line()

View File

@@ -13,29 +13,43 @@ import asyncio
import datetime
import json
import logging
import platform
import re
import types
from collections import OrderedDict
from importlib.metadata import version as get_package_version_from_metadata
from typing import Union, List, Tuple
# Third party modules
from sqlalchemy.exc import ResourceClosedError
# Project modules
from . import messages
from .bot import Bot
from .utilities import (
from davtelepot.messages import default_admin_messages, default_talk_messages
from davtelepot.bot import Bot
from davtelepot.utilities import (
async_wrapper, CachedPage, Confirmator, extract, get_cleaned_text,
get_user, escape_html_chars, line_drawing_unordered_list, make_button,
make_inline_keyboard, remove_html_tags, send_part_of_text_file,
send_csv_file, make_lines_of_buttons
get_secure_key, get_user, clean_html_string, line_drawing_unordered_list,
make_button, make_inline_keyboard, remove_html_tags, send_part_of_text_file,
send_csv_file, make_lines_of_buttons, join_path
)
# Use this parameter in SQL `LIMIT x OFFSET y` clauses
rows_number_limit = 10
command_description_parser = re.compile(r'(?P<command>\w+)(\s?-\s?(?P<description>.*))?')
variable_regex = re.compile(r"(?P<name>[a-zA-Z]\w*)\s*=\s*"
r"(?P<value>\d*[.,]?\d+|"
r"True|False|"
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,
@@ -130,7 +144,7 @@ def get_talk_panel(bot: Bot,
'help_text',
update=update,
user_record=user_record,
q=escape_html_chars(
q=clean_html_string(
remove_html_tags(text)
)
)
@@ -155,7 +169,7 @@ def get_talk_panel(bot: Bot,
'user_not_found',
update=update,
user_record=user_record,
q=escape_html_chars(
q=clean_html_string(
remove_html_tags(text)
)
)
@@ -218,7 +232,7 @@ def get_talk_panel(bot: Bot,
return text, reply_markup
async def _talk_command(bot: Bot,
async def talk_command(bot: Bot,
update,
user_record):
text = get_cleaned_text(
@@ -338,7 +352,7 @@ async def end_session(bot: Bot,
return
async def _talk_button(bot: Bot,
async def talk_button(bot: Bot,
update,
user_record,
data):
@@ -348,7 +362,7 @@ async def _talk_button(bot: Bot,
if command == 'search':
bot.set_individual_text_message_handler(
await async_wrapper(
_talk_command,
talk_command,
),
update
)
@@ -362,7 +376,10 @@ async def _talk_button(bot: Bot,
len(arguments) < 1
or type(arguments[0]) is not int
):
result = "Errore!"
result = bot.get_message(
'talk', 'error', 'text',
update=update, user_record=user_record
)
else:
with bot.db as db:
other_user_record = db['users'].find_one(
@@ -381,7 +398,10 @@ async def _talk_button(bot: Bot,
len(arguments) < 1
or type(arguments[0]) is not int
):
result = "Errore!"
result = bot.get_message(
'talk', 'error', 'text',
update=update, user_record=user_record
)
elif not Confirmator.get('stop_bots').confirm(telegram_id):
result = bot.get_message(
'talk', 'end_session',
@@ -415,7 +435,7 @@ async def _talk_button(bot: Bot,
return result
async def _restart_command(bot: Bot,
async def restart_command(bot: Bot,
update,
user_record):
with bot.db as db:
@@ -442,7 +462,7 @@ async def _restart_command(bot: Bot,
return
async def _stop_command(bot: Bot,
async def stop_command(bot: Bot,
update,
user_record):
text = bot.get_message(
@@ -484,7 +504,7 @@ async def stop_bots(bot: Bot):
return
async def _stop_button(bot: Bot,
async def stop_button(bot: Bot,
update,
user_record,
data: List[Union[int, str]]):
@@ -524,7 +544,7 @@ async def _stop_button(bot: Bot,
return result
async def _send_bot_database(bot, update, user_record):
async def send_bot_database(bot: Bot, user_record: OrderedDict, language: str):
if not all(
[
bot.db_url.endswith('.db'),
@@ -533,24 +553,25 @@ async def _send_bot_database(bot, update, user_record):
):
return bot.get_message(
'admin', 'db_command', 'not_sqlite',
update=update, user_record=user_record,
language=language,
db_type=bot.db_url.partition(':///')[0]
)
await bot.send_document(
sent_update = await bot.send_document(
chat_id=user_record['telegram_id'],
document_path=extract(bot.db.url, starter='sqlite:///'),
caption=bot.get_message(
'admin', 'db_command', 'file_caption',
update=update, user_record=user_record
language=language
)
)
return bot.get_message(
'admin', 'db_command', 'db_sent',
update=update, user_record=user_record
'admin', 'db_command',
('error' if isinstance(sent_update, Exception) else 'db_sent'),
language=language
)
async def _query_command(bot, update, user_record):
async def query_command(bot, update, user_record):
query = get_cleaned_text(
update,
bot,
@@ -628,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
command = data[0] if len(data) else 'default'
error_message = bot.get_message(
@@ -665,7 +686,7 @@ async def _query_button(bot, update, user_record, data):
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:
return bot.get_message(
'admin', 'log_command', 'no_log',
@@ -718,7 +739,7 @@ async def _log_command(bot, update, user_record):
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
chat_id = update['from']['id']
if bot.errors_file_path is None:
@@ -762,7 +783,7 @@ async def _errors_command(bot, update, user_record):
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'])
if maintenance_message.startswith('{'):
maintenance_message = json.loads(maintenance_message)
@@ -852,7 +873,12 @@ async def get_new_versions(bot: Bot,
"skipping...")
continue
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(
package=package.__name__,
order_by=['-id'],
@@ -871,7 +897,7 @@ async def get_new_versions(bot: Bot,
return news
async def _version_command(bot: Bot, update: dict,
async def version_command(bot: Bot, update: dict,
user_record: OrderedDict, language: str):
last_commit = await get_last_commit()
text = bot.get_message(
@@ -879,9 +905,10 @@ async def _version_command(bot: Bot, update: dict,
last_commit=last_commit,
update=update, user_record=user_record
) + '\n\n'
text += f'<b>Python: </b> <code>{platform.python_version()}</code>\n'
text += '\n'.join(
f"<b>{package.__name__}</b>: "
f"<code>{package.__version__}</code>"
f"<code>{get_package_version(package)}</code>"
for package in bot.packages
)
temporary_message = await bot.send_message(
@@ -925,7 +952,7 @@ async def notify_new_version(bot: Bot):
order_by=['-id']
)
current_versions = {
f"{package.__name__}_version": package.__version__
f"{package.__name__}_version": get_package_version(package)
for package in bot.packages
}
current_versions['last_commit'] = last_commit
@@ -1020,7 +1047,7 @@ async def get_package_updates(bot: Bot,
await asyncio.sleep(monitoring_interval)
async def _send_start_messages(bot: Bot):
async def send_start_messages(bot: Bot):
"""Send restart messages at restart."""
for restart_message in bot.db['restart_messages'].find(sent=None):
asyncio.ensure_future(
@@ -1048,7 +1075,7 @@ async def _send_start_messages(bot: Bot):
return
async def _load_talking_sessions(bot: Bot):
async def load_talking_sessions(bot: Bot):
sessions = []
for session in bot.db.query(
"""SELECT *
@@ -1127,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 = [
{
key: (
@@ -1431,12 +1458,12 @@ async def edit_bot_father_settings_via_message(bot: Bot,
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):
"""Handle BotFather button.
Operational modes
- main: back to main page (see _father_command)
- main: back to main page (see `father_command`)
- get: show commands stored by @BotFather
- set: edit commands stored by @BotFather
"""
@@ -1530,7 +1557,7 @@ async def _father_button(bot: Bot, user_record: OrderedDict,
elif command == 'main':
return dict(
text='',
edit=(await _father_command(bot=bot, language=language))
edit=(await father_command(bot=bot, language=language))
)
elif command == 'set':
stored_commands = await bot.getMyCommands()
@@ -1704,7 +1731,7 @@ async def _father_button(bot: Bot, user_record: OrderedDict,
prefix='father:///',
delimiter='|',
data=['settings', 'edit', 'select',
selected_record['id'], 'edit_descr']
selected_record['id'], 'edit_description']
),
make_button(
text=bot.get_message(
@@ -1730,7 +1757,7 @@ async def _father_button(bot: Bot, user_record: OrderedDict,
],
2
)
elif len(data) > 3 and data[3] == 'edit_descr':
elif len(data) > 3 and data[3] == 'edit_description':
result, text, reply_markup = await edit_bot_father_settings_via_message(
bot=bot,
user_record=user_record,
@@ -1798,6 +1825,84 @@ async def _father_button(bot: Bot, user_record: OrderedDict,
return result
async def config_command(bot: Bot, update: dict,
user_record: dict, language: str):
text = get_cleaned_text(
update,
bot,
['config']
)
if not text:
return bot.get_message('admin', 'config_command',
'instructions',
user_record=user_record,
language=language)
match = variable_regex.match(text)
if not match:
return bot.get_message('admin', 'config_command',
'invalid_input',
user_record=user_record,
language=language)
match = match.groupdict()
if (',' in match['value']
and not match['value'].startswith('\'')
and not match['value'].startswith('"')):
match['value'] = match['value'].replace(',', '.')
new_variable = f"{match['name']} = {match['value']}"
with open(join_path(bot.path, 'data', 'config.py'),
'a') as configuration_file:
configuration_file.write(f"{new_variable}\n")
return bot.get_message('admin', 'config_command',
'success',
new_variable=new_variable,
user_record=user_record,
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()
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,
talk_messages: dict = None,
admin_messages: dict = None,
@@ -1811,10 +1916,10 @@ def init(telegram_bot: Bot,
)
asyncio.ensure_future(get_package_updates(telegram_bot))
if talk_messages is None:
talk_messages = messages.default_talk_messages
talk_messages = default_talk_messages
telegram_bot.messages['talk'] = talk_messages
if admin_messages is None:
admin_messages = messages.default_admin_messages
admin_messages = default_admin_messages
telegram_bot.messages['admin'] = admin_messages
db = telegram_bot.db
if 'bot_father_commands' not in db.tables:
@@ -1823,11 +1928,11 @@ def init(telegram_bot: Bot,
)
table.create_column(
'command',
db.types.string
db.types.string(100)
)
table.create_column(
'description',
db.types.string
db.types.string(300)
)
table.create_column(
'hidden',
@@ -1839,7 +1944,7 @@ def init(telegram_bot: Bot,
)
if 'talking_sessions' not in db.tables:
table = db.create_table(
table_name='users'
table_name='talking_sessions'
)
table.create_column(
'user',
@@ -1861,16 +1966,16 @@ def init(telegram_bot: Bot,
# Tasks to complete before starting bot
@telegram_bot.additional_task(when='BEFORE')
async def load_talking_sessions():
return await _load_talking_sessions(bot=telegram_bot)
async def _load_talking_sessions():
return await load_talking_sessions(bot=telegram_bot)
@telegram_bot.additional_task(when='BEFORE', bot=telegram_bot)
async def notify_version(bot):
return await notify_new_version(bot=bot)
@telegram_bot.additional_task('BEFORE')
async def send_restart_messages():
return await _send_start_messages(bot=telegram_bot)
async def _send_start_messages():
return await send_start_messages(bot=telegram_bot)
# Administration commands
@telegram_bot.command(command='/db',
@@ -1879,8 +1984,10 @@ def init(telegram_bot: Bot,
description=admin_messages[
'db_command']['description'],
authorization_level='admin')
async def send_bot_database(bot, update, user_record):
return await _send_bot_database(bot, update, user_record)
async def _send_bot_database(bot, user_record, language):
return await send_bot_database(bot=bot,
user_record=user_record,
language=language)
@telegram_bot.command(command='/errors',
aliases=[],
@@ -1888,8 +1995,8 @@ def init(telegram_bot: Bot,
description=admin_messages[
'errors_command']['description'],
authorization_level='admin')
async def errors_command(bot, update, user_record):
return await _errors_command(bot, update, user_record)
async def _errors_command(bot, update, user_record):
return await errors_command(bot, update, user_record)
@telegram_bot.command(command='/father',
aliases=[],
@@ -1900,14 +2007,14 @@ def init(telegram_bot: Bot,
if key in ('description', )
},
authorization_level='admin')
async def father_command(bot, language):
return await _father_command(bot=bot, language=language)
async def _father_command(bot, language):
return await father_command(bot=bot, language=language)
@telegram_bot.button(prefix='father:///',
separator='|',
authorization_level='admin')
async def query_button(bot, user_record, language, data):
return await _father_button(bot=bot,
async def _father_button(bot, user_record, language, data):
return await father_button(bot=bot,
user_record=user_record,
language=language,
data=data)
@@ -1918,16 +2025,16 @@ def init(telegram_bot: Bot,
description=admin_messages[
'log_command']['description'],
authorization_level='admin')
async def log_command(bot, update, user_record):
return await _log_command(bot, update, user_record)
async def _log_command(bot, update, user_record):
return await log_command(bot, update, user_record)
@telegram_bot.command(command='/maintenance', aliases=[],
show_in_keyboard=False,
description=admin_messages[
'maintenance_command']['description'],
authorization_level='admin')
async def maintenance_command(bot, update, user_record):
return await _maintenance_command(bot, update, user_record)
async def _maintenance_command(bot, update, user_record):
return await maintenance_command(bot, update, user_record)
@telegram_bot.command(command='/query',
aliases=[],
@@ -1935,16 +2042,16 @@ def init(telegram_bot: Bot,
description=admin_messages[
'query_command']['description'],
authorization_level='admin')
async def query_command(bot, update, user_record):
return await _query_command(bot, update, user_record)
async def _query_command(bot, update, user_record):
return await query_command(bot, update, user_record)
@telegram_bot.button(prefix='db_query:///',
separator='|',
description=admin_messages[
'query_command']['description'],
authorization_level='admin')
async def query_button(bot, update, user_record, data):
return await _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)
@telegram_bot.command(command='/restart',
aliases=[],
@@ -1952,8 +2059,8 @@ def init(telegram_bot: Bot,
description=admin_messages[
'restart_command']['description'],
authorization_level='admin')
async def restart_command(bot, update, user_record):
return await _restart_command(bot, update, user_record)
async def _restart_command(bot, update, user_record):
return await restart_command(bot, update, user_record)
@telegram_bot.command(command='/select',
aliases=[],
@@ -1961,8 +2068,8 @@ def init(telegram_bot: Bot,
description=admin_messages[
'select_command']['description'],
authorization_level='admin')
async def select_command(bot, update, user_record):
return await _query_command(bot, update, user_record)
async def _select_command(bot, update, user_record):
return await query_command(bot, update, user_record)
@telegram_bot.command(command='/stop',
aliases=[],
@@ -1970,16 +2077,16 @@ def init(telegram_bot: Bot,
description=admin_messages[
'stop_command']['description'],
authorization_level='admin')
async def stop_command(bot, update, user_record):
return await _stop_command(bot, update, user_record)
async def _stop_command(bot, update, user_record):
return await stop_command(bot, update, user_record)
@telegram_bot.button(prefix='stop:///',
separator='|',
description=admin_messages[
'stop_command']['description'],
authorization_level='admin')
async def stop_button(bot, update, user_record, data):
return await _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)
@telegram_bot.command(command='/talk',
aliases=[],
@@ -1987,14 +2094,14 @@ def init(telegram_bot: Bot,
description=admin_messages[
'talk_command']['description'],
authorization_level='admin')
async def talk_command(bot, update, user_record):
return await _talk_command(bot, update, user_record)
async def _talk_command(bot, update, user_record):
return await talk_command(bot, update, user_record)
@telegram_bot.button(prefix='talk:///',
separator='|',
authorization_level='admin')
async def talk_button(bot, update, user_record, data):
return await _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)
@telegram_bot.command(command='/version',
aliases=[],
@@ -2005,8 +2112,26 @@ def init(telegram_bot: Bot,
},
show_in_keyboard=False,
authorization_level='admin')
async def version_command(bot, update, user_record, language):
return await _version_command(bot=bot,
async def _version_command(bot, update, user_record, language):
return await version_command(bot=bot,
update=update,
user_record=user_record,
language=language)
@telegram_bot.command(command='/config',
aliases=[],
**{key: admin_messages['config_command'][key]
for key in ('reply_keyboard_button',
'description',
'help_section',)
},
show_in_keyboard=False,
authorization_level='admin')
async def _config_command(bot, update, user_record, language):
return await config_command(bot=bot,
update=update,
user_record=user_record,
language=language)
if len(telegram_bot.administrators) == 0:
asyncio.ensure_future(create_promotion_command(bot=telegram_bot))

File diff suppressed because it is too large Load Diff

View File

@@ -3,14 +3,18 @@
# Standard library modules
import argparse
import asyncio
import inspect
import logging
# Third party modules
import os
from typing import List
import aiohttp
from bs4 import BeautifulSoup
# Project modules
from . import api
from davtelepot.api import TelegramBot
api_url = "https://core.telegram.org/bots/api"
@@ -42,6 +46,29 @@ class TelegramApiMethod(object):
"""Return method description."""
return self._description
@property
def description_80chars(self):
"""Return method description, breaking lines at 80 characters."""
result, current_line = '', ''
indentation = 8
redundant_string = "Use this method to "
for n, paragraph in enumerate(self.description.replace('.', '.\n').split('\n')):
additional_indentation = 0
if n == 0 and paragraph.startswith(redundant_string):
paragraph = paragraph[len(redundant_string)].upper() + paragraph[len(redundant_string)+1:]
for word in paragraph.split(' '):
if len(current_line) + len(word) > 80 - indentation - additional_indentation:
additional_indentation = max(additional_indentation, 4)
result += f"{current_line.strip()}\n{' ' * additional_indentation}"
current_line = ""
current_line += f"{word} "
if len(current_line):
result += f"{current_line.strip()}\n"
current_line = ""
if n == 0:
result += '\n'
return result.strip()
@property
def table(self):
"""Return method parameters table."""
@@ -52,7 +79,7 @@ class TelegramApiMethod(object):
return self._parameters
@property
def parameters_with_types(self):
def parameters_with_types(self) -> List[str]:
return [
f"{parameter['name']}: {parameter['type']}"
for parameter in self._parameters
@@ -98,16 +125,14 @@ class TelegramApiMethod(object):
return parameters
async def print_api_methods(loop=None,
filename=None,
async def print_api_methods(filename=None,
print_all=False,
output_file=None):
output_file=None,
input_file=None):
"""Get information from Telegram bot API web page."""
if loop is None:
loop = asyncio.get_event_loop()
implemented_methods = dir(api.TelegramBot)
implemented_methods = dir(TelegramBot)
if input_file is None or not os.path.isfile(input_file):
async with aiohttp.ClientSession(
loop=loop,
timeout=aiohttp.ClientTimeout(
total=100
)
@@ -119,6 +144,12 @@ async def print_api_methods(loop=None,
await response.text(),
"html.parser"
)
else:
with open(input_file, 'r') as local_web_page:
web_page = BeautifulSoup(
''.join(local_web_page.readlines()),
"html.parser"
)
if filename is not None:
with open(filename, 'w') as _file:
_file.write(web_page.decode())
@@ -146,40 +177,84 @@ async def print_api_methods(loop=None,
)
)
new_line = '\n'
new_methods = []
edited_methods = []
for method in methods:
if print_all or method.name not in implemented_methods:
new_methods.append(method)
else:
parameters = set(parameter['name'] for parameter in method.parameters)
implemented_parameters = set(
parameter.strip('_') # Parameter `type` becomes `type_` in python
for parameter in inspect.signature(
getattr(TelegramBot,
method.name)
).parameters.keys()
if parameter != 'self'
)
new_parameters = parameters - implemented_parameters
deprecated_parameters = implemented_parameters - parameters - {'kwargs'}
if new_parameters or deprecated_parameters:
edited_methods.append(
dict(
name=method.name,
new_parameters=new_parameters,
deprecated_parameters=deprecated_parameters
)
)
if output_file:
with open(output_file, 'w') as file:
if new_methods:
file.write(
"from typing import List, Union\n"
"from davtelepot.api import TelegramBot\n"
"self = TelegramBot('fake_token')\n\n\n"
"from davtelepot.api import TelegramBot\n\n\n"
"# noinspection PyPep8Naming\n"
"class Bot(TelegramBot):\n\n"
)
file.writelines(
f"async def {method.name}("
f"{', '.join(method.parameters_with_types)}"
"):\n"
" \"\"\""
f"{method.description.replace(new_line, new_line + ' ' * 4)}\n"
" See https://core.telegram.org/bots/api#"
f" async def {method.name}("
f"{', '.join(['self'] + method.parameters_with_types)}"
f"):\n"
f" \"\"\""
f"{method.description_80chars.replace(new_line, new_line + ' ' * 8)}\n"
f" See https://core.telegram.org/bots/api#"
f"{method.name.lower()} for details.\n"
" \"\"\"\n"
" return await self.api_request(\n"
f" \"\"\"\n"
f" return await self.api_request(\n"
f" '{method.name}',\n"
" parameters=locals()\n"
" )\n\n\n"
for method in methods
if print_all or method.name not in implemented_methods
f" parameters=locals()\n"
f" )\n\n"
for method in new_methods
)
if edited_methods:
file.write('\n# === EDITED METHODS ===\n')
for method in edited_methods:
file.write(f'\n"""{method["name"]}\n')
if method['new_parameters']:
file.write(" New parameters: "
+ ", ".join(method['new_parameters'])
+ "\n")
if method['deprecated_parameters']:
file.write(" Deprecated parameters: "
+ ", ".join(method['deprecated_parameters'])
+ "\n")
file.write('"""\n')
else:
print(
'\n'.join(
f"NAME\n\t{method.name}\n"
f"PARAMETERS\n\t{', '.join(method.parameters_with_types)}\n"
f"PARAMETERS\n\t{', '.join(['self'] + method.parameters_with_types)}\n"
f"DESCRIPTION\n\t{method.description}\n"
f"TABLE\n\t{method.print_parameters_table()}\n\n"
for method in methods
if print_all or method.name not in implemented_methods
for method in new_methods
)
)
for method in edited_methods:
print(method['name'])
if method['new_parameters']:
print("\tNew parameters: " + ", ".join(method['new_parameters']))
if method['deprecated_parameters']:
print("\tDeprecated parameters: " + ", ".join(method['deprecated_parameters']))
def main():
@@ -202,16 +277,20 @@ def main():
default=None,
required=False,
help='File path to store methods implementation')
cli_parser.add_argument('--in', '--input', '-i', type=str,
default=None,
required=False,
help='File path to read Telegram API web page')
cli_arguments = vars(cli_parser.parse_args())
filename = cli_arguments['file']
print_all = cli_arguments['all']
output_file = cli_arguments['out']
loop = asyncio.get_event_loop()
loop.run_until_complete(
print_api_methods(loop=loop,
filename=filename,
input_file = cli_arguments['in']
asyncio.run(
print_api_methods(filename=filename,
print_all=print_all,
output_file=output_file)
output_file=output_file,
input_file=input_file)
)
logging.info("Done!")

View File

@@ -5,9 +5,9 @@ from collections import OrderedDict
from typing import Callable, List, Union
# Project modules
from .bot import Bot
from .messages import default_authorization_messages
from .utilities import (
from davtelepot.bot import Bot
from davtelepot.messages import default_authorization_messages
from davtelepot.utilities import (
Confirmator, get_cleaned_text, get_user, make_button, make_inline_keyboard
)
@@ -61,6 +61,8 @@ DEFAULT_ROLES[100] = {
'can_be_appointed_by': [1, 2, 3]
}
max_records_per_query = 15
class Role:
"""Authorization level for users of a bot."""
@@ -74,7 +76,7 @@ class Role:
"""Instantiate Role object.
code : int
The higher the code, the less privileges are connected to that
The higher the code, the fewer privileges are connected to that
role. Use 0 for banned users.
name : str
Short name for role.
@@ -198,8 +200,16 @@ class Role:
"""
if user_role is None:
user_role = cls.get_user_role(user_record=user_record)
long_name = ' '.join(
[user_record[key]
for key in ('first_name', 'last_name')
if key in user_record
and user_record[key]]
)
if long_name:
long_name = f" ({long_name})"
return (
f"""👤 {get_user(record=user_record)}\n"""
f"👤 {get_user(record=user_record)}{long_name}\n"
f"🔑 <i>{user_role.singular.capitalize()}</i> {user_role.symbol}"
)
@@ -228,6 +238,7 @@ class Role:
make_button(
f"{role.symbol} {role.singular.capitalize()}",
prefix='auth:///',
delimiter='|',
data=['set', user_record['id'], code]
)
for code, role in cls.roles.items()
@@ -252,7 +263,7 @@ class Role:
return text, buttons
def __eq__(self, other: 'Role'):
"""Return True if self is equal to other."""
"""Return True if `self` is equal to `other`."""
return self.code == other.code
def __gt__(self, other: 'Role'):
@@ -274,11 +285,11 @@ class Role:
return not self.__ge__(other)
def __le__(self, other: 'Role'):
"""Return True if self is superior or equal to other."""
"""Return True if `self` is superior or equal to `other`."""
return not self.__gt__(other)
def __ne__(self, other: 'Role'):
"""Return True if self is not equal to other."""
"""Return True if `self` is not equal to `other`."""
return not self.__eq__(other)
def __str__(self):
@@ -312,14 +323,37 @@ def get_authorization_function(bot: Bot):
return is_authorized
def get_browse_buttons(bot: Bot, language: str):
return [
make_button(
text=bot.get_message('authorization', 'auth_button',
'browse', 'browse_button_az',
language=language),
prefix='auth:///',
delimiter='|',
data=['browse', 'az'],
),
make_button(
text=bot.get_message('authorization', 'auth_button',
'browse', 'browse_button_by_role',
language=language),
prefix='auth:///',
delimiter='|',
data=['browse', 'role'],
),
]
async def _authorization_command(bot: Bot,
update: dict,
user_record: OrderedDict,
language: str,
mode: str = 'auth'):
db = bot.db
text = get_cleaned_text(bot=bot, update=update, replace=[mode])
reply_markup = None
admin_record = user_record.copy()
query_id = None
user_record = None
admin_role = bot.Role.get_user_role(user_record=admin_record)
result = bot.get_message(
@@ -333,6 +367,9 @@ async def _authorization_command(bot: Bot,
update=update, user_record=admin_record,
command=mode
)
buttons = get_browse_buttons(bot=bot, language=language)
reply_markup = make_inline_keyboard(buttons, 2)
user_record = -1
else: # No text, command used in reply to another message
update = update['reply_to_message']
# Forwarded message: get both the user who forwarded and the original author
@@ -349,6 +386,36 @@ async def _authorization_command(bot: Bot,
telegram_id=update['from']['id']
)
else: # Get users matching the input text
query_text = (
"SELECT * "
"FROM users "
"WHERE COALESCE("
" first_name || last_name || username,"
" last_name || username,"
" first_name || username,"
" username,"
" first_name || last_name,"
" last_name,"
" first_name"
f") LIKE '%{text}%' "
"ORDER BY COALESCE("
" username || first_name || last_name,"
" username || last_name,"
" username || first_name,"
" username,"
" first_name || last_name,"
" first_name,"
" last_name"
")"
)
query_id = bot.db['queries'].upsert(
dict(query=query_text),
['query']
)
if query_id is True:
query_id = bot.db['queries'].find_one(
query=query_text
)['id']
user_record = list(
db.query(
"SELECT * "
@@ -372,28 +439,21 @@ async def _authorization_command(bot: Bot,
update=update, user_record=admin_record
)
elif type(user_record) is list and len(user_record) > 1: # If many users match
result = bot.get_message(
'authorization', 'auth_command', 'choose_user',
update=update, user_record=admin_record,
n=len(user_record)
)
reply_markup = make_inline_keyboard(
[
make_button(
f"👤 {get_user(user, link_profile=False)}",
prefix='auth:///',
data=['show', user['id']]
)
for user in user_record[:30]
],
3
)
browse_panel = await _authorization_button(bot=bot,
update=update,
user_record=admin_record,
language=language,
data=['browse', query_id])
if 'edit' in browse_panel:
return browse_panel['edit']
elif type(user_record) is list and len(user_record) == 0: # If query was provided but no user matches
result = bot.get_message(
'authorization', 'auth_command', 'no_match',
update=update, user_record=admin_record,
)
elif isinstance(user_record, dict): # If 1 user matches
elif (type(user_record) is list and len(user_record) == 1) or isinstance(user_record, dict): # If 1 user matches
if type(user_record) is list:
user_record = user_record[0]
# Ban user if admin can do it
user_role = bot.Role.get_user_role(user_record=user_record)
if mode == 'ban' and admin_role > user_role:
@@ -407,7 +467,19 @@ async def _authorization_command(bot: Bot,
user_record=user_record,
admin_record=admin_record
)
if bot.db['user_profile_photos'].find_one(user_id=user_record['id']):
buttons.append(
make_button(
text=bot.get_message('authorization', 'auth_button',
'profile_picture_button',
language=language),
prefix='auth:///',
delimiter='|',
data=['picture', user_record['id']]
)
)
reply_markup = make_inline_keyboard(buttons, 1)
reply_markup['inline_keyboard'].append(get_browse_buttons(bot=bot, language=language))
return dict(
text=result,
reply_markup=reply_markup,
@@ -418,23 +490,145 @@ async def _authorization_command(bot: Bot,
async def _authorization_button(bot: Bot,
update: dict,
user_record: OrderedDict,
language: str,
data: Union[str, List[Union[int, str]]]):
if len(data) == 0:
data = ['']
command, *arguments = data
user_id = user_record['telegram_id']
if len(arguments) > 0:
if len(arguments) > 0 and command in ['show', 'set']:
other_user_id = arguments[0]
else:
other_user_id = None
result, text, reply_markup = '', '', None
db = bot.db
if command in ['show']:
other_user_record = db['users'].find_one(id=other_user_id)
query_text = None
if command in ['browse'] and len(arguments) >= 1:
mode = arguments[0]
offset = arguments[1] if len(arguments) > 1 else 0
user_records = []
if mode == 'choose':
update['text'] = ''
answer_update = await _authorization_command(bot=bot,
update=update,
user_record=user_record,
language=language,
mode='auth')
text = answer_update['text']
reply_markup = answer_update['reply_markup']
elif isinstance(mode, int) or (isinstance(mode, str) and mode.isnumeric()):
query_record = bot.db['queries'].find_one(id=int(mode))
if query_record:
query_text = query_record['query']
elif mode == 'az':
query_text = (
"SELECT * "
"FROM users "
"ORDER BY COALESCE("
" username || first_name || last_name,"
" username || last_name,"
" username || first_name,"
" username,"
" first_name || last_name,"
" first_name,"
" last_name"
")"
)
elif mode == 'role':
query_text = (
"SELECT * "
"FROM users "
"ORDER BY ("
" CASE WHEN privileges = 0 THEN 5000 "
" ELSE privileges END) "
"ASC, COALESCE( "
" username || first_name || last_name,"
" username || last_name,"
" username || first_name,"
" username,"
" first_name || last_name,"
" first_name,"
" last_name"
") "
)
n = 0
if query_text:
user_records = list(
bot.db.query(
f"{query_text} "
f"LIMIT {max_records_per_query + 1} "
f"OFFSET {offset * max_records_per_query} "
)
)
for record in bot.db.query("SELECT COUNT(*) n "
f"FROM ({query_text})"):
n = record['n']
if user_records:
text = bot.get_message(
'authorization', 'auth_command', 'choose_user',
update=update, user_record=user_record,
n=n
)
reply_markup = make_inline_keyboard(
[
make_button(
f"{bot.Role.get_user_role(user_record=user).symbol} {get_user(user, link_profile=False)}",
prefix='auth:///',
delimiter='|',
data=['show', user['id'], command, mode, offset]
)
for user in user_records[:max_records_per_query]
],
3
)
if n > max_records_per_query:
reply_markup['inline_keyboard'].append(
[
make_button(
text='◀️',
prefix='auth:///',
delimiter='|',
data=['browse', mode, offset - 1 if offset else n // max_records_per_query]
),
make_button(
text='↩️',
prefix='auth:///',
delimiter='|',
data=['browse', 'choose']
),
make_button(
text='▶️',
prefix='auth:///',
delimiter='|',
data=['browse', mode, offset + 1 if n > (offset + 1) * max_records_per_query else 0]
)
]
)
elif command in ['show']:
other_user_record = bot.db['users'].find_one(id=other_user_id)
text, buttons = bot.Role.get_user_role_text_and_buttons(
user_record=other_user_record,
admin_record=user_record
)
if bot.db['user_profile_photos'].find_one(user_id=other_user_record['id']):
buttons.append(
make_button(
text=bot.get_message('authorization', 'auth_button',
'profile_picture_button',
language=language),
prefix='auth:///',
delimiter='|',
data=['picture', other_user_record['id']]
)
)
if len(arguments) > 2:
buttons.append(
make_button(
text='↩️',
prefix='auth:///',
delimiter='|',
data=data[2:]
)
)
reply_markup = make_inline_keyboard(buttons, 1)
elif command in ['set'] and len(arguments) > 1:
other_user_id, new_privileges, *_ = arguments
@@ -446,7 +640,7 @@ async def _authorization_button(bot: Bot,
'authorization', 'auth_button', 'confirm',
update=update, user_record=user_record,
)
other_user_record = db['users'].find_one(id=other_user_id)
other_user_record = bot.db['users'].find_one(id=other_user_id)
user_role = bot.Role.get_user_role(user_record=user_record)
other_user_role = bot.Role.get_user_role(user_record=other_user_record)
if other_user_role.code == new_privileges:
@@ -467,6 +661,7 @@ async def _authorization_button(bot: Bot,
update=update, user_record=user_record
),
prefix='auth:///',
delimiter='|',
data=['show', other_user_id]
)
],
@@ -485,20 +680,21 @@ async def _authorization_button(bot: Bot,
update=update, user_record=user_record
),
prefix='auth:///',
delimiter='|',
data=['show', other_user_id]
)
],
1
)
else:
db['users'].update(
bot.db['users'].update(
dict(
id=other_user_id,
privileges=new_privileges
),
['id']
)
other_user_record = db['users'].find_one(id=other_user_id)
other_user_record = bot.db['users'].find_one(id=other_user_id)
result = bot.get_message(
'authorization', 'auth_button', 'appointed',
update=update, user_record=user_record
@@ -507,7 +703,41 @@ async def _authorization_button(bot: Bot,
user_record=other_user_record,
admin_record=user_record
)
if bot.db['user_profile_photos'].find_one(user_id=other_user_record['id']):
buttons.append(
make_button(
text=bot.get_message('authorization', 'auth_button',
'profile_picture_button',
language=language),
prefix='auth:///',
delimiter='|',
data=['picture', other_user_record['id']]
)
)
reply_markup = make_inline_keyboard(buttons, 1)
elif command in ['picture'] and len(arguments) > 0:
photo_record = bot.db['user_profile_photos'].find_one(
user_id=arguments[0],
order_by=['-update_datetime'],
)
other_user_record = bot.db['users'].find_one(id=arguments[0])
if photo_record is None:
result = bot.get_message('admin', 'error', 'text',
language=language)
else:
caption, buttons = bot.Role.get_user_role_text_and_buttons(
user_record=other_user_record,
admin_record=user_record
)
await bot.sendPhoto(
chat_id=user_record['telegram_id'],
photo=photo_record['telegram_file_id'],
caption=caption,
reply_markup=make_inline_keyboard(
buttons=buttons
),
parse_mode='HTML'
)
if text:
return dict(
text=result,
@@ -570,18 +800,24 @@ def init(telegram_bot: Bot,
authorization_messages['auth_command']['description']
),
authorization_level='moderator')
async def authorization_command(bot, update, user_record):
return await _authorization_command(bot, update, user_record)
async def authorization_command(bot, update, user_record, language):
return await _authorization_command(bot=bot, update=update,
user_record=user_record,
language=language)
@telegram_bot.button('auth:///',
description=authorization_messages['auth_button']['description'],
separator='|',
authorization_level='moderator')
async def authorization_button(bot, update, user_record, data):
return await _authorization_button(bot, update, user_record, data)
async def authorization_button(bot, update, user_record, language, data):
return await _authorization_button(bot=bot, update=update,
user_record=user_record,
language=language, data=data)
@telegram_bot.command('/ban', aliases=[], show_in_keyboard=False,
description=authorization_messages['ban_command']['description'],
authorization_level='moderator')
async def ban_command(bot, update, user_record):
return await _authorization_command(bot, update, user_record, mode='ban')
async def ban_command(bot, update, user_record, language):
return await _authorization_command(bot=bot, update=update,
user_record=user_record,
language=language, mode='ban')

File diff suppressed because it is too large Load Diff

199
davtelepot/cli.py Normal file
View File

@@ -0,0 +1,199 @@
import argparse
import asyncio
import logging
import os.path
import sys
from typing import Any, Dict, Union
import davtelepot.authorization as authorization
import davtelepot.administration_tools as administration_tools
import davtelepot.helper as helper
from davtelepot.bot import Bot
from davtelepot.utilities import (get_cleaned_text, get_secure_key,
get_user, join_path, json_read, json_write,
line_drawing_unordered_list)
def dir_path(path):
if os.path.isdir(path) and os.access(path, os.W_OK):
return path
else:
raise argparse.ArgumentTypeError(f"`{path}` is not a valid path")
def get_cli_arguments() -> Dict[str, Any]:
default_path = join_path(os.path.dirname(__file__), 'data')
cli_parser = argparse.ArgumentParser(
description='Run a davtelepot-powered Telegram bot from command line.',
allow_abbrev=False,
)
cli_parser.add_argument('-a', '--action', type=str,
default='run',
required=False,
help='Action to perform (currently supported: run).')
cli_parser.add_argument('-p', '--path', type=dir_path,
default=default_path,
required=False,
help='Folder to store secrets, data and log files.')
cli_parser.add_argument('-l', '--log_file', type=argparse.FileType('a'),
default=None,
required=False,
help='File path to store full log')
cli_parser.add_argument('-e', '--error_log_file', type=argparse.FileType('a'),
default=None,
required=False,
help='File path to store only error log')
cli_parser.add_argument('-t', '--token', type=str,
required=False,
help='Telegram bot token (you may get one from t.me/botfather)')
cli_parsed_arguments = vars(cli_parser.parse_args())
for key in cli_parsed_arguments:
if key.endswith('_file') and cli_parsed_arguments[key]:
cli_parsed_arguments[key] = cli_parsed_arguments[key].name
for key, default in {'error_log_file': "davtelepot.errors",
'log_file': "davtelepot.log"}.items():
if cli_parsed_arguments[key] is None:
cli_parsed_arguments[key] = join_path(cli_parsed_arguments['path'], default)
return cli_parsed_arguments
def set_loggers(log_file: str = 'davtelepot.log',
error_log_file: str = 'davtelepot.errors'):
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG)
log_formatter = logging.Formatter(
"%(asctime)s [%(module)-10s %(levelname)-8s] %(message)s",
style='%'
)
file_handler = logging.FileHandler(log_file, mode="a", encoding="utf-8")
file_handler.setFormatter(log_formatter)
file_handler.setLevel(logging.DEBUG)
root_logger.addHandler(file_handler)
file_handler = logging.FileHandler(error_log_file, mode="a", encoding="utf-8")
file_handler.setFormatter(log_formatter)
file_handler.setLevel(logging.ERROR)
root_logger.addHandler(file_handler)
console_handler = logging.StreamHandler()
console_handler.setFormatter(log_formatter)
console_handler.setLevel(logging.DEBUG)
root_logger.addHandler(console_handler)
async def elevate_to_admin(bot: Bot, update: dict, user_record: dict,
secret: str) -> Union[str, None]:
text = get_cleaned_text(update=update, bot=bot,
replace=['00elevate_', 'elevate '])
if text == secret:
bot.db['users'].upsert(dict(id=user_record['id'], privileges=1), ['id'])
return "👑 You have been granted full powers! 👑"
else:
print(f"The secret entered (`{text}`) is wrong. Enter `{secret}` instead.")
def allow_elevation_to_admin(telegram_bot: Bot) -> None:
secret = get_secure_key(length=15)
@telegram_bot.additional_task('BEFORE')
async def print_secret():
await telegram_bot.get_me()
logging.info(f"To get administration privileges, enter code {secret} "
f"or click here: https://t.me/{telegram_bot.name}?start=00elevate_{secret}")
@telegram_bot.command(command='/elevate', aliases=['00elevate_'], show_in_keyboard=False,
authorization_level='anybody')
async def _elevate_to_admin(bot, update, user_record):
return await elevate_to_admin(bot=bot, update=update,
user_record=user_record,
secret=secret)
return
def send_single_message(telegram_bot: Bot):
records = []
text, last_text = '', ''
offset = 0
max_shown = 3
while True:
if text == '+' and len(records) > max_shown:
offset += 1
elif offset > 0 and text == '-':
offset -= 1
else:
offset = 0
if text in ('+', '-'):
text = last_text
condition = (f"WHERE username LIKE '%{text}%' "
f"OR first_name LIKE '%{text}%' "
f"OR last_name LIKE '%{text}%' ")
records = list(telegram_bot.db.query("SELECT username, first_name, "
"last_name, telegram_id "
"FROM users "
f"{condition} "
f"LIMIT {max_shown+1} "
f"OFFSET {offset*max_shown} "))
if len(records) == 1 and offset == 0:
break
last_text = text
print("=== Users ===",
line_drawing_unordered_list(
list(map(lambda x: get_user(x, False),
records[:max_shown]))
+ (['...'] if len(records) >= max_shown else [])
),
sep='\n')
text = input("Select a recipient: write part of their name.\t\t")
while True:
text = input(f"Write a message for {get_user(records[0], False)}\t\t")
if input("Should I send it? Y to send, anything else cancel\t\t").lower() == "y":
break
async def send_and_print_message():
sent_message = await telegram_bot.send_one_message(chat_id=records[0]['telegram_id'], text=text)
print(sent_message)
asyncio.run(send_and_print_message())
return
def run_from_command_line():
arguments = get_cli_arguments()
stored_arguments_file = os.path.join(arguments['path'],
'cli_args.json')
for key, value in json_read(file_=stored_arguments_file,
default={}).items():
if key not in arguments or not arguments[key]:
arguments[key] = value
set_loggers(**{k: v
for k, v in arguments.items()
if k in ('log_file', 'error_log_file')})
if 'error_log_file' in arguments:
Bot.set_class_errors_file_path(file_path=arguments['error_log_file'])
if 'log_file' in arguments:
Bot.set_class_log_file_path(file_path=arguments['log_file'])
if 'path' in arguments:
Bot.set_class_path(arguments['path'])
if 'token' in arguments and arguments['token']:
token = arguments['token']
else:
token = input("Enter bot Token:\t\t")
arguments['token'] = token
json_write(arguments, stored_arguments_file)
bot = Bot(token=token, database_url=join_path(arguments['path'], 'bot.db'))
action = arguments['action'] if 'action' in arguments else 'run'
if action == 'run':
administration_tools.init(telegram_bot=bot)
authorization.init(telegram_bot=bot)
allow_elevation_to_admin(telegram_bot=bot)
helper.init(telegram_bot=bot)
exit_state = Bot.run(**{k: v
for k, v in arguments.items()
if k in ('local_host', 'port')})
sys.exit(exit_state)
if action == 'send':
try:
send_single_message(telegram_bot=bot)
except KeyboardInterrupt:
print("\nExiting...")

View File

@@ -2,6 +2,7 @@
# Standard library modules
import logging
from typing import Tuple
# Third party modules
import dataset
@@ -26,7 +27,7 @@ class ObjectWithDatabase(object):
"""Instantiate object and open connection with database."""
if database_url is None:
database_url = 'database.db'
if ':///' not in database_url:
if '://' not in database_url:
# Default database engine is sqlite, which operates on a
# single-file database having `.db` extension
if not database_url.endswith('.db'):
@@ -71,3 +72,32 @@ class ObjectWithDatabase(object):
)
except Exception as e:
logging.error(f"{e}")
def add_table_and_columns_if_not_existent(self,
table_name: str,
columns: Tuple[
Tuple[str,
dataset.database.Types],
...] = None):
"""Create table (if it does not exist) and add given columns (if missing).
@param table_name: Table name (string)
@param columns: Table columns as tuples of column name and type
@return: None
"""
if table_name not in self.db.tables:
table = self.db.create_table(table_name=table_name)
logging.info(f"Created table `{table_name}`")
else:
table = self.db[table_name]
if columns is None:
columns = []
for column_name, column_type in columns:
if not table.has_column(column_name):
table.create_column(
column_name,
column_type
)
logging.info(f"Added column `{column_name}` "
f"(type `{column_type}`) "
f"to table `{table_name}`")

View File

@@ -1,11 +1,14 @@
"""Make a self-consistent bot help section."""
# Project modules
from .bot import Bot
from .messages import default_help_messages
from .utilities import (
from collections import OrderedDict
from davtelepot.bot import Bot
from davtelepot.messages import default_help_messages
from davtelepot.utilities import (
get_cleaned_text, make_inline_keyboard,
make_lines_of_buttons, make_button
make_lines_of_buttons, make_button,
recursive_dictionary_update
)
@@ -233,27 +236,35 @@ async def _help_button(bot, update, user_record, data):
return result
async def _start_command(bot, update, user_record):
async def _start_command(bot: Bot, update: dict,
user_record: OrderedDict, language: str):
text = get_cleaned_text(update=update, bot=bot, replace=['start'])
if not text:
return await _help_command(bot, update, user_record)
update['text'] = text
await bot.text_message_handler(
update=update,
user_record=None
user_record=user_record,
language=language
)
return
def init(telegram_bot, help_messages=None):
def init(telegram_bot: Bot, help_messages: dict = None):
"""Assign parsers, commands, buttons and queries to given `bot`."""
if help_messages is None:
help_messages = default_help_messages
else:
help_messages = recursive_dictionary_update(
default_help_messages.copy(),
help_messages.copy()
)
telegram_bot.messages['help'] = help_messages
@telegram_bot.command("/start", authorization_level='everybody')
async def start_command(bot, update, user_record):
return await _start_command(bot, update, user_record)
async def start_command(bot, update, user_record, language):
return await _start_command(bot=bot, update=update,
user_record=user_record, language=language)
@telegram_bot.command(command='/help', aliases=['00help'],
reply_keyboard_button=help_messages['help_command'][

View File

@@ -6,8 +6,8 @@ from collections import OrderedDict
import logging
# Project modules
from .messages import default_language_messages
from .utilities import extract, make_button, make_inline_keyboard
from davtelepot.messages import default_language_messages
from davtelepot.utilities import extract, make_button, make_inline_keyboard
class MultiLanguageObject(object):

View File

@@ -14,10 +14,24 @@ davtelepot_messages = {
"Questo messaggio è troppo lungo per essere inviato come "
"messaggi separati.",
}
}
},
'part': {
'en': "part",
'it': "parte",
},
}
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': {
'button': {
'en': "↩️ Cancel",
@@ -32,14 +46,73 @@ default_admin_messages = {
'it': "annulla",
},
},
'config_command': {
'description': {
'en': "Add a variable to the configuration file",
'it': "Aggiungi una variabile al file di configurazione",
},
'help_section': None,
'instructions': {
'en': "<b>Config 🔧</b>\n\n"
"<i>Send me a new configuration variable, for example:</i>\n"
"<code>variable = 2</code>",
'it': "<b>Configurazione 🔧</b>\n\n"
"<i>Mandami una variabile da aggiungere al file di "
"configurazione, per esempio:</i>\n"
"<code>variabile = 2</code>",
},
'invalid_input': {
'en': "<b>❌ Invalid input 🔧</b>\n\n"
"<i>Send me a new configuration variable, for example:</i>\n"
"<code>int_var = 2\n"
"str_var = 'test'\n"
"bool_var = False\n"
"float_var = 3.5</code>",
'it': "<b>❌ Configurazione scorretta 🔧</b>\n\n"
"<i>Mandami una variabile da aggiungere al file di "
"configurazione, per esempio:</i>\n"
"<code>var_int = 2\n"
"var_str = 'test'\n"
"var_bool = False\n"
"var_float = 3.5</code>",
},
'reply_keyboard_button': {
'en': "Config 🔧",
'it': "Configurazione 🔧",
},
'success': {
'en': "<b>✔️ Configuration updated 🔧</b>\n\n"
"The following variable has been added to the configuration "
"file:\n"
"<code>{new_variable}</code>\n\n"
"/restart to apply",
'it': "<b>✔️ Configurazione aggiornata 🔧</b>\n\n"
"La seguente variabile è stata aggiunta al file di "
"configurazione:\n"
"<code>{new_variable}</code>\n\n"
"Per rendere effettive le modifiche fai /restart",
},
},
'confirm': {
'en': "🔄 Click again to confirm",
'it': "🔄 Clicka di nuovo per confermare",
},
'db_command': {
'db_sent': {
'en': "Database sent.",
'it': "Database inviato.",
},
'description': {
'en': "Ask for bot database via Telegram",
'it': "Ricevi il database del bot via Telegram"
'it': "Ricevi il database del bot via Telegram",
},
'error': {
'en': "Error sending database.",
'it': "Errore durante l'invio del database.",
},
'file_caption': {
'en': "Here is bot database.",
'it': "Ecco il database!"
},
'not_sqlite': {
'en': "Only SQLite databases may be sent via Telegram, since they "
@@ -49,14 +122,6 @@ default_admin_messages = {
"in quanto composti di un solo file.\n"
"Questo bot ha invece un database `{db_type}`."
},
'file_caption': {
'en': "Here is bot database.",
'it': "Ecco il database!"
},
'db_sent': {
'en': "Database sent.",
'it': "Database inviato."
}
},
'error': {
'text': {
@@ -71,12 +136,12 @@ default_admin_messages = {
},
'no_log': {
'en': "Sorry but no errors log file is set.\n"
"To set it, use `bot.set_errors_file_name` instance method"
"To set it, use `bot.set_errors_file_name` instance method "
"or `Bot.set_class_errors_file_name` class method.",
'it': "Spiacente ma il file di log degli errori non è stato "
"impostato.\n"
"Per impostarlo, usa il metodo d'istanza "
"`bot.set_errors_file_name` o il metodo di classe"
"`bot.set_errors_file_name` o il metodo di classe "
"`Bot.set_class_errors_file_name`."
},
'empty_log': {
@@ -701,17 +766,35 @@ default_authorization_messages = {
}
},
'auth_button': {
'description': {
'en': "Edit user permissions",
'it': "Cambia il grado di autorizzazione di un utente"
'appointed': {
'en': "Permission granted",
'it': "Permesso conferito"
},
'browse': {
'browse_button_az': {
'en': "🆎 Users A-Z",
'it': "🆎 Utenti A-Z",
},
'browse_button_by_role': {
'en': "👑 Users by role",
'it': "👑 Utenti per ruolo",
},
},
'back_to_user': {
'en': "Back to user",
'it': "Torna all'utente"
},
'confirm': {
'en': "Are you sure?",
'it': "Sicuro sicuro?"
},
'back_to_user': {
'en': "Back to user",
'it': "Torna all'utente"
'description': {
'en': "Edit user permissions",
'it': "Cambia il grado di autorizzazione di un utente"
},
'no_change': {
'en': "No change suggested!",
'it': "È già così!"
},
'permission_denied': {
'user': {
@@ -724,14 +807,10 @@ default_authorization_messages = {
'it': "Non hai l'autorità di conferire questo permesso!"
}
},
'no_change': {
'en': "No change suggested!",
'it': "È già così!"
'profile_picture_button': {
'en': "🖼 Profile picture",
'it': "🖼 Foto profilo",
},
'appointed': {
'en': "Permission granted",
'it': "Permesso conferito"
}
},
}
@@ -973,6 +1052,12 @@ default_talk_messages = {
'en': 'End session?',
'it': 'Chiudere la sessione?',
},
'error': {
'text': {
'en': "❌️ Error!",
'it': "❌️ Errore!"
},
},
'help_text': {
'en': 'Press the button to search for user.',
'it': 'Premi il pulsante per scegliere un utente.',

View File

@@ -4,12 +4,10 @@
import asyncio
import datetime
# Third party modules
import davtelepot
# Project modules
from .messages import default_suggestion_messages
from .utilities import (
import davtelepot
from davtelepot.messages import default_suggestion_messages
from davtelepot.utilities import (
async_wrapper, get_cleaned_text, make_button,
make_inline_keyboard, send_csv_file
)
@@ -262,7 +260,7 @@ def init(telegram_bot: davtelepot.bot.Bot, suggestion_messages=None):
)
table.create_column(
'suggestion',
types.text
types.string(2048)
)
table.create_column(
'created',

View File

View File

@@ -0,0 +1,66 @@
"""Use this tool to merge davtelepot-split files.
Documents larger than 50 MB are automatically split by `send_document` method
and can be merged with this tool.
Example:
python3 -m davtelepot.tools.merge_files my_file.pdf
"""
import argparse
import glob
import logging
import os
def merge_files(input_file_path, output_file_path):
input_directory = os.path.dirname(os.path.abspath(input_file_path))
input_file_name = os.path.basename(os.path.abspath(input_file_path))
if output_file_path:
output_directory = os.path.dirname(os.path.abspath(output_file_path))
output_file_name = os.path.basename(os.path.abspath(output_file_path))
else:
output_directory = input_directory
output_file_name = input_file_name + 'out'
with open(os.path.join(output_directory, output_file_name), 'wb') as output_file:
for file_name in sorted(glob.glob(os.path.join(input_directory, input_file_name) + '*')):
with open(file_name, 'rb') as input_file:
output_file.write(input_file.read())
def main():
# noinspection SpellCheckingInspection
log_formatter = logging.Formatter(
"%(asctime)s [%(module)-15s %(levelname)-8s] %(message)s",
style='%'
)
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG)
console_handler = logging.StreamHandler()
console_handler.setFormatter(log_formatter)
console_handler.setLevel(logging.DEBUG)
root_logger.addHandler(console_handler)
# Parse command-line arguments
cli_parser = argparse.ArgumentParser(description='Merge split files',
allow_abbrev=False)
cli_parser.add_argument('--input', '--file-path', '--path',
'-i', '-f', '-p',
type=str,
required=True,
help='Input file name (except ` - part n`)')
cli_parser.add_argument('--output', '--output-file-path', '--out',
'-o',
type=str,
default=None,
required=False,
help='Output file name (defaults to input file name)')
cli_arguments = vars(cli_parser.parse_args())
input_file_path = cli_arguments['input']
output_file_path = cli_arguments['output']
merge_files(input_file_path=input_file_path,
output_file_path=output_file_path)
if __name__ == '__main__':
main()

View File

@@ -13,11 +13,12 @@ from collections import OrderedDict
from typing import List, Union
# Project modules
from .api import TelegramError
from .bot import Bot
from .messages import default_useful_tools_messages
from .utilities import (get_cleaned_text, get_user, make_button,
make_inline_keyboard, recursive_dictionary_update, )
from davtelepot.api import TelegramError
from davtelepot.bot import Bot
from davtelepot.messages import default_useful_tools_messages
from davtelepot.utilities import (get_cleaned_text, get_user, make_button,
make_inline_keyboard,
recursive_dictionary_update, )
def get_calc_buttons() -> OrderedDict:
@@ -664,7 +665,7 @@ def init(telegram_bot: Bot, useful_tools_messages=None):
)
table.create_column(
'expression',
types.string
types.string(8192)
)
@telegram_bot.command(command='/calc',

View File

@@ -16,12 +16,12 @@ import string
import time
from difflib import SequenceMatcher
from typing import Tuple, Union
# Third party modules
from typing import Union
import aiohttp
from aiohttp import web
import dataset
from bs4 import BeautifulSoup
@@ -504,7 +504,9 @@ async def async_wrapper(coroutine, *args1, **kwargs1):
)
await my_coroutine(a=1, b=5)
asyncio.get_event_loop().run_until_complete(main())
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
asyncio.run(main())
```
"""
async def wrapped_coroutine(*args2, bot=None, update=None, user_record=None, **kwargs2):
@@ -1250,7 +1252,7 @@ def parse_datetime_interval_string(text):
result_text.pop()
if len(result_text) > 0 and result_text[-1].lower() in TIME_WORDS:
result_text.pop()
result_text = escape_html_chars(
result_text = clean_html_string(
' '.join(result_text)
)
parsers = list(
@@ -1329,6 +1331,22 @@ MONTH_NAMES_ITA[10] = "ottobre"
MONTH_NAMES_ITA[11] = "novembre"
MONTH_NAMES_ITA[12] = "dicembre"
allowed_html_tags = ['b', 'strong',
'i', 'em',
'u', 'ins',
's', 'strike', 'del',
'span', 'tg-spoiler',
'a',
'code', 'pre']
HTML_SYMBOLS = collections.OrderedDict()
HTML_SYMBOLS["&"] = "&amp;"
HTML_SYMBOLS["<"] = "&lt;"
HTML_SYMBOLS[">"] = "&gt;"
HTML_SYMBOLS["\""] = "&quot;"
html_numeric_code_regex = re.compile(r'&amp;(?P<code>#\d{2,3};)')
def beautytd(td):
"""Format properly timedeltas."""
@@ -1409,67 +1427,57 @@ def beautydt(dt):
return result
HTML_SYMBOLS = MyOD()
HTML_SYMBOLS["&"] = "&amp;"
HTML_SYMBOLS["<"] = "&lt;"
HTML_SYMBOLS[">"] = "&gt;"
HTML_SYMBOLS["\""] = "&quot;"
HTML_SYMBOLS["&lt;b&gt;"] = "<b>"
HTML_SYMBOLS["&lt;/b&gt;"] = "</b>"
HTML_SYMBOLS["&lt;i&gt;"] = "<i>"
HTML_SYMBOLS["&lt;/i&gt;"] = "</i>"
HTML_SYMBOLS["&lt;code&gt;"] = "<code>"
HTML_SYMBOLS["&lt;/code&gt;"] = "</code>"
HTML_SYMBOLS["&lt;pre&gt;"] = "<pre>"
HTML_SYMBOLS["&lt;/pre&gt;"] = "</pre>"
HTML_SYMBOLS["&lt;a href=&quot;"] = "<a href=\""
HTML_SYMBOLS["&quot;&gt;"] = "\">"
HTML_SYMBOLS["&lt;/a&gt;"] = "</a>"
def clean_html_string(text: str) -> str:
"""Escape HTML symbols, unless part of a valid tag or numeric code character.
HTML_TAGS = [
None, "<b>", "</b>",
None, "<i>", "</i>",
None, "<code>", "</code>",
None, "<pre>", "</pre>",
None, "<a href=\"", "\">", "</a>",
None
]
def remove_html_tags(text):
"""Remove HTML tags from `text`."""
for tag in HTML_TAGS:
if tag is None:
continue
text = text.replace(tag, '')
Find valid HTML tags;
if there are any, choose the first occurring and call the function
recursively on what comes before the tag, inside the tag and after the
tag, preserving the tag opening and close as they are;
if there aren't any, escape HTML symbols except for `&` in HTML numeric code
characters (`&#` followed by 2 or 3 digits followed by `;`).
"""
first_match = None
for tag in allowed_html_tags:
if tag in ('a', ): # <a> must have href attribute
attribute = r" href=\".*\""
elif tag in ('span', ): # <span> must have class attribute with "tg-spoiler" value
attribute = r" class=\"tg-spoiler\""
elif tag in ('code',): # <code> may have a class with a programming language as value
attribute = r"( class=\".*\")?"
else:
attribute = ""
match = re.search(
rf'(?P<opening><{tag}{attribute}>)'
rf'(?P<body>.*?)'
rf'(?P<close></{tag}>)',
text,
flags=re.DOTALL
)
if match and (first_match is None or match.start() < first_match.start()):
first_match = match
if first_match is not None:
groups = first_match.groupdict()
text = (f"{clean_html_string(text[:first_match.start()])}"
f"{groups['opening']}{clean_html_string(groups['body'])}{groups['close']}"
f"{clean_html_string(text[first_match.end():])}")
else:
for key, value in HTML_SYMBOLS.items():
text = text.replace(key, value)
if re.search(html_numeric_code_regex, text):
text = re.sub(html_numeric_code_regex, r'&\g<code>', text)
return text
def escape_html_chars(text):
"""Escape HTML chars if not part of a tag."""
for s, r in HTML_SYMBOLS.items():
text = text.replace(s, r)
copy = text
expected_tag = None
while copy:
min_ = min(
(
dict(
position=copy.find(tag) if tag in copy else len(copy),
tag=tag
)
for tag in HTML_TAGS
if tag
),
key=lambda x: x['position'],
default=0
)
if min_['position'] == len(copy):
break
if expected_tag and min_['tag'] != expected_tag:
return text.replace('<', '_').replace('>', '_')
expected_tag = HTML_TAGS[HTML_TAGS.index(min_['tag'])+1]
copy = extract(copy, min_['tag'])
logging.error("`escape_html_chars` function deprecated, use `clean_html_string` instead.")
return clean_html_string(text)
def remove_html_tags(text):
"""Remove HTML tags from `text`."""
for tag in allowed_html_tags:
text = re.sub(rf'</?{tag}( (href|class)=\".*\")?>', '', text)
return text
@@ -1566,7 +1574,7 @@ def run_aiohttp_server(app, *args, **kwargs):
"""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
web.run_app(app, *args, **kwargs)
aiohttp.web.run_app(app, *args, **kwargs)
def custom_join(_list, joiner, final=None):
@@ -1609,8 +1617,9 @@ async def dummy_coroutine(*args, **kwargs):
return
async def send_csv_file(bot, chat_id, query, caption=None,
file_name='File.csv', user_record=None, update=None):
async def send_csv_file(bot, chat_id: int, query: str, caption: str = None,
file_name: str = 'File.csv', language: str = None,
user_record=None, update=None):
"""Run a query on `bot` database and send result as CSV file to `chat_id`.
Optional parameters `caption` and `file_name` may be passed to this
@@ -1618,6 +1627,9 @@ async def send_csv_file(bot, chat_id, query, caption=None,
"""
if update is None:
update = dict()
if language is None:
language = bot.get_language(update=update,
user_record=user_record)
try:
with bot.db as db:
record = db.query(
@@ -1633,14 +1645,14 @@ async def send_csv_file(bot, chat_id, query, caption=None,
except Exception as e:
text = "{message}\n{e}".format(
message=bot.get_message('admin', 'query_button', 'error',
user_record=user_record, update=update),
language=language),
e=e
)
for x, y in {'&lt;': '<', '\n': '\r\n'}.items():
text = text.replace(x, y)
if len(text) == 0:
text = bot.get_message('admin', 'query_button', 'empty_file',
user_record=user_record, update=update)
language=language)
with io.BytesIO(text.encode('utf-8')) as f:
f.name = file_name
return await bot.send_document(
@@ -1654,7 +1666,8 @@ async def send_part_of_text_file(bot, chat_id, file_path, caption=None,
file_name='File.txt', user_record=None,
update=None,
reversed_=True,
limit=None):
limit=None,
encoding='utf-8'):
"""Send `lines` lines of text file via `bot` in `chat_id`.
If `reversed`, read the file from last line.
@@ -1665,7 +1678,7 @@ async def send_part_of_text_file(bot, chat_id, file_path, caption=None,
if update is None:
update = dict()
try:
with open(file_path, 'r') as log_file:
with open(file_path, 'r', encoding=encoding) as log_file:
lines = log_file.readlines()
if reversed_:
lines = lines[::-1]
@@ -1700,3 +1713,54 @@ def recursive_dictionary_update(one: dict, other: dict) -> dict:
else:
one[key] = val
return one
async def aio_subprocess_shell(command: str,
stdout=None,
stderr=None) -> Tuple[str, str]:
"""Run `command` in a subprocess shell.
Await for the subprocess to end and return standard error and output.
On error, log errors.
If `stdout` and/or `stderr` are given, use them as output and/or error pipes.
Example of non-null standard output/error pipe: asyncio.subprocess.PIPE
"""
try:
_subprocess = await asyncio.create_subprocess_shell(
command,
stdout=stdout,
stderr=stderr
)
stdout, stderr = await _subprocess.communicate()
if stdout:
stdout = stdout.decode().strip()
if stderr:
stderr = stderr.decode().strip()
except Exception as e:
logging.error(
"Exception {e}:\n{o}\n{er}".format(
e=e,
o=(stdout.decode().strip() if stdout else ''),
er=(stderr.decode().strip() if stderr else '')
)
)
return stdout, stderr
def join_path(*args):
return os.path.abspath(os.path.join(*args))
def add_table_and_columns_if_not_existent(database: dataset.Database,
table_name: str,
columns: Tuple[Tuple, ...]):
if table_name not in database.tables:
table = database.create_table(table_name=table_name)
else:
table = database[table_name]
for column_name, column_type in columns:
if not table.has_column(column_name):
table.create_column(
column_name,
column_type
)

View File

@@ -46,7 +46,7 @@ import sys
try:
from davtelepot.bot import Bot
from davtelepot.utilities import (
get_cleaned_text, make_inline_keyboard, make_button
get_cleaned_text, get_user, make_inline_keyboard, make_button
)
except ImportError:
logging.error(
@@ -64,23 +64,27 @@ except ImportError:
path = os.path.dirname(os.path.abspath(__file__))
def initialize_bot(bot):
def initialize_bot(telegram_bot):
"""Take a bot and set commands."""
bot.set_callback_data_separator('|')
telegram_bot.set_callback_data_separator('|')
@bot.command(command='foo', aliases=['Foo'], show_in_keyboard=True,
@telegram_bot.command(command='foo', aliases=['Foo'],
show_in_keyboard=True,
description="Reply 'bar' to 'foo'",
authorization_level='everybody')
async def foo_command(bot, update, user_record):
return 'Bar!'
return (
"Bar!\n\n"
f"You wrote: {update['text']}\n"
f"I am @{bot.name} and you are {get_user(user_record)}"
)
def is_bar_text_message(text):
return text.startswith('bar')
def is_bar_text_message(lowered_text):
return lowered_text.startswith('bar')
@bot.parser(condition=is_bar_text_message,
@telegram_bot.parser(condition=is_bar_text_message,
description='Reply Foo to users who write Bar',
authorization_level='everybody',
argument='text')
authorization_level='everybody')
async def bar_parser(bot, update):
text_except_foo = get_cleaned_text(update, bot, ['bar'])
return f"Foo!\n{text_except_foo}"
@@ -99,16 +103,16 @@ def initialize_bot(bot):
3
)
@bot.command(command='buttons')
@telegram_bot.command(command='buttons')
async def buttons_command():
return dict(
text="Press a button!",
reply_markup=get_keyboard()
)
@bot.button(prefix='button:///', separator='|',
@telegram_bot.button(prefix='button:///', separator='|',
authorization_level='everybody')
async def buttons_button(bot, update, user_record, data):
async def buttons_button(data):
button_number = data[0]
return dict(
edit=dict(
@@ -120,12 +124,12 @@ def initialize_bot(bot):
def starts_with_a(text):
return text.startswith('a')
@bot.query(
@telegram_bot.query(
condition=starts_with_a,
description='Mirror query text if it starts with letter `a`',
authorization_level='everybody'
)
async def inline_query(bot, update, user_record):
async def inline_query(update):
return dict(
type='article',
id=10,
@@ -135,7 +139,7 @@ def initialize_bot(bot):
)
)
bot.set_default_inline_query_answer(
telegram_bot.set_default_inline_query_answer(
dict(
type='article',
id=0,
@@ -146,7 +150,7 @@ def initialize_bot(bot):
)
)
bot.set_unknown_command_message(
telegram_bot.set_unknown_command_message(
"<b>Currently supported features</b>\n\n"
"- /foo (or text starting with `foo`): replies `Bar!`.\n"
"- Text starting with `bar`: replies `Foo!` followed by the rest of "
@@ -177,10 +181,10 @@ def _main():
)
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(log_formatter)
consoleHandler.setLevel(logging.DEBUG)
root_logger.addHandler(consoleHandler)
console_handler = logging.StreamHandler()
console_handler.setFormatter(log_formatter)
console_handler.setLevel(logging.DEBUG)
root_logger.addHandler(console_handler)
# Instantiate, initialize and make `simple_bot` run.
simple_bot = Bot(token=simple_bot_token, database_url=f"{path}/bot.db")

View File

@@ -28,7 +28,7 @@ except ImportError:
from a_simple_bot import initialize_bot
# Get path of current script
os.path.dirname(os.path.abspath(__file__))
path = os.path.dirname(os.path.abspath(__file__))
def _main():
@@ -96,10 +96,10 @@ def _main():
)
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(log_formatter)
consoleHandler.setLevel(logging.DEBUG)
root_logger.addHandler(consoleHandler)
console_handler = logging.StreamHandler()
console_handler.setFormatter(log_formatter)
console_handler.setLevel(logging.DEBUG)
root_logger.addHandler(console_handler)
# Instantiate, initialize and make bots run.
webhook_bot = Bot(

View File

@@ -1,10 +0,0 @@
#!/bin/bash
# Merge develop into master and push both branches; checkout to develop at the end.
git checkout master;
git merge develop;
git checkout develop;
git push origin master develop;
git push bitbucket master develop;
git push github master;

7
push_to_remotes.sh Normal file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
# Merge develop into master and push both branches; checkout to develop at the end.
git push origin main develop;
git push bitbucket main develop;
git push github main;

View File

@@ -1,3 +1,5 @@
aiohttp
bs4
dataset
beautifulsoup4
SQLAlchemy

17
update_package.sh Normal file → Executable file
View File

@@ -1,23 +1,24 @@
#!/bin/bash
# Get current directory
this_script_directory=$(cd `dirname $0` && pwd)
this_script_directory=$(cd "$(dirname "$0")" && pwd)
packenv="";
# Python virtual environment directory: packenv variable in my_config.sh
source $this_script_directory/my_config.sh;
source "$this_script_directory"/my_config.sh;
# Ensure the success of importing procedure
if [ -z ${packenv} ];
if [ -z "${packenv}" ];
then
printf "Please set in ""my_config.sh"" the path to bot python virtual environment\n\nExample:\npackenv=""path/to/virtual/env""\n";
exit;
fi
# Merge, push, build and publish package to pypi.org
bash merge_and_push.sh;
# Push, build and publish package to pypi.org
bash "$this_script_directory"/push_to_remotes.sh;
rm -rf "$this_script_directory/build";
rm -rf "$this_script_directory/dist";
rm -rf "$this_script_directory/davtelepot.egg-info";
$packenv/python setup.py sdist;
$packenv/pip wheel --no-index --no-deps --wheel-dir dist dist/*.tar.gz;
$packenv/twine upload --skip-existing dist/*;
"$packenv"/python setup.py sdist;
"$packenv"/pip wheel --no-index --no-deps --wheel-dir dist dist/*.tar.gz;
"$packenv"/twine upload --skip-existing dist/*;