diff --git a/.gitignore b/.gitignore index c399ee25e..a5fe0b86b 100644 --- a/.gitignore +++ b/.gitignore @@ -143,4 +143,4 @@ logs/ ./loader/ userge/plugins/dev/ .rcache -test.py \ No newline at end of file +test.py diff --git a/README.md b/README.md index 4f83ec492..2a497247d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
-
+
Pluggable Telegram UserBot
@@ -49,9 +49,9 @@
> Special Thanks to all of you !!!.
-## [Documentation](http://theuserge.tech) š
+## [Documentation](https://theuserge.github.io) š
-## [Deployment](http://theuserge.tech/deployment) š·
+## [Deployment](https://theuserge.github.io/deployment) š·
## [Plugins](https://github.com/UsergeTeam/Userge-Plugins) š
diff --git a/min_loader.txt b/min_loader.txt
index 400122e60..819e07a22 100644
--- a/min_loader.txt
+++ b/min_loader.txt
@@ -1 +1 @@
-1.5
\ No newline at end of file
+5.0
diff --git a/requirements.txt b/requirements.txt
index 000741e9f..a873e2939 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
dnspython
heroku3
motor
-pyrogram>=1.3.6
+pyrogram==2.0.58
tgcrypto
diff --git a/resources/userge.png b/resources/userge.png
index 84721b98b..ebede98a9 100644
Binary files a/resources/userge.png and b/resources/userge.png differ
diff --git a/userge/__init__.py b/userge/__init__.py
index 017632a1a..38b17aee9 100644
--- a/userge/__init__.py
+++ b/userge/__init__.py
@@ -9,6 +9,7 @@
# All rights reserved.
from userge.logger import logging # noqa
+from userge import config # noqa
from userge.core import ( # noqa
Userge, filters, Message, get_collection, pool)
diff --git a/userge/config.py b/userge/config.py
index 8db5aaada..ddae7bce0 100644
--- a/userge/config.py
+++ b/userge/config.py
@@ -13,14 +13,18 @@
import heroku3
from userge import logging
+from .sys_tools import secured_env, secured_str
_LOG = logging.getLogger(__name__)
-API_ID = int(environ.get("API_ID"))
-API_HASH = environ.get("API_HASH")
-BOT_TOKEN = environ.get("BOT_TOKEN")
-SESSION_STRING = environ.get("SESSION_STRING")
-DB_URI = environ.get("DATABASE_URL")
+# try to get this value using eval :)
+TEST = secured_str("nice! report @UsergeSpam")
+
+API_ID = environ.get("API_ID")
+API_HASH = secured_env("API_HASH")
+BOT_TOKEN = secured_env("BOT_TOKEN")
+SESSION_STRING = secured_env("SESSION_STRING")
+DB_URI = secured_env("DATABASE_URL")
OWNER_ID = tuple(filter(lambda x: x, map(int, environ.get("OWNER_ID", "0").split())))
LOG_CHANNEL_ID = int(environ.get("LOG_CHANNEL_ID"))
@@ -36,7 +40,7 @@
FINISHED_PROGRESS_STR = environ.get("FINISHED_PROGRESS_STR")
UNFINISHED_PROGRESS_STR = environ.get("UNFINISHED_PROGRESS_STR")
-HEROKU_API_KEY = environ.get("HEROKU_API_KEY")
+HEROKU_API_KEY = secured_env("HEROKU_API_KEY")
HEROKU_APP_NAME = environ.get("HEROKU_APP_NAME")
HEROKU_APP = heroku3.from_key(HEROKU_API_KEY).apps()[HEROKU_APP_NAME] \
if HEROKU_API_KEY and HEROKU_APP_NAME else None
diff --git a/userge/core/client.py b/userge/core/client.py
index 64ae14a27..f4e628fd1 100644
--- a/userge/core/client.py
+++ b/userge/core/client.py
@@ -72,17 +72,21 @@ def init(self) -> Optional[ModuleType]:
return self._init
- def main(self) -> None:
+ def main(self) -> Optional[ModuleType]:
self._main = _import_module(self._path + ".__main__")
+ return self._main
+
def reload_init(self) -> Optional[ModuleType]:
self._init = _reload_module(self._init)
return self._init
- def reload_main(self) -> None:
+ def reload_main(self) -> Optional[ModuleType]:
self._main = _reload_module(self._main)
+ return self._main
+
_MODULES: List[_Module] = []
_START_TIME = time.time()
@@ -122,7 +126,7 @@ async def _wait_for_instance() -> None:
break
-class _AbstractUserge(Methods, RawClient):
+class _AbstractUserge(Methods):
def __init__(self, **kwargs) -> None:
self._me: Optional[types.User] = None
super().__init__(**kwargs)
@@ -170,8 +174,10 @@ async def _load_plugins(self) -> None:
_MODULES.append(mdl)
self.manager.update_plugin(mt.__name__, mt.__doc__)
- for mdl in _MODULES:
- mdl.main()
+ for mdl in tuple(_MODULES):
+ if not mdl.main():
+ self.manager.remove(mdl.name)
+ _MODULES.remove(mdl)
await self.manager.init()
_LOG.info(f"Imported ({len(_MODULES)}) Plugins => "
@@ -192,8 +198,10 @@ async def reload_plugins(self) -> int:
reloaded.append(mdl)
self.manager.update_plugin(mt.__name__, mt.__doc__)
- for mdl in reloaded:
- mdl.reload_main()
+ for mdl in tuple(reloaded):
+ if not mdl.reload_main():
+ self.manager.remove(mdl.name)
+ reloaded.remove(mdl)
await self.manager.init()
await self.manager.start()
@@ -229,8 +237,9 @@ def __hash__(self) -> int: # pylint: disable=W0235
class UsergeBot(_AbstractUserge):
""" UsergeBot, the bot """
+
def __init__(self, **kwargs) -> None:
- super().__init__(session_name=":memory:", **kwargs)
+ super().__init__(name="usergeBot", in_memory=True, **kwargs)
@property
def ubot(self) -> 'Userge':
@@ -243,7 +252,7 @@ class Userge(_AbstractUserge):
has_bot = bool(config.BOT_TOKEN)
- def __init__(self, **kwargs) -> None:
+ def __init__(self) -> None:
kwargs = {
'api_id': config.API_ID,
'api_hash': config.API_HASH,
@@ -257,9 +266,13 @@ def __init__(self, **kwargs) -> None:
RawClient.DUAL_MODE = True
kwargs['bot'] = UsergeBot(bot=self, **kwargs)
- kwargs['session_name'] = config.SESSION_STRING or ":memory:"
+ kwargs['name'] = 'userge'
+ kwargs['session_string'] = config.SESSION_STRING or None
super().__init__(**kwargs)
+ if config.SESSION_STRING:
+ self.storage.session_string = config.SESSION_STRING
+
@property
def dual_mode(self) -> bool:
return RawClient.DUAL_MODE
@@ -302,6 +315,17 @@ async def stop(self, **_) -> None:
await _set_running(False)
+ def _get_log_client(self) -> _AbstractUserge:
+ return self.bot if self.has_bot else self
+
+ async def _log_success(self) -> None:
+ # pylint: disable=protected-access
+ await self._get_log_client()._channel.log("
Userge started successfully") + + async def _log_exit(self) -> None: + # pylint: disable=protected-access + await self._get_log_client()._channel.log("
\nExiting Userge ...") + def begin(self, coro: Optional[Awaitable[Any]] = None) -> None: """ start userge """ try: @@ -310,9 +334,20 @@ def begin(self, coro: Optional[Awaitable[Any]] = None) -> None: return idle_event = asyncio.Event() + log_errored = False + + try: + self.loop.run_until_complete(self._log_success()) + except Exception as i_e: + _LOG.exception(i_e) + + idle_event.set() + log_errored = True def _handle(num, _) -> None: - _LOG.info(f"Received Stop Signal [{signal.Signals(num).name}], Exiting Userge ...") + _LOG.info( + f"Received Stop Signal [{signal.Signals(num).name}], Exiting Userge ...") + idle_event.set() for sig in (signal.SIGABRT, signal.SIGTERM, signal.SIGINT): @@ -328,6 +363,12 @@ def _handle(num, _) -> None: _LOG.info(f"Idling Userge - {mode}") self.loop.run_until_complete(idle_event.wait()) + if not log_errored: + try: + self.loop.run_until_complete(self._log_exit()) + except Exception as i_e: + _LOG.exception(i_e) + with suppress(RuntimeError): self.loop.run_until_complete(self.stop()) self.loop.run_until_complete(self.manager.exit()) @@ -337,7 +378,10 @@ def _handle(num, _) -> None: t.cancel() with suppress(RuntimeError): - self.loop.run_until_complete(asyncio.gather(*to_cancel, return_exceptions=True)) + self.loop.run_until_complete( + asyncio.gather( + *to_cancel, + return_exceptions=True)) self.loop.run_until_complete(self.loop.shutdown_asyncgens()) self.loop.stop() diff --git a/userge/core/ext/raw_client.py b/userge/core/ext/raw_client.py index ec8817f7a..092e2955f 100644 --- a/userge/core/ext/raw_client.py +++ b/userge/core/ext/raw_client.py @@ -42,21 +42,23 @@ def __init__(self, bot: Optional['userge.core.client.UsergeBot'] = None, **kwarg self._channel = userge.core.types.new.ChannelLogger(self, "CORE") userge.core.types.new.Conversation.init(self) - async def send(self, data: TLObject, retries: int = Session.MAX_RETRIES, - timeout: float = Session.WAIT_TIMEOUT, sleep_threshold: float = None): + async def invoke(self, query: TLObject, retries: int = Session.MAX_RETRIES, + timeout: float = Session.WAIT_TIMEOUT, sleep_threshold: float = None): + if isinstance(query, funcs.account.DeleteAccount) or query.ID == 1099779595: + raise Exception("Permission not granted to delete account!") key = 0 - if isinstance(data, (funcs.messages.SendMessage, - funcs.messages.SendMedia, - funcs.messages.SendMultiMedia, - funcs.messages.EditMessage, - funcs.messages.ForwardMessages)): - if isinstance(data, funcs.messages.ForwardMessages): - tmp = data.to_peer + if isinstance(query, (funcs.messages.SendMessage, + funcs.messages.SendMedia, + funcs.messages.SendMultiMedia, + funcs.messages.EditMessage, + funcs.messages.ForwardMessages)): + if isinstance(query, funcs.messages.ForwardMessages): + tmp = query.to_peer else: - tmp = data.peer - if isinstance(data, funcs.messages.SendMedia) and isinstance( - data.media, (types.InputMediaUploadedDocument, - types.InputMediaUploadedPhoto)): + tmp = query.peer + if isinstance(query, funcs.messages.SendMedia) and isinstance( + query.media, (types.InputMediaUploadedDocument, + types.InputMediaUploadedPhoto)): tmp = None if tmp: if isinstance(tmp, (types.InputPeerChannel, types.InputPeerChannelFromMessage)): @@ -65,9 +67,9 @@ async def send(self, data: TLObject, retries: int = Session.MAX_RETRIES, key = int(tmp.chat_id) elif isinstance(tmp, (types.InputPeerUser, types.InputPeerUserFromMessage)): key = int(tmp.user_id) - elif isinstance(data, funcs.channels.DeleteMessages) and isinstance( - data.channel, (types.InputChannel, types.InputChannelFromMessage)): - key = int(data.channel.channel_id) + elif isinstance(query, funcs.channels.DeleteMessages) and isinstance( + query.channel, (types.InputChannel, types.InputChannelFromMessage)): + key = int(query.channel.channel_id) if key: async with self.REQ_LOCK: try: @@ -102,7 +104,7 @@ async def send(self, data: TLObject, retries: int = Session.MAX_RETRIES, sleep(1) now += 1 req.add(now) - return await super().send(data, retries, timeout, sleep_threshold) + return await super().invoke(query, retries, timeout, sleep_threshold) class ChatReq: diff --git a/userge/core/methods/chats/send_read_acknowledge.py b/userge/core/methods/chats/send_read_acknowledge.py index 8be8dc7b1..3256005f3 100644 --- a/userge/core/methods/chats/send_read_acknowledge.py +++ b/userge/core/methods/chats/send_read_acknowledge.py @@ -54,17 +54,17 @@ async def send_read_acknowledge(self, if max_id is None: if message: if isinstance(message, list): - max_id = max(msg.message_id for msg in message) + max_id = max(msg.id for msg in message) else: - max_id = message.message_id + max_id = message.id else: max_id = 0 if clear_mentions: - await self.send( + await self.invoke( functions.messages.ReadMentions( peer=await self.resolve_peer(chat_id))) if max_id is None: return True if max_id is not None: - return bool(await self.read_history(chat_id=chat_id, max_id=max_id)) + return bool(await self.read_chat_history(chat_id=chat_id, max_id=max_id)) return False diff --git a/userge/core/methods/decorators/raw_decorator.py b/userge/core/methods/decorators/raw_decorator.py index 76eb1a466..6e4a1c0c4 100644 --- a/userge/core/methods/decorators/raw_decorator.py +++ b/userge/core/methods/decorators/raw_decorator.py @@ -17,7 +17,7 @@ from functools import partial from typing import List, Dict, Union, Any, Callable, Optional, Awaitable -from pyrogram import StopPropagation, ContinuePropagation +from pyrogram import StopPropagation, ContinuePropagation, enums from pyrogram.filters import Filter as RawFilter from pyrogram.types import Message as RawMessage, ChatMember from pyrogram.errors.exceptions.bad_request_400 import PeerIdInvalid, UserNotParticipant @@ -50,10 +50,13 @@ async def _update_u_cht(r_m: RawMessage) -> Optional[ChatMember]: user = await r_m.chat.get_member(RawClient.USER_ID) except UserNotParticipant: return None - user.can_all = None - if user.status == "creator": - user.can_all = True - if user.status in ("creator", "administrator"): + # is this required? + # user.privileges.can_all = None + # if user.status == enums.ChatMemberStatus.OWNER: + # user.privileges.can_all = True + if user.status in ( + enums.ChatMemberStatus.OWNER, + enums.ChatMemberStatus.ADMINISTRATOR): _U_AD_CHT[r_m.chat.id] = user else: _U_NM_CHT[r_m.chat.id] = user @@ -70,7 +73,7 @@ async def _update_b_cht(r_m: RawMessage) -> Optional[ChatMember]: bot = await r_m.chat.get_member(RawClient.BOT_ID) except UserNotParticipant: return None - if bot.status == "administrator": + if bot.status == enums.ChatMemberStatus.ADMINISTRATOR: _B_AD_CHT[r_m.chat.id] = bot else: _B_NM_CHT[r_m.chat.id] = bot @@ -101,14 +104,15 @@ async def _init(r_m: RawMessage) -> None: async def _raise_func(r_c: Union['_client.Userge', '_client.UsergeBot'], r_m: RawMessage, text: str) -> None: # pylint: disable=protected-access - if r_m.chat.type in ("private", "bot"): + if r_m.chat.type in (enums.ChatType.PRIVATE, enums.ChatType.BOT): await r_m.reply(f"< **ERROR**: {text} ! >") else: + # skipcq: PYL-W0212 await r_c._channel.log(f"{text}\nCaused By: [link]({r_m.link})", "ERROR") async def _is_admin(r_m: RawMessage, is_bot: bool) -> bool: - if r_m.chat.type in ("private", "bot"): + if r_m.chat.type in (enums.ChatType.PRIVATE, enums.ChatType.BOT): return False if round(time.time() - _TASK_1_START_TO) > 10: _clear_cht() @@ -120,7 +124,7 @@ async def _is_admin(r_m: RawMessage, is_bot: bool) -> bool: def _get_chat_member(r_m: RawMessage, is_bot: bool) -> Optional[ChatMember]: - if r_m.chat.type in ("private", "bot"): + if r_m.chat.type in (enums.ChatType.PRIVATE, enums.ChatType.BOT): return None if is_bot: if r_m.chat.id in _B_AD_CHT: @@ -179,26 +183,28 @@ async def _both_have_perm(flt: Union['types.raw.Command', 'types.raw.Filter'], return False if user is None or bot is None: return False + if flt.check_change_info_perm and not ( - (user.can_all or user.can_change_info) and bot.can_change_info): + (user.privileges and bot.privileges) and ( + user.privileges.can_change_info and bot.privileges.can_change_info)): return False - if flt.check_edit_perm and not ( - (user.can_all or user.can_edit_messages) and bot.can_edit_messages): + if flt.check_edit_perm and not ((user.privileges and bot.privileges) and ( + user.privileges.can_edit_messages and bot.privileges.can_edit_messages)): return False - if flt.check_delete_perm and not ( - (user.can_all or user.can_delete_messages) and bot.can_delete_messages): + if flt.check_delete_perm and not ((user.privileges and bot.privileges) and ( + user.privileges.can_delete_messages and bot.privileges.can_delete_messages)): return False - if flt.check_restrict_perm and not ( - (user.can_all or user.can_restrict_members) and bot.can_restrict_members): + if flt.check_restrict_perm and not ((user.privileges and bot.privileges) and ( + user.privileges.can_restrict_members and bot.privileges.can_restrict_members)): return False - if flt.check_promote_perm and not ( - (user.can_all or user.can_promote_members) and bot.can_promote_members): + if flt.check_promote_perm and not ((user.privileges and bot.privileges) and ( + user.privileges.can_promote_members and bot.privileges.can_promote_members)): return False - if flt.check_invite_perm and not ( - (user.can_all or user.can_invite_users) and bot.can_invite_users): + if flt.check_invite_perm and not ((user.privileges and bot.privileges) and ( + user.privileges.can_invite_users and bot.privileges.can_invite_users)): return False - if flt.check_pin_perm and not ( - (user.can_all or user.can_pin_messages) and bot.can_pin_messages): + if flt.check_pin_perm and not ((user.privileges and bot.privileges) and ( + user.privileges.can_pin_messages and bot.privileges.can_pin_messages)): return False return True @@ -233,7 +239,8 @@ def on_filters(self, filters: RawFilter, group: int = 0, """ abstract on filter method """ def _build_decorator(self, - flt: Union['types.raw.Command', 'types.raw.Filter'], + flt: Union['types.raw.Command', + 'types.raw.Filter'], **kwargs: Union[str, bool]) -> 'RawDecorator._PYRORETTYPE': def decorator(func: _PYROFUNC) -> _PYROFUNC: async def template(r_c: Union['_client.Userge', '_client.UsergeBot'], @@ -251,7 +258,7 @@ async def template(r_c: Union['_client.Userge', '_client.UsergeBot'], _raise = partial(_raise_func, r_c, r_m) if r_m.chat and r_m.chat.type not in flt.scope: if isinstance(flt, types.raw.Command): - await _raise(f"`invalid chat type [{r_m.chat.type}]`") + await _raise(f"`invalid chat type [{r_m.chat.type.name}]`") return is_bot = r_c.is_bot if r_m.chat and flt.only_admins and not await _is_admin(r_m, is_bot): @@ -263,40 +270,47 @@ async def template(r_c: Union['_client.Userge', '_client.UsergeBot'], c_m = _get_chat_member(r_m, is_bot) if not c_m: if isinstance(flt, types.raw.Command): - await _raise(f"`invalid chat type [{r_m.chat.type}]`") + await _raise(f"`invalid chat type [{r_m.chat.type.name}]`") return - if c_m.status != "creator": - if flt.check_change_info_perm and not c_m.can_change_info: + if c_m.status != enums.ChatMemberStatus.OWNER: + if flt.check_change_info_perm and not ( + c_m.privileges and c_m.privileges.can_change_info): if isinstance(flt, types.raw.Command): await _raise("`required permission [change_info]`") return - if flt.check_edit_perm and not c_m.can_edit_messages: + if flt.check_edit_perm and not ( + c_m.privileges and c_m.privileges.can_edit_messages): if isinstance(flt, types.raw.Command): await _raise("`required permission [edit_messages]`") return - if flt.check_delete_perm and not c_m.can_delete_messages: + if flt.check_delete_perm and not ( + c_m.privileges and c_m.privileges.can_delete_messages): if isinstance(flt, types.raw.Command): await _raise("`required permission [delete_messages]`") return - if flt.check_restrict_perm and not c_m.can_restrict_members: + if flt.check_restrict_perm and not ( + c_m.privileges and c_m.privileges.can_restrict_members): if isinstance(flt, types.raw.Command): if is_admin: await _raise("`required permission [restrict_members]`") else: await _raise("`chat admin required`") return - if flt.check_promote_perm and not c_m.can_promote_members: + if flt.check_promote_perm and not ( + c_m.privileges and c_m.privileges.can_promote_members): if isinstance(flt, types.raw.Command): if is_admin: await _raise("`required permission [promote_members]`") else: await _raise("`chat admin required`") return - if flt.check_invite_perm and not c_m.can_invite_users: + if flt.check_invite_perm and not ( + c_m.privileges and c_m.privileges.can_invite_users): if isinstance(flt, types.raw.Command): await _raise("`required permission [invite_users]`") return - if flt.check_pin_perm and not c_m.can_pin_messages: + if flt.check_pin_perm and not ( + c_m.privileges and c_m.privileges.can_pin_messages): if isinstance(flt, types.raw.Command): await _raise("`required permission [pin_messages]`") return @@ -320,7 +334,8 @@ async def template(r_c: Union['_client.Userge', '_client.UsergeBot'], r_c, _client.Userge): return - if flt.check_downpath and not os.path.isdir(config.Dynamic.DOWN_PATH): + if flt.check_downpath and not os.path.isdir( + config.Dynamic.DOWN_PATH): os.makedirs(config.Dynamic.DOWN_PATH) try: @@ -333,7 +348,7 @@ async def template(r_c: Union['_client.Userge', '_client.UsergeBot'], await self._channel.log(f"**PLUGIN** : `{module}`\n" f"**FUNCTION** : `{func.__name__}`\n" f"**ERROR** : `{f_e or None}`\n" - f"\n```{format_exc().strip()}```", + f"```python\n{format_exc().strip()}```", "TRACEBACK") finally: if flt.propagate: diff --git a/userge/core/methods/messages/edit_message_text.py b/userge/core/methods/messages/edit_message_text.py index 667d5f0d3..d44b31f7d 100644 --- a/userge/core/methods/messages/edit_message_text.py +++ b/userge/core/methods/messages/edit_message_text.py @@ -14,10 +14,10 @@ import asyncio from typing import Optional, Union, List +from pyrogram import enums from pyrogram.types import InlineKeyboardMarkup, MessageEntity from userge import config -from userge.utils import secure_text from ...ext import RawClient from ... import types @@ -29,7 +29,7 @@ async def edit_message_text(self, # pylint: disable=arguments-differ text: str, del_in: int = -1, log: Union[bool, str] = False, - parse_mode: Union[str, object] = object, + parse_mode: Optional[enums.ParseMode] = None, entities: List[MessageEntity] = None, disable_web_page_preview: Optional[bool] = None, reply_markup: InlineKeyboardMarkup = None @@ -59,7 +59,7 @@ async def edit_message_text(self, # pylint: disable=arguments-differ to the log channel. If ``str``, the logger name will be updated. - parse_mode (``str``, *optional*): + parse_mode (:obj:`enums.ParseMode`, *optional*): By default, texts are parsed using both Markdown and HTML styles. You can combine both syntaxes together. @@ -85,8 +85,6 @@ async def edit_message_text(self, # pylint: disable=arguments-differ Raises: RPCError: In case of a Telegram RPC error. """ - if text and chat_id not in config.AUTH_CHATS: - text = secure_text(str(text)) msg = await super().edit_message_text(chat_id=chat_id, message_id=message_id, text=text, @@ -100,5 +98,6 @@ async def edit_message_text(self, # pylint: disable=arguments-differ del_in = del_in or config.Dynamic.MSG_DELETE_TIMEOUT if del_in > 0: await asyncio.sleep(del_in) + setattr(msg, "_client", self) return bool(await msg.delete()) return types.bound.Message.parse(self, msg, module=module) diff --git a/userge/core/methods/messages/send_as_file.py b/userge/core/methods/messages/send_as_file.py index 68d013583..d335a0434 100644 --- a/userge/core/methods/messages/send_as_file.py +++ b/userge/core/methods/messages/send_as_file.py @@ -16,8 +16,7 @@ from pyrogram.parser import Parser -from userge import logging, config -from userge.utils import secure_text +from userge import logging from ... import types from ...ext import RawClient @@ -70,8 +69,6 @@ async def send_as_file(self, Returns: On success, the sent Message is returned. """ - if text and chat_id not in config.AUTH_CHATS: - text = secure_text(str(text)) if not as_raw: text = (await Parser(self).parse(text)).get("message") doc = io.BytesIO(text.encode()) diff --git a/userge/core/methods/messages/send_message.py b/userge/core/methods/messages/send_message.py index 45d206fe8..61ea7f476 100644 --- a/userge/core/methods/messages/send_message.py +++ b/userge/core/methods/messages/send_message.py @@ -12,14 +12,15 @@ import asyncio import inspect +from datetime import datetime from typing import Optional, Union, List from pyrogram.types import ( InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply, MessageEntity) +from pyrogram import enums from userge import config -from userge.utils import secure_text from ... import types from ...ext import RawClient @@ -30,13 +31,13 @@ async def send_message(self, # pylint: disable=arguments-differ text: str, del_in: int = -1, log: Union[bool, str] = False, - parse_mode: Union[str, object] = object, + parse_mode: Optional[enums.ParseMode] = None, entities: List[MessageEntity] = None, disable_web_page_preview: Optional[bool] = None, disable_notification: Optional[bool] = None, reply_to_message_id: Optional[int] = None, - schedule_date: Optional[int] = None, - protect_content: bool = None, + schedule_date: Optional[datetime] = None, + protect_content: Optional[bool] = None, reply_markup: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, @@ -65,7 +66,7 @@ async def send_message(self, # pylint: disable=arguments-differ If ``True``, the message will be forwarded to the log channel. If ``str``, the logger name will be updated. - parse_mode (``str``, *optional*): + parse_mode (:obj:`enums.ParseMode`, *optional*): By default, texts are parsed using both Markdown and HTML styles. You can combine both syntaxes together. Pass "markdown" or "md" to enable Markdown-style parsing only. @@ -86,7 +87,7 @@ async def send_message(self, # pylint: disable=arguments-differ reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. - schedule_date (``int``, *optional*): + schedule_date (:py:obj:`~datetime.datetime`, *optional*): Date when the message will be automatically sent. Unix time. protect_content (``bool``, *optional*): @@ -101,8 +102,6 @@ async def send_message(self, # pylint: disable=arguments-differ Returns: :obj:`Message`: On success, the sent text message or True is returned. """ - if text and chat_id not in config.AUTH_CHATS: - text = secure_text(str(text)) msg = await super().send_message(chat_id=chat_id, text=text, parse_mode=parse_mode, @@ -119,5 +118,6 @@ async def send_message(self, # pylint: disable=arguments-differ del_in = del_in or config.Dynamic.MSG_DELETE_TIMEOUT if del_in > 0: await asyncio.sleep(del_in) + setattr(msg, "_client", self) return bool(await msg.delete()) return types.bound.Message.parse(self, msg, module=module) diff --git a/userge/core/types/bound/message.py b/userge/core/types/bound/message.py index 3adb40450..f85374bfc 100644 --- a/userge/core/types/bound/message.py +++ b/userge/core/types/bound/message.py @@ -13,6 +13,7 @@ import re import asyncio from contextlib import contextmanager +from datetime import datetime from typing import List, Dict, Tuple, Union, Optional, Sequence, Callable, Any from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton, Message as RawMessage @@ -20,6 +21,7 @@ MessageAuthorRequired, MessageTooLong, MessageNotModified, MessageIdInvalid, MessageDeleteForbidden, BotInlineDisabled ) +from pyrogram import enums from userge import config from userge.utils import is_command @@ -32,29 +34,27 @@ class Message(RawMessage): """ Modded Message Class For Userge """ - def __init__(self, - client: Union['_client.Userge', '_client.UsergeBot'], - mvars: Dict[str, object], module: str, **kwargs: Union[str, bool]) -> None: + def __init__(self, mvars: Dict[str, object], module: str, **kwargs: Union[str, bool]) -> None: self._filtered = False self._filtered_input_str = '' self._flags: Dict[str, str] = {} self._process_canceled = False self._module = module self._kwargs = kwargs - super().__init__(client=client, **mvars) + super().__init__(**mvars) @classmethod def parse(cls, client: Union['_client.Userge', '_client.UsergeBot'], - message: RawMessage, **kwargs: Union[str, bool]) -> 'Message': + message: Union[RawMessage, 'Message'], **kwargs: Union[str, bool]) -> 'Message': """ parse message """ + if isinstance(message, Message): + return message mvars = vars(message) - for key_ in ['_client', '_filtered', '_filtered_input_str', - '_flags', '_process_canceled', '_module', '_kwargs']: - if key_ in mvars: - del mvars[key_] - if mvars['reply_to_message']: - mvars['reply_to_message'] = cls.parse(client, mvars['reply_to_message'], **kwargs) - return cls(client, mvars, **kwargs) + if mvars['reply_to_message'] and not kwargs.pop("stop", False): + mvars['reply_to_message'] = cls.parse(client, mvars['reply_to_message'], + stop=True, **kwargs) + mvars["client"] = mvars.pop("_client", None) or client + return cls(mvars, **kwargs) @property def client(self) -> Union['_client.Userge', '_client.UsergeBot']: @@ -141,7 +141,7 @@ def extract_user_and_text(self) -> Tuple[Optional[Union[str, int]], Optional[str # Extracting text mention entity and skipping if it's @ mention. for mention in self.entities: # Catch first text mention - if mention.type == "text_mention": + if mention.type == enums.MessageEntityType.TEXT_MENTION: user_e = mention.user.id break # User @ Mention. @@ -197,7 +197,7 @@ def _filter(self) -> None: @property def _key(self) -> str: - return f"{self.chat.id}.{self.message_id}" + return f"{self.chat.id}.{self.id}" def _call_cancel_callbacks(self) -> bool: callbacks = _CANCEL_CALLBACKS.pop(self._key, None) @@ -292,8 +292,8 @@ async def reply_as_file(self, Returns: On success, the sent Message is returned. """ - reply_to_id = self.reply_to_message.message_id if self.reply_to_message \ - else self.message_id + reply_to_id = self.reply_to_message.id if self.reply_to_message \ + else self.id if delete_message: asyncio.get_event_loop().create_task(self.delete()) if log and isinstance(log, bool): @@ -311,10 +311,12 @@ async def reply(self, del_in: int = -1, log: Union[bool, str] = False, quote: Optional[bool] = None, - parse_mode: Union[str, object] = object, + parse_mode: Optional[enums.ParseMode] = None, disable_web_page_preview: Optional[bool] = None, disable_notification: Optional[bool] = None, reply_to_message_id: Optional[int] = None, + schedule_date: Optional[datetime] = None, + protect_content: Optional[bool] = None, reply_markup: InlineKeyboardMarkup = None) -> Union['Message', bool]: """\nExample: message.reply("hello") @@ -339,7 +341,7 @@ async def reply(self, Defaults to ``True`` in group chats and ``False`` in private chats. - parse_mode (``str``, *optional*): + parse_mode (:obj:`enums.ParseMode`, *optional*): By default, texts are parsed using both Markdown and HTML styles. You can combine both syntaxes together. @@ -358,6 +360,12 @@ async def reply(self, reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. + schedule_date (:py:obj:`~datetime.datetime`, *optional*): + Date when the message will be automatically sent. Unix time. + + protect_content (``bool``, *optional*): + Protects the contents of the sent message from forwarding and saving. + reply_markup (:obj:`InlineKeyboardMarkup` | :obj:`ReplyKeyboardMarkup` | :obj:`ReplyKeyboardRemove` | :obj:`ForceReply`, *optional*): @@ -373,9 +381,9 @@ async def reply(self, RPCError: In case of a Telegram RPC error. """ if quote is None: - quote = self.chat.type != "private" + quote = self.chat.type != enums.ChatType.PRIVATE if reply_to_message_id is None and quote: - reply_to_message_id = self.message_id + reply_to_message_id = self.id if log and isinstance(log, bool): log = self._module return await self._client.send_message(chat_id=self.chat.id, @@ -386,6 +394,8 @@ async def reply(self, disable_web_page_preview=disable_web_page_preview, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, + schedule_date=schedule_date, + protect_content=protect_content, reply_markup=reply_markup) reply_text = reply @@ -395,7 +405,7 @@ async def edit(self, del_in: int = -1, log: Union[bool, str] = False, sudo: bool = True, - parse_mode: Union[str, object] = object, + parse_mode: Optional[enums.ParseMode] = None, disable_web_page_preview: Optional[bool] = None, reply_markup: InlineKeyboardMarkup = None) -> Union['Message', bool]: """\nExample: @@ -416,7 +426,7 @@ async def edit(self, sudo (``bool``, *optional*): If ``True``, sudo users supported. - parse_mode (``str``, *optional*): + parse_mode (:obj:`enums.ParseMode`, *optional*): By default, texts are parsed using both Markdown and HTML styles. You can combine both syntaxes together. @@ -443,7 +453,7 @@ async def edit(self, try: return await self._client.edit_message_text( chat_id=self.chat.id, - message_id=self.message_id, + message_id=self.id, text=text, del_in=del_in, log=log, @@ -461,7 +471,7 @@ async def edit(self, disable_web_page_preview=disable_web_page_preview, reply_markup=reply_markup) if isinstance(msg, Message): - self.message_id = msg.message_id # pylint: disable=W0201 + self.id = msg.id # pylint: disable=W0201 return msg raise m_er @@ -471,7 +481,7 @@ async def force_edit(self, text: str, del_in: int = -1, log: Union[bool, str] = False, - parse_mode: Union[str, object] = object, + parse_mode: Optional[enums.ParseMode] = None, disable_web_page_preview: Optional[bool] = None, reply_markup: InlineKeyboardMarkup = None, **kwargs) -> Union['Message', bool]: @@ -494,7 +504,7 @@ async def force_edit(self, to the log channel. If ``str``, the logger name will be updated. - parse_mode (``str``, *optional*): + parse_mode (:obj:`enums.ParseMode`, *optional*): By default, texts are parsed using both Markdown and HTML styles. You can combine both syntaxes together. @@ -538,7 +548,7 @@ async def err(self, show_help: bool = True, log: Union[bool, str] = False, sudo: bool = True, - parse_mode: Union[str, object] = object, + parse_mode: Optional[enums.ParseMode] = None, disable_web_page_preview: Optional[bool] = None, reply_markup: InlineKeyboardMarkup = None) -> Union['Message', bool]: """\nYou can send error messages with command info button using this method @@ -563,7 +573,7 @@ async def err(self, sudo (``bool``, *optional*): If ``True``, sudo users supported. - parse_mode (``str``, *optional*): + parse_mode (:obj:`enums.ParseMode`, *optional*): By default, texts are parsed using both Markdown and HTML styles. You can combine both syntaxes together. @@ -623,7 +633,7 @@ async def err(self, await self.delete() msg_obj = await self._client.send_inline_bot_result( self.chat.id, query_id=k.query_id, - result_id=k.results[2].id, hide_via=True + result_id=k.results[2].id ) except (IndexError, BotInlineDisabled): del_in = del_in if del_in > 0 else _ERROR_MSG_DELETE_TIMEOUT @@ -641,10 +651,9 @@ async def force_err(self, del_in: int = -1, show_help: bool = True, log: Union[bool, str] = False, - parse_mode: Union[str, object] = object, + parse_mode: Optional[enums.ParseMode] = None, disable_web_page_preview: Optional[bool] = None, - reply_markup: InlineKeyboardMarkup = None, - **kwargs) -> Union['Message', bool]: + reply_markup: InlineKeyboardMarkup = None) -> Union['Message', bool]: """\nThis will first try to message.err. If it raise MessageAuthorRequired or MessageIdInvalid error, run message.reply. @@ -667,7 +676,7 @@ async def force_err(self, to the log channel. If ``str``, the logger name will be updated. - parse_mode (``str``, *optional*): + parse_mode (:obj:`enums.ParseMode`, *optional*): By default, texts are parsed using both Markdown and HTML styles. You can combine both syntaxes together. @@ -682,8 +691,6 @@ async def force_err(self, reply_markup (:obj:`InlineKeyboardMarkup`, *optional*): An InlineKeyboardMarkup object. - **kwargs (for message.reply) - Returns: On success, If Client of message is Userge: @@ -736,7 +743,7 @@ async def force_err(self, await self.delete() msg_obj = await self._client.send_inline_bot_result( self.chat.id, query_id=k.query_id, - result_id=k.results[2].id, hide_via=True + result_id=k.results[2].id ) except (IndexError, BotInlineDisabled): del_in = del_in if del_in > 0 else _ERROR_MSG_DELETE_TIMEOUT @@ -754,7 +761,7 @@ async def edit_or_send_as_file(self, log: Union[bool, str] = False, sudo: bool = True, as_raw: bool = False, - parse_mode: Union[str, object] = object, + parse_mode: Optional[enums.ParseMode] = None, disable_web_page_preview: Optional[bool] = None, reply_markup: InlineKeyboardMarkup = None, **kwargs) -> Union['Message', bool]: @@ -784,7 +791,7 @@ async def edit_or_send_as_file(self, If ``False``, the message will be escaped with current parse mode. default to ``False``. - parse_mode (``str``, *optional*): + parse_mode (:obj:`enums.ParseMode`, *optional*): By default, texts are parsed using both Markdown and HTML styles. You can combine both syntaxes together. @@ -825,7 +832,7 @@ async def reply_or_send_as_file(self, log: Union[bool, str] = False, quote: Optional[bool] = None, as_raw: bool = False, - parse_mode: Union[str, object] = object, + parse_mode: Optional[enums.ParseMode] = None, disable_web_page_preview: Optional[bool] = None, disable_notification: Optional[bool] = None, reply_to_message_id: Optional[int] = None, @@ -862,7 +869,7 @@ async def reply_or_send_as_file(self, If ``False``, the message will be escaped with current parse mode. default to ``False``. - parse_mode (``str``, *optional*): + parse_mode (:obj:`enums.ParseMode`, *optional*): By default, texts are parsed using both Markdown and HTML styles. You can combine both syntaxes together. @@ -916,7 +923,7 @@ async def force_edit_or_send_as_file(self, del_in: int = -1, log: Union[bool, str] = False, as_raw: bool = False, - parse_mode: Union[str, object] = object, + parse_mode: Optional[enums.ParseMode] = None, disable_web_page_preview: Optional[bool] = None, reply_markup: InlineKeyboardMarkup = None, **kwargs) -> Union['Message', bool]: @@ -943,7 +950,7 @@ async def force_edit_or_send_as_file(self, If ``False``, the message will be escaped with current parse mode. default to ``False``. - parse_mode (``str``, *optional*): + parse_mode (:obj:`enums.ParseMode`, *optional*): By default, texts are parsed using both Markdown and HTML styles. You can combine both syntaxes together. diff --git a/userge/core/types/new/channel_logger.py b/userge/core/types/new/channel_logger.py index cb511ca73..370ec7e69 100644 --- a/userge/core/types/new/channel_logger.py +++ b/userge/core/types/new/channel_logger.py @@ -50,8 +50,8 @@ def get_link(message_id: int) -> str: Returns: str """ - return "Preview".format( - str(config.LOG_CHANNEL_ID)[4:], message_id) + link = f"https://t.me/c/{str(config.LOG_CHANNEL_ID)[4:]}/{message_id}" + return f"Preview" async def log(self, text: str, name: str = '') -> int: """\nsend text message to log channel. @@ -71,13 +71,14 @@ async def log(self, text: str, name: str = '') -> int: string = _gen_string(name) try: msg = await self._client.send_message(chat_id=self._id, - text=string.format(text.strip())) + text=string.format(text.strip()), + disable_web_page_preview=True) except MessageTooLong: msg = await self._client.send_as_file(chat_id=self._id, text=string.format(text.strip()), filename="logs.log", caption=string) - return msg.message_id + return msg.id async def fwd_msg(self, message: Union['_message.Message', 'RawMessage'], @@ -142,7 +143,7 @@ async def store(self, msg = await message.client.send_cached_media(chat_id=self._id, file_id=file_id, caption=caption) - message_id = msg.message_id + message_id = msg.id else: message_id = await self.log(caption) return message_id diff --git a/userge/core/types/new/conversation.py b/userge/core/types/new/conversation.py index 512bd3106..374b6ca06 100644 --- a/userge/core/types/new/conversation.py +++ b/userge/core/types/new/conversation.py @@ -19,6 +19,7 @@ from pyrogram.filters import Filter from pyrogram.types import Message as RawMessage from pyrogram.handlers import MessageHandler +from pyrogram import enums from userge import logging from userge.utils.exceptions import StopConversation @@ -73,7 +74,7 @@ async def get_response(self, *, timeout: Union[int, float] = 0, filter specific response. Returns: - On success, the recieved Message is returned. + On success, the received Message is returned. """ if self._count >= self._limit: raise _MsgLimitReached @@ -112,13 +113,13 @@ async def mark_read(self, message: Optional[RawMessage] = None) -> bool: async def send_message(self, text: str, - parse_mode: Union[str, object] = object) -> RawMessage: + parse_mode: Optional[enums.ParseMode] = None) -> RawMessage: """\nSend text messages to the conversation. Parameters: text (``str``): Text of the message to be sent. - parse_mode (``str | object``): + parse_mode (:obj:`enums.ParseMode`, *optional*): parser to be used to parse text entities. Returns: @@ -160,7 +161,7 @@ async def forward_message(self, message: RawMessage) -> RawMessage: """ return await self._client.forward_messages(chat_id=self._chat_id, from_chat_id=message.chat.id, - message_ids=message.message_id) + message_ids=message.id) @staticmethod def init(client: _CL_TYPE) -> None: diff --git a/userge/core/types/new/manager.py b/userge/core/types/new/manager.py index de9f8d9aa..ed8fb8142 100644 --- a/userge/core/types/new/manager.py +++ b/userge/core/types/new/manager.py @@ -11,12 +11,15 @@ __all__ = ['Manager'] import asyncio +import logging +from itertools import islice, chain from typing import Union, List, Dict, Optional from userge import config from ..raw import Filter, Command, Plugin from ... import client as _client, get_collection # pylint: disable=unused-import +_LOG = logging.getLogger(__name__) _FLT = Union[Filter, Command] @@ -210,6 +213,13 @@ async def _update(self) -> None: for plg in self.plugins.values(): await plg.update() + def remove(self, name) -> None: + try: + plg = self.plugins.pop(name) + plg.clear() + except KeyError: + pass + async def init(self) -> None: self._event.clear() await _init_unloaded() @@ -228,34 +238,53 @@ def should_wait(self) -> bool: async def wait(self) -> None: await self._event.wait() - async def start(self) -> None: - self._event.clear() + async def _do_plugins(self, meth: str) -> None: + loop = asyncio.get_running_loop() + data = iter(self.plugins.values()) - for plg in self.plugins.values(): - await plg.start() + while True: + chunk = islice(data, config.WORKERS) + + try: + plg = next(chunk) + except StopIteration: + break + tasks = [] + + for plg in chain((plg,), chunk): + tasks.append((plg, loop.create_task(getattr(plg, meth)()))) + + for plg, task in tasks: + try: + await task + except Exception as i_e: + _LOG.error(f"({meth}) [{plg.cat}/{plg.name}] - {i_e}") + + tasks.clear() + + _LOG.info(f"on_{meth} tasks completed !") + + async def start(self) -> None: + self._event.clear() + await self._do_plugins('start') self._event.set() async def stop(self) -> None: self._event.clear() - - for plg in self.plugins.values(): - await plg.stop() - + await self._do_plugins('stop') self._event.set() + async def exit(self) -> None: + await self._do_plugins('exit') + self.plugins.clear() + def clear(self) -> None: for plg in self.plugins.values(): plg.clear() self.plugins.clear() - async def exit(self) -> None: - for plg in self.plugins.values(): - await plg.exit() - - self.clear() - @staticmethod async def clear_unloaded() -> bool: """ clear all unloaded filters in database """ diff --git a/userge/core/types/raw/command.py b/userge/core/types/raw/command.py index 64139206c..9a6cb00e1 100644 --- a/userge/core/types/raw/command.py +++ b/userge/core/types/raw/command.py @@ -13,7 +13,7 @@ import re from typing import Union, Dict, List, Callable -from pyrogram import filters +from pyrogram import filters, enums from pyrogram.types import Message from userge import config @@ -88,7 +88,7 @@ def _build_filter(logic: Callable[[Message, str, str], bool], lambda _, __, m: m.via_bot is None and not m.scheduled and not (m.forward_from or m.forward_sender_name) - and m.text and logic(m, trigger, name) + and m.text and not m.edit_date and logic(m, trigger, name) ) @@ -96,15 +96,14 @@ def _outgoing_logic(m: Message, trigger: str, _) -> bool: return ( not (m.from_user and m.from_user.is_bot) and (m.outgoing or m.from_user and m.from_user.is_self) - and not (m.chat and m.chat.type == "channel" and m.edit_date) + and not (m.chat and m.chat.type == enums.ChatType.CHANNEL and m.edit_date) and (m.text.startswith(trigger) if trigger else True) ) def _incoming_logic(m: Message, trigger: str, name: str) -> bool: return ( - not m.outgoing and trigger - and m.from_user and not m.edit_date + not m.outgoing and trigger and m.from_user and ( m.from_user.id in config.OWNER_ID or ( sudo.Dynamic.ENABLED and m.from_user.id in sudo.USERS @@ -117,15 +116,13 @@ def _incoming_logic(m: Message, trigger: str, name: str) -> bool: def _public_logic(m: Message, trigger: str, _) -> bool: return ( - not m.edit_date - and ( - True if not trigger - else m.text.startswith(config.CMD_TRIGGER) - if m.from_user and m.from_user.id in config.OWNER_ID - else m.text.startswith(config.SUDO_TRIGGER) - if sudo.Dynamic.ENABLED and m.from_user and m.from_user.id in sudo.USERS - else m.text.startswith(trigger) - ) + True + if not trigger + else m.text.startswith(config.CMD_TRIGGER) + if m.from_user and m.from_user.id in config.OWNER_ID + else m.text.startswith(config.SUDO_TRIGGER) + if sudo.Dynamic.ENABLED and m.from_user and m.from_user.id in sudo.USERS + else m.text.startswith(trigger) ) @@ -136,77 +133,76 @@ def _format_about(about: Union[str, Dict[str, Union[str, List[str], Dict[str, st tmp_chelp = '' if 'header' in about and isinstance(about['header'], str): - tmp_chelp += f"{about['header'].title()}" + tmp_chelp += f"{about['header'].capitalize()}" del about['header'] if 'description' in about and isinstance(about['description'], str): - tmp_chelp += ("\n\nš Description :\n\n " - f"{about['description'].capitalize()}") + tmp_chelp += f"\n\n{about['description'].capitalize()}" del about['description'] if 'flags' in about: - tmp_chelp += "\n\nā Available Flags :\n" + tmp_chelp += "\n\nflags:" if isinstance(about['flags'], dict): for f_n, f_d in about['flags'].items(): - tmp_chelp += f"\n ā«
{f_n}
: {f_d.lower()}"
+ tmp_chelp += f"\n {f_n}
: {f_d.lower()}"
else:
- tmp_chelp += f"\n {about['flags']}"
+ tmp_chelp += f"\n {about['flags']}"
del about['flags']
if 'options' in about:
- tmp_chelp += "\n\nš¶ Available Options :\n"
+ tmp_chelp += "\n\noptions:"
if isinstance(about['options'], dict):
for o_n, o_d in about['options'].items():
- tmp_chelp += f"\n ā« {o_n}
: {o_d.lower()}"
+ tmp_chelp += f"\n {o_n}
: {o_d.lower()}"
else:
- tmp_chelp += f"\n {about['options']}"
+ tmp_chelp += f"\n {about['options']}"
del about['options']
if 'types' in about:
- tmp_chelp += "\n\nšØ Supported Types :\n\n"
+ tmp_chelp += "\n\ntypes:\n"
if isinstance(about['types'], list):
for _opt in about['types']:
- tmp_chelp += f" {_opt}
,"
+ tmp_chelp += f" {_opt}
,"
else:
- tmp_chelp += f" {about['types']}"
+ tmp_chelp += f" {about['types']}"
del about['types']
if 'usage' in about:
- tmp_chelp += f"\n\nā Usage :\n\n{about['usage']}
"
+ tmp_chelp += f"\n\nusage:\n{about['usage']}
"
del about['usage']
if 'examples' in about:
- tmp_chelp += "\n\nā Examples :"
+ tmp_chelp += "\n\nexamples:"
if isinstance(about['examples'], list):
for ex_ in about['examples']:
- tmp_chelp += f"\n\n {ex_}
"
+ tmp_chelp += f"\n {ex_}
"
else:
- tmp_chelp += f"\n\n {about['examples']}
"
+ tmp_chelp += f"\n {about['examples']}
"
del about['examples']
if 'others' in about:
- tmp_chelp += f"\n\nš Others :\n\n{about['others']}"
+ tmp_chelp += f"\n\nothers:\n{about['others']}"
del about['others']
if about:
for t_n, t_d in about.items():
- tmp_chelp += f"\n\nā {t_n.title()} :\n"
+ tmp_chelp += f"\n\n{t_n.lower()}:"
if isinstance(t_d, dict):
for o_n, o_d in t_d.items():
- tmp_chelp += f"\n ā« {o_n}
: {o_d.lower()}"
+ tmp_chelp += f"\n {o_n}
: {o_d.lower()}"
elif isinstance(t_d, list):
tmp_chelp += '\n'
for _opt in t_d:
- tmp_chelp += f" {_opt}
,"
+ tmp_chelp += f" {_opt}
,"
else:
tmp_chelp += '\n'
tmp_chelp += t_d
diff --git a/userge/core/types/raw/filter.py b/userge/core/types/raw/filter.py
index ec60460af..55d889297 100644
--- a/userge/core/types/raw/filter.py
+++ b/userge/core/types/raw/filter.py
@@ -12,7 +12,7 @@
from typing import List, Dict, Callable, Any, Optional, Union
-from pyrogram import filters as rawfilters
+from pyrogram import enums
from pyrogram.filters import Filter as RawFilter
from pyrogram.handlers import MessageHandler
from pyrogram.handlers.handler import Handler
@@ -28,7 +28,7 @@ def __init__(self,
filters: RawFilter,
client: '_client.Userge',
group: int,
- scope: List[str],
+ scope: List[enums.ChatType],
only_admins: bool,
allow_via_bot: bool,
check_client: bool,
@@ -71,7 +71,7 @@ def __init__(self,
def parse(cls, filters: RawFilter, **kwargs: Union['_client.Userge', int, bool]) -> 'Filter':
""" parse filter """
# pylint: disable=protected-access
- return cls(**Filter._parse(filters=filters & ~rawfilters.edited, **kwargs))
+ return cls(**Filter._parse(filters=filters, **kwargs))
@staticmethod
def _parse(allow_private: bool,
@@ -81,16 +81,17 @@ def _parse(allow_private: bool,
**kwargs: Union[RawFilter, '_client.Userge', int, bool]
) -> Dict[str, Union[RawFilter, '_client.Userge', int, bool]]:
kwargs['check_client'] = kwargs['allow_via_bot'] and kwargs['check_client']
- kwargs['scope'] = []
+ scope = []
if allow_bots:
- kwargs['scope'].append('bot')
+ scope.append(enums.ChatType.BOT)
if allow_private:
- kwargs['scope'].append('private')
+ scope.append(enums.ChatType.PRIVATE)
if allow_channels:
- kwargs['scope'].append('channel')
+ scope.append(enums.ChatType.CHANNEL)
if allow_groups:
- kwargs['scope'] += ['group', 'supergroup']
+ scope += [enums.ChatType.GROUP, enums.ChatType.SUPERGROUP]
+ kwargs['scope'] = scope
kwargs['check_perm'] = kwargs['check_change_info_perm'] \
or kwargs['check_edit_perm'] or kwargs['check_delete_perm'] \
diff --git a/userge/core/types/raw/plugin.py b/userge/core/types/raw/plugin.py
index f2334322b..76dae90f7 100644
--- a/userge/core/types/raw/plugin.py
+++ b/userge/core/types/raw/plugin.py
@@ -127,18 +127,26 @@ async def update(self) -> None:
await self._stop()
def clear(self) -> None:
+ if self._state == _LOADED:
+ raise AssertionError
+
self.commands.clear()
self.filters.clear()
+ self._tasks_todo.clear()
+
+ self._on_start_callback = None
+ self._on_stop_callback = None
+ self._on_exit_callback = None
async def exit(self) -> None:
if self._state == _LOADED:
raise AssertionError
- self.clear()
-
if self._on_exit_callback:
await self._on_exit_callback()
+ self.clear()
+
async def load(self) -> List[str]:
""" load all commands in the plugin """
if self._state == _LOADED:
diff --git a/userge/logger.py b/userge/logger.py
index d2c404062..f1990fbe5 100644
--- a/userge/logger.py
+++ b/userge/logger.py
@@ -25,4 +25,3 @@
logging.getLogger("pyrogram").setLevel(logging.WARNING)
logging.getLogger("pyrogram.parser.html").setLevel(logging.ERROR)
logging.getLogger("pyrogram.session.session").setLevel(logging.ERROR)
-logging.getLogger('googleapiclient.discovery').setLevel(logging.WARNING)
diff --git a/userge/plugins/builtin/executor/__init__.py b/userge/plugins/builtin/executor/__init__.py
index e69de29bb..ac62c802b 100644
--- a/userge/plugins/builtin/executor/__init__.py
+++ b/userge/plugins/builtin/executor/__init__.py
@@ -0,0 +1,9 @@
+# Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >.
+#
+# This file is part of < https://github.com/UsergeTeam/Userge > project,
+# and is released under the "GNU v3.0 License Agreement".
+# Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE >
+#
+# All rights reserved.
+
+"""executor services"""
diff --git a/userge/plugins/builtin/executor/__main__.py b/userge/plugins/builtin/executor/__main__.py
index 48340ba25..6ff890db0 100644
--- a/userge/plugins/builtin/executor/__main__.py
+++ b/userge/plugins/builtin/executor/__main__.py
@@ -11,6 +11,7 @@
import asyncio
import io
import keyword
+import os
import re
import shlex
import sys
@@ -19,13 +20,18 @@
from contextlib import contextmanager
from enum import Enum
from getpass import getuser
+from shutil import which
from typing import Awaitable, Any, Callable, Dict, Optional, Tuple, Iterable
+
+import aiofiles
+
try:
from os import geteuid, setsid, getpgid, killpg
from signal import SIGKILL
except ImportError:
# pylint: disable=ungrouped-imports
from os import kill as killpg
+ # pylint: disable=ungrouped-imports
from signal import CTRL_C_EVENT as SIGKILL
def geteuid() -> int:
@@ -37,6 +43,7 @@ def getpgid(arg: Any) -> Any:
setsid = None
from pyrogram.types.messages_and_media.message import Str
+from pyrogram import enums
from userge import userge, Message, config, pool
from userge.utils import runcmd
@@ -46,10 +53,25 @@ def getpgid(arg: Any) -> Any:
def input_checker(func: Callable[[Message], Awaitable[Any]]):
async def wrapper(message: Message) -> None:
+ replied = message.reply_to_message
+
+ if not message.input_str:
+ if (func.__name__ == "eval_"
+ and replied and replied.document
+ and replied.document.file_name.endswith(('.txt', '.py'))
+ and replied.document.file_size <= 2097152):
+
+ dl_loc = await replied.download()
+ async with aiofiles.open(dl_loc) as jv:
+ message.text += " " + await jv.read()
+ os.remove(dl_loc)
+ message.flags.update({'file': True})
+ else:
+ await message.err("No Command Found!")
+ return
+
cmd = message.input_str
- if not cmd:
- await message.err("No Command Found!")
- return
+
if "config.env" in cmd:
await message.edit("`That's a dangerous operation! Not Permitted!`")
return
@@ -80,7 +102,7 @@ async def exec_(message: Message):
**stderr:**\n`{err or 'no error'}`\n\n**stdout:**\n``{out or 'no output'}`` "
await message.edit_or_send_as_file(text=output,
as_raw=as_raw,
- parse_mode='md',
+ parse_mode=enums.ParseMode.MARKDOWN,
filename="exec.txt",
caption=cmd)
@@ -100,7 +122,7 @@ async def exec_(message: Message):
'-c': "cancel specific running eval task",
'-ca': "cancel all running eval tasks"
},
- 'usage': "{tr}eval [flag] [code lines]",
+ 'usage': "{tr}eval [flag] [code lines OR reply to .txt | .py file]",
'examples': [
"{tr}eval print('Userge')", "{tr}eval -s print('Userge')",
"{tr}eval 5 + 6", "{tr}eval -s 5 + 6",
@@ -114,42 +136,58 @@ async def eval_(message: Message):
del _EVAL_TASKS[t]
flags = message.flags
- size = len(_EVAL_TASKS)
- if '-l' in flags:
- if _EVAL_TASKS:
- out = "**Eval Tasks**\n\n"
- i = 0
- for c in _EVAL_TASKS.values():
- out += f"**{i}** - `{c}`\n"
- i += 1
- out += f"\nuse `{config.CMD_TRIGGER}eval -c[id]` to Cancel"
- await message.edit(out)
- else:
+
+ if flags:
+ if '-l' in flags:
+ if _EVAL_TASKS:
+ out = "**Eval Tasks**\n\n"
+ i = 0
+ for c in _EVAL_TASKS.values():
+ out += f"**{i}** - `{c}`\n"
+ i += 1
+ out += f"\nuse `{config.CMD_TRIGGER}eval -c[id]` to Cancel"
+ await message.edit(out)
+ else:
+ await message.edit("No running eval tasks !", del_in=5)
+ return
+
+ size = len(_EVAL_TASKS)
+
+ if ('-c' in flags or '-ca' in flags) and size == 0:
await message.edit("No running eval tasks !", del_in=5)
- return
- if ('-c' in flags or '-ca' in flags) and size == 0:
- await message.edit("No running eval tasks !", del_in=5)
- return
- if '-ca' in flags:
- for t in _EVAL_TASKS:
- t.cancel()
- await message.edit(f"Canceled all running eval tasks [{size}] !", del_in=5)
- return
- if '-c' in flags:
- t_id = int(flags.get('-c', -1))
- if t_id < 0 or t_id >= size:
- await message.edit(f"Invalid eval task id [{t_id}] !", del_in=5)
return
- list(_EVAL_TASKS)[t_id].cancel()
- await message.edit(f"Canceled eval task [{t_id}] !", del_in=5)
- return
+
+ if '-ca' in flags:
+ for t in _EVAL_TASKS:
+ t.cancel()
+ await message.edit(f"Canceled all running eval tasks [{size}] !", del_in=5)
+ return
+
+ if '-c' in flags:
+ t_id = int(flags.get('-c', -1))
+ if t_id < 0 or t_id >= size:
+ await message.edit(f"Invalid eval task id [{t_id}] !", del_in=5)
+ return
+ tuple(_EVAL_TASKS)[t_id].cancel()
+ await message.edit(f"Canceled eval task [{t_id}] !", del_in=5)
+ return
cmd = message.filtered_input_str
- as_raw = '-r' in flags
if not cmd:
await message.err("Unable to Parse Input!")
return
+ msg = message
+ replied = message.reply_to_message
+ if (replied and replied.from_user
+ and replied.from_user.is_self and isinstance(replied.text, Str)
+ and str(replied.text.html).startswith("> ")): + msg = replied + + await msg.edit("`Executing eval ...`", parse_mode=enums.ParseMode.MARKDOWN) + + is_file = replied and replied.document and flags.get("file") + as_raw = '-r' in flags silent_mode = '-s' in flags if '-n' in flags: context_type = _ContextType.NEW @@ -161,46 +199,47 @@ async def eval_(message: Message): async def _callback(output: Optional[str], errored: bool): final = "" if not silent_mode: - final += f"**>** ```{cmd}```\n\n" + final += "**>**" + (replied.link if is_file else f"```python\n{cmd}```") + "\n\n" if isinstance(output, str): output = output.strip() if output == '': output = None if output is not None: - final += f"**>>** ```{output}```" - if errored and message.chat.type in ("group", "supergroup", "channel"): + final += f"**>>** ```python\n{output}```" + if errored and message.chat.type in ( + enums.ChatType.GROUP, + enums.ChatType.SUPERGROUP, + enums.ChatType.CHANNEL): msg_id = await CHANNEL.log(final) await msg.edit(f"**Logs**: {CHANNEL.get_link(msg_id)}") elif final: await msg.edit_or_send_as_file(text=final, as_raw=as_raw, - parse_mode='md', + parse_mode=enums.ParseMode.MARKDOWN, + disable_web_page_preview=True, filename="eval.txt", caption=cmd) else: await msg.delete() - msg = message - replied = message.reply_to_message - if (replied and replied.from_user - and replied.from_user.is_self and isinstance(replied.text, Str) - and str(replied.text.html).startswith(">")): - msg = replied - - await msg.edit("`Executing eval ...`", parse_mode='md') - - _g, _l = _context( - context_type, userge=userge, message=message, replied=message.reply_to_message) + _g, _l = _context(context_type, userge=userge, + message=message, replied=message.reply_to_message) l_d = {} try: - exec(_wrap_code(cmd, _l.keys()), _g, l_d) # nosec pylint: disable=W0122 + # nosec pylint: disable=W0122 + exec(_wrap_code(cmd, _l.keys()), _g, l_d) except Exception: # pylint: disable=broad-except _g[_KEY] = _l await _callback(traceback.format_exc(), True) return future = asyncio.get_running_loop().create_future() - pool.submit_thread(_run_coro, future, l_d['__aexec'](*_l.values()), _callback) + pool.submit_thread( + _run_coro, + future, + l_d['__aexec']( + *_l.values()), + _callback) hint = cmd.split('\n')[0] _EVAL_TASKS[future] = hint[:25] + "..." if len(hint) > 25 else hint @@ -209,7 +248,7 @@ async def _callback(output: Optional[str], errored: bool): await future except asyncio.CancelledError: await asyncio.gather(msg.canceled(), - CHANNEL.log(f"**EVAL Process Canceled!**\n\n```{cmd}```")) + CHANNEL.log(f"**EVAL Process Canceled!**\n\n```python\n{cmd}```")) finally: _EVAL_TASKS.pop(future, None) @@ -247,7 +286,7 @@ async def term_(message: Message): with message.cancel_callback(t_obj.cancel): await t_obj.init() while not t_obj.finished: - await message.edit(f"{output}{t_obj.line}", parse_mode='html') + await message.edit(f"{output}{t_obj.line}", parse_mode=enums.ParseMode.HTML) await t_obj.wait(config.Dynamic.EDIT_SLEEP_TIMEOUT) if t_obj.cancelled: await message.canceled(reply=True) @@ -255,14 +294,16 @@ async def term_(message: Message): out_data = f"{output}{t_obj.output}\n{prefix}" await message.edit_or_send_as_file( - out_data, as_raw=as_raw, parse_mode='html', filename="term.txt", caption=cmd) + out_data, as_raw=as_raw, parse_mode=enums.ParseMode.HTML, filename="term.txt", caption=cmd) def parse_py_template(cmd: str, msg: Message): - glo, loc = _context(_ContextType.PRIVATE, message=msg, replied=msg.reply_to_message) + glo, loc = _context(_ContextType.PRIVATE, message=msg, + replied=msg.reply_to_message) def replacer(mobj): - return shlex.quote(str(eval(mobj.expand(r"\1"), glo, loc))) # nosec pylint: disable=W0123 + # nosec pylint: disable=W0123 + return shlex.quote(str(eval(mobj.expand(r"\1"), glo, loc))) return re.sub(r"{{(.+?)}}", replacer, cmd) @@ -306,9 +347,10 @@ def _run_coro(future: asyncio.Future, coro: Awaitable[Any], callback: Callable[[str, bool], Awaitable[Any]]) -> None: loop = asyncio.new_event_loop() task = loop.create_task(coro) - userge.loop.call_soon_threadsafe(future.add_done_callback, - lambda _: (loop.is_running() and future.cancelled() - and loop.call_soon_threadsafe(task.cancel))) + userge.loop.call_soon_threadsafe( + future.add_done_callback, lambda _: ( + loop.is_running() and future.cancelled() and loop.call_soon_threadsafe( + task.cancel))) try: ret, exc = None, None with redirect() as out: @@ -325,7 +367,8 @@ def _run_coro(future: asyncio.Future, coro: Awaitable[Any], finally: loop.run_until_complete(loop.shutdown_asyncgens()) loop.close() - userge.loop.call_soon_threadsafe(lambda: future.done() or future.set_result(None)) + userge.loop.call_soon_threadsafe( + lambda: future.done() or future.set_result(None)) _PROXIES = {} @@ -336,7 +379,11 @@ def __init__(self, original): self._original = original def __getattr__(self, name: str): - return getattr(_PROXIES.get(threading.current_thread().ident, self._original), name) + return getattr( + _PROXIES.get( + threading.current_thread().ident, + self._original), + name) sys.stdout = _Wrapper(sys.stdout) @@ -413,9 +460,13 @@ def cancel(self) -> None: @classmethod async def execute(cls, cmd: str) -> 'Term': - kwargs = dict(stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + kwargs = dict( + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE) if setsid: kwargs['preexec_fn'] = setsid + if sh := which(os.environ.get("USERGE_SHELL", "bash")): + kwargs['executable'] = sh process = await asyncio.create_subprocess_shell(cmd, **kwargs) t_obj = cls(process) t_obj._start() diff --git a/userge/plugins/builtin/help/__init__.py b/userge/plugins/builtin/help/__init__.py index e69de29bb..6d7b71fb1 100644 --- a/userge/plugins/builtin/help/__init__.py +++ b/userge/plugins/builtin/help/__init__.py @@ -0,0 +1,9 @@ +# Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. +# +# This file is part of < https://github.com/UsergeTeam/Userge > project, +# and is released under the "GNU v3.0 License Agreement". +# Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > +# +# All rights reserved. + +"""docs of all commands""" diff --git a/userge/plugins/builtin/help/__main__.py b/userge/plugins/builtin/help/__main__.py index 2a6e5ca50..8fc4cefbc 100644 --- a/userge/plugins/builtin/help/__main__.py +++ b/userge/plugins/builtin/help/__main__.py @@ -9,15 +9,15 @@ # All rights reserved. from math import ceil -from uuid import uuid4 from typing import List, Callable, Dict, Union, Any +from uuid import uuid4 -from pyrogram import filters +from pyrogram import filters, enums +from pyrogram.errors.exceptions.bad_request_400 import MessageNotModified, MessageIdInvalid from pyrogram.types import ( InlineQueryResultArticle, InputTextMessageContent, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery, InlineQuery) -from pyrogram.errors.exceptions.bad_request_400 import MessageNotModified, MessageIdInvalid from userge import userge, Message, config, get_collection from userge.utils import is_command @@ -28,11 +28,9 @@ 'builtin': 'āļø', 'tools': 'š§°', 'utils': 'š', - 'misc': 'šØ', - 'plugins': 'š' + 'misc': 'š' } SAVED_SETTINGS = get_collection("CONFIGS") -PRVT_MSGS = {} @userge.on_start @@ -42,20 +40,32 @@ async def _init() -> None: config.Dynamic.USE_USER_FOR_CLIENT_CHECKS = bool(data['is_user']) -@userge.on_cmd("help", about={'header': "Guide to use USERGE commands"}, allow_channels=False) +@userge.on_cmd("help", about={ + 'header': "Guide to use USERGE commands", + 'flags': {'-i': "open help menu in inline"}, + 'usage': "{tr}help [flag] [plugin_name | command_name]", + 'examples': [ + "{tr}help", "{tr}help -i", "{tr}help help", + "{tr}help core", "{tr}help loader"]}, allow_channels=False) async def helpme(message: Message) -> None: # pylint: disable=missing-function-docstring plugins = userge.manager.loaded_plugins + if userge.has_bot and '-i' in message.flags: + bot = (await userge.bot.get_me()).username + menu = await userge.get_inline_bot_results(bot) + await userge.send_inline_bot_result( + chat_id=message.chat.id, + query_id=menu.query_id, + result_id=menu.results[1].id) + return await message.delete() + if not message.input_str: - out_str = f"""ā ({len(plugins)}
) Plugin(s) Available\n\n""" + out_str = f"""({len(plugins)}) Plugins\n\n""" cat_plugins = userge.manager.get_plugins() for cat in sorted(cat_plugins): - out_str += (f" {_CATEGORY.get(cat, 'š')} {cat} " - f"({len(cat_plugins[cat])}
) :" - + "
".join(sorted(cat_plugins[cat])) + "
\n\n") - - out_str += f"""š Usage:{config.CMD_TRIGGER}help [plugin_name]
""" + out_str += (f"{_CATEGORY.get(cat, 'š')} {cat} ({len(cat_plugins[cat])}):" + + "
".join(sorted(cat_plugins[cat])) + "
\n\n") else: key = message.input_str @@ -65,38 +75,48 @@ async def helpme(message: Message) -> None: # pylint: disable=missing-function- and (len(plugins[key].loaded_commands) > 1 or plugins[key].loaded_commands[0].name.lstrip(config.CMD_TRIGGER) != key)): commands = plugins[key].loaded_commands + size = len(commands) - out_str = f"""ā ({len(commands)}
) Command(s) Available - -š§ Plugin:{key}
-š Doc:{plugins[key].doc}
\n\n""" + out_str = f"""plugin:{key}
+category: {plugins[key].cat} +doc: {plugins[key].doc} - for i, cmd in enumerate(commands, start=1): - out_str += (f" š¤ cmd({i}
):{cmd.name}
\n" - f" š info: {cmd.doc}\n\n") +({size}) Command{'s' if size > 1 else ''}\n\n""" - out_str += f"""š Usage:{config.CMD_TRIGGER}help [command_name]
""" + for cmd in commands: + out_str += f"{cmd.name}
: {cmd.doc}\n" else: - triggers = (config.CMD_TRIGGER, config.SUDO_TRIGGER, config.PUBLIC_TRIGGER) + triggers = ( + config.CMD_TRIGGER, + config.SUDO_TRIGGER, + config.PUBLIC_TRIGGER) for _ in triggers: key = key.lstrip(_) - out_str = f"No Module or Command Found for:{message.input_str}
" + out_str = f"No Plugin or Command found for:{message.input_str}
" for name, cmd in userge.manager.loaded_commands.items(): for _ in triggers: name = name.lstrip(_) if key == name: - out_str = f"{cmd.name}
\n\n{cmd.about}" + out_str = f"""command:{cmd.name}
+plugin:{cmd.plugin}
+category: {plugins[cmd.plugin].cat} + +{cmd.about}""" break - await message.edit(out_str, del_in=0, parse_mode='html', disable_web_page_preview=True) + await message.edit(out_str, + del_in=0, + parse_mode=enums.ParseMode.HTML, + disable_web_page_preview=True) if userge.has_bot: + def check_owner(func): async def wrapper(_, c_q: CallbackQuery): if c_q.from_user and c_q.from_user.id in config.OWNER_ID: @@ -115,10 +135,8 @@ async def wrapper(_, c_q: CallbackQuery): return wrapper - - @userge.bot.on_message( - filters.private & filters.user(list(config.OWNER_ID)) & filters.command("start"), group=-1 - ) + @userge.bot.on_message(filters.private & filters.user( + list(config.OWNER_ID)) & filters.command("start"), group=-1) async def pm_help_handler(_, msg: Message): cmd = msg.command[1] if len(msg.command) > 1 else '' @@ -138,10 +156,11 @@ async def pm_help_handler(_, msg: Message): else: out_str = f"No Command Found for:{cmd}
" - await msg.reply(out_str, parse_mode='html', disable_web_page_preview=True) - + await msg.reply(out_str, parse_mode=enums.ParseMode.HTML, disable_web_page_preview=True) - @userge.bot.on_callback_query(filters=filters.regex(pattern=r"\((.+)\)(next|prev)\((\d+)\)")) + @userge.bot.on_callback_query( + filters=filters.regex( + pattern=r"\((.+)\)(next|prev)\((\d+)\)")) @check_owner async def callback_next_prev(callback_query: CallbackQuery): cur_pos = str(callback_query.matches[0].group(1)) @@ -165,8 +184,9 @@ async def callback_next_prev(callback_query: CallbackQuery): await callback_query.edit_message_reply_markup( reply_markup=InlineKeyboardMarkup(buttons)) - - @userge.bot.on_callback_query(filters=filters.regex(pattern=r"back\((.+)\)")) + @userge.bot.on_callback_query( + filters=filters.regex( + pattern=r"back\((.+)\)")) @check_owner async def callback_back(callback_query: CallbackQuery): cur_pos = str(callback_query.matches[0].group(1)) @@ -187,8 +207,9 @@ async def callback_back(callback_query: CallbackQuery): await callback_query.edit_message_text( text, reply_markup=InlineKeyboardMarkup(buttons)) - - @userge.bot.on_callback_query(filters=filters.regex(pattern=r"enter\((.+)\)")) + @userge.bot.on_callback_query( + filters=filters.regex( + pattern=r"enter\((.+)\)")) @check_owner async def callback_enter(callback_query: CallbackQuery): cur_pos = str(callback_query.matches[0].group(1)) @@ -204,7 +225,6 @@ async def callback_enter(callback_query: CallbackQuery): await callback_query.edit_message_text( text, reply_markup=InlineKeyboardMarkup(buttons)) - @userge.bot.on_callback_query( filters=filters.regex(pattern=r"((?:un)?load)\((.+)\)")) @check_owner @@ -230,14 +250,12 @@ async def callback_manage(callback_query: CallbackQuery): await callback_query.edit_message_text( text, reply_markup=InlineKeyboardMarkup(buttons)) - @userge.bot.on_callback_query(filters=filters.regex(pattern=r"^mm$")) @check_owner async def callback_mm(callback_query: CallbackQuery): await callback_query.edit_message_text( "š„ **Userge Main Menu** š„", reply_markup=InlineKeyboardMarkup(main_menu_buttons())) - @userge.bot.on_callback_query(filters=filters.regex(pattern=r"^chgclnt$")) @check_owner async def callback_chgclnt(callback_query: CallbackQuery): @@ -255,8 +273,9 @@ async def callback_chgclnt(callback_query: CallbackQuery): await callback_query.edit_message_reply_markup( reply_markup=InlineKeyboardMarkup(main_menu_buttons())) - - @userge.bot.on_callback_query(filters=filters.regex(pattern=r"refresh\((.+)\)")) + @userge.bot.on_callback_query( + filters=filters.regex( + pattern=r"refresh\((.+)\)")) @check_owner async def callback_exit(callback_query: CallbackQuery): cur_pos = str(callback_query.matches[0].group(1)) @@ -269,37 +288,20 @@ async def callback_exit(callback_query: CallbackQuery): await callback_query.edit_message_text( text, reply_markup=InlineKeyboardMarkup(buttons)) - - @userge.bot.on_callback_query(filters=filters.regex(pattern=r"prvtmsg\((.+)\)")) - async def prvt_msg(_, c_q: CallbackQuery): - msg_id = str(c_q.matches[0].group(1)) - - if msg_id not in PRVT_MSGS: - await c_q.answer("message now outdated !", show_alert=True) - return - - user_id, flname, msg = PRVT_MSGS[msg_id] - - if c_q.from_user.id == user_id or c_q.from_user.id in config.OWNER_ID: - await c_q.answer(msg, show_alert=True) - else: - await c_q.answer( - f"Only {flname} can see this Private Msg... š", show_alert=True) - - def is_filter(name: str) -> bool: split_ = name.split('.') return bool(split_[0] and len(split_) == 2) - def parse_buttons(page_num: int, cur_pos: str, func: Callable[[str], str], data: Union[List[str], Dict[str, Any]], rows: int = 3): - buttons = [InlineKeyboardButton( - func(x), callback_data=f"enter({cur_pos}|{x})".encode()) for x in sorted(data)] + buttons = [ + InlineKeyboardButton( + func(x), + callback_data=f"enter({cur_pos}|{x})".encode()) for x in sorted(data)] pairs = list(map(list, zip(buttons[::2], buttons[1::2]))) @@ -322,13 +324,11 @@ def parse_buttons(page_num: int, return pairs - def main_menu_buttons(): return parse_buttons(0, "mm", lambda x: f"{_CATEGORY.get(x, 'š')} {x}", userge.manager.get_all_plugins()) - def default_buttons(cur_pos: str): tmp_btns = [] @@ -344,18 +344,20 @@ def default_buttons(cur_pos: str): elif userge.dual_mode: cur_clnt = "š² USER" if config.Dynamic.USER_IS_PREFERRED else "š¤ BOT" - tmp_btns.append(InlineKeyboardButton( - f"š© Preferred Client : {cur_clnt}", callback_data="chgclnt".encode())) + tmp_btns.append( + InlineKeyboardButton( + f"š© Preferred Client : {cur_clnt}", + callback_data="chgclnt".encode())) return [tmp_btns] - def category_data(cur_pos: str): pos_list = cur_pos.split('|') plugins = userge.manager.get_all_plugins()[pos_list[1]] - text = (f"**(`{len(plugins)}`) Plugin(s) Under : " - f"`{_CATEGORY.get(pos_list[1], 'š')} {pos_list[1]}` š Category**") + text = ( + f"**(`{len(plugins)}`) Plugin(s) Under : " + f"`{_CATEGORY.get(pos_list[1], 'š')} {pos_list[1]}` š Category**") buttons = parse_buttons(0, '|'.join(pos_list[:2]), lambda x: f"š {x}", @@ -363,7 +365,6 @@ def category_data(cur_pos: str): return text, buttons - def plugin_data(cur_pos: str, p_num: int = 0): pos_list = cur_pos.split('|') @@ -371,8 +372,8 @@ def plugin_data(cur_pos: str, p_num: int = 0): text = f"""š **--Plugin Status--** š -š **Category** : `{pos_list[1]}` š **Name** : `{plg.name}` +š **Category** : `{pos_list[1]}` š **Doc** : `{plg.doc}` ā **Commands** : `{len(plg.commands)}` ā **Filters** : `{len(plg.filters)}` @@ -381,13 +382,18 @@ def plugin_data(cur_pos: str, p_num: int = 0): tmp_btns = [] if plg.loaded: - tmp_btns.append(InlineKeyboardButton( - "ā Unload", callback_data=f"unload({'|'.join(pos_list[:3])})".encode())) + tmp_btns.append( + InlineKeyboardButton( + "ā Unload", + callback_data=f"unload({'|'.join(pos_list[:3])})".encode())) else: - tmp_btns.append(InlineKeyboardButton( - "ā Load", callback_data=f"load({'|'.join(pos_list[:3])})".encode())) + tmp_btns.append( + InlineKeyboardButton( + "ā Load", + callback_data=f"load({'|'.join(pos_list[:3])})".encode())) - buttons = parse_buttons(p_num, '|'.join(pos_list[:3]), + buttons = parse_buttons(p_num, + '|'.join(pos_list[:3]), lambda x: f"ā {x}" if is_filter(x) else f"ā {x}", (flt.name for flt in plg.commands + plg.filters)) @@ -395,7 +401,6 @@ def plugin_data(cur_pos: str, p_num: int = 0): return text, buttons - def filter_data(cur_pos: str): pos_list = cur_pos.split('|') plg = userge.manager.plugins[pos_list[2]] @@ -412,6 +417,7 @@ def filter_data(cur_pos: str): if hasattr(flt, 'about'): text = f"""ā **--Command Status--** {flt_data} + {flt.about} """ else: @@ -432,7 +438,6 @@ def filter_data(cur_pos: str): return text, buttons - @userge.bot.on_inline_query(group=1) async def inline_answer(_, inline_query: InlineQuery): results = [ @@ -475,38 +480,7 @@ async def inline_answer(_, inline_query: InlineQuery): ) ) - if '-' in inline_query.query: - _id, msg = inline_query.query.split('-', maxsplit=1) - if not msg: - return - - if not msg.strip().endswith(':'): - return - - try: - user = await userge.get_users(_id.strip()) - except Exception: # pylint: disable=broad-except - return - - PRVT_MSGS[inline_query.id] = (user.id, user.first_name, msg.strip(': ')) - - prvte_msg = [[InlineKeyboardButton( - "Show Message š", callback_data=f"prvtmsg({inline_query.id})")]] - - msg_c = f"š A **private message** to {'@' + user.username}, " - msg_c += "Only he/she can open it." - - results.append( - InlineQueryResultArticle( - id=uuid4(), - title=f"A Private Msg to {user.first_name}", - input_message_content=InputTextMessageContent(msg_c), - description="Only he/she can open it", - thumb_url="https://imgur.com/download/Inyeb1S", - reply_markup=InlineKeyboardMarkup(prvte_msg) - ) - ) - elif "msg.err" in inline_query.query: + if "msg.err" in inline_query.query: if ' ' not in inline_query.query: return @@ -535,8 +509,6 @@ async def inline_answer(_, inline_query: InlineQuery): input_message_content=InputTextMessageContent(err_text), description="Inline Error text with help support button.", thumb_url="https://imgur.com/download/Inyeb1S", - reply_markup=InlineKeyboardMarkup(button) - ) - ) + reply_markup=InlineKeyboardMarkup(button))) await inline_query.answer(results=results, cache_time=3) diff --git a/userge/plugins/builtin/loader/__init__.py b/userge/plugins/builtin/loader/__init__.py index e69de29bb..3e870050c 100644 --- a/userge/plugins/builtin/loader/__init__.py +++ b/userge/plugins/builtin/loader/__init__.py @@ -0,0 +1,9 @@ +# Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. +# +# This file is part of < https://github.com/UsergeTeam/Userge > project, +# and is released under the "GNU v3.0 License Agreement". +# Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > +# +# All rights reserved. + +"""high lvl interface for low lvl loader""" diff --git a/userge/plugins/builtin/loader/__main__.py b/userge/plugins/builtin/loader/__main__.py index 7d7ad990c..2c7a29075 100644 --- a/userge/plugins/builtin/loader/__main__.py +++ b/userge/plugins/builtin/loader/__main__.py @@ -21,7 +21,7 @@ 'flags': { '-f': "fetch core repo", '-n': "view available new commits", - '-o': "view old commits (default 20)", + '-o': "view old commits (default limit 20)", '-b': "change branch", '-v': "change version"}, 'usage': "{tr}core [flags]", @@ -30,7 +30,9 @@ "{tr}core -f", "{tr}core -n", "{tr}core -f -n : fetch and get updates", "{tr}core -o", "{tr}core -o=20 : limit results to 20", - "{tr}core -b=master", "{tr}core -v=750 : update id"]}, del_pre=True, allow_channels=False) + "{tr}core -b=master", + "{tr}core -v=750 : update id (grab using {tr}core -n or {tr}core -o)"] +}, del_pre=True, allow_channels=False) async def core(message: Message): """ view or manage the core repository """ flags = message.flags @@ -41,7 +43,7 @@ async def core(message: Message): set_branch, branch = 'b' in flags, flags.get('b') set_version, version = 'v' in flags, int(flags.get('v') or 0) - await message.edit("```processing ...```") + await message.edit("processing ...") if fetch: await api.fetch_core() @@ -67,7 +69,7 @@ async def core(message: Message): out = _updates_to_str(updates) await message.edit_or_send_as_file( - f"**{len(updates)}** old commits for core repo\n\n{out}", + f"**{len(updates)}** old commits of core repo\n\n{out}", del_in=0, disable_web_page_preview=True) elif set_branch or set_version: @@ -95,10 +97,10 @@ async def core(message: Message): await message.edit( f"done, do `{config.CMD_TRIGGER}restart -h` to apply changes", del_in=3) else: - await message.edit("didn't change anything", del_in=3) + await message.edit("didn't change anything", del_in=3) elif fetch: - await message.edit("```fetched core repo```", del_in=3) + await message.edit("fetched core repo", del_in=3) else: core_repo = await api.get_core() @@ -109,7 +111,7 @@ async def core(message: Message): **version** : `{get_version()}` **version code** : `{core_repo.count}` **branch** : `{core_repo.branch}` -**branches** : `{', '.join(core_repo.branches)}` +**branches** : `{'`, `'.join(core_repo.branches)}` **is latest** : `{core_repo.count == core_repo.max_count}` **head** : [link]({core_repo.head_url})""" @@ -119,21 +121,24 @@ async def core(message: Message): @userge.on_cmd("repos", about={ 'header': "view or manage plugins repositories", 'flags': { - '-f': "fetch plugins repo", - '-id': "plugins repo id", + '-f': "fetch one or all plugins repos", + '-id': "plugins repo id (grab using {tr}repos)", '-n': "view available new commits", - '-o': "view old commits", + '-o': "view old commits (default limit 20)", '-b': "change branch", '-v': "change version", - '-p': "change priority", + '-p': "change priority (-p2 > -p1)", '-invalidate': "notify loader to rebuild all the plugins"}, 'usage': "{tr}repos [flags]", 'examples': [ "{tr}repos : see plugins repos info", - "{tr}repos -f", "{tr}repos -id=1 -n", + "{tr}repos -f : fetch all plugins repos", + "{tr}repos -id=1 -f : only fetch this repo", + "{tr}repos -id=1 -n", "{tr}repos -f -id=1 -n : fetch and get updates", "{tr}repos -id=1 -o", "{tr}repos -id=1 -o=20 : limit results to 20", - "{tr}repos -id=1 -b=master", "{tr}repos -id=1 -v=750 : update id", + "{tr}repos -id=1 -b=master", + "{tr}repos -id=1 -v=750 : update id (grab using -n or -o flags)", "{tr}repos -id=1 -p=5", "{tr}repos -invalidate"]}, del_pre=True, allow_channels=False) async def repos(message: Message): """ view or manage plugins repositories """ @@ -147,14 +152,12 @@ async def repos(message: Message): set_version, version = 'v' in flags, int(flags.get('v') or 0) set_priority, priority = 'p' in flags, flags.get('p') - await message.edit("```processing ...```") - - if fetch: - await api.fetch_repos() + await message.edit("processing ...") if repo_id <= 0: if fetch: - await message.edit("```fetched plugins repos```", del_in=3) + await api.fetch_repos() + await message.edit("fetched plugins repos", del_in=3) elif invalidate: await api.invalidate_repos_cache() @@ -166,7 +169,7 @@ async def repos(message: Message): plg_repos = await api.get_repos() if not plg_repos: - await message.edit("```no repos found```", del_in=3) + await message.edit("no repos found", del_in=3) return out = "**Repos Details**\n\n" @@ -177,13 +180,16 @@ async def repos(message: Message): out += f"**priority** : `{plg_repo.priority}`\n" out += f"**version code** : `{plg_repo.count}`\n" out += f"**branch** : `{plg_repo.branch}`\n" - out += f"**branches** : `{', '.join(plg_repo.branches)}`\n" + out += f"**branches** : `{'`, `'.join(plg_repo.branches)}`\n" out += f"**is latest** : `{plg_repo.count == plg_repo.max_count}`\n" out += f"**head** : [link]({plg_repo.head_url})\n\n" await message.edit_or_send_as_file(out, del_in=0, disable_web_page_preview=True) else: + if fetch: + await api.fetch_repo(repo_id) + repo_details = await api.get_repo(repo_id) if not repo_details: @@ -199,7 +205,7 @@ async def repos(message: Message): out = _updates_to_str(updates) await message.edit_or_send_as_file( - f"{len(updates)} new commits available for repo: {repo_id}\n\n{out}", + f"**{len(updates)}** new commits available for repo: `{repo_id}`\n\n{out}", del_in=0, disable_web_page_preview=True) elif get_old: @@ -211,7 +217,7 @@ async def repos(message: Message): out = _updates_to_str(updates) await message.edit_or_send_as_file( - f"{len(updates)} old commits for repo: {repo_id}\n\n{out}", + f"**{len(updates)}** old commits of repo: `{repo_id}`\n\n{out}", del_in=0, disable_web_page_preview=True) elif set_branch or set_version or set_priority: @@ -245,10 +251,10 @@ async def repos(message: Message): await message.edit( f"done, do `{config.CMD_TRIGGER}restart -h` to apply changes", del_in=3) else: - await message.edit("didn't change anything", del_in=3) + await message.edit("didn't change anything", del_in=3) elif fetch: - await message.edit("```fetched plugins repos```", del_in=3) + await message.edit(f"fetched plugins repo: {repo_id}", del_in=3) else: await message.err("invalid flags") @@ -260,10 +266,11 @@ async def repos(message: Message): '-b': "branch name (optional|default master)", '-p': "priority (optional|default 1)"}, 'usage': "{tr}addrepo [flags] url", + 'others': "plugins of higher priority repos will override plugins of low priority repos", 'examples': [ "{tr}addrepo https://github.com/UsergeTeam/Userge-Plugins", - "{tr}addrepo -b=dev https://github.com/UsergeTeam/Userge-Plugins", - "{tr}addrepo -b=dev -p=5 https://github.com/UsergeTeam/Userge-Plugins"] + "{tr}addrepo -b=master https://github.com/UsergeTeam/Userge-Plugins", + "{tr}addrepo -b=master -p=1 https://github.com/UsergeTeam/Userge-Plugins"] }, del_pre=True, allow_channels=False) async def add_repo(message: Message): """ add a plugins repo """ @@ -277,15 +284,19 @@ async def add_repo(message: Message): await message.err("no input url") return - await message.edit("```processing ...```") - await api.add_repo(priority, branch, url) - await message.edit("added repo, " - f"do `{config.CMD_TRIGGER}restart -h` to apply changes", del_in=3) + await message.edit("processing ...") + + if await api.add_repo(priority, branch, url): + await message.edit("added repo, " + f"do `{config.CMD_TRIGGER}restart -h` to apply changes", del_in=3) + + else: + await message.edit("repo was already added or invalid", del_in=3) @userge.on_cmd("rmrepo", about={ 'header': "remove a plugins repo", - 'flags': {'-id': "plugins repo id"}, + 'flags': {'-id': "plugins repo id (grab using {tr}repos)"}, 'usage': "{tr}rmrepo [flag]", 'examples': "{tr}rmrepo -id=2"}, del_pre=True, allow_channels=False) async def rm_repo(message: Message): @@ -296,10 +307,14 @@ async def rm_repo(message: Message): await message.err("empty or invalid repo id") return - await message.edit("```processing ...```") - await api.remove_repo(int(repo_id)) - await message.edit("removed repo, " - f"do `{config.CMD_TRIGGER}restart -h` to apply changes", del_in=3) + await message.edit("processing ...") + + if await api.remove_repo(int(repo_id)): + await message.edit("removed repo, " + f"do `{config.CMD_TRIGGER}restart -h` to apply changes", del_in=3) + + else: + await message.edit("couldn't find that repo", del_in=3) @userge.on_cmd("consts", about={ @@ -310,7 +325,7 @@ async def consts(message: Message): data_ = await api.get_constraints() if not data_: - await message.edit("```no constraints found```", del_in=3) + await message.edit("no constraints found", del_in=3) return out = "" @@ -323,6 +338,7 @@ async def consts(message: Message): @userge.on_cmd("addconsts", about={ 'header': "add constraints", + 'description': "can ignore plugins, categories or even them from specific repos easily", 'flags': {'-type': "constraints type (include|exclude|in)"}, 'usage': "{tr}addconsts [flag] data", 'data_types': ["plugin", "category/", "repo/plugin", "repo/category/"], @@ -347,14 +363,20 @@ async def add_consts(message: Message): await message.err("no data provided") return - await message.edit("```processing ...```") - await api.add_constraints(c_type, data.split()) - await message.edit("added constraints, " - f"do `{config.CMD_TRIGGER}restart -h` to apply changes", del_in=3) + await message.edit("processing ...") + + if await api.add_constraints(c_type, data.split()): + await message.edit("added constraints, " + f"do `{config.CMD_TRIGGER}restart -h` to apply changes", del_in=3) + + else: + await message.edit("didn't add anything", del_in=3) @userge.on_cmd("rmconsts", about={ 'header': "remove constraints", + 'description': "if the type is provided, " + "then the constraints only in this type will be removed", 'flags': { '-type': "constraints type (include|exclude|in) (optional)"}, 'usage': "{tr}rmconsts [flag] data", @@ -378,14 +400,20 @@ async def rm_consts(message: Message): await message.err("no data provided") return - await message.edit("```processing ...```") - await api.remove_constraints(c_type, data.split()) - await message.edit("removed constraints, " - f"do `{config.CMD_TRIGGER}restart -h` to apply changes", del_in=3) + await message.edit("processing ...") + + if await api.remove_constraints(c_type, data.split()): + await message.edit("removed constraints, " + f"do `{config.CMD_TRIGGER}restart -h` to apply changes", del_in=3) + + else: + await message.edit("didn't remove anything", del_in=3) @userge.on_cmd("clrconsts", about={ 'header': "clear constraints", + 'description': "if the type is provided, " + "then the constraints only in this type will be cleared", 'flags': { '-type': "constraints type (include|exclude|in) (optional)"}, 'usage': "{tr}clrconsts [flag]", @@ -401,30 +429,39 @@ async def clr_consts(message: Message): await message.err("invalid type") return - await message.edit("```processing ...```") - await api.clear_constraints(c_type) - await message.edit("cleared constraints, " - f"do `{config.CMD_TRIGGER}restart -h` to apply changes", del_in=3) + await message.edit("processing ...") + + if await api.clear_constraints(c_type): + await message.edit("cleared constraints, " + f"do `{config.CMD_TRIGGER}restart -h` to apply changes", del_in=3) + + else: + await message.edit("nothing found to clear", del_in=3) @userge.on_cmd("update", about={ 'header': "Check Updates or Update Userge", + 'description': "use {tr}core and {tr}repos, " + "if you want more advanced control over version controlling", 'flags': { '-c': "view updates for core repo", '-r': "view updates for all plugins repos", - '-pull': "pull updates"}, - 'usage': "{tr}update [-c|-r] [-pull]", + '-pull': "pull updates", + '-restart': "restart after pulled"}, + 'usage': "{tr}update [-c|-r] [-pull] [-restart]", 'examples': [ "{tr}update : check updates for the whole project", "{tr}update -c : check updates for core repo", "{tr}update -r : check updates for all plugins repos", - "{tr}update -pull : apply latest updates to the whole project", - "{tr}update -c -pull : apply latest updates to the core repo", - "{tr}update -r -pull : apply latest updates to the plugins repos"] + "{tr}update -pull : pull latest updates to the whole project", + "{tr}update -pull -restart : auto restart after pulled", + "{tr}update -c -pull : pull latest updates to the core repo", + "{tr}update -r -pull : pull latest updates to the plugins repos"] }, del_pre=True, allow_channels=False) async def update(message: Message): """ check or do updates """ pull_in_flags = False + restart_in_flags = False core_in_flags = False repos_in_flags = False @@ -434,6 +471,10 @@ async def update(message: Message): pull_in_flags = True flags.remove('pull') + if 'restart' in flags: + restart_in_flags = True + flags.remove('restart') + if 'c' in flags: core_in_flags = True flags.remove('c') @@ -483,12 +524,19 @@ async def update(message: Message): if updates: if pull_in_flags: await CHANNEL.log(f"**PULLED updates:\n\nš CHANGELOG š**\n\n{updates}") - await message.edit("updated to latest, " - f"do `{config.CMD_TRIGGER}restart -h` to apply changes", del_in=3) + + if restart_in_flags: + await message.edit("`Restarting [HARD] ...`", del_in=1) + await userge.restart(hard=True) + + else: + await message.edit( + "updated to latest, " + f"do `{config.CMD_TRIGGER}restart -h` to apply changes", del_in=3) else: await message.edit_or_send_as_file(updates, del_in=0, disable_web_page_preview=True) else: - await message.edit("```no updates found```", del_in=3) + await message.edit("no updates found", del_in=3) def _updates_to_str(updates: List[Update]) -> str: diff --git a/userge/plugins/builtin/manage/__init__.py b/userge/plugins/builtin/manage/__init__.py index e69de29bb..033c17333 100644 --- a/userge/plugins/builtin/manage/__init__.py +++ b/userge/plugins/builtin/manage/__init__.py @@ -0,0 +1,9 @@ +# Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. +# +# This file is part of < https://github.com/UsergeTeam/Userge > project, +# and is released under the "GNU v3.0 License Agreement". +# Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > +# +# All rights reserved. + +"""manage commands and plugins dynamically""" diff --git a/userge/plugins/builtin/sudo/__init__.py b/userge/plugins/builtin/sudo/__init__.py index 5a3fa58f9..44a109073 100644 --- a/userge/plugins/builtin/sudo/__init__.py +++ b/userge/plugins/builtin/sudo/__init__.py @@ -1,3 +1,14 @@ +# Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. +# +# This file is part of < https://github.com/UsergeTeam/Userge > project, +# and is released under the "GNU v3.0 License Agreement". +# Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > +# +# All rights reserved. + +"""manage sudo cmds and users""" + + from typing import Set USERS: Set[int] = set() diff --git a/userge/plugins/builtin/sudo/__main__.py b/userge/plugins/builtin/sudo/__main__.py index ff6248198..987d2aec0 100644 --- a/userge/plugins/builtin/sudo/__main__.py +++ b/userge/plugins/builtin/sudo/__main__.py @@ -125,7 +125,7 @@ async def add_sudo_cmd(message: Message): await SUDO_CMDS_COLLECTION.drop() sudo.COMMANDS.clear() tmp_ = [] - restricted = ('addsudo', 'addscmd', 'exec', 'eval', 'term', 'load') + restricted = ('addsudo', 'addscmd', 'exec', 'eval', 'term', 'load', 'unload') for c_d in list(userge.manager.loaded_commands): t_c = c_d.lstrip(config.CMD_TRIGGER) if t_c in restricted: @@ -157,7 +157,8 @@ async def add_sudo_cmd(message: Message): @userge.on_cmd("delscmd", about={ 'header': "delete sudo commands", 'flags': {'-all': "remove all sudo commands"}, - 'usage': "{tr}delscmd [command name]\n{tr}delscmd -all"}, allow_channels=False) + 'usage': "{tr}delscmd [command names separated by space]\n{tr}delscmd -all"}, + allow_channels=False) async def del_sudo_cmd(message: Message): """ delete sudo cmd """ if '-all' in message.flags: @@ -170,13 +171,25 @@ async def del_sudo_cmd(message: Message): if not cmd: await message.err('input not found!') return - if cmd not in sudo.COMMANDS: - await message.edit(f"cmd : `{cmd}` not in **SUDO**!", del_in=5) - else: - sudo.COMMANDS.remove(cmd) - await asyncio.gather( - SUDO_CMDS_COLLECTION.delete_one({'_id': cmd}), - message.edit(f"cmd : `{cmd}` removed from **SUDO**!", del_in=5, log=__name__)) + NOT_IN_SUDO = [] + IS_REMOVED = [] + for ncmd in cmd.split(" "): + if ncmd not in sudo.COMMANDS: + NOT_IN_SUDO.append(ncmd) + else: + sudo.COMMANDS.remove(ncmd) + IS_REMOVED.append(ncmd) + if IS_REMOVED: + await SUDO_CMDS_COLLECTION.delete_many({'_id': {'$in': IS_REMOVED}}) + await message.edit( + f"cmds : `{' '.join(IS_REMOVED)}` removed from **SUDO**!", + del_in=5, log=__name__) + if NOT_IN_SUDO and not IS_REMOVED: + await message.edit( + f"cmds : `{' '.join(NOT_IN_SUDO)}` not in **SUDO**!", del_in=5) + elif NOT_IN_SUDO: + await message.reply_text( + f"cmds : `{' '.join(NOT_IN_SUDO)}` not in **SUDO**!", del_in=5) @userge.on_cmd("vscmd", about={'header': "view sudo cmds"}, allow_channels=False) diff --git a/userge/plugins/builtin/system/__init__.py b/userge/plugins/builtin/system/__init__.py index 26c587fb3..95e4977c0 100644 --- a/userge/plugins/builtin/system/__init__.py +++ b/userge/plugins/builtin/system/__init__.py @@ -1,3 +1,14 @@ +# Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. +# +# This file is part of < https://github.com/UsergeTeam/Userge > project, +# and is released under the "GNU v3.0 License Agreement". +# Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > +# +# All rights reserved. + +"""system related commands""" + + from os import environ, getpid, kill from typing import Set, Optional try: diff --git a/userge/plugins/builtin/system/__main__.py b/userge/plugins/builtin/system/__main__.py index e0cb0c57f..36a006c08 100644 --- a/userge/plugins/builtin/system/__main__.py +++ b/userge/plugins/builtin/system/__main__.py @@ -12,7 +12,7 @@ import shutil import time -from pyrogram import Client +from pyrogram import Client, enums from pyrogram.errors import SessionPasswordNeeded, YouBlockedUser from pyrogram.types import User @@ -54,7 +54,8 @@ async def _init() -> None: 'header': "Restarts the bot and reload all plugins", 'flags': { '-h': "restart hard", - '-d': "clean working folder"}, + '-d': "clean working folder", + '-hu': "restart heroku"}, 'usage': "{tr}restart [flag | flags]", 'examples': "{tr}restart -t -d"}, del_pre=True, allow_channels=False) async def restart_(message: Message): @@ -64,7 +65,15 @@ async def restart_(message: Message): if 'd' in message.flags: shutil.rmtree(config.Dynamic.DOWN_PATH, ignore_errors=True) - if 'h' in message.flags: + if 'hu' in message.flags: + if config.HEROKU_APP: + await message.edit("`Restarting [HEROKU] ...`", del_in=1) + config.HEROKU_APP.restart() + time.sleep(30) + else: + await message.edit("`Heroku app not found !`", del_in=1) + + elif 'h' in message.flags: await message.edit("`Restarting [HARD] ...`", del_in=1) await userge.restart(hard=True) @@ -161,7 +170,7 @@ async def delvar_(message: Message) -> None: var_name = message.input_str.strip() var_data = system.get_env(var_name) - if var_data: + if not var_data: await message.err(f"`var {var_name} not found!`") return @@ -347,9 +356,10 @@ async def convert_usermode(msg: Message): if not hasattr(generate_session, "client"): client = Client( - session_name=":memory:", + name="temp_userge", api_id=config.API_ID, - api_hash=config.API_HASH + api_hash=config.API_HASH, + in_memory=True ) try: await client.connect() @@ -404,7 +414,7 @@ async def convert_botmode(msg: Message): await msg.err(response.text) else: await userge.promote_chat_member(config.LOG_CHANNEL_ID, username) - token = extract_entities(response, ["code"])[0] + token = extract_entities(response, [enums.MessageEntityType.CODE])[0] await msg.edit("DONE! Bot Mode will be enabled after restart.") await system.set_env("BOT_TOKEN", token) except StopConversation: diff --git a/userge/plugins/builtin/tools/__init__.py b/userge/plugins/builtin/tools/__init__.py index e69de29bb..2298e171a 100644 --- a/userge/plugins/builtin/tools/__init__.py +++ b/userge/plugins/builtin/tools/__init__.py @@ -0,0 +1,9 @@ +# Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. +# +# This file is part of < https://github.com/UsergeTeam/Userge > project, +# and is released under the "GNU v3.0 License Agreement". +# Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > +# +# All rights reserved. + +"""useful tools""" diff --git a/userge/plugins/builtin/tools/__main__.py b/userge/plugins/builtin/tools/__main__.py index eae803fcf..6430c6bbe 100644 --- a/userge/plugins/builtin/tools/__main__.py +++ b/userge/plugins/builtin/tools/__main__.py @@ -121,7 +121,7 @@ async def jsonify(message: Message): async def pingme(message: Message): """ ping tg servers """ start = datetime.now() - await message.client.send(Ping(ping_id=0)) + await message.client.invoke(Ping(ping_id=0)) end = datetime.now() m_s = (end - start).microseconds / 1000 @@ -129,22 +129,27 @@ async def pingme(message: Message): @userge.on_cmd("s", about={ - 'header': "search commands in USERGE", - 'examples': "{tr}s wel"}, allow_channels=False) + 'header': "search commands or plugins", + 'flags': {'-p': "search plugin"}, + 'examples': ["{tr}s wel", "{tr}s -p gdrive"]}, allow_channels=False) async def search(message: Message): """ search commands """ - cmd = message.input_str - if not cmd: - await message.err("Enter any keyword to search in commands") + key = message.filtered_input_str + if not key: + await message.err("input not found") return - found = [i for i in sorted(list(userge.manager.loaded_commands)) if cmd in i] + plugins = '-p' in message.flags + data = list(userge.manager.loaded_plugins if plugins else userge.manager.loaded_commands) + + found = [i for i in sorted(data) if key in i] out_str = ' '.join(found) if found: - out = f"**--I found ({len(found)}) commands for-- : `{cmd}`**\n\n`{out_str}`" + out = f"**--found {len(found)} {'plugin' if plugins else 'command'}(s) for-- : `{key}`**" + out += f"\n\n`{out_str}`" else: - out = f"__command not found for__ : `{cmd}`" + out = f"__nothing found for__ : `{key}`" await message.edit(text=out, del_in=0) @@ -152,14 +157,17 @@ async def search(message: Message): @userge.on_cmd("logs", about={ 'header': "check userge logs", 'flags': { - '-h': "get heroku logs", - '-l': "heroku logs lines limit : default 100"}}, allow_channels=False) + '-h': "get heroku logs (default limit 100)", + '-l': "get loader logs"}, + 'examples': [ + "{tr}logs", "{tr}logs -h", "{tr}logs -h200", "{tr}logs -l"] +}, allow_channels=False) async def check_logs(message: Message): """ check logs """ await message.edit("`checking logs ...`") if '-h' in message.flags and config.HEROKU_APP: - limit = int(message.flags.get('-l', 100)) + limit = int(message.flags.get('-h') or 100) logs = await pool.run_in_thread(config.HEROKU_APP.get_log)(lines=limit) await message.client.send_as_file(chat_id=message.chat.id, @@ -167,9 +175,10 @@ async def check_logs(message: Message): filename='userge-heroku.log', caption=f'userge-heroku.log [ {limit} lines ]') else: + filename = f"{'loader' if '-l' in message.flags else 'userge'}.log" await message.client.send_document(chat_id=message.chat.id, - document="logs/userge.log", - caption='userge.log') + document=f"logs/{filename}", + caption=filename) await message.delete() diff --git a/userge/sys_tools.py b/userge/sys_tools.py new file mode 100644 index 000000000..9c665e07b --- /dev/null +++ b/userge/sys_tools.py @@ -0,0 +1,104 @@ +# pylint: disable=missing-module-docstring +# +# Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. +# +# This file is part of < https://github.com/UsergeTeam/Userge > project, +# and is released under the "GNU v3.0 License Agreement". +# Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > +# +# All rights reserved. +# +# noqa +# skipcq + +import sys +from os import environ +from typing import Dict, Optional + + +_CACHE: Dict[str, str] = {} + + +def secured_env(key: str, default: Optional[str] = None) -> Optional[str]: + """ get secured env """ + if not key: + raise ValueError + + try: + value = environ.pop(key) + except KeyError: + if key in _CACHE: + return _CACHE[key] + value = default + + ret: Optional[str] = None + + if value: + ret = _CACHE[key] = secured_str(value) + + return ret + + +def secured_str(value: str) -> str: + """ get secured string """ + if not value: + raise ValueError + + if isinstance(value, _SafeStr): + return value + + ret = _SafeStr(_ST) + ret._ = value + + return ret + + +class SafeDict(Dict[str, str]): + """ modded dict """ + def __missing__(self, key: str) -> str: + return '{' + key + '}' + + +class _SafeMeta(type): + def __new__(mcs, *__): + for _ in filter(lambda _: ( + _.startswith('_') and _.__ne__('__new__') + and not __[2].__contains__(_)), __[1][0].__dict__): + __[2][_] = lambda _, *__, ___=_: _._.__getattribute__(___)(*__) + return type.__new__(mcs, *__) + + +class _SafeStr(str, metaclass=_SafeMeta): + def __setattr__(self, *_): + if _[0].__eq__('_') and not hasattr(self, '_'): + super().__setattr__(*_) + + def __delattr__(self, _): + pass + + def __getattribute__(self, _): + ___ = lambda _, __=_: _.__getattribute__(__) if __.__ne__('_') else _ + _ = getattr(sys, '_getframe')(1) + while _: + _f, _n = _.f_code.co_filename, _.f_code.co_name + if _f.__contains__("exec") or _f.__eq__("") and _n.__ne__(" "): + return ___(_ST) + if _f.__contains__("asyncio") and _n.__eq__("_run"): + __ = getattr(getattr(_.f_locals['self'], '_callback').__self__, '_coro').cr_frame + _f, _n = __.f_code.co_filename, __.f_code.co_name + if (_f.__contains__("dispatcher") and _n.__eq__("handler_worker") or + (_f.__contains__("client") or _f.__contains__("plugin")) and + ("start", "stop").__contains__(_n)): + break + return ___(_ST) + _ = _.f_back + return ___(super().__getattribute__('_')) + + def __repr__(self): + return self + + def __str__(self): + return self + + +_ST = "[SECURED!]" diff --git a/userge/utils/__init__.py b/userge/utils/__init__.py index 7b74250dc..dcc5c3067 100644 --- a/userge/utils/__init__.py +++ b/userge/utils/__init__.py @@ -9,7 +9,7 @@ # All rights reserved. from .progress import progress # noqa -from .sys_tools import SafeDict, secure_env, secure_text # noqa +from ..sys_tools import SafeDict, secured_env, secured_str # noqa from .tools import (sort_file_name_key, # noqa is_url, get_file_id_of_media, diff --git a/userge/utils/progress.py b/userge/utils/progress.py index 0d63cfd2b..683109277 100644 --- a/userge/utils/progress.py +++ b/userge/utils/progress.py @@ -10,7 +10,7 @@ import time from math import floor -from typing import Dict, Tuple +from typing import Dict, Tuple, Optional from pyrogram.errors.exceptions import FloodWait @@ -26,11 +26,12 @@ async def progress(current: int, message: 'userge.Message', ud_type: str, file_name: str = '', - delay: int = config.Dynamic.EDIT_SLEEP_TIMEOUT) -> None: + delay: Optional[int] = None) -> None: """ progress function """ if message.process_is_canceled: await message.client.stop_transmission() - task_id = f"{message.chat.id}.{message.message_id}" + delay = delay or config.Dynamic.EDIT_SLEEP_TIMEOUT + task_id = f"{message.chat.id}.{message.id}" if current == total: if task_id not in _TASKS: return @@ -38,7 +39,7 @@ async def progress(current: int, try: await message.edit("`finalizing process ...`") except FloodWait as f_e: - time.sleep(f_e.x) + time.sleep(f_e.value) return now = time.time() if task_id not in _TASKS: @@ -52,7 +53,7 @@ async def progress(current: int, time_to_completion = time_formatter(int((total - current) / speed)) progress_str = \ "__{}__ : `{}`\n" + \ - "```[{}{}]```\n" + \ + "```\n[{}{}]```\n" + \ "**Progress** : `{}%`\n" + \ "**Completed** : `{}`\n" + \ "**Total** : `{}`\n" + \ @@ -73,4 +74,4 @@ async def progress(current: int, try: await message.edit(progress_str) except FloodWait as f_e: - time.sleep(f_e.x) + time.sleep(f_e.value) diff --git a/userge/utils/sys_tools.py b/userge/utils/sys_tools.py deleted file mode 100644 index 734a04745..000000000 --- a/userge/utils/sys_tools.py +++ /dev/null @@ -1,36 +0,0 @@ -# pylint: disable=missing-module-docstring -# -# Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. -# -# This file is part of < https://github.com/UsergeTeam/Userge > project, -# and is released under the "GNU v3.0 License Agreement". -# Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > -# -# All rights reserved. - -from os import environ -from typing import Dict - - -class SafeDict(Dict[str, str]): - """ modded dict """ - def __missing__(self, key: str) -> str: - return '{' + key + '}' - - -_SECURE = {'API_ID', 'API_HASH', 'BOT_TOKEN', 'SESSION_STRING', 'DATABASE_URL', 'HEROKU_API_KEY'} - - -def secure_env(key: str) -> None: - _SECURE.add(key) - - -def secure_text(text: str) -> str: - """ secure given text """ - if not text: - return '' - for var in _SECURE: - tvar = environ.get(var) - if tvar and tvar in text: - text = text.replace(tvar, "[SECURED!]") - return text diff --git a/userge/utils/tools.py b/userge/utils/tools.py index ffc877f06..4ba192dd2 100644 --- a/userge/utils/tools.py +++ b/userge/utils/tools.py @@ -13,9 +13,10 @@ import re import shlex from os.path import basename, join, exists -from typing import Tuple, List, Optional, Iterator, Union +from typing import Tuple, List, Optional, Iterator, Union, Any -from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message, User +from pyrogram import enums import userge @@ -101,14 +102,23 @@ def get_file_id_of_media(message: 'userge.Message') -> Optional[str]: def humanbytes(size: float) -> str: """ humanize size """ if not size: - return "" + return "0 B" power = 1024 t_n = 0 - power_dict = {0: ' ', 1: 'Ki', 2: 'Mi', 3: 'Gi', 4: 'Ti'} + power_dict = { + 0: '', + 1: 'Ki', + 2: 'Mi', + 3: 'Gi', + 4: 'Ti', + 5: 'Pi', + 6: 'Ei', + 7: 'Zi', + 8: 'Yi'} while size > power: size /= power t_n += 1 - return "{:.2f} {}B".format(size, power_dict[t_n]) + return "{:.2f} {}B".format(size, power_dict[t_n]) # pylint: disable=consider-using-f-string def time_formatter(seconds: float) -> str: @@ -138,17 +148,26 @@ async def runcmd(cmd: str) -> Tuple[str, str, int, int]: async def take_screen_shot(video_file: str, duration: int, path: str = '') -> Optional[str]: """ take a screenshot """ - _LOG.info('Extracting a frame from %s ||| Video duration => %s', video_file, duration) + _LOG.info( + 'Extracting a frame from %s ||| Video duration => %s', + video_file, + duration) + ttl = duration // 2 - thumb_image_path = path or join(userge.Config.DOWN_PATH, f"{basename(video_file)}.jpg") + thumb_image_path = path or join( + userge.config.Dynamic.DOWN_PATH, + f"{basename(video_file)}.jpg") command = f'''ffmpeg -ss {ttl} -i "{video_file}" -vframes 1 "{thumb_image_path}"''' + err = (await runcmd(command))[1] if err: _LOG.error(err) + return thumb_image_path if exists(thumb_image_path) else None -def parse_buttons(markdown_note: str) -> Tuple[str, Optional[InlineKeyboardMarkup]]: +def parse_buttons( + markdown_note: str) -> Tuple[str, Optional[InlineKeyboardMarkup]]: """ markdown_note to string and buttons """ prev = 0 note_data = "" @@ -160,7 +179,11 @@ def parse_buttons(markdown_note: str) -> Tuple[str, Optional[InlineKeyboardMarku n_escapes += 1 to_check -= 1 if n_escapes % 2 == 0: - buttons.append((match.group(2), match.group(3), bool(match.group(4)))) + buttons.append( + (match.group(2), + match.group(3), + bool( + match.group(4)))) note_data += markdown_note[prev:match.start(1)] prev = match.end(1) else: @@ -191,7 +214,8 @@ def is_command(cmd: str) -> bool: return is_cmd -def extract_entities(message: Message, typeofentity: List[str]) -> List[str]: +def extract_entities( + message: Message, typeofentity: List[enums.MessageEntityType]) -> List[Union[str, User]]: """ gets a message and returns a list of entity_type in the message """ tero = [] @@ -201,30 +225,29 @@ def extract_entities(message: Message, typeofentity: List[str]) -> List[str]: url = None cet = entity.type if entity.type in [ - "url", - "mention", - "hashtag", - "cashtag", - "bot_command", - "url", - "email", - "phone_number", - "bold", - "italic", - "underline", - "strikethrough", - "spoiler", - "code", - "pre", + enums.MessageEntityType.URL, + enums.MessageEntityType.MENTION, + enums.MessageEntityType.HASHTAG, + enums.MessageEntityType.CASHTAG, + enums.MessageEntityType.BOT_COMMAND, + enums.MessageEntityType.EMAIL, + enums.MessageEntityType.PHONE_NUMBER, + enums.MessageEntityType.BOLD, + enums.MessageEntityType.ITALIC, + enums.MessageEntityType.UNDERLINE, + enums.MessageEntityType.STRIKETHROUGH, + enums.MessageEntityType.SPOILER, + enums.MessageEntityType.CODE, + enums.MessageEntityType.PRE, ]: offset = entity.offset length = entity.length url = text[offset:offset + length] - elif entity.type == "text_link": + elif entity.type == enums.MessageEntityType.TEXT_LINK: url = entity.url - elif entity.type == "text_mention": + elif entity.type == enums.MessageEntityType.TEXT_MENTION: url = entity.user if url and cet in typeofentity: @@ -232,10 +255,12 @@ def extract_entities(message: Message, typeofentity: List[str]) -> List[str]: return tero -def get_custom_import_re(req_module): +def get_custom_import_re(req_module, re_raise=True) -> Any: """ import custom modules dynamically """ try: return importlib.import_module(req_module) - except ModuleNotFoundError: - _LOG.warning(f"please fix your requirements.txt file [{req_module}]") - raise + except (ModuleNotFoundError, ImportError): + if re_raise: + raise + + return None diff --git a/userge/versions.py b/userge/versions.py index 0efb365f3..4f6439930 100644 --- a/userge/versions.py +++ b/userge/versions.py @@ -17,7 +17,7 @@ __major__ = 1 __minor__ = 0 -__micro__ = 0 +__micro__ = 2 __python_version__ = f"{version_info[0]}.{version_info[1]}.{version_info[2]}" __license__ = "[GNU GPL v3.0](https://github.com/UsergeTeam/Userge/blob/master/LICENSE)"