diff --git a/languages/built-in/en.yml b/languages/built-in/en.yml index 2180e18..dc6af9b 100644 --- a/languages/built-in/en.yml +++ b/languages/built-in/en.yml @@ -556,3 +556,6 @@ sb_channel: Successfully blocked this channel in this group. #reload reload_des: Reload the PagerMaid-Pyro instance. reload_ok: Successfully reloaded. +#conversation +conversation_already_in_error: Another conversation is already in progress. +conversation_timed_out_error: Response timed out. diff --git a/languages/built-in/ja.yml b/languages/built-in/ja.yml index 4eb3809..be9f5d7 100644 --- a/languages/built-in/ja.yml +++ b/languages/built-in/ja.yml @@ -556,3 +556,6 @@ sb_channel: Successfully blocked this channel in this group. #reload reload_des: PagerMaid-Pyroインスタンスをリロードします。 reload_ok: 再読み込みに成功しました。 +#conversation +conversation_already_in_error: すでに別の会話が進行中です。 +conversation_timed_out_error: 応答がタイムアウトしました。 diff --git a/languages/built-in/zh-cn.yml b/languages/built-in/zh-cn.yml index f231e10..325ed3d 100644 --- a/languages/built-in/zh-cn.yml +++ b/languages/built-in/zh-cn.yml @@ -556,3 +556,6 @@ sb_channel: 成功在本群封禁此频道。 #reload reload_des: 重新加载内置模块和插件。 reload_ok: 重新加载内置模块和插件成功。 +#conversation +conversation_already_in_error: 另一个操作正在进行中,请稍等片刻后重试。 +conversation_timed_out_error: 响应超时,请稍等片刻后重试。 diff --git a/languages/built-in/zh-tw.yml b/languages/built-in/zh-tw.yml index f84c21e..38b4b6e 100644 --- a/languages/built-in/zh-tw.yml +++ b/languages/built-in/zh-tw.yml @@ -556,3 +556,6 @@ sb_channel: 成功在本群封禁此頻道。 #reload reload_des: 重新加載內置模塊和插件。 reload_ok: 重新加載內置模塊和插件成功。 +#conversation +conversation_already_in_error: 另一個操作正在進行中,請稍等片刻後重試。 +conversation_timed_out_error: 響應超時,請稍等片刻後重試。 diff --git a/pagermaid/__init__.py b/pagermaid/__init__.py index 655dc46..f337e80 100644 --- a/pagermaid/__init__.py +++ b/pagermaid/__init__.py @@ -4,13 +4,15 @@ from datetime import datetime, timezone from logging import getLogger, StreamHandler, CRITICAL, INFO, basicConfig, DEBUG from os import getcwd +from pyrogram.errors import PeerIdInvalid + from pagermaid.config import Config from pagermaid.scheduler import scheduler import pyromod.listen from pyrogram import Client import sys -pgm_version = "1.1.1" +pgm_version = "1.1.2" CMD_LIST = {} module_dir = __path__[0] working_dir = getcwd() @@ -63,7 +65,10 @@ async def log(message): ) if not Config.LOG: return - await bot.send_message( - Config.LOG_ID, - message - ) + try: + await bot.send_message( + Config.LOG_ID, + message + ) + except PeerIdInvalid: + Config.LOG = False diff --git a/pagermaid/listener.py b/pagermaid/listener.py index f9dc124..be8e6f1 100644 --- a/pagermaid/listener.py +++ b/pagermaid/listener.py @@ -16,7 +16,7 @@ from pyrogram.handlers import MessageHandler, EditedMessageHandler from pagermaid import help_messages, logs, Config, bot, read_context, all_permissions from pagermaid.group_manager import Permission -from pagermaid.single_utils import Message +from pagermaid.single_utils import Message, AlreadyInConversationError, TimeoutConversationError from pagermaid.utils import lang, attach_report, sudo_filter, alias_command, get_permission_name, process_exit from pagermaid.utils import client as httpx_client @@ -133,6 +133,16 @@ def listener(**args): logs.warning( "Please Don't Delete Commands While it's Processing.." ) + except AlreadyInConversationError: + logs.warning( + "Please Don't Send Commands In The Same Conversation.." + ) + await message.edit(lang("conversation_already_in_error")) + except TimeoutConversationError: + logs.warning( + "Conversation Timed out while processing commands.." + ) + await message.edit(lang("conversation_timed_out_error")) except UserNotParticipant: pass except ContinuePropagation as e: @@ -195,6 +205,20 @@ def raw_listener(filter_s): raise StopPropagation from e except ContinuePropagation as e: raise ContinuePropagation from e + except MessageIdInvalid: + logs.warning( + "Please Don't Delete Commands While it's Processing.." + ) + except AlreadyInConversationError: + logs.warning( + "Please Don't Send Commands In The Same Conversation.." + ) + await message.edit(lang("conversation_already_in_error")) + except TimeoutConversationError: + logs.warning( + "Conversation Timed out while processing commands.." + ) + await message.edit(lang("conversation_timed_out_error")) except SystemExit: await process_exit(start=False, _client=client, message=message) sys.exit(0) diff --git a/pagermaid/modules/plugin.py b/pagermaid/modules/plugin.py index 1d5979a..793ee4a 100644 --- a/pagermaid/modules/plugin.py +++ b/pagermaid/modules/plugin.py @@ -77,8 +77,7 @@ async def plugin(message: Message): move_plugin(file_path) await message.edit(f"{lang('apt_name')}\n\n" f"{lang('apt_plugin')} " - f"{path.basename(file_path)[:-3]} {lang('apt_installed')}," - f"{lang('apt_reboot')}") + f"{path.basename(file_path)[:-3]} {lang('apt_installed')}") await log(f"{lang('apt_install_success')} {path.basename(file_path)[:-3]}.") reload_all() elif len(message.parameter) >= 2: @@ -128,8 +127,6 @@ async def plugin(message: Message): text += lang('apt_no_update') + " %s\n" % ", ".join(no_need_list) await log(text) restart = len(success_list) > 0 - if restart: - text += lang('apt_reboot') await message.edit(text) if restart: reload_all() @@ -146,8 +143,7 @@ async def plugin(message: Message): version_json[message.parameter[1]] = "0.0" with open(f"{plugin_directory}version.json", 'w') as f: json.dump(version_json, f) - await message.edit(f"{lang('apt_remove_success')} {message.parameter[1]}, " - f"{lang('apt_reboot')} ") + await message.edit(f"{lang('apt_remove_success')} {message.parameter[1]}") await log(f"{lang('apt_remove')} {message.parameter[1]}.") reload_all() elif "/" in message.parameter[1]: @@ -198,7 +194,7 @@ async def plugin(message: Message): rename(f"{plugin_directory}{message.parameter[1]}.py.disabled", f"{plugin_directory}{message.parameter[1]}.py") await message.edit(f"{lang('apt_plugin')} {message.parameter[1]} " - f"{lang('apt_enable')},{lang('apt_reboot')}") + f"{lang('apt_enable')}") await log(f"{lang('apt_enable')} {message.parameter[1]}.") reload_all() else: @@ -211,7 +207,7 @@ async def plugin(message: Message): rename(f"{plugin_directory}{message.parameter[1]}.py", f"{plugin_directory}{message.parameter[1]}.py.disabled") await message.edit(f"{lang('apt_plugin')} {message.parameter[1]} " - f"{lang('apt_disable')},{lang('apt_reboot')}") + f"{lang('apt_disable')}") await log(f"{lang('apt_disable')} {message.parameter[1]}.") reload_all() else: diff --git a/pagermaid/single_utils.py b/pagermaid/single_utils.py index ca8a5e4..31594be 100644 --- a/pagermaid/single_utils.py +++ b/pagermaid/single_utils.py @@ -1,12 +1,16 @@ import contextlib from os import sep, remove, mkdir from os.path import exists -from typing import List, Optional +from typing import List, Optional, Union from apscheduler.schedulers.asyncio import AsyncIOScheduler from httpx import AsyncClient from pyrogram import Client from pyrogram.types import Message + +from pyromod.utils.conversation import Conversation +from pyromod.utils.errors import AlreadyInConversationError, TimeoutConversationError + from sqlitedict import SqliteDict # init folders @@ -31,6 +35,25 @@ def safe_remove(name: str) -> None: class Client(Client): # noqa job: Optional[AsyncIOScheduler] = None + async def listen(self, chat_id, filters=None, timeout=None) -> Optional[Message]: + return + + async def ask(self, chat_id, text, filters=None, timeout=None, *args, **kwargs) -> Optional[Message]: + return + + def cancel_listener(self, chat_id): + """ Cancel the conversation with the given chat_id. """ + return + + def cancel_all_listeners(self): + """ Cancel all conversations. """ + return + + def conversation(self, chat_id: Union[int, str], + once_timeout: int = 60, filters=None) -> Optional[Conversation]: + """ Initialize a conversation with the given chat_id. """ + return + class Message(Message): # noqa arguments: str diff --git a/pyromod/filters/__init__.py b/pyromod/filters/__init__.py index f03398c..4b8f182 100644 --- a/pyromod/filters/__init__.py +++ b/pyromod/filters/__init__.py @@ -18,4 +18,4 @@ You should have received a copy of the GNU General Public License along with pyromod. If not, see . """ -from .filters import dice \ No newline at end of file +from .filters import dice diff --git a/pyromod/filters/filters.py b/pyromod/filters/filters.py index c50906b..bae411d 100644 --- a/pyromod/filters/filters.py +++ b/pyromod/filters/filters.py @@ -18,8 +18,11 @@ You should have received a copy of the GNU General Public License along with pyromod. If not, see . """ -import pyrogram +import pyrogram + def dice(ctx, message): return hasattr(message, 'dice') and message.dice + + pyrogram.filters.dice = dice diff --git a/pyromod/helpers/__init__.py b/pyromod/helpers/__init__.py index 5b393e1..03dda1a 100644 --- a/pyromod/helpers/__init__.py +++ b/pyromod/helpers/__init__.py @@ -17,4 +17,4 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with pyromod. If not, see . """ -from .helpers import ikb, bki, ntb, btn, kb, kbtn, array_chunk, force_reply \ No newline at end of file +from .helpers import ikb, bki, ntb, btn, kb, kbtn, array_chunk, force_reply diff --git a/pyromod/helpers/helpers.py b/pyromod/helpers/helpers.py index 10682ea..b9cc3a5 100644 --- a/pyromod/helpers/helpers.py +++ b/pyromod/helpers/helpers.py @@ -1,19 +1,24 @@ from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ReplyKeyboardMarkup, ForceReply -def ikb(rows = []): + +def ikb(rows=None): + if rows is None: + rows = [] lines = [] for row in rows: line = [] for button in row: - button = btn(*button) # InlineKeyboardButton + button = btn(*button) # InlineKeyboardButton line.append(button) lines.append(line) return InlineKeyboardMarkup(inline_keyboard=lines) - #return {'inline_keyboard': lines} + # return {'inline_keyboard': lines} -def btn(text, value, type = 'callback_data'): + +def btn(text, value, type='callback_data'): return InlineKeyboardButton(text, **{type: value}) - #return {'text': text, type: value} + # return {'text': text, type: value} + # The inverse of above def bki(keyboard): @@ -21,14 +26,16 @@ def bki(keyboard): for row in keyboard.inline_keyboard: line = [] for button in row: - button = ntb(button) # btn() format + button = ntb(button) # btn() format line.append(button) lines.append(line) return lines - #return ikb() format + # return ikb() format + def ntb(button): - for btn_type in ['callback_data', 'url', 'switch_inline_query', 'switch_inline_query_current_chat', 'callback_game']: + for btn_type in ['callback_data', 'url', 'switch_inline_query', 'switch_inline_query_current_chat', + 'callback_game']: value = getattr(button, btn_type) if value: break @@ -36,9 +43,12 @@ def ntb(button): if btn_type != 'callback_data': button.append(btn_type) return button - #return {'text': text, type: value} + # return {'text': text, type: value} -def kb(rows = [], **kwargs): + +def kb(rows=None, **kwargs): + if rows is None: + rows = [] lines = [] for row in rows: line = [] @@ -48,16 +58,18 @@ def kb(rows = [], **kwargs): button = KeyboardButton(button) elif button_type == dict: button = KeyboardButton(**button) - + line.append(button) lines.append(line) return ReplyKeyboardMarkup(keyboard=lines, **kwargs) + kbtn = KeyboardButton + def force_reply(selective=True): return ForceReply(selective=selective) -def array_chunk(input, size): - return [input[i:i+size] for i in range(0, len(input), size)] +def array_chunk(input_, size): + return [input_[i:i + size] for i in range(0, len(input_), size)] diff --git a/pyromod/listen/listen.py b/pyromod/listen/listen.py index e3eb42b..39e2e70 100644 --- a/pyromod/listen/listen.py +++ b/pyromod/listen/listen.py @@ -20,7 +20,7 @@ along with pyromod. If not, see . import asyncio import functools -from typing import Optional, List +from typing import Optional, List, Union import pyrogram @@ -28,6 +28,8 @@ from pagermaid.single_utils import get_sudo_list, Message from pagermaid.scheduler import add_delete_message_job from ..utils import patch, patchable +from ..utils.conversation import Conversation +from ..utils.errors import TimeoutConversationError class ListenerCanceled(Exception): @@ -59,7 +61,10 @@ class Client: self.listening.update({ chat_id: {"future": future, "filters": filters} }) - return await asyncio.wait_for(future, timeout) + try: + return await asyncio.wait_for(future, timeout) + except asyncio.exceptions.TimeoutError as e: + raise TimeoutConversationError() from e @patchable async def ask(self, chat_id, text, filters=None, timeout=None, *args, **kwargs): @@ -82,6 +87,15 @@ class Client: listener['future'].set_exception(ListenerCanceled()) self.clear_listener(chat_id, listener['future']) + @patchable + def cancel_all_listener(self): + for chat_id in self.listening: + self.cancel_listener(chat_id) + + @patchable + def conversation(self, chat_id: Union[int, str], once_timeout: int = 60, filters=None): + return Conversation(self, chat_id, once_timeout, filters) + @patch(pyrogram.handlers.message_handler.MessageHandler) class MessageHandler: diff --git a/pyromod/nav/__init__.py b/pyromod/nav/__init__.py index e917502..2d451b8 100644 --- a/pyromod/nav/__init__.py +++ b/pyromod/nav/__init__.py @@ -17,4 +17,4 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with pyromod. If not, see . """ -from .pagination import Pagination \ No newline at end of file +from .pagination import Pagination diff --git a/pyromod/nav/pagination.py b/pyromod/nav/pagination.py index 607be32..f113e7e 100644 --- a/pyromod/nav/pagination.py +++ b/pyromod/nav/pagination.py @@ -20,6 +20,7 @@ along with pyromod. If not, see . import math from ..helpers import array_chunk + class Pagination: def __init__(self, objects, page_data=None, item_data=None, item_title=None): default_page_callback = (lambda x: str(x)) @@ -28,26 +29,25 @@ class Pagination: self.page_data = page_data or default_page_callback self.item_data = item_data or default_item_callback self.item_title = item_title or default_item_callback - + def create(self, page, lines=5, columns=1): - quant_per_page = lines*columns + quant_per_page = lines * columns page = 1 if page <= 0 else page - offset = (page-1)*quant_per_page - stop = offset+quant_per_page + offset = (page - 1) * quant_per_page + stop = offset + quant_per_page cutted = self.objects[offset:stop] total = len(self.objects) - pages_range = [*range(1, math.ceil(total/quant_per_page)+1)] # each item is a page + pages_range = [*range(1, math.ceil(total / quant_per_page) + 1)] # each item is a page last_page = len(pages_range) - nav = [] if page <= 3: - for n in [1,2,3]: + for n in [1, 2, 3]: if n not in pages_range: continue text = f"· {n} ·" if n == page else n - nav.append( (text, self.page_data(n)) ) + nav.append((text, self.page_data(n))) if last_page >= 4: nav.append( ('4 ›' if last_page > 5 else 4, self.page_data(4)) @@ -56,30 +56,29 @@ class Pagination: nav.append( (f'{last_page} »' if last_page > 5 else last_page, self.page_data(last_page)) ) - elif page >= last_page-2: + elif page >= last_page - 2: nav.extend( [ ('« 1' if last_page > 5 else 1, self.page_data(1)), ( - f'‹ {last_page-3}' if last_page > 5 else last_page - 3, + f'‹ {last_page - 3}' if last_page > 5 else last_page - 3, self.page_data(last_page - 3), ), ] ) - for n in range(last_page-2, last_page+1): + for n in range(last_page - 2, last_page + 1): text = f"· {n} ·" if n == page else n - nav.append( (text, self.page_data(n)) ) + nav.append((text, self.page_data(n))) else: nav = [ ('« 1', self.page_data(1)), - (f'‹ {page-1}', self.page_data(page - 1)), + (f'‹ {page - 1}', self.page_data(page - 1)), (f'· {page} ·', "noop"), - (f'{page+1} ›', self.page_data(page + 1)), + (f'{page + 1} ›', self.page_data(page + 1)), (f'{last_page} »', self.page_data(last_page)), ] - buttons = [ (self.item_title(item, page), self.item_data(item, page)) for item in cutted @@ -89,4 +88,4 @@ class Pagination: if last_page > 1: kb_lines.append(nav) - return kb_lines \ No newline at end of file + return kb_lines diff --git a/pyromod/utils/__init__.py b/pyromod/utils/__init__.py index b743c60..3c9f81a 100644 --- a/pyromod/utils/__init__.py +++ b/pyromod/utils/__init__.py @@ -18,4 +18,4 @@ You should have received a copy of the GNU General Public License along with pyromod. If not, see . """ -from .utils import patch, patchable \ No newline at end of file +from .utils import patch, patchable diff --git a/pyromod/utils/conversation.py b/pyromod/utils/conversation.py new file mode 100644 index 0000000..bcaf795 --- /dev/null +++ b/pyromod/utils/conversation.py @@ -0,0 +1,89 @@ +import asyncio +import functools +from typing import Union +from pyrogram.raw.types import InputPeerUser, InputPeerChat, InputPeerChannel +from pyromod.utils.errors import AlreadyInConversationError + + +def _checks_cancelled(f): + @functools.wraps(f) + def wrapper(self, *args, **kwargs): + if self._cancelled: + raise asyncio.CancelledError("The conversation was cancelled before") + + return f(self, *args, **kwargs) + return wrapper + + +class Conversation: + def __init__(self, client, chat_id: Union[int, str], + once_timeout: int = 60, filters=None): + self._client = client + self._chat_id = chat_id + self._once_timeout = once_timeout + self._filters = filters + self._cancelled = False + + @_checks_cancelled + async def send_message(self, *args, **kwargs): + return await self._client.send_message(self._chat_id, *args, **kwargs) + + @_checks_cancelled + async def send_media_group(self, *args, **kwargs): + return await self._client.send_media_group(self._chat_id, *args, **kwargs) + + @_checks_cancelled + async def send_photo(self, *args, **kwargs): + return await self._client.send_photo(self._chat_id, *args, **kwargs) + + @_checks_cancelled + async def send_document(self, *args, **kwargs): + return await self._client.send_document(self._chat_id, *args, **kwargs) + + @_checks_cancelled + async def send_sticker(self, *args, **kwargs): + return await self._client.send_sticker(self._chat_id, *args, **kwargs) + + @_checks_cancelled + async def send_voice(self, *args, **kwargs): + return await self._client.send_voice(self._chat_id, *args, **kwargs) + + @_checks_cancelled + async def send_video(self, *args, **kwargs): + return await self._client.send_video(self._chat_id, *args, **kwargs) + + @_checks_cancelled + async def ask(self, text, filters=None, timeout=None, *args, **kwargs): + filters = filters or self._filters + timeout = timeout or self._once_timeout + return await self._client.ask(self._chat_id, text, filters=filters, timeout=timeout, *args, **kwargs) + + @_checks_cancelled + async def get_response(self, filters=None, timeout=None): + filters = filters or self._filters + timeout = timeout or self._once_timeout + return await self._client.listen(self._chat_id, filters, timeout) + + def mark_as_read(self, message=None): + return self._client.read_chat_history(self._chat_id, max_id=message.id if message else 0) + + def cancel(self): + self._cancelled = True + self._client.cancel_listener(self._chat_id) + + async def __aenter__(self): + self._peer_chat = await self._client.resolve_peer(self._chat_id) + if isinstance(self._peer_chat, InputPeerUser): + self._chat_id = self._peer_chat.user_id + elif isinstance(self._peer_chat, InputPeerChat): + self._chat_id = self._peer_chat.chat_id + elif isinstance(self._peer_chat, InputPeerChannel): + self._chat_id = self._peer_chat.channel_id + + if self._client.listening.get(self._chat_id, False): + raise AlreadyInConversationError() + self._cancelled = False + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + self.cancel() diff --git a/pyromod/utils/errors.py b/pyromod/utils/errors.py new file mode 100644 index 0000000..5117ee8 --- /dev/null +++ b/pyromod/utils/errors.py @@ -0,0 +1,19 @@ +class AlreadyInConversationError(Exception): + """ + Occurs when another exclusive conversation is opened in the same chat. + """ + def __init__(self): + super().__init__( + "Cannot open exclusive conversation in a " + "chat that already has one open conversation" + ) + + +class TimeoutConversationError(Exception): + """ + Occurs when the conversation times out. + """ + def __init__(self): + super().__init__( + "Response read timed out" + ) diff --git a/pyromod/utils/utils.py b/pyromod/utils/utils.py index d175144..cb9edc3 100644 --- a/pyromod/utils/utils.py +++ b/pyromod/utils/utils.py @@ -18,18 +18,21 @@ You should have received a copy of the GNU General Public License along with pyromod. If not, see . """ + def patch(obj): def is_patchable(item): return getattr(item[1], 'patchable', False) def wrapper(container): - for name,func in filter(is_patchable, container.__dict__.items()): + for name, func in filter(is_patchable, container.__dict__.items()): old = getattr(obj, name, None) setattr(obj, f'old{name}', old) setattr(obj, name, func) return container + return wrapper + def patchable(func): func.patchable = True - return func \ No newline at end of file + return func