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 @@

- Userge + Userge
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)"