Skip to content

Commit

Permalink
refactor(command): improve readability and maintainability
Browse files Browse the repository at this point in the history
Signed-off-by: Rongrong <i@rong.moe>
Rongronggg9 committed Jul 28, 2024

Verified

This commit was signed with the committer’s verified signature.
Rongronggg9 Rongrong
1 parent bf571d0 commit 587c993
Showing 7 changed files with 604 additions and 383 deletions.
160 changes: 103 additions & 57 deletions src/command/administration.py
Original file line number Diff line number Diff line change
@@ -15,13 +15,12 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from __future__ import annotations
from typing import Union, Optional
from typing import Optional
from typing_extensions import Final

import asyncio
import re
from telethon import events, Button
from telethon.tl.patched import Message
from telethon import Button
from telethon.tl import types
from telethon.utils import get_peer_id

@@ -30,6 +29,7 @@
from ..parsing.post import get_post_from_entry
from .utils import command_gatekeeper, parse_command, logger, parse_customization_callback_data
from . import inner
from .types import *

SELECTED_EMOJI: Final = '🔘'
UNSELECTED_EMOJI: Final = '⚪️'
@@ -38,17 +38,19 @@


@command_gatekeeper(only_manager=True)
async def cmd_set_option(event: Union[events.NewMessage.Event, Message], *_, lang: Optional[str] = None, **__):
async def cmd_set_option(event: TypeEventMsgHint, *_, lang: Optional[str] = None, **__):
kv = parseKeyValuePair.match(event.raw_text)
if not kv: # return options info
options = db.EffectiveOptions.options
msg = (
f'<b>{i18n[lang]["current_options"]}</b>\n\n'
+ '\n'.join(f'<code>{key}</code> = <code>{value}</code> '
f'({i18n[lang]["option_value_type"]}: <code>{type(value).__name__}</code>)'
for key, value in options.items())
+ '\n\n' + i18n[lang]['cmd_set_option_usage_prompt_html']
)
msg = '\n\n'.join((
f'<b>{i18n[lang]["current_options"]}</b>',
'\n'.join(
f'<code>{key}</code> = <code>{value}</code> '
f'({i18n[lang]["option_value_type"]}: <code>{type(value).__name__}</code>)'
for key, value in options.items()
),
i18n[lang]['cmd_set_option_usage_prompt_html'],
))
await event.respond(msg, parse_mode='html')
return
key, value = kv.groups()
@@ -72,18 +74,21 @@ async def cmd_set_option(event: Union[events.NewMessage.Event, Message], *_, lan
env.loop.create_task(inner.utils.update_interval(feed))
logger.info("Flushed the interval of all feeds")

await event.respond(f'<b>{i18n[lang]["option_updated"]}</b>\n'
f'<code>{key}</code> = <code>{value}</code>',
parse_mode='html')
await event.respond(
f'<b>{i18n[lang]["option_updated"]}</b>\n'
f'<code>{key}</code> = <code>{value}</code>',
parse_mode='html',
)


@command_gatekeeper(only_manager=True, only_in_private_chat=False, timeout=None if env.DEBUG else 300)
async def cmd_test(
event: Union[events.NewMessage.Event, Message],
event: TypeEventMsgHint,
*_,
lang: Optional[str] = None,
chat_id: Optional[int] = None,
**__):
**__,
):
chat_id = chat_id or event.chat_id

args = parse_command(event.raw_text)
@@ -123,7 +128,10 @@ async def cmd_test(
return

await asyncio.gather(
*(__send(chat_id, entry, rss_d.feed.title, wf.url) for entry in entries_to_send)
*(
__send(chat_id, entry, rss_d.feed.title, wf.url)
for entry in entries_to_send
)
)

except Exception as e:
@@ -139,26 +147,38 @@ async def __send(chat_id, entry, feed_title, link):


@command_gatekeeper(only_manager=True)
async def cmd_user_info_or_callback_set_user(event: Union[events.NewMessage.Event, Message, events.CallbackQuery.Event],
*_,
lang: Optional[str] = None,
user_id: Optional[int] = None,
**__):
async def cmd_user_info_or_callback_set_user(
event: TypeEventCollectionMsgOrCb,
*_,
lang: Optional[str] = None,
user_id: Optional[int] = None,
**__,
):
"""
command = `/user_info user_id` or `/user_info @username` or `/user_info`
callback data = set_user={user_id},{state}
"""
is_callback = isinstance(event, events.CallbackQuery.Event)
is_callback = isinstance(event, TypeEventCb)
if user_id:
state = None
user_entity_like = user_id
elif is_callback:
user_entity_like, state, _, _ = parse_customization_callback_data(event.data)
assert user_entity_like is not None
state = int(state)
else:
state = None
args = parse_command(event.raw_text, strip_target_chat=False)
if len(args) < 2 or (not args[1].lstrip('-').isdecimal() and not args[1].startswith('@')):
if (
len(args) < 2
or
not (
args[1].lstrip('-').isdecimal()
or
args[1].startswith('@')
)

):
await event.respond(i18n[lang]['cmd_user_info_usage_prompt_html'], parse_mode='html')
return
user_entity_like = int(args[1]) if args[1].lstrip('-').isdecimal() else args[1].lstrip('@')
@@ -192,49 +212,75 @@ async def cmd_user_info_or_callback_set_user(event: Union[events.NewMessage.Even
user.state = state
await user.save()
state = None if user_id in env.MANAGER else user.state
default_sub_limit = (db.EffectiveOptions.user_sub_limit
if user_id > 0
else db.EffectiveOptions.channel_or_group_sub_limit)
default_sub_limit = (
db.EffectiveOptions.user_sub_limit
if user_id > 0
else db.EffectiveOptions.channel_or_group_sub_limit
)
if user_created:
sub_count = 0
sub_limit = default_sub_limit
is_default_limit = True
else:
_, sub_count, sub_limit, is_default_limit = await inner.utils.check_sub_limit(user_id, force_count_current=True)

msg_text = (
f"<b>{i18n[lang]['user_info']}</b>\n\n"
+ (f"{name}\n" if name else '')
+ (f"{user_type} " if user_type else '') + f"<code>{user_id}</code>\n"
+ (f"@{username}\n" if username else '')
+ f"\n{i18n[lang]['sub_count']}: {sub_count}"
+ f"\n{i18n[lang]['sub_limit']}: {sub_limit if sub_limit > 0 else i18n[lang]['sub_limit_unlimited']}"
+ (f" ({i18n[lang]['sub_limit_default']})" if is_default_limit else '')
+ (f"\n{i18n[lang]['participant_count']}: {participant_count}" if participant_count else '')
+ (f"\n\n{i18n[lang]['user_state']}: {i18n[lang][f'user_state_{state}']} "
f"({i18n[lang][f'user_state_description_{state}']})" if state is not None else '')
)
buttons = None if user_id in env.MANAGER else tuple(filter(None, (
*(
(Button.inline(
(SELECTED_EMOJI if user.state == btn_state else UNSELECTED_EMOJI)
+ '{prompt} "{state}"'.format(prompt=i18n[lang]['set_user_state_as'],
state=i18n[lang][f'user_state_{btn_state}']),
data='null' if user.state == btn_state else f"set_user={user_id},{btn_state}"
),)
for btn_state in range(-1, 2)
),
(Button.inline(f"{i18n[lang]['reset_sub_limit_to_default']} "
f"({default_sub_limit if default_sub_limit > 0 else i18n[lang]['sub_limit_unlimited']})",
data=f"reset_sub_limit={user_id}"),) if not is_default_limit else None,
(Button.switch_inline(i18n[lang]['set_sub_limit_to'], query=f'/set_sub_limit {user_id} ', same_peer=True),),
msg_text = '\n\n'.join(filter(None, (
f"<b>{i18n[lang]['user_info']}</b>",
'\n'.join(filter(None, (
name,
(f'{user_type} ' if user_type else '') + f'<code>{user_id}</code>',
f'@{username}' if username else '',
))),
'\n'.join(filter(None, (
f"{i18n[lang]['sub_count']}: {sub_count}",
f"{i18n[lang]['sub_limit']}: {sub_limit if sub_limit > 0 else i18n[lang]['sub_limit_unlimited']}" + (
f" ({i18n[lang]['sub_limit_default']})" if is_default_limit else ''
),
f"{i18n[lang]['participant_count']}: {participant_count}" if participant_count else '',
))),
''
if state is None
else f"{i18n[lang]['user_state']}: {i18n[lang][f'user_state_{state}']} "
f"({i18n[lang][f'user_state_description_{state}']})",
)))
await event.edit(msg_text, parse_mode='html', buttons=buttons) if is_callback \
else await event.respond(msg_text, parse_mode='html', buttons=buttons)
buttons = (
None
if user_id in env.MANAGER
else tuple(filter(None, (
*(
(Button.inline(
'{emoji}{prompt} "{state}"'.format(
emoji=SELECTED_EMOJI if user.state == btn_state else UNSELECTED_EMOJI,
prompt=i18n[lang]['set_user_state_as'],
state=i18n[lang][f'user_state_{btn_state}'],
),
data='null' if user.state == btn_state else f"set_user={user_id},{btn_state}"
),)
for btn_state in range(-1, 2)
),
None
if is_default_limit
else (Button.inline(
f"{i18n[lang]['reset_sub_limit_to_default']} "
f"({default_sub_limit if default_sub_limit > 0 else i18n[lang]['sub_limit_unlimited']})",
data=f"reset_sub_limit={user_id}",
),),
(Button.switch_inline(
i18n[lang]['set_sub_limit_to'],
query=f'/set_sub_limit {user_id} ',
same_peer=True,
),),
)))
)
await (
event.edit(msg_text, parse_mode='html', buttons=buttons)
if is_callback
else event.respond(msg_text, parse_mode='html', buttons=buttons)
)


@command_gatekeeper(only_manager=True)
async def callback_reset_sub_limit(event: events.CallbackQuery.Event, *_, lang: Optional[str] = None, **__):
async def callback_reset_sub_limit(event: TypeEventCb, *_, lang: Optional[str] = None, **__):
"""
callback data = reset_sub_limit={user_id}
"""
@@ -248,7 +294,7 @@ async def callback_reset_sub_limit(event: events.CallbackQuery.Event, *_, lang:


@command_gatekeeper(only_manager=True)
async def cmd_set_sub_limit(event: Union[events.NewMessage.Event, Message], *_, lang: Optional[str] = None, **__):
async def cmd_set_sub_limit(event: TypeEventMsgHint, *_, lang: Optional[str] = None, **__):
"""
command = `/set_sub_limit user_id sub_limit`
"""
389 changes: 231 additions & 158 deletions src/command/customization.py

Large diffs are not rendered by default.

62 changes: 29 additions & 33 deletions src/command/misc.py
Original file line number Diff line number Diff line change
@@ -15,11 +15,10 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from __future__ import annotations
from typing import Optional, Union
from typing import Optional

from contextlib import suppress
from telethon import events, types, Button
from telethon.tl.patched import Message
from telethon.errors import RPCError

from .. import env, db
@@ -29,14 +28,12 @@
)
from ..i18n import i18n, get_commands_list
from . import inner
from .types import *


@command_gatekeeper(only_manager=False, ignore_tg_lang=True)
async def cmd_start(
event: Union[
events.NewMessage.Event, Message,
events.ChatAction.Event,
],
event: TypeEventCollectionMsgOrChatAction,
*_,
lang=None,
**__,
@@ -49,10 +46,7 @@ async def cmd_start(

@command_gatekeeper(only_manager=False)
async def cmd_lang(
event: Union[
events.NewMessage.Event, Message,
events.ChatAction.Event,
],
event: TypeEventCollectionMsgOrChatAction,
*_,
chat_id: Optional[int] = None,
**__,
@@ -66,7 +60,7 @@ async def cmd_lang(

@command_gatekeeper(only_manager=False)
async def callback_set_lang(
event: events.CallbackQuery.Event,
event: TypeEventCb,
*_,
chat_id: Optional[int] = None,
**__,
@@ -75,21 +69,19 @@ async def callback_set_lang(
lang, _ = parse_callback_data_with_page(event.data)
welcome_msg = i18n[lang]['welcome_prompt']
await db.User.update_or_create(defaults={'lang': lang}, id=chat_id)
await set_bot_commands(scope=types.BotCommandScopePeer(await event.get_input_chat()),
lang_code='',
commands=get_commands_list(lang=lang, manager=chat_id in env.MANAGER))
await set_bot_commands(
scope=types.BotCommandScopePeer(await event.get_input_chat()),
lang_code='',
commands=get_commands_list(lang=lang, manager=chat_id in env.MANAGER),
)
logger.info(f'Changed language to {lang} for {chat_id}')
help_button = Button.inline(text=i18n[lang]['cmd_description_help'], data='help')
await event.edit(welcome_msg, buttons=help_button)


@command_gatekeeper(only_manager=False)
async def cmd_or_callback_help(
event: Union[
events.NewMessage.Event, Message,
events.ChatAction.Event,
events.CallbackQuery.Event,
],
event: TypeEventCollectionMsgLike,
*_,
lang: Optional[str] = None,
**__,
@@ -99,19 +91,19 @@ async def cmd_or_callback_help(
msg += '\n\n' + i18n[lang]['usage_in_channel_or_group_prompt_html']
await (
event.respond(msg, parse_mode='html', link_preview=False)
if isinstance(event, events.NewMessage.Event) or not hasattr(event, 'edit')
if isinstance(event, TypeEventMsg) or not hasattr(event, 'edit')
else event.edit(msg, parse_mode='html', link_preview=False)
)


@command_gatekeeper(only_manager=False)
async def cmd_version(event: Union[events.NewMessage.Event, Message], *_, **__):
async def cmd_version(event: TypeEventMsgHint, *_, **__):
await event.respond(env.VERSION)


@command_gatekeeper(only_manager=False)
async def callback_cancel(
event: events.CallbackQuery.Event,
event: TypeEventCb,
*_,
lang: Optional[str] = None,
**__,
@@ -121,7 +113,7 @@ async def callback_cancel(

@command_gatekeeper(only_manager=False, allow_in_old_fashioned_groups=True)
async def callback_get_group_migration_help(
event: events.CallbackQuery.Event,
event: TypeEventCb,
*_,
**__,
): # callback data: get_group_migration_help={lang_code}
@@ -136,14 +128,14 @@ async def callback_get_group_migration_help(


# bypassing command gatekeeper
async def callback_null(event: events.CallbackQuery.Event): # callback data = null
async def callback_null(event: TypeEventCb): # callback data = null
await event.answer(cache_time=3600)
raise events.StopPropagation


@command_gatekeeper(only_manager=False)
async def callback_del_buttons(
event: events.CallbackQuery.Event,
event: TypeEventCb,
*_,
**__,
): # callback data = del_buttons
@@ -154,7 +146,7 @@ async def callback_del_buttons(

@command_gatekeeper(only_manager=False, allow_in_others_private_chat=False, quiet=True)
async def inline_command_constructor(
event: events.InlineQuery.Event,
event: TypeEventInline,
*_,
lang: Optional[str] = None,
**__,
@@ -163,11 +155,15 @@ async def inline_command_constructor(
builder = event.builder
text = query.query.strip()
if not text:
await event.answer(switch_pm=i18n[lang]['permission_denied_input_command'],
switch_pm_param=str(event.id),
cache_time=3600,
private=False)
await event.answer(
switch_pm=i18n[lang]['permission_denied_input_command'],
switch_pm_param=str(event.id),
cache_time=3600,
private=False,
)
return
await event.answer(results=[builder.article(title=text, text=text)],
cache_time=3600,
private=False)
await event.answer(
results=[builder.article(title=text, text=text)],
cache_time=3600,
private=False,
)
103 changes: 65 additions & 38 deletions src/command/opml.py
Original file line number Diff line number Diff line change
@@ -15,12 +15,12 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from __future__ import annotations
from typing import Union, Optional
from typing import Optional

import listparser
from datetime import datetime
from datetime import datetime, timezone
from functools import partial
from telethon import events, Button
from telethon import Button
from telethon.tl import types
from telethon.tl.patched import Message

@@ -29,57 +29,69 @@
from ..aio_helper import run_async
from ..i18n import i18n
from . import inner
from .types import *
from .utils import command_gatekeeper, logger, send_success_and_failure_msg, get_callback_tail, check_sub_limit


@command_gatekeeper(only_manager=False)
async def cmd_import(event: Union[events.NewMessage.Event, Message],
*_,
chat_id: Optional[int] = None,
lang: Optional[str] = None,
**__):
async def cmd_import(
event: TypeEventMsgHint,
*_,
chat_id: Optional[int] = None,
lang: Optional[str] = None,
**__,
):
chat_id = chat_id or event.chat_id

await check_sub_limit(event, user_id=chat_id, lang=lang)

await event.respond(
i18n[lang]['send_opml_prompt'] + (
'\n\n'
+ i18n[lang]['import_for_channel_or_group_prompt'] if event.is_private else ''
),
'\n\n'.join(filter(None, (
i18n[lang]['send_opml_prompt'],
i18n[lang]['import_for_channel_or_group_prompt'] if event.is_private else '',
))),
buttons=(
Button.force_reply(
single_use=True,
selective=True,
placeholder=i18n[lang]['send_opml_reply_placeholder']
) if event.is_group and chat_id == event.chat_id else None
placeholder=i18n[lang]['send_opml_reply_placeholder'],
)
if event.is_group and chat_id == event.chat_id
else None
),
reply_to=event.id if event.is_group else None
)


@command_gatekeeper(only_manager=False)
async def cmd_export(event: Union[events.NewMessage.Event, Message],
*_,
lang: Optional[str] = None,
chat_id: Optional[int] = None,
**__):
async def cmd_export(
event: TypeEventMsgHint,
*_,
lang: Optional[str] = None,
chat_id: Optional[int] = None,
**__,
):
chat_id = chat_id or event.chat_id
opml_file = await inner.sub.export_opml(chat_id)
if opml_file is None:
await event.respond(i18n[lang]['no_subscription'])
return
await event.respond(file=opml_file,
attributes=(types.DocumentAttributeFilename(
f"RSStT_export_{datetime.utcnow().strftime('%Y%m%d%H%M%S')}.opml"),))
await event.respond(
file=opml_file,
attributes=(
types.DocumentAttributeFilename(f"RSStT_export_{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')}.opml"),
),
)


@command_gatekeeper(only_manager=False, timeout=300)
async def opml_import(event: Union[events.NewMessage.Event, Message],
*_,
lang: Optional[str] = None,
chat_id: Optional[int] = None,
**__):
async def opml_import(
event: TypeEventMsgHint,
*_,
lang: Optional[str] = None,
chat_id: Optional[int] = None,
**__,
):
chat_id = chat_id or event.chat_id

reply_message: Optional[Message] = await event.get_reply_message()
@@ -98,13 +110,20 @@ async def opml_import(event: Union[events.NewMessage.Event, Message],
logger.warning(f'Failed to get opml file from {chat_id}: ', exc_info=e)
return

reply: Message = await event.reply(i18n[lang]['processing'] + '\n' + i18n[lang]['opml_import_processing'])
reply: Message = await event.reply(
'\n'.join((
i18n[lang]['processing'],
i18n[lang]['opml_import_processing'],
))
)
logger.info(f'Got an opml file from {chat_id}')

opml_d = await run_async(
partial(bozo_exception_removal_wrapper,
listparser.parse, opml_file),
prefer_pool='thread' if len(opml_file) < 64 * 1024 else None
partial(
bozo_exception_removal_wrapper,
listparser.parse, opml_file,
),
prefer_pool='thread' if len(opml_file) < 64 * 1024 else None,
)
if not opml_d.feeds:
await reply.edit('ERROR: ' + i18n[lang]['opml_parse_error'])
@@ -114,8 +133,11 @@ async def opml_import(event: Union[events.NewMessage.Event, Message],
import_result = await inner.sub.subs(
chat_id,
tuple(
(feed.url, feed.text) if feed.text and feed.text != feed.title_orig else feed.url
for feed in opml_d.feeds
(
(feed.url, feed.text)
if feed.text and feed.text != feed.title_orig
else feed.url
) for feed in opml_d.feeds
),
lang=lang
)
@@ -139,9 +161,11 @@ async def opml_import(event: Union[events.NewMessage.Event, Message],
continue
elif sum(sub.title is not None for sub in subs
if sub.id in range(curr_start, curr_id + 1)): # if any sub has custom title
subs_between_w_title_count = await db.Sub.filter(user_id=chat_id,
id__in=(curr_id + 1, next_id - 1),
title__not_isnull=True).count()
subs_between_w_title_count = await db.Sub.filter(
user_id=chat_id,
id__in=(curr_id + 1, next_id - 1),
title__not_isnull=True,
).count()
if not subs_between_w_title_count:
continue
sub_ranges.append((curr_start, curr_id))
@@ -152,8 +176,11 @@ async def opml_import(event: Union[events.NewMessage.Event, Message],
if not sub_ranges:
return # no subscription set custom title

button_data = 'del_subs_title=' + '|'.join(f'{start}-{end}' for start, end in sub_ranges) \
+ get_callback_tail(event, chat_id)
button_data = ''.join((
'del_subs_title=',
'|'.join(f'{start}-{end}' for start, end in sub_ranges),
get_callback_tail(event, chat_id),
))
if len(button_data) <= 64: # Telegram API limit
button = [
[Button.inline(i18n[lang]['delete_subs_title_button'], button_data)],
168 changes: 100 additions & 68 deletions src/command/sub.py
Original file line number Diff line number Diff line change
@@ -15,25 +15,30 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from __future__ import annotations
from typing import Union, Optional
from typing import Optional

from telethon import events, Button
from telethon import Button
from telethon.tl import types
from telethon.tl.patched import Message

from .. import env
from ..i18n import i18n
from . import inner
from .utils import command_gatekeeper, parse_command, escape_html, parse_callback_data_with_page, \
send_success_and_failure_msg, get_callback_tail, check_sub_limit
from .types import *
from .utils import (
command_gatekeeper, parse_command, escape_html, parse_callback_data_with_page,
send_success_and_failure_msg, get_callback_tail, check_sub_limit,
)


@command_gatekeeper(only_manager=False)
async def cmd_sub(event: Union[events.NewMessage.Event, Message],
*_,
lang: Optional[str] = None,
chat_id: Optional[int] = None,
**__):
async def cmd_sub(
event: TypeEventMsgHint,
*_,
lang: Optional[str] = None,
chat_id: Optional[int] = None,
**__,
):
chat_id = chat_id or event.chat_id

await check_sub_limit(event, user_id=chat_id, lang=lang)
@@ -42,9 +47,11 @@ async def cmd_sub(event: Union[events.NewMessage.Event, Message],
filtered_urls = inner.utils.filter_urls(args)

allow_reply = (event.is_private or event.is_group) and chat_id == event.chat_id
prompt = (i18n[lang]['sub_reply_feed_url_prompt_html']
if allow_reply
else i18n[lang]['sub_usage_in_channel_html'])
prompt = (
i18n[lang]['sub_reply_feed_url_prompt_html']
if allow_reply
else i18n[lang]['sub_usage_in_channel_html']
)

if not filtered_urls:
await event.respond(
@@ -57,7 +64,8 @@ async def cmd_sub(event: Union[events.NewMessage.Event, Message],
placeholder='url1 url2 url3 ...'
)
# do not force reply in private chat
if event.is_group else None
if event.is_group
else None
),
reply_to=event.id if event.is_group else None
)
@@ -66,7 +74,8 @@ async def cmd_sub(event: Union[events.NewMessage.Event, Message],
# delete the force reply message
reply_message: Optional[Message] = await event.get_reply_message()
if (
reply_message and reply_message.sender_id == env.bot_id
reply_message
and reply_message.sender_id == env.bot_id
and isinstance(reply_message.reply_markup, types.ReplyKeyboardForceReply)
):
env.loop.create_task(reply_message.delete())
@@ -83,37 +92,45 @@ async def cmd_sub(event: Union[events.NewMessage.Event, Message],


@command_gatekeeper(only_manager=False)
async def cmd_unsub(event: Union[events.NewMessage.Event, Message],
*_,
lang: Optional[str] = None,
chat_id: Optional[int] = None,
**__):
async def cmd_unsub(
event: TypeEventMsgHint,
*_,
lang: Optional[str] = None,
chat_id: Optional[int] = None,
**__,
):
chat_id = chat_id or event.chat_id
callback_tail = get_callback_tail(event, chat_id)
args = parse_command(event.raw_text)

unsub_result = await inner.sub.unsubs(chat_id, args, lang=lang)

if unsub_result is None:
buttons = await inner.utils.get_sub_choosing_buttons(chat_id, lang=lang, page_number=1, callback='unsub',
get_page_callback='get_unsub_page', tail=callback_tail)
await event.respond(i18n[lang]['unsub_choose_sub_prompt_html'] if buttons else i18n[lang]['no_subscription'],
buttons=buttons,
parse_mode='html')
buttons = await inner.utils.get_sub_choosing_buttons(
chat_id, lang=lang,
page_number=1, callback='unsub', get_page_callback='get_unsub_page', tail=callback_tail,
)
await event.respond(
i18n[lang]['unsub_choose_sub_prompt_html'] if buttons else i18n[lang]['no_subscription'],
buttons=buttons,
parse_mode='html',
)
return

await send_success_and_failure_msg(event, **unsub_result, lang=lang, edit=False)


@command_gatekeeper(only_manager=False)
async def cmd_or_callback_unsub_all(event: Union[events.NewMessage.Event, Message, events.CallbackQuery.Event],
*_,
lang: Optional[str] = None,
chat_id: Optional[int] = None,
**__): # command = /unsub_all, callback data = unsub_all
async def cmd_or_callback_unsub_all(
event: TypeEventCollectionMsgOrCb,
*_,
lang: Optional[str] = None,
chat_id: Optional[int] = None,
**__,
): # command = /unsub_all, callback data = unsub_all
chat_id = chat_id or event.chat_id
callback_tail = get_callback_tail(event, chat_id)
is_callback = isinstance(event, events.CallbackQuery.Event)
is_callback = isinstance(event, TypeEventCb)
if is_callback:
backup_file = await inner.sub.export_opml(chat_id)
if backup_file is None:
@@ -123,7 +140,7 @@ async def cmd_or_callback_unsub_all(event: Union[events.NewMessage.Event, Messag
file=backup_file,
attributes=(
types.DocumentAttributeFilename("RSStT_unsub_all_backup.opml"),
)
),
)

unsub_all_result = await inner.sub.unsub_all(chat_id, lang=lang)
@@ -135,58 +152,70 @@ async def cmd_or_callback_unsub_all(event: Union[events.NewMessage.Event, Messag
i18n[lang]['unsub_all_confirm_prompt'],
buttons=[
[Button.inline(i18n[lang]['unsub_all_confirm'], data=f'unsub_all{callback_tail}')],
[Button.inline(i18n[lang]['unsub_all_cancel'], data='cancel')]
]
[Button.inline(i18n[lang]['unsub_all_cancel'], data='cancel')],
],
)
return
await event.respond(i18n[lang]['no_subscription'])


@command_gatekeeper(only_manager=False)
async def cmd_list_or_callback_get_list_page(event: Union[events.NewMessage.Event, Message, events.CallbackQuery.Event],
*_,
lang: Optional[str] = None,
chat_id: Optional[int] = None,
**__): # command = /list, callback data = get_list_page|{page_number}
async def cmd_list_or_callback_get_list_page(
event: TypeEventCollectionMsgOrCb,
*_,
lang: Optional[str] = None,
chat_id: Optional[int] = None,
**__,
): # command = /list, callback data = get_list_page|{page_number}
chat_id = chat_id or event.chat_id
callback_tail = get_callback_tail(event, chat_id)
is_callback = isinstance(event, events.CallbackQuery.Event)
is_callback = isinstance(event, TypeEventCb)
if is_callback:
_, page_number = parse_callback_data_with_page(event.data)
else:
page_number = 1

# Telegram only allow <= 100 parsing entities in a message
page_number, page_count, page, sub_count = \
await inner.utils.get_sub_list_by_page(user_id=chat_id, page_number=page_number, size=99)
page_number, page_count, page, sub_count = await inner.utils.get_sub_list_by_page(
user_id=chat_id, page_number=page_number, size=99,
)

if page_count == 0:
await event.respond(i18n[lang]['no_subscription'])
return

list_result = (
f'<b>{i18n[lang]["subscription_list"]}</b>' # it occupies a parsing entity
+ '\n'
+ '\n'.join(f'<a href="{sub.feed.link}">{escape_html(sub.title or sub.feed.title)}</a>' for sub in page)
list_result = ''.join((
f'<b>{i18n[lang]["subscription_list"]}</b>\n', # it occupies a parsing entity
'\n'.join(
f'<a href="{sub.feed.link}">{escape_html(sub.title or sub.feed.title)}</a>'
for sub in page
)
))

page_buttons = inner.utils.get_page_buttons(
page_number=page_number,
page_count=page_count,
get_page_callback='get_list_page',
total_count=sub_count,
lang=lang,
tail=callback_tail,
)

page_buttons = inner.utils.get_page_buttons(page_number=page_number,
page_count=page_count,
get_page_callback='get_list_page',
total_count=sub_count,
lang=lang,
tail=callback_tail)

await event.edit(list_result, parse_mode='html', buttons=page_buttons) if is_callback else \
await event.respond(list_result, parse_mode='html', buttons=page_buttons)
await (
event.edit(list_result, parse_mode='html', buttons=page_buttons)
if is_callback
else event.respond(list_result, parse_mode='html', buttons=page_buttons)
)


@command_gatekeeper(only_manager=False)
async def callback_unsub(event: events.CallbackQuery.Event,
*_,
lang: Optional[str] = None,
chat_id: Optional[int] = None,
**__): # callback data = unsub={sub_id}|{page}
async def callback_unsub(
event: TypeEventCb,
*_,
lang: Optional[str] = None,
chat_id: Optional[int] = None,
**__,
): # callback data = unsub={sub_id}|{page}
chat_id = chat_id or event.chat_id
sub_id, page = parse_callback_data_with_page(event.data)
sub_id = int(sub_id)
@@ -200,17 +229,20 @@ async def callback_unsub(event: events.CallbackQuery.Event,


@command_gatekeeper(only_manager=False)
async def callback_get_unsub_page(event: events.CallbackQuery.Event,
*_,
page: Optional[int] = None,
lang: Optional[str] = None,
chat_id: Optional[int] = None,
**__): # callback data = get_unsub_page|{page_number}
async def callback_get_unsub_page(
event: TypeEventCb,
*_,
page: Optional[int] = None,
lang: Optional[str] = None,
chat_id: Optional[int] = None,
**__,
): # callback data = get_unsub_page|{page_number}
chat_id = chat_id or event.chat_id
callback_tail = get_callback_tail(event, chat_id)
if not page:
_, page = parse_callback_data_with_page(event.data)
buttons = await inner.utils.get_sub_choosing_buttons(chat_id, page, callback='unsub',
get_page_callback='get_unsub_page', lang=lang,
tail=callback_tail)
buttons = await inner.utils.get_sub_choosing_buttons(
chat_id, page, callback='unsub',
get_page_callback='get_unsub_page', lang=lang, tail=callback_tail,
)
await event.edit(None if buttons else i18n[lang]['no_subscription'], buttons=buttons)
59 changes: 59 additions & 0 deletions src/command/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# RSS to Telegram Bot
# Copyright (C) 2024 Rongrong <i@rong.moe>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from __future__ import annotations
from typing import Union

from telethon import events
from telethon.tl.patched import Message

__all__ = [
'TypeEventMsg', 'TypeEventMsgHint', 'TypeEventCb', 'TypeEventInline', 'TypeEventChatAction',
'TypeEventCollectionAll', 'TypeEventCollectionMsgLike', 'TypeEventCollectionMsgOrCb',
'TypeEventCollectionMsgOrChatAction'
]

# Has: respond(), reply(), edit(), delete(), get_reply_message()
TypeEventMsg = events.NewMessage.Event
TypeEventMsgHint = Union[events.NewMessage.Event, Message]
# Has: respond(), reply(), edit(), delete(), answer()
TypeEventCb = events.CallbackQuery.Event
# Has: answer()
TypeEventInline = events.InlineQuery.Event
# Has: respond(), reply(), delete()
# Note: `events.ChatAction.Event` only have ChatGetter, do not have SenderGetter like others
TypeEventChatAction = events.ChatAction.Event

# All have: get_chat(), get_input_chat()
TypeEventCollectionAll = Union[
events.NewMessage.Event, Message, # Has: respond(), reply(), edit(), delete(), get_reply_message()
events.CallbackQuery.Event, # Has: respond(), reply(), edit(), delete(), answer()
events.InlineQuery.Event, # Has: answer()
events.ChatAction.Event, # Has: respond(), reply(), delete()
]
TypeEventCollectionMsgLike = Union[
events.NewMessage.Event, Message,
events.CallbackQuery.Event,
events.ChatAction.Event,
]
TypeEventCollectionMsgOrCb = Union[
events.NewMessage.Event, Message,
events.CallbackQuery.Event,
]
TypeEventCollectionMsgOrChatAction = Union[
events.NewMessage.Event, Message,
events.ChatAction.Event,
]
46 changes: 17 additions & 29 deletions src/command/utils.py
Original file line number Diff line number Diff line change
@@ -37,6 +37,7 @@
from .. import env, log, db, locks, errors_collection
from ..i18n import i18n
from . import inner
from .types import *
from ..errors_collection import UserBlockedErrors
from ..compat import cached_async

@@ -136,12 +137,7 @@ def parse_customization_callback_data(


async def respond_or_answer(
event: Union[
events.NewMessage.Event, Message,
events.CallbackQuery.Event,
events.InlineQuery.Event,
events.ChatAction.Event,
],
event: TypeEventCollectionAll,
msg: str,
alert: bool = True,
cache_time: int = 120,
@@ -160,12 +156,12 @@ async def respond_or_answer(
"""
with suppress(*UserBlockedErrors): # silently ignore
# noinspection PyProtectedMember
if isinstance(event, events.CallbackQuery.Event) and not event._answered:
if isinstance(event, TypeEventCb) and not event._answered:
# answering callback query is of a tolerant rate limit, no lock needed
with suppress(QueryIdInvalidError): # callback query expired, respond instead
await event.answer(msg, alert=alert, cache_time=cache_time)
return # return if answering successfully
elif isinstance(event, events.InlineQuery.Event):
elif isinstance(event, TypeEventInline):
# noinspection PyProtectedMember
if event._answered:
return
@@ -181,7 +177,7 @@ async def respond_or_answer(
**kwargs,
reply_to=(
event.message
if isinstance(event, events.NewMessage.Event) and event.is_group
if isinstance(event, TypeEventMsg) and event.is_group
else None
),
)
@@ -272,12 +268,7 @@ def command_gatekeeper(
@wraps(func)
async def wrapper(
# Note: `events.ChatAction.Event` only have ChatGetter, do not have SenderGetter like others
event: Union[
events.NewMessage.Event, Message,
events.CallbackQuery.Event,
events.InlineQuery.Event,
events.ChatAction.Event
],
event: TypeEventCollectionAll,
*args,
**kwargs,
):
@@ -293,9 +284,9 @@ async def wrapper(
chat_id = event.chat_id
flood_lock = locks.user_flood_lock(chat_id)
pending_callbacks = locks.user_pending_callbacks(chat_id)
is_callback = isinstance(event, events.CallbackQuery.Event)
is_inline = isinstance(event, events.InlineQuery.Event)
is_chat_action = isinstance(event, events.ChatAction.Event)
is_callback = isinstance(event, TypeEventCb)
is_inline = isinstance(event, TypeEventInline)
is_chat_action = isinstance(event, TypeEventChatAction)

def describe_user():
chat_info = None
@@ -713,7 +704,7 @@ def __init__(
pattern=pattern,
)

def filter(self, event: Union[events.NewMessage.Event, Message]):
def filter(self, event: TypeEventMsgHint):
document: types.Document = event.message.document
if not document:
return
@@ -756,7 +747,7 @@ def __init__(
pattern=pattern,
)

async def __reply_verify(self, event: Union[events.NewMessage.Event, Message]):
async def __reply_verify(self, event: TypeEventMsgHint):
if event.is_reply:
reply_to_msg: Optional[Message] = await event.get_reply_message()
if reply_to_msg is not None and self.reply_to_peer_id == reply_to_msg.sender_id:
@@ -788,7 +779,7 @@ def __init__(
)

@staticmethod
def __in_private_chat(event: Union[events.NewMessage.Event, Message]):
def __in_private_chat(event: TypeEventMsgHint):
return event.is_private


@@ -799,7 +790,7 @@ def __init__(self, chats=None, *, blacklist_chats=False):
super().__init__(chats, blacklist_chats=blacklist_chats, func=self.__added_to_group)

@staticmethod
def __added_to_group(event: events.ChatAction.Event):
def __added_to_group(event: TypeEventChatAction):
if not event.is_group:
return False
if event.created:
@@ -864,7 +855,7 @@ async def set_bot_commands(


async def send_success_and_failure_msg(
message: Union[Message, events.NewMessage.Event, events.CallbackQuery.Event],
message: TypeEventCollectionMsgOrCb,
success_msg: str,
failure_msg: str,
success_count: int,
@@ -873,7 +864,7 @@ async def send_success_and_failure_msg(
lang: Optional[str] = None,
edit: bool = False,
**__,
) -> Union[Message, events.NewMessage.Event, events.CallbackQuery.Event]:
) -> TypeEventCollectionMsgOrCb:
success_msg_raw = success_msg
failure_msg_raw = failure_msg
success_msg_short = (
@@ -920,10 +911,7 @@ def get_group_migration_help_msg(


def get_callback_tail(
event: Union[
events.NewMessage.Event, Message,
events.CallbackQuery.Event,
],
event: TypeEventCollectionMsgOrCb,
chat_id: int,
) -> str:
if not event.is_private or event.chat.id == chat_id:
@@ -934,7 +922,7 @@ def get_callback_tail(
return f'%{ori_chat_id}' if ori_chat_id < 0 else f'%+{ori_chat_id}'


async def check_sub_limit(event: Union[events.NewMessage.Event, Message], user_id: int, lang: Optional[str] = None):
async def check_sub_limit(event: TypeEventMsgHint, user_id: int, lang: Optional[str] = None):
limit_reached, curr_count, limit, _ = await inner.utils.check_sub_limit(user_id)
if limit_reached:
logger.warning(f'Refused user {user_id} to add new subscriptions due to limit reached ({curr_count}/{limit})')

0 comments on commit 587c993

Please sign in to comment.