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