├── .deepsource.toml ├── .github └── pull.yml ├── .gitignore ├── .pep8speaks.yml ├── LICENSE ├── README.md ├── min_loader.txt ├── requirements.txt ├── resources └── userge.png ├── tea.yaml └── userge ├── __init__.py ├── config.py ├── core ├── __init__.py ├── client.py ├── database.py ├── ext │ ├── __init__.py │ ├── pool.py │ └── raw_client.py ├── methods │ ├── __init__.py │ ├── chats │ │ ├── __init__.py │ │ ├── conversation.py │ │ └── send_read_acknowledge.py │ ├── decorators │ │ ├── __init__.py │ │ ├── on_cmd.py │ │ ├── on_filters.py │ │ ├── on_left_member.py │ │ ├── on_new_member.py │ │ └── raw_decorator.py │ ├── messages │ │ ├── __init__.py │ │ ├── edit_message_text.py │ │ ├── send_as_file.py │ │ └── send_message.py │ ├── users │ │ ├── __init__.py │ │ └── get_user_dict.py │ └── utils │ │ ├── __init__.py │ │ ├── get_channel_logger.py │ │ ├── get_logger.py │ │ ├── restart.py │ │ └── terminate.py └── types │ ├── __init__.py │ ├── bound │ ├── __init__.py │ └── message.py │ ├── new │ ├── __init__.py │ ├── channel_logger.py │ ├── conversation.py │ └── manager.py │ └── raw │ ├── __init__.py │ ├── command.py │ ├── filter.py │ └── plugin.py ├── logger.py ├── main.py ├── plugins ├── __init__.py └── builtin │ ├── __init__.py │ ├── executor │ ├── __init__.py │ └── __main__.py │ ├── help │ ├── __init__.py │ └── __main__.py │ ├── loader │ ├── __init__.py │ └── __main__.py │ ├── manage │ ├── __init__.py │ └── __main__.py │ ├── sudo │ ├── __init__.py │ └── __main__.py │ ├── system │ ├── __init__.py │ └── __main__.py │ └── tools │ ├── __init__.py │ └── __main__.py ├── sys_tools.py ├── utils ├── __init__.py ├── exceptions.py ├── progress.py └── tools.py └── versions.py /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "python" 5 | enabled = true 6 | dependency_file_paths = ["requirements.txt"] 7 | 8 | [analyzers.meta] 9 | runtime_version = "3.x.x" 10 | max_line_length = 100 11 | -------------------------------------------------------------------------------- /.github/pull.yml: -------------------------------------------------------------------------------- 1 | version: "1" 2 | rules: 3 | - base: alpha 4 | upstream: UsergeTeam:alpha 5 | mergeMethod: rebase 6 | mergeUnstable: false 7 | - base: beta 8 | upstream: UsergeTeam:beta 9 | mergeMethod: rebase 10 | - base: master 11 | upstream: UsergeTeam:master 12 | mergeMethod: rebase 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | env/ 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | 132 | # config files 133 | .apt/ 134 | .heroku/ 135 | .profile.d/ 136 | vendor/ 137 | config.env 138 | .vscode/ 139 | .idea/ 140 | *.session 141 | unknown_errors.txt 142 | logs/ 143 | ./loader/ 144 | userge/plugins/dev/ 145 | .rcache 146 | test.py 147 | -------------------------------------------------------------------------------- /.pep8speaks.yml: -------------------------------------------------------------------------------- 1 | # File : .pep8speaks.yml 2 | 3 | scanner: 4 | linter: flake8 5 | 6 | flake8: 7 | max-line-length: 100 8 | ignore: 9 | - W503 # line break before binary operator 10 | 11 | message: 12 | opened: 13 | header: "@{name}, Thanks for opening this PR." 14 | updated: 15 | header: "@{name}, Thanks for updating this PR." 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Userge 4 | 5 |
6 | Pluggable Telegram UserBot 7 |
8 | Inspiration 9 |  •  10 | Documentation 11 |  •  12 | Deployment 13 |  •  14 | Project Credits 15 |  •  16 | Copyright & License 17 |

18 | 19 | # Userge 🔥 20 | 21 | [![Build Status](https://travis-ci.com/UsergeTeam/Userge.svg?branch=alpha)](https://travis-ci.com/UsergeTeam/Userge) 22 | ![Python Version](https://img.shields.io/badge/python-3.8/3.9-lightgrey) 23 | ![Release](https://img.shields.io/github/v/release/UsergeTeam/Userge) 24 | ![Stars](https://img.shields.io/github/stars/UsergeTeam/Userge) 25 | ![Forks](https://img.shields.io/github/forks/UsergeTeam/Userge) 26 | ![Issues Open](https://img.shields.io/github/issues/UsergeTeam/Userge) 27 | ![Issues Closed](https://img.shields.io/github/issues-closed/UsergeTeam/Userge) 28 | ![PRs Open](https://img.shields.io/github/issues-pr/UsergeTeam/Userge) 29 | ![PRs Closed](https://img.shields.io/github/issues-pr-closed/UsergeTeam/Userge) 30 | ![Contributors](https://img.shields.io/github/contributors/UsergeTeam/Userge) 31 | ![Repo Size](https://img.shields.io/github/repo-size/UsergeTeam/Userge) 32 | ![License](https://img.shields.io/github/license/UsergeTeam/Userge) 33 | ![Commit Activity](https://img.shields.io/github/commit-activity/m/UsergeTeam/Userge) 34 | [![Plugins Repo!](https://img.shields.io/badge/Plugins%20Repo-!-orange)](https://github.com/UsergeTeam/Userge-Plugins) 35 | [![Join Channel!](https://img.shields.io/badge/Join%20Channel-!-red)](https://t.me/theUserge) 36 | [![DeepSource](https://static.deepsource.io/deepsource-badge-light-mini.svg)](https://deepsource.io/gh/UsergeTeam/Userge/?ref=repository-badge) 37 | [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/UsergeTeam/Userge) 38 | 39 | > **Userge** is a Powerful , _Pluggable_ Telegram UserBot written in _Python_ using [Pyrogram](https://github.com/pyrogram/pyrogram). 40 | 41 | ## Inspiration 😇 42 | 43 | > This project is inspired by the following projects :) 44 | 45 | * [tg_userbot](https://github.com/watzon/tg_userbot) ( heavily ) 🤗 46 | * [PyroGramBot](https://github.com/SpEcHiDe/PyroGramBot) 47 | * [Telegram-Paperplane](https://github.com/RaphielGang/Telegram-Paperplane) 48 | * [UniBorg](https://github.com/SpEcHiDe/UniBorg) 49 | 50 | > Special Thanks to all of you !!!. 51 | 52 | ## [Documentation](https://theuserge.github.io) 📘 53 | 54 | ## [Deployment](https://theuserge.github.io/deployment) 👷 55 | 56 | ## [Plugins](https://github.com/UsergeTeam/Userge-Plugins) 🔌 57 | 58 | ### Support & Discussions 👥 59 | 60 | > Head over to the [Discussion Group](https://t.me/usergeot) and [Update Channel](https://t.me/theUserge) 61 | 62 | ### Project Credits 💆‍♂️ 63 | 64 | * [Specially to these projects](https://github.com/UsergeTeam/Userge#inspiration-) 🥰 65 | * [Contributors](https://github.com/UsergeTeam/Userge/graphs/contributors) 👥 66 | 67 | ### Copyright & License 👮 68 | 69 | * Copyright (C) 2020 - 2022 by [UsergeTeam](https://github.com/UsergeTeam) ❤️️ 70 | * Licensed under the terms of the [GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007](https://github.com/UsergeTeam/Userge/blob/master/LICENSE) 71 | -------------------------------------------------------------------------------- /min_loader.txt: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dnspython 2 | heroku3 3 | motor 4 | pyrogram==2.0.58 5 | tgcrypto 6 | -------------------------------------------------------------------------------- /resources/userge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UsergeTeam/Userge/ffdd4cc5c9d91e8f2f2fdec310ac2c4058da4c16/resources/userge.png -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | --- 3 | version: 1.0.0 4 | codeOwners: 5 | - '0x7E5712FC870142029bDcDeBC105Bc4999C8A812d' 6 | quorum: 1 7 | -------------------------------------------------------------------------------- /userge/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | from userge.logger import logging # noqa 12 | from userge import config # noqa 13 | from userge.core import ( # noqa 14 | Userge, filters, Message, get_collection, pool) 15 | 16 | userge = Userge() # userge is the client name 17 | -------------------------------------------------------------------------------- /userge/config.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | from os import environ 12 | 13 | import heroku3 14 | 15 | from userge import logging 16 | from .sys_tools import secured_env, secured_str 17 | 18 | _LOG = logging.getLogger(__name__) 19 | 20 | # try to get this value using eval :) 21 | TEST = secured_str("nice! report @UsergeSpam") 22 | 23 | API_ID = environ.get("API_ID") 24 | API_HASH = secured_env("API_HASH") 25 | BOT_TOKEN = secured_env("BOT_TOKEN") 26 | SESSION_STRING = secured_env("SESSION_STRING") 27 | DB_URI = secured_env("DATABASE_URL") 28 | 29 | OWNER_ID = tuple(filter(lambda x: x, map(int, environ.get("OWNER_ID", "0").split()))) 30 | LOG_CHANNEL_ID = int(environ.get("LOG_CHANNEL_ID")) 31 | AUTH_CHATS = (OWNER_ID[0], LOG_CHANNEL_ID) if OWNER_ID else (LOG_CHANNEL_ID,) 32 | 33 | CMD_TRIGGER = environ.get("CMD_TRIGGER") 34 | SUDO_TRIGGER = environ.get("SUDO_TRIGGER") 35 | PUBLIC_TRIGGER = '/' 36 | 37 | WORKERS = int(environ.get("WORKERS")) 38 | MAX_MESSAGE_LENGTH = 4096 39 | 40 | FINISHED_PROGRESS_STR = environ.get("FINISHED_PROGRESS_STR") 41 | UNFINISHED_PROGRESS_STR = environ.get("UNFINISHED_PROGRESS_STR") 42 | 43 | HEROKU_API_KEY = secured_env("HEROKU_API_KEY") 44 | HEROKU_APP_NAME = environ.get("HEROKU_APP_NAME") 45 | HEROKU_APP = heroku3.from_key(HEROKU_API_KEY).apps()[HEROKU_APP_NAME] \ 46 | if HEROKU_API_KEY and HEROKU_APP_NAME else None 47 | 48 | ASSERT_SINGLE_INSTANCE = environ.get("ASSERT_SINGLE_INSTANCE", '').lower() == "true" 49 | IGNORE_VERIFIED_CHATS = True 50 | 51 | 52 | class Dynamic: 53 | DOWN_PATH = environ.get("DOWN_PATH") 54 | 55 | MSG_DELETE_TIMEOUT = 120 56 | EDIT_SLEEP_TIMEOUT = 10 57 | 58 | USER_IS_PREFERRED = False 59 | -------------------------------------------------------------------------------- /userge/core/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | from pyrogram import filters # noqa 12 | 13 | from .database import get_collection # noqa 14 | from .ext import pool # noqa 15 | from .types.bound import Message # noqa 16 | from .client import Userge # noqa 17 | -------------------------------------------------------------------------------- /userge/core/client.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['Userge'] 12 | 13 | import asyncio 14 | import functools 15 | import importlib 16 | import inspect 17 | import os 18 | import signal 19 | import threading 20 | import time 21 | from contextlib import suppress 22 | from types import ModuleType 23 | from typing import List, Awaitable, Any, Optional, Union 24 | 25 | from pyrogram import types 26 | from pyrogram.methods import Methods as RawMethods 27 | 28 | from userge import logging, config 29 | from userge.utils import time_formatter 30 | from userge.utils.exceptions import UsergeBotNotFound 31 | from .database import get_collection 32 | from .ext import RawClient 33 | from .methods import Methods 34 | 35 | _LOG = logging.getLogger(__name__) 36 | 37 | 38 | def _import_module(path: str) -> Optional[ModuleType]: 39 | imported: Optional[ModuleType] = None 40 | 41 | try: 42 | imported = importlib.import_module(path) 43 | except Exception as i_e: 44 | _LOG.error(f"[{path}] - {i_e}") 45 | 46 | return imported 47 | 48 | 49 | def _reload_module(module: Optional[ModuleType]) -> Optional[ModuleType]: 50 | reloaded: Optional[ModuleType] = None 51 | 52 | if module: 53 | try: 54 | reloaded = importlib.reload(module) 55 | except Exception as i_e: 56 | _LOG.error(i_e) 57 | reloaded = module 58 | 59 | return reloaded 60 | 61 | 62 | class _Module: 63 | def __init__(self, cat: str, name: str): 64 | self.cat = cat 65 | self.name = name 66 | self._path = f"userge.plugins.{cat}.{name}" 67 | self._init: Optional[ModuleType] = None 68 | self._main: Optional[ModuleType] = None 69 | 70 | def init(self) -> Optional[ModuleType]: 71 | self._init = _import_module(self._path) 72 | 73 | return self._init 74 | 75 | def main(self) -> Optional[ModuleType]: 76 | self._main = _import_module(self._path + ".__main__") 77 | 78 | return self._main 79 | 80 | def reload_init(self) -> Optional[ModuleType]: 81 | self._init = _reload_module(self._init) 82 | 83 | return self._init 84 | 85 | def reload_main(self) -> Optional[ModuleType]: 86 | self._main = _reload_module(self._main) 87 | 88 | return self._main 89 | 90 | 91 | _MODULES: List[_Module] = [] 92 | _START_TIME = time.time() 93 | _USERGE_STATUS = get_collection("USERGE_STATUS") 94 | 95 | 96 | async def _set_running(is_running: bool) -> None: 97 | await _USERGE_STATUS.update_one( 98 | {'_id': 'USERGE_STATUS'}, 99 | {"$set": {'is_running': is_running}}, 100 | upsert=True 101 | ) 102 | 103 | 104 | async def _is_running() -> bool: 105 | if config.ASSERT_SINGLE_INSTANCE: 106 | data = await _USERGE_STATUS.find_one({'_id': 'USERGE_STATUS'}) 107 | if data: 108 | return bool(data['is_running']) 109 | 110 | return False 111 | 112 | 113 | async def _wait_for_instance() -> None: 114 | counter = 0 115 | timeout = 30 # 30 sec 116 | max_ = 1800 # 30 min 117 | 118 | while await _is_running(): 119 | _LOG.info("Waiting for the Termination of " 120 | f"previous Userge instance ... [{timeout} sec]") 121 | time.sleep(timeout) 122 | 123 | counter += timeout 124 | if counter >= max_: 125 | _LOG.info(f"Max timeout reached ! [{max_} sec]") 126 | break 127 | 128 | 129 | class _AbstractUserge(Methods): 130 | def __init__(self, **kwargs) -> None: 131 | self._me: Optional[types.User] = None 132 | super().__init__(**kwargs) 133 | 134 | @property 135 | def id(self) -> int: 136 | """ returns client id """ 137 | if self.is_bot: 138 | return RawClient.BOT_ID 139 | 140 | return RawClient.USER_ID 141 | 142 | @property 143 | def is_bot(self) -> bool: 144 | """ returns client is bot or not """ 145 | if self._bot is not None: 146 | return hasattr(self, 'ubot') 147 | 148 | return bool(config.BOT_TOKEN) 149 | 150 | @property 151 | def uptime(self) -> str: 152 | """ returns userge uptime """ 153 | return time_formatter(time.time() - _START_TIME) 154 | 155 | async def _load_plugins(self) -> None: 156 | _LOG.info("Importing All Plugins") 157 | 158 | _MODULES.clear() 159 | base = os.path.join("userge", "plugins") 160 | 161 | for cat in os.listdir(base): 162 | cat_path = os.path.join(base, cat) 163 | if not os.path.isdir(cat_path) or cat.startswith("_"): 164 | continue 165 | 166 | for plg in os.listdir(cat_path): 167 | plg_path = os.path.join(cat_path, plg) 168 | if not os.path.isdir(plg_path) or plg.startswith("_"): 169 | continue 170 | 171 | mdl = _Module(cat, plg) 172 | mt = mdl.init() 173 | if mt: 174 | _MODULES.append(mdl) 175 | self.manager.update_plugin(mt.__name__, mt.__doc__) 176 | 177 | for mdl in tuple(_MODULES): 178 | if not mdl.main(): 179 | self.manager.remove(mdl.name) 180 | _MODULES.remove(mdl) 181 | 182 | await self.manager.init() 183 | _LOG.info(f"Imported ({len(_MODULES)}) Plugins => " 184 | + str(['.'.join((mdl.cat, mdl.name)) for mdl in _MODULES])) 185 | 186 | async def reload_plugins(self) -> int: 187 | """ Reload all Plugins """ 188 | _LOG.info("Reloading All Plugins") 189 | 190 | await self.manager.stop() 191 | self.manager.clear() 192 | 193 | reloaded: List[_Module] = [] 194 | 195 | for mdl in _MODULES: 196 | mt = mdl.reload_init() 197 | if mt: 198 | reloaded.append(mdl) 199 | self.manager.update_plugin(mt.__name__, mt.__doc__) 200 | 201 | for mdl in tuple(reloaded): 202 | if not mdl.reload_main(): 203 | self.manager.remove(mdl.name) 204 | reloaded.remove(mdl) 205 | 206 | await self.manager.init() 207 | await self.manager.start() 208 | 209 | _LOG.info(f"Reloaded {len(reloaded)} Plugins => " 210 | + str([mdl.name for mdl in reloaded])) 211 | 212 | return len(reloaded) 213 | 214 | async def get_me(self, cached: bool = True) -> types.User: 215 | if not cached or self._me is None: 216 | self._me = await super().get_me() 217 | 218 | return self._me 219 | 220 | async def start(self): 221 | await super().start() 222 | self._me = await self.get_me() 223 | 224 | if self.is_bot: 225 | RawClient.BOT_ID = self._me.id 226 | _LOG.info(f"started bot: {self._me.username}") 227 | else: 228 | RawClient.USER_ID = self._me.id 229 | _LOG.info(f"started user: {self._me.first_name}") 230 | 231 | def __eq__(self, o: object) -> bool: 232 | return isinstance(o, _AbstractUserge) and self.id == o.id 233 | 234 | def __hash__(self) -> int: # pylint: disable=W0235 235 | return super().__hash__() 236 | 237 | 238 | class UsergeBot(_AbstractUserge): 239 | """ UsergeBot, the bot """ 240 | 241 | def __init__(self, **kwargs) -> None: 242 | super().__init__(name="usergeBot", in_memory=True, **kwargs) 243 | 244 | @property 245 | def ubot(self) -> 'Userge': 246 | """ returns userbot """ 247 | return self._bot 248 | 249 | 250 | class Userge(_AbstractUserge): 251 | """ Userge, the userbot """ 252 | 253 | has_bot = bool(config.BOT_TOKEN) 254 | 255 | def __init__(self) -> None: 256 | kwargs = { 257 | 'api_id': config.API_ID, 258 | 'api_hash': config.API_HASH, 259 | 'workers': config.WORKERS 260 | } 261 | 262 | if config.BOT_TOKEN: 263 | kwargs['bot_token'] = config.BOT_TOKEN 264 | 265 | if config.SESSION_STRING and config.BOT_TOKEN: 266 | RawClient.DUAL_MODE = True 267 | kwargs['bot'] = UsergeBot(bot=self, **kwargs) 268 | 269 | kwargs['name'] = 'userge' 270 | kwargs['session_string'] = config.SESSION_STRING or None 271 | super().__init__(**kwargs) 272 | 273 | if config.SESSION_STRING: 274 | self.storage.session_string = config.SESSION_STRING 275 | 276 | @property 277 | def dual_mode(self) -> bool: 278 | return RawClient.DUAL_MODE 279 | 280 | @property 281 | def bot(self) -> Union['UsergeBot', 'Userge']: 282 | """ returns usergebot """ 283 | if self._bot is None: 284 | if config.BOT_TOKEN: 285 | return self 286 | raise UsergeBotNotFound("Need BOT_TOKEN ENV!") 287 | 288 | return self._bot 289 | 290 | async def start(self) -> None: 291 | """ start client and bot """ 292 | await _wait_for_instance() 293 | await _set_running(True) 294 | 295 | _LOG.info("Starting ...") 296 | 297 | await super().start() 298 | 299 | if self._bot is not None: 300 | await self._bot.start() 301 | 302 | await self._load_plugins() 303 | await self.manager.start() 304 | 305 | async def stop(self, **_) -> None: 306 | """ stop client and bot """ 307 | _LOG.info("Stopping ...") 308 | 309 | await self.manager.stop() 310 | 311 | if self._bot is not None: 312 | await self._bot.stop() 313 | 314 | await super().stop() 315 | 316 | await _set_running(False) 317 | 318 | def _get_log_client(self) -> _AbstractUserge: 319 | return self.bot if self.has_bot else self 320 | 321 | async def _log_success(self) -> None: 322 | # pylint: disable=protected-access 323 | await self._get_log_client()._channel.log("
Userge started successfully
") 324 | 325 | async def _log_exit(self) -> None: 326 | # pylint: disable=protected-access 327 | await self._get_log_client()._channel.log("
\nExiting Userge ...
") 328 | 329 | def begin(self, coro: Optional[Awaitable[Any]] = None) -> None: 330 | """ start userge """ 331 | try: 332 | self.loop.run_until_complete(self.start()) 333 | except (RuntimeError, KeyboardInterrupt): 334 | return 335 | 336 | idle_event = asyncio.Event() 337 | log_errored = False 338 | 339 | try: 340 | self.loop.run_until_complete(self._log_success()) 341 | except Exception as i_e: 342 | _LOG.exception(i_e) 343 | 344 | idle_event.set() 345 | log_errored = True 346 | 347 | def _handle(num, _) -> None: 348 | _LOG.info( 349 | f"Received Stop Signal [{signal.Signals(num).name}], Exiting Userge ...") 350 | 351 | idle_event.set() 352 | 353 | for sig in (signal.SIGABRT, signal.SIGTERM, signal.SIGINT): 354 | signal.signal(sig, _handle) 355 | 356 | mode = "[DUAL]" if RawClient.DUAL_MODE else "[BOT]" if config.BOT_TOKEN else "[USER]" 357 | 358 | with suppress(asyncio.exceptions.CancelledError, RuntimeError): 359 | if coro: 360 | _LOG.info(f"Running Coroutine - {mode}") 361 | self.loop.run_until_complete(coro) 362 | else: 363 | _LOG.info(f"Idling Userge - {mode}") 364 | self.loop.run_until_complete(idle_event.wait()) 365 | 366 | if not log_errored: 367 | try: 368 | self.loop.run_until_complete(self._log_exit()) 369 | except Exception as i_e: 370 | _LOG.exception(i_e) 371 | 372 | with suppress(RuntimeError): 373 | self.loop.run_until_complete(self.stop()) 374 | self.loop.run_until_complete(self.manager.exit()) 375 | 376 | to_cancel = asyncio.all_tasks(self.loop) 377 | for t in to_cancel: 378 | t.cancel() 379 | 380 | with suppress(RuntimeError): 381 | self.loop.run_until_complete( 382 | asyncio.gather( 383 | *to_cancel, 384 | return_exceptions=True)) 385 | self.loop.run_until_complete(self.loop.shutdown_asyncgens()) 386 | 387 | self.loop.stop() 388 | _LOG.info("Loop Stopped !") 389 | 390 | 391 | def _un_wrapper(obj, name, function): 392 | loop = asyncio.get_event_loop() 393 | 394 | @functools.wraps(function) 395 | def _wrapper(*args, **kwargs): 396 | coroutine = function(*args, **kwargs) 397 | if (threading.current_thread() is not threading.main_thread() 398 | and inspect.iscoroutine(coroutine)): 399 | async def _(): 400 | return await asyncio.wrap_future(asyncio.run_coroutine_threadsafe(coroutine, loop)) 401 | return _() 402 | return coroutine 403 | 404 | setattr(obj, name, _wrapper) 405 | 406 | 407 | def _un_wrap(source): 408 | for name in dir(source): 409 | if name.startswith("_"): 410 | continue 411 | wrapped = getattr(getattr(source, name), '__wrapped__', None) 412 | if wrapped and (inspect.iscoroutinefunction(wrapped) 413 | or inspect.isasyncgenfunction(wrapped)): 414 | _un_wrapper(source, name, wrapped) 415 | 416 | 417 | _un_wrap(RawMethods) 418 | for class_name in dir(types): 419 | cls = getattr(types, class_name) 420 | if inspect.isclass(cls): 421 | _un_wrap(cls) 422 | -------------------------------------------------------------------------------- /userge/core/database.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['get_collection'] 12 | 13 | from motor.motor_asyncio import AsyncIOMotorClient 14 | from motor.core import AgnosticDatabase, AgnosticCollection 15 | 16 | from userge import config 17 | 18 | _DATABASE: AgnosticDatabase = AsyncIOMotorClient(config.DB_URI)["Userge"] 19 | 20 | 21 | def get_collection(name: str) -> AgnosticCollection: 22 | """ Create or Get Collection from your database """ 23 | return _DATABASE[name] 24 | -------------------------------------------------------------------------------- /userge/core/ext/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | from pyrogram.types import Message as RawMessage # noqa 12 | 13 | from . import pool # noqa 14 | from .raw_client import RawClient # noqa 15 | -------------------------------------------------------------------------------- /userge/core/ext/pool.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['submit_thread', 'run_in_thread'] 12 | 13 | import asyncio 14 | import atexit 15 | from concurrent.futures import ThreadPoolExecutor, Future 16 | from functools import wraps, partial 17 | from typing import Any, Callable 18 | 19 | from userge import logging, config 20 | 21 | _LOG = logging.getLogger(__name__) 22 | _EXECUTOR = ThreadPoolExecutor(config.WORKERS) 23 | # pylint: disable=protected-access 24 | _MAX = _EXECUTOR._max_workers 25 | 26 | 27 | def submit_thread(func: Callable[..., Any], *args: Any, **kwargs: Any) -> Future: 28 | """ submit thread to thread pool """ 29 | return _EXECUTOR.submit(func, *args, **kwargs) 30 | 31 | 32 | def run_in_thread(func: Callable[..., Any]) -> Callable[..., Any]: 33 | """ run in a thread """ 34 | @wraps(func) 35 | async def wrapper(*args: Any, **kwargs: Any) -> Any: 36 | loop = asyncio.get_running_loop() 37 | return await loop.run_in_executor(_EXECUTOR, partial(func, *args, **kwargs)) 38 | return wrapper 39 | 40 | 41 | def _stop(): 42 | _EXECUTOR.shutdown() 43 | _LOG.info(f"Stopped Pool : {_MAX} Workers") 44 | 45 | 46 | atexit.register(_stop) 47 | _LOG.info(f"Started Pool : {_MAX} Workers") 48 | -------------------------------------------------------------------------------- /userge/core/ext/raw_client.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['RawClient'] 12 | 13 | import asyncio 14 | from math import floor 15 | from typing import Optional, Dict, List 16 | from time import time, perf_counter, sleep 17 | 18 | import pyrogram.raw.functions as funcs 19 | import pyrogram.raw.types as types 20 | from pyrogram import Client 21 | from pyrogram.session import Session 22 | from pyrogram.raw.core import TLObject 23 | 24 | import userge # pylint: disable=unused-import 25 | 26 | _LOG = userge.logging.getLogger(__name__) 27 | _LOG_STR = "FLOOD CONTROL : sleeping %.2fs in %d" 28 | 29 | 30 | class RawClient(Client): 31 | """ userge raw client """ 32 | DUAL_MODE = False 33 | USER_ID = 0 34 | BOT_ID = 0 35 | LAST_OUTGOING_TIME = time() 36 | REQ_LOGS: Dict[int, 'ChatReq'] = {} 37 | REQ_LOCK = asyncio.Lock() 38 | 39 | def __init__(self, bot: Optional['userge.core.client.UsergeBot'] = None, **kwargs) -> None: 40 | self._bot = bot 41 | super().__init__(**kwargs) 42 | self._channel = userge.core.types.new.ChannelLogger(self, "CORE") 43 | userge.core.types.new.Conversation.init(self) 44 | 45 | async def invoke(self, query: TLObject, retries: int = Session.MAX_RETRIES, 46 | timeout: float = Session.WAIT_TIMEOUT, sleep_threshold: float = None): 47 | if isinstance(query, funcs.account.DeleteAccount) or query.ID == 1099779595: 48 | raise Exception("Permission not granted to delete account!") 49 | key = 0 50 | if isinstance(query, (funcs.messages.SendMessage, 51 | funcs.messages.SendMedia, 52 | funcs.messages.SendMultiMedia, 53 | funcs.messages.EditMessage, 54 | funcs.messages.ForwardMessages)): 55 | if isinstance(query, funcs.messages.ForwardMessages): 56 | tmp = query.to_peer 57 | else: 58 | tmp = query.peer 59 | if isinstance(query, funcs.messages.SendMedia) and isinstance( 60 | query.media, (types.InputMediaUploadedDocument, 61 | types.InputMediaUploadedPhoto)): 62 | tmp = None 63 | if tmp: 64 | if isinstance(tmp, (types.InputPeerChannel, types.InputPeerChannelFromMessage)): 65 | key = int(tmp.channel_id) 66 | elif isinstance(tmp, types.InputPeerChat): 67 | key = int(tmp.chat_id) 68 | elif isinstance(tmp, (types.InputPeerUser, types.InputPeerUserFromMessage)): 69 | key = int(tmp.user_id) 70 | elif isinstance(query, funcs.channels.DeleteMessages) and isinstance( 71 | query.channel, (types.InputChannel, types.InputChannelFromMessage)): 72 | key = int(query.channel.channel_id) 73 | if key: 74 | async with self.REQ_LOCK: 75 | try: 76 | req = self.REQ_LOGS[key] 77 | except KeyError: 78 | req = self.REQ_LOGS[key] = ChatReq() 79 | async with req.lock: 80 | now = perf_counter() 81 | req.update(now - 60) 82 | if req.has: 83 | to_sl = 0.0 84 | diff = now - req.last 85 | if 0 < diff < 1: 86 | to_sl = 1 - diff 87 | diff = now - req.first 88 | if req.count > 18: 89 | to_sl = max(to_sl, 60 - diff) 90 | if to_sl > 0: 91 | if to_sl > 1: 92 | _LOG.info(_LOG_STR, to_sl, key) 93 | else: 94 | _LOG.debug(_LOG_STR, to_sl, key) 95 | await asyncio.sleep(to_sl) 96 | now += to_sl 97 | count = 0 98 | counter = floor(now - 1) 99 | for r in self.REQ_LOGS.values(): 100 | if r.has and r.last > counter: 101 | count += 1 102 | if count > 25: 103 | _LOG.info(_LOG_STR, 1, key) 104 | sleep(1) 105 | now += 1 106 | req.add(now) 107 | return await super().invoke(query, retries, timeout, sleep_threshold) 108 | 109 | 110 | class ChatReq: 111 | def __init__(self) -> None: 112 | self._lock = asyncio.Lock() 113 | self._logs: List[float] = [] 114 | 115 | @property 116 | def lock(self): 117 | return self._lock 118 | 119 | @property 120 | def has(self) -> bool: 121 | return len(self._logs) != 0 122 | 123 | @property 124 | def first(self) -> float: 125 | return self._logs[0] 126 | 127 | @property 128 | def last(self) -> Optional[float]: 129 | return self._logs[-1] 130 | 131 | @property 132 | def count(self) -> int: 133 | return len(self._logs) 134 | 135 | def add(self, log: float) -> None: 136 | self._logs.append(log) 137 | 138 | def update(self, t: float) -> None: 139 | self._logs = [i for i in self._logs if i > t] 140 | -------------------------------------------------------------------------------- /userge/core/methods/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['Methods'] 12 | 13 | from .chats import Chats 14 | from .decorators import Decorators 15 | from .messages import Messages 16 | from .users import Users 17 | from .utils import Utils 18 | 19 | 20 | class Methods(Chats, Decorators, Messages, Users, Utils): 21 | """ userge.methods """ 22 | -------------------------------------------------------------------------------- /userge/core/methods/chats/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['Chats'] 12 | 13 | from .conversation import Conversation 14 | from .send_read_acknowledge import SendReadAcknowledge 15 | 16 | 17 | class Chats(Conversation, SendReadAcknowledge): 18 | """ methods.chats """ 19 | -------------------------------------------------------------------------------- /userge/core/methods/chats/conversation.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['Conversation'] 12 | 13 | from typing import Union 14 | 15 | from ... import types 16 | 17 | 18 | class Conversation: # pylint: disable=missing-class-docstring 19 | def conversation(self, 20 | chat_id: Union[str, int], 21 | *, user_id: Union[str, int] = 0, 22 | timeout: Union[int, float] = 10, 23 | limit: int = 10) -> 'types.new.Conversation': 24 | """\nThis returns new conversation object. 25 | 26 | Parameters: 27 | chat_id (``int`` | ``str``): 28 | Unique identifier (int) or username (str) of the target chat. 29 | For your personal cloud (Saved Messages) 30 | you can simply use "me" or "self". 31 | For a contact that exists in your Telegram address book 32 | you can use his phone number (str). 33 | 34 | user_id (``int`` | ``str`` | , *optional*): 35 | define a specific user in this chat. 36 | 37 | timeout (``int`` | ``float`` | , *optional*): 38 | set conversation timeout. 39 | defaults to 10. 40 | 41 | limit (``int`` | , *optional*): 42 | set conversation message limit. 43 | defaults to 10. 44 | """ 45 | return types.new.Conversation(self, chat_id, user_id, timeout, limit) 46 | -------------------------------------------------------------------------------- /userge/core/methods/chats/send_read_acknowledge.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['SendReadAcknowledge'] 12 | 13 | from typing import List, Optional, Union 14 | 15 | from pyrogram.raw import functions 16 | 17 | from ...ext import RawClient, RawMessage 18 | 19 | 20 | class SendReadAcknowledge(RawClient): # pylint: disable=missing-class-docstring 21 | async def send_read_acknowledge(self, 22 | chat_id: Union[int, str], 23 | message: Union[List[RawMessage], 24 | Optional[RawMessage]] = None, 25 | *, max_id: Optional[int] = None, 26 | clear_mentions: bool = False) -> bool: 27 | """\nMarks messages as read and optionally clears mentions. 28 | 29 | Parameters: 30 | chat_id (``int`` | ``str``): 31 | Unique identifier (int) or username (str) of the target chat. 32 | For your personal cloud (Saved Messages) 33 | you can simply use "me" or "self". 34 | For a contact that exists in your Telegram address book 35 | you can use his phone number (str). 36 | 37 | message (``list`` | :obj: `Message`, *optional*): 38 | Either a list of messages or a single message. 39 | 40 | max_id (``int``, *optional*): 41 | Until which message should the read acknowledge be sent for. 42 | This has priority over the ``message`` parameter. 43 | 44 | clear_mentions (``bool``, *optional*): 45 | Whether the mention badge should be cleared (so that 46 | there are no more mentions) or not for the given entity. 47 | If no message is provided, this will be the only action 48 | taken. 49 | defaults to False. 50 | 51 | Returns: 52 | On success, True is returned. 53 | """ 54 | if max_id is None: 55 | if message: 56 | if isinstance(message, list): 57 | max_id = max(msg.id for msg in message) 58 | else: 59 | max_id = message.id 60 | else: 61 | max_id = 0 62 | if clear_mentions: 63 | await self.invoke( 64 | functions.messages.ReadMentions( 65 | peer=await self.resolve_peer(chat_id))) 66 | if max_id is None: 67 | return True 68 | if max_id is not None: 69 | return bool(await self.read_chat_history(chat_id=chat_id, max_id=max_id)) 70 | return False 71 | -------------------------------------------------------------------------------- /userge/core/methods/decorators/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['Decorators'] 12 | 13 | from .raw_decorator import RawDecorator # noqa 14 | from .on_cmd import OnCmd 15 | from .on_filters import OnFilters 16 | from .on_left_member import OnLeftMember 17 | from .on_new_member import OnNewMember 18 | 19 | 20 | class Decorators(OnCmd, OnFilters, OnLeftMember, OnNewMember): 21 | """ methods.decorators """ 22 | -------------------------------------------------------------------------------- /userge/core/methods/decorators/on_cmd.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['OnCmd'] 12 | 13 | from typing import Dict, List, Union, Optional 14 | 15 | from userge import config 16 | from ... import types 17 | from . import RawDecorator 18 | 19 | 20 | class OnCmd(RawDecorator): # pylint: disable=missing-class-docstring 21 | def on_cmd(self, 22 | command: str, 23 | about: Union[str, Dict[str, Union[str, List[str], Dict[str, str]]]], 24 | *, 25 | group: int = 0, 26 | name: str = '', 27 | trigger: Optional[str] = config.CMD_TRIGGER, 28 | filter_me: bool = True, 29 | allow_private: bool = True, 30 | allow_bots: bool = True, 31 | allow_groups: bool = True, 32 | allow_channels: bool = True, 33 | only_admins: bool = False, 34 | allow_via_bot: bool = True, 35 | check_client: bool = False, 36 | check_downpath: bool = False, 37 | propagate: Optional[bool] = None, 38 | check_change_info_perm: bool = False, 39 | check_edit_perm: bool = False, 40 | check_delete_perm: bool = False, 41 | check_restrict_perm: bool = False, 42 | check_promote_perm: bool = False, 43 | check_invite_perm: bool = False, 44 | check_pin_perm: bool = False, 45 | **kwargs: Union[str, bool] 46 | ) -> RawDecorator._PYRORETTYPE: 47 | """\nDecorator for handling messages. 48 | 49 | Example: 50 | @userge.on_cmd('test', about='for testing') 51 | 52 | Parameters: 53 | command (``str``): 54 | command or name to execute (without trigger!). 55 | 56 | about (``str`` | ``dict``): 57 | help string or dict for command. 58 | { 59 | 'header': ``str``, 60 | 'description': ``str``, 61 | 'flags': ``str`` | ``dict``, 62 | 'options': ``str`` | ``dict``, 63 | 'types': ``str`` | ``list``, 64 | 'usage': ``str``, 65 | 'examples': ``str`` | ``list``, 66 | 'others': ``str``, 67 | 'any_title': ``str`` | ``list`` | ``dict`` 68 | } 69 | 70 | group (``int``, *optional*): 71 | The group identifier, defaults to 0. 72 | 73 | name (``str``, *optional*): 74 | name for command. 75 | 76 | trigger (``str``, *optional*): 77 | trigger to start command. 78 | 79 | filter_me (``bool``, *optional*): 80 | If ``False``, anyone can access, defaults to True. 81 | 82 | allow_private (``bool``, *optional*): 83 | If ``False``, prohibit private chats, defaults to True. 84 | 85 | allow_bots (``bool``, *optional*): 86 | If ``False``, prohibit bot chats, defaults to True. 87 | 88 | allow_groups (``bool``, *optional*): 89 | If ``False``, prohibit group chats, defaults to True. 90 | 91 | allow_channels (``bool``, *optional*): 92 | If ``False``, prohibit channel chats, defaults to True. 93 | 94 | only_admins (``bool``, *optional*): 95 | If ``True``, client should be an admin, defaults to False. 96 | 97 | allow_via_bot (``bool``, *optional*): 98 | If ``True``, allow this via your bot, defaults to True. 99 | 100 | check_client (``bool``, *optional*): 101 | If ``True``, check client is bot or not before execute, defaults to False. 102 | 103 | check_downpath (``bool``, *optional*): 104 | If ``True``, check downpath and make if not exist, defaults to False. 105 | 106 | propagate (``bool``, *optional*): 107 | If ``False``, stop propagation to other groups, 108 | if ``True`` continue propagation in this group. defaults to None. 109 | 110 | check_change_info_perm (``bool``, *optional*): 111 | If ``True``, check user has change_info permission before execute, 112 | defaults to False. 113 | 114 | check_edit_perm (``bool``, *optional*): 115 | If ``True``, check user has edit permission before execute, 116 | defaults to False. 117 | 118 | check_delete_perm (``bool``, *optional*): 119 | If ``True``, check user has delete permission before execute, 120 | defaults to False. 121 | 122 | check_restrict_perm (``bool``, *optional*): 123 | If ``True``, check user has restrict permission before execute, 124 | defaults to False. 125 | 126 | check_promote_perm (``bool``, *optional*): 127 | If ``True``, check user has promote permission before execute, 128 | defaults to False. 129 | 130 | check_invite_perm (``bool``, *optional*): 131 | If ``True``, check user has invite permission before execute, 132 | defaults to False. 133 | 134 | check_pin_perm (``bool``, *optional*): 135 | If ``True``, check user has pin permission before execute, 136 | defaults to False. 137 | 138 | kwargs: 139 | prefix (``str``, *optional*): 140 | set prefix for flags, defaults to '-'. 141 | 142 | del_pre (``bool``, *optional*): 143 | If ``True``, flags returns without prefix, 144 | defaults to False. 145 | """ 146 | return self._build_decorator( 147 | types.raw.Command.parse(command, about, 148 | trigger or '', name, filter_me, 149 | client=self, 150 | group=group, 151 | allow_private=allow_private, 152 | allow_bots=allow_bots, 153 | allow_groups=allow_groups, 154 | allow_channels=allow_channels, 155 | only_admins=only_admins, 156 | allow_via_bot=allow_via_bot, 157 | check_client=check_client, 158 | check_downpath=check_downpath, 159 | propagate=propagate, 160 | check_change_info_perm=check_change_info_perm, 161 | check_edit_perm=check_edit_perm, 162 | check_delete_perm=check_delete_perm, 163 | check_restrict_perm=check_restrict_perm, 164 | check_promote_perm=check_promote_perm, 165 | check_invite_perm=check_invite_perm, 166 | check_pin_perm=check_pin_perm), **kwargs) 167 | -------------------------------------------------------------------------------- /userge/core/methods/decorators/on_filters.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['OnFilters'] 12 | 13 | from typing import Optional 14 | 15 | from pyrogram.filters import Filter as RawFilter 16 | 17 | from ... import types 18 | from . import RawDecorator 19 | 20 | 21 | class OnFilters(RawDecorator): # pylint: disable=missing-class-docstring 22 | def on_filters(self, # pylint: disable=arguments-differ 23 | filters: RawFilter, 24 | group: int = 0, 25 | allow_private: bool = True, 26 | allow_bots: bool = True, 27 | allow_groups: bool = True, 28 | allow_channels: bool = True, 29 | only_admins: bool = False, 30 | allow_via_bot: bool = True, 31 | check_client: bool = True, 32 | check_downpath: bool = False, 33 | propagate: Optional[bool] = None, 34 | check_change_info_perm: bool = False, 35 | check_edit_perm: bool = False, 36 | check_delete_perm: bool = False, 37 | check_restrict_perm: bool = False, 38 | check_promote_perm: bool = False, 39 | check_invite_perm: bool = False, 40 | check_pin_perm: bool = False) -> RawDecorator._PYRORETTYPE: 41 | """\nDecorator for handling filters 42 | 43 | Parameters: 44 | filters (:obj:`~pyrogram.filters`): 45 | Pass one or more filters to allow only a subset of 46 | messages to be passed in your function. 47 | 48 | group (``int``, *optional*): 49 | The group identifier, defaults to 0. 50 | 51 | allow_private (``bool``, *optional*): 52 | If ``False``, prohibit private chats, defaults to True. 53 | 54 | allow_bots (``bool``, *optional*): 55 | If ``False``, prohibit bot chats, defaults to True. 56 | 57 | allow_groups (``bool``, *optional*): 58 | If ``False``, prohibit group chats, defaults to True. 59 | 60 | allow_channels (``bool``, *optional*): 61 | If ``False``, prohibit channel chats, defaults to True. 62 | 63 | only_admins (``bool``, *optional*): 64 | If ``True``, client should be an admin, defaults to False. 65 | 66 | allow_via_bot (``bool``, *optional*): 67 | If ``True``, allow this via your bot, defaults to True. 68 | 69 | check_client (``bool``, *optional*): 70 | If ``True``, check client is bot or not before execute, defaults to True. 71 | 72 | check_downpath (``bool``, *optional*): 73 | If ``True``, check downpath and make if not exist, defaults to False. 74 | 75 | propagate (``bool``, *optional*): 76 | If ``False``, stop propagation to other groups, 77 | if ``True`` continue propagation in this group. defaults to None. 78 | 79 | check_change_info_perm (``bool``, *optional*): 80 | If ``True``, check user has change_info permission before execute, 81 | defaults to False. 82 | 83 | check_edit_perm (``bool``, *optional*): 84 | If ``True``, check user has edit permission before execute, 85 | defaults to False. 86 | 87 | check_delete_perm (``bool``, *optional*): 88 | If ``True``, check user has delete permission before execute, 89 | defaults to False. 90 | 91 | check_restrict_perm (``bool``, *optional*): 92 | If ``True``, check user has restrict permission before execute, 93 | defaults to False. 94 | 95 | check_promote_perm (``bool``, *optional*): 96 | If ``True``, check user has promote permission before execute, 97 | defaults to False. 98 | 99 | check_invite_perm (``bool``, *optional*): 100 | If ``True``, check user has invite permission before execute, 101 | defaults to False. 102 | 103 | check_pin_perm (``bool``, *optional*): 104 | If ``True``, check user has pin permission before execute, 105 | defaults to False. 106 | """ 107 | return self._build_decorator( 108 | types.raw.Filter.parse(client=self, 109 | filters=filters, 110 | group=group, 111 | allow_private=allow_private, 112 | allow_bots=allow_bots, 113 | allow_groups=allow_groups, 114 | allow_channels=allow_channels, 115 | only_admins=only_admins, 116 | allow_via_bot=allow_via_bot, 117 | check_client=check_client, 118 | check_downpath=check_downpath, 119 | propagate=propagate, 120 | check_change_info_perm=check_change_info_perm, 121 | check_edit_perm=check_edit_perm, 122 | check_delete_perm=check_delete_perm, 123 | check_restrict_perm=check_restrict_perm, 124 | check_promote_perm=check_promote_perm, 125 | check_invite_perm=check_invite_perm, 126 | check_pin_perm=check_pin_perm)) 127 | -------------------------------------------------------------------------------- /userge/core/methods/decorators/on_left_member.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['OnLeftMember'] 12 | 13 | from pyrogram import filters 14 | from pyrogram.filters import Filter as RawFilter 15 | 16 | from . import RawDecorator 17 | 18 | 19 | class OnLeftMember(RawDecorator): # pylint: disable=missing-class-docstring 20 | def on_left_member(self, 21 | leaving_chats: RawFilter, 22 | group: int = -2, 23 | allow_via_bot: bool = True, 24 | check_client: bool = True, 25 | check_downpath: bool = False) -> RawDecorator._PYRORETTYPE: 26 | """\nDecorator for handling left members 27 | 28 | Parameters: 29 | leaving_chats (:obj:`~pyrogram.filters.chat`): 30 | Pass filters.chat to allow only a subset of 31 | messages to be passed in your function. 32 | 33 | group (``int``, *optional*): 34 | The group identifier, defaults to 0. 35 | 36 | allow_via_bot (``bool``, *optional*): 37 | If ``True``, allow this via your bot, defaults to True. 38 | 39 | check_client (``bool``, *optional*): 40 | If ``True``, check client is bot or not before execute, defaults to True. 41 | 42 | check_downpath (``bool``, *optional*): 43 | If ``True``, check downpath and make if not exist, defaults to False. 44 | """ 45 | return self.on_filters( 46 | filters=filters.group & filters.left_chat_member & leaving_chats, 47 | group=group, allow_via_bot=allow_via_bot, check_client=check_client, 48 | check_downpath=check_downpath) 49 | -------------------------------------------------------------------------------- /userge/core/methods/decorators/on_new_member.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['OnNewMember'] 12 | 13 | from pyrogram import filters 14 | from pyrogram.filters import Filter as RawFilter 15 | 16 | from . import RawDecorator 17 | 18 | 19 | class OnNewMember(RawDecorator): # pylint: disable=missing-class-docstring 20 | def on_new_member(self, 21 | welcome_chats: RawFilter, 22 | group: int = -2, 23 | allow_via_bot: bool = True, 24 | check_client: bool = True, 25 | check_downpath: bool = False) -> RawDecorator._PYRORETTYPE: 26 | """\nDecorator for handling new members 27 | 28 | Parameters: 29 | welcome_chats (:obj:`~pyrogram.filters.chat`): 30 | Pass filters.chat to allow only a subset of 31 | messages to be passed in your function. 32 | 33 | group (``int``, *optional*): 34 | The group identifier, defaults to 0. 35 | 36 | allow_via_bot (``bool``, *optional*): 37 | If ``True``, allow this via your bot, defaults to True. 38 | 39 | check_client (``bool``, *optional*): 40 | If ``True``, check client is bot or not before execute, defaults to True. 41 | 42 | check_downpath (``bool``, *optional*): 43 | If ``True``, check downpath and make if not exist, defaults to False. 44 | """ 45 | return self.on_filters( 46 | filters=filters.group & filters.new_chat_members & welcome_chats, 47 | group=group, allow_via_bot=allow_via_bot, check_client=check_client, 48 | check_downpath=check_downpath) 49 | -------------------------------------------------------------------------------- /userge/core/methods/decorators/raw_decorator.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['RawDecorator'] 12 | 13 | import os 14 | import time 15 | import asyncio 16 | from traceback import format_exc 17 | from functools import partial 18 | from typing import List, Dict, Union, Any, Callable, Optional, Awaitable 19 | 20 | from pyrogram import StopPropagation, ContinuePropagation, enums 21 | from pyrogram.filters import Filter as RawFilter 22 | from pyrogram.types import Message as RawMessage, ChatMember 23 | from pyrogram.errors.exceptions.bad_request_400 import PeerIdInvalid, UserNotParticipant 24 | 25 | from userge import logging, config 26 | from userge.plugins.builtin import sudo, system 27 | from ...ext import RawClient 28 | from ... import types, client as _client # pylint: disable=unused-import 29 | 30 | _LOG = logging.getLogger(__name__) 31 | 32 | _PYROFUNC = Callable[['types.bound.Message'], Any] 33 | _TASK_1_START_TO = time.time() 34 | _TASK_2_START_TO = time.time() 35 | 36 | _B_CMN_CHT: List[int] = [] 37 | _B_AD_CHT: Dict[int, ChatMember] = {} 38 | _B_NM_CHT: Dict[int, ChatMember] = {} 39 | 40 | _U_AD_CHT: Dict[int, ChatMember] = {} 41 | _U_NM_CHT: Dict[int, ChatMember] = {} 42 | 43 | _CH_LKS: Dict[str, asyncio.Lock] = {} 44 | _CH_LKS_LK = asyncio.Lock() 45 | 46 | 47 | async def _update_u_cht(r_m: RawMessage) -> Optional[ChatMember]: 48 | if r_m.chat.id not in {**_U_AD_CHT, **_U_NM_CHT}: 49 | try: 50 | user = await r_m.chat.get_member(RawClient.USER_ID) 51 | except UserNotParticipant: 52 | return None 53 | # is this required? 54 | # user.privileges.can_all = None 55 | # if user.status == enums.ChatMemberStatus.OWNER: 56 | # user.privileges.can_all = True 57 | if user.status in ( 58 | enums.ChatMemberStatus.OWNER, 59 | enums.ChatMemberStatus.ADMINISTRATOR): 60 | _U_AD_CHT[r_m.chat.id] = user 61 | else: 62 | _U_NM_CHT[r_m.chat.id] = user 63 | elif r_m.chat.id in _U_AD_CHT: 64 | user = _U_AD_CHT[r_m.chat.id] 65 | else: 66 | user = _U_NM_CHT[r_m.chat.id] 67 | return user 68 | 69 | 70 | async def _update_b_cht(r_m: RawMessage) -> Optional[ChatMember]: 71 | if r_m.chat.id not in {**_B_AD_CHT, **_B_NM_CHT}: 72 | try: 73 | bot = await r_m.chat.get_member(RawClient.BOT_ID) 74 | except UserNotParticipant: 75 | return None 76 | if bot.status == enums.ChatMemberStatus.ADMINISTRATOR: 77 | _B_AD_CHT[r_m.chat.id] = bot 78 | else: 79 | _B_NM_CHT[r_m.chat.id] = bot 80 | elif r_m.chat.id in _B_AD_CHT: 81 | bot = _B_AD_CHT[r_m.chat.id] 82 | else: 83 | bot = _B_NM_CHT[r_m.chat.id] 84 | return bot 85 | 86 | 87 | def _clear_cht() -> None: 88 | global _TASK_1_START_TO # pylint: disable=global-statement 89 | _U_AD_CHT.clear() 90 | _U_NM_CHT.clear() 91 | _B_AD_CHT.clear() 92 | _B_NM_CHT.clear() 93 | _TASK_1_START_TO = time.time() 94 | 95 | 96 | async def _init(r_m: RawMessage) -> None: 97 | if r_m.from_user and ( 98 | r_m.from_user.is_self or ( 99 | r_m.from_user.id in sudo.USERS) or ( 100 | r_m.from_user.id in config.OWNER_ID)): 101 | RawClient.LAST_OUTGOING_TIME = time.time() 102 | 103 | 104 | async def _raise_func(r_c: Union['_client.Userge', '_client.UsergeBot'], 105 | r_m: RawMessage, text: str) -> None: 106 | # pylint: disable=protected-access 107 | if r_m.chat.type in (enums.ChatType.PRIVATE, enums.ChatType.BOT): 108 | await r_m.reply(f"< **ERROR**: {text} ! >") 109 | else: 110 | # skipcq: PYL-W0212 111 | await r_c._channel.log(f"{text}\nCaused By: [link]({r_m.link})", "ERROR") 112 | 113 | 114 | async def _is_admin(r_m: RawMessage, is_bot: bool) -> bool: 115 | if r_m.chat.type in (enums.ChatType.PRIVATE, enums.ChatType.BOT): 116 | return False 117 | if round(time.time() - _TASK_1_START_TO) > 10: 118 | _clear_cht() 119 | if is_bot: 120 | await _update_b_cht(r_m) 121 | return r_m.chat.id in _B_AD_CHT 122 | await _update_u_cht(r_m) 123 | return r_m.chat.id in _U_AD_CHT 124 | 125 | 126 | def _get_chat_member(r_m: RawMessage, is_bot: bool) -> Optional[ChatMember]: 127 | if r_m.chat.type in (enums.ChatType.PRIVATE, enums.ChatType.BOT): 128 | return None 129 | if is_bot: 130 | if r_m.chat.id in _B_AD_CHT: 131 | return _B_AD_CHT[r_m.chat.id] 132 | if r_m.chat.id in _B_NM_CHT: 133 | return _B_NM_CHT[r_m.chat.id] 134 | if r_m.chat.id in _U_AD_CHT: 135 | return _U_AD_CHT[r_m.chat.id] 136 | if r_m.chat.id in _U_NM_CHT: 137 | return _U_NM_CHT[r_m.chat.id] 138 | return None 139 | 140 | 141 | async def _get_lock(key: str) -> asyncio.Lock: 142 | async with _CH_LKS_LK: 143 | if key not in _CH_LKS: 144 | _CH_LKS[key] = asyncio.Lock() 145 | return _CH_LKS[key] 146 | 147 | 148 | async def _bot_is_present(r_c: Union['_client.Userge', '_client.UsergeBot'], 149 | r_m: RawMessage, is_bot: bool) -> bool: 150 | global _TASK_2_START_TO # pylint: disable=global-statement 151 | if is_bot: 152 | if r_m.chat.id not in _B_CMN_CHT: 153 | _B_CMN_CHT.append(r_m.chat.id) 154 | else: 155 | if round(time.time() - _TASK_2_START_TO) > 10: 156 | try: 157 | chats = await r_c.get_common_chats(RawClient.BOT_ID) 158 | _B_CMN_CHT.clear() 159 | for chat in chats: 160 | _B_CMN_CHT.append(chat.id) 161 | except PeerIdInvalid: 162 | pass 163 | _TASK_2_START_TO = time.time() 164 | return r_m.chat.id in _B_CMN_CHT 165 | 166 | 167 | async def _both_are_admins(r_c: Union['_client.Userge', '_client.UsergeBot'], 168 | r_m: RawMessage, is_bot: bool) -> bool: 169 | if not await _bot_is_present(r_c, r_m, is_bot): 170 | return False 171 | return r_m.chat.id in _B_AD_CHT and r_m.chat.id in _U_AD_CHT 172 | 173 | 174 | async def _both_have_perm(flt: Union['types.raw.Command', 'types.raw.Filter'], 175 | r_c: Union['_client.Userge', '_client.UsergeBot'], 176 | r_m: RawMessage, is_bot: bool) -> bool: 177 | if not await _bot_is_present(r_c, r_m, is_bot): 178 | return False 179 | try: 180 | user = await _update_u_cht(r_m) 181 | bot = await _update_b_cht(r_m) 182 | except PeerIdInvalid: 183 | return False 184 | if user is None or bot is None: 185 | return False 186 | 187 | if flt.check_change_info_perm and not ( 188 | (user.privileges and bot.privileges) and ( 189 | user.privileges.can_change_info and bot.privileges.can_change_info)): 190 | return False 191 | if flt.check_edit_perm and not ((user.privileges and bot.privileges) and ( 192 | user.privileges.can_edit_messages and bot.privileges.can_edit_messages)): 193 | return False 194 | if flt.check_delete_perm and not ((user.privileges and bot.privileges) and ( 195 | user.privileges.can_delete_messages and bot.privileges.can_delete_messages)): 196 | return False 197 | if flt.check_restrict_perm and not ((user.privileges and bot.privileges) and ( 198 | user.privileges.can_restrict_members and bot.privileges.can_restrict_members)): 199 | return False 200 | if flt.check_promote_perm and not ((user.privileges and bot.privileges) and ( 201 | user.privileges.can_promote_members and bot.privileges.can_promote_members)): 202 | return False 203 | if flt.check_invite_perm and not ((user.privileges and bot.privileges) and ( 204 | user.privileges.can_invite_users and bot.privileges.can_invite_users)): 205 | return False 206 | if flt.check_pin_perm and not ((user.privileges and bot.privileges) and ( 207 | user.privileges.can_pin_messages and bot.privileges.can_pin_messages)): 208 | return False 209 | return True 210 | 211 | 212 | class RawDecorator(RawClient): 213 | """ userge raw decorator """ 214 | _PYRORETTYPE = Callable[[_PYROFUNC], _PYROFUNC] 215 | 216 | def __init__(self, **kwargs) -> None: 217 | self.manager = types.new.Manager(self) 218 | super().__init__(**kwargs) 219 | 220 | def add_task(self, task: Callable[[], Awaitable[Any]]) -> Callable[[], Awaitable[Any]]: 221 | """ add a background task which is attached to this plugin. """ 222 | self.manager.get_plugin(task.__module__).add_task(task) 223 | return task 224 | 225 | def on_start(self, callback: Callable[[], Awaitable[Any]]) -> None: 226 | """ set a callback to calls when the plugin is loaded """ 227 | self.manager.get_plugin(callback.__module__).set_on_start_callback(callback) 228 | 229 | def on_stop(self, callback: Callable[[], Awaitable[Any]]) -> None: 230 | """ set a callback to calls when the plugin is unloaded """ 231 | self.manager.get_plugin(callback.__module__).set_on_stop_callback(callback) 232 | 233 | def on_exit(self, callback: Callable[[], Awaitable[Any]]) -> None: 234 | """ set a callback to calls when the userge is exiting """ 235 | self.manager.get_plugin(callback.__module__).set_on_exit_callback(callback) 236 | 237 | def on_filters(self, filters: RawFilter, group: int = 0, 238 | **kwargs: Union[str, bool]) -> 'RawDecorator._PYRORETTYPE': 239 | """ abstract on filter method """ 240 | 241 | def _build_decorator(self, 242 | flt: Union['types.raw.Command', 243 | 'types.raw.Filter'], 244 | **kwargs: Union[str, bool]) -> 'RawDecorator._PYRORETTYPE': 245 | def decorator(func: _PYROFUNC) -> _PYROFUNC: 246 | async def template(r_c: Union['_client.Userge', '_client.UsergeBot'], 247 | r_m: RawMessage) -> None: 248 | await self.manager.wait() 249 | 250 | if system.Dynamic.DISABLED_ALL and r_m.chat.id != config.LOG_CHANNEL_ID: 251 | raise StopPropagation 252 | if r_m.chat and r_m.chat.id in system.DISABLED_CHATS: 253 | raise StopPropagation 254 | if config.IGNORE_VERIFIED_CHATS and r_m.from_user and r_m.from_user.is_verified: 255 | raise StopPropagation 256 | 257 | await _init(r_m) 258 | _raise = partial(_raise_func, r_c, r_m) 259 | if r_m.chat and r_m.chat.type not in flt.scope: 260 | if isinstance(flt, types.raw.Command): 261 | await _raise(f"`invalid chat type [{r_m.chat.type.name}]`") 262 | return 263 | is_bot = r_c.is_bot 264 | if r_m.chat and flt.only_admins and not await _is_admin(r_m, is_bot): 265 | if isinstance(flt, types.raw.Command): 266 | await _raise("`chat admin required`") 267 | return 268 | if r_m.chat and flt.check_perm: 269 | is_admin = await _is_admin(r_m, is_bot) 270 | c_m = _get_chat_member(r_m, is_bot) 271 | if not c_m: 272 | if isinstance(flt, types.raw.Command): 273 | await _raise(f"`invalid chat type [{r_m.chat.type.name}]`") 274 | return 275 | if c_m.status != enums.ChatMemberStatus.OWNER: 276 | if flt.check_change_info_perm and not ( 277 | c_m.privileges and c_m.privileges.can_change_info): 278 | if isinstance(flt, types.raw.Command): 279 | await _raise("`required permission [change_info]`") 280 | return 281 | if flt.check_edit_perm and not ( 282 | c_m.privileges and c_m.privileges.can_edit_messages): 283 | if isinstance(flt, types.raw.Command): 284 | await _raise("`required permission [edit_messages]`") 285 | return 286 | if flt.check_delete_perm and not ( 287 | c_m.privileges and c_m.privileges.can_delete_messages): 288 | if isinstance(flt, types.raw.Command): 289 | await _raise("`required permission [delete_messages]`") 290 | return 291 | if flt.check_restrict_perm and not ( 292 | c_m.privileges and c_m.privileges.can_restrict_members): 293 | if isinstance(flt, types.raw.Command): 294 | if is_admin: 295 | await _raise("`required permission [restrict_members]`") 296 | else: 297 | await _raise("`chat admin required`") 298 | return 299 | if flt.check_promote_perm and not ( 300 | c_m.privileges and c_m.privileges.can_promote_members): 301 | if isinstance(flt, types.raw.Command): 302 | if is_admin: 303 | await _raise("`required permission [promote_members]`") 304 | else: 305 | await _raise("`chat admin required`") 306 | return 307 | if flt.check_invite_perm and not ( 308 | c_m.privileges and c_m.privileges.can_invite_users): 309 | if isinstance(flt, types.raw.Command): 310 | await _raise("`required permission [invite_users]`") 311 | return 312 | if flt.check_pin_perm and not ( 313 | c_m.privileges and c_m.privileges.can_pin_messages): 314 | if isinstance(flt, types.raw.Command): 315 | await _raise("`required permission [pin_messages]`") 316 | return 317 | 318 | if RawClient.DUAL_MODE and ( 319 | flt.check_client or ( 320 | r_m.from_user and r_m.from_user.id != RawClient.USER_ID 321 | and (r_m.from_user.id in config.OWNER_ID 322 | or r_m.from_user.id in sudo.USERS))): 323 | cond = True 324 | async with await _get_lock(str(flt)): 325 | if flt.only_admins: 326 | cond = cond and await _both_are_admins(r_c, r_m, is_bot) 327 | if flt.check_perm: 328 | cond = cond and await _both_have_perm(flt, r_c, r_m, is_bot) 329 | if cond: 330 | if config.Dynamic.USER_IS_PREFERRED: 331 | if isinstance(r_c, _client.UsergeBot): 332 | return 333 | elif await _bot_is_present(r_c, r_m, is_bot) and isinstance( 334 | r_c, _client.Userge): 335 | return 336 | 337 | if flt.check_downpath and not os.path.isdir( 338 | config.Dynamic.DOWN_PATH): 339 | os.makedirs(config.Dynamic.DOWN_PATH) 340 | 341 | try: 342 | await func(types.bound.Message.parse( 343 | r_c, r_m, module=module, **kwargs)) 344 | except (StopPropagation, ContinuePropagation): # pylint: disable=W0706 345 | raise 346 | except Exception as f_e: # pylint: disable=broad-except 347 | _LOG.exception(f_e) 348 | await self._channel.log(f"**PLUGIN** : `{module}`\n" 349 | f"**FUNCTION** : `{func.__name__}`\n" 350 | f"**ERROR** : `{f_e or None}`\n" 351 | f"```python\n{format_exc().strip()}```", 352 | "TRACEBACK") 353 | finally: 354 | if flt.propagate: 355 | raise ContinuePropagation 356 | if flt.propagate is not None: 357 | raise StopPropagation 358 | 359 | module = func.__module__ 360 | 361 | flt.update(func, template) 362 | self.manager.get_plugin(module).add(flt) 363 | 364 | return func 365 | return decorator 366 | -------------------------------------------------------------------------------- /userge/core/methods/messages/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['Messages'] 12 | 13 | from .send_message import SendMessage 14 | from .edit_message_text import EditMessageText 15 | from .send_as_file import SendAsFile 16 | 17 | 18 | class Messages(SendMessage, EditMessageText, SendAsFile): 19 | """ methods.messages """ 20 | -------------------------------------------------------------------------------- /userge/core/methods/messages/edit_message_text.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['EditMessageText'] 12 | 13 | import inspect 14 | import asyncio 15 | from typing import Optional, Union, List 16 | 17 | from pyrogram import enums 18 | from pyrogram.types import InlineKeyboardMarkup, MessageEntity 19 | 20 | from userge import config 21 | from ...ext import RawClient 22 | from ... import types 23 | 24 | 25 | class EditMessageText(RawClient): # pylint: disable=missing-class-docstring 26 | async def edit_message_text(self, # pylint: disable=arguments-differ 27 | chat_id: Union[int, str], 28 | message_id: int, 29 | text: str, 30 | del_in: int = -1, 31 | log: Union[bool, str] = False, 32 | parse_mode: Optional[enums.ParseMode] = None, 33 | entities: List[MessageEntity] = None, 34 | disable_web_page_preview: Optional[bool] = None, 35 | reply_markup: InlineKeyboardMarkup = None 36 | ) -> Union['types.bound.Message', bool]: 37 | """\nExample: 38 | message.edit_text("hello") 39 | 40 | Parameters: 41 | chat_id (``int`` | ``str``): 42 | Unique identifier (int) or username (str) of the target chat. 43 | For your personal cloud (Saved Messages) 44 | you can simply use "me" or "self". 45 | For a contact that exists in your Telegram address book 46 | you can use his phone number (str). 47 | 48 | message_id (``int``): 49 | Message identifier in the chat specified in chat_id. 50 | 51 | text (``str``): 52 | New text of the message. 53 | 54 | del_in (``int``): 55 | Time in Seconds for delete that message. 56 | 57 | log (``bool`` | ``str``, *optional*): 58 | If ``True``, the message will be forwarded 59 | to the log channel. 60 | If ``str``, the logger name will be updated. 61 | 62 | parse_mode (:obj:`enums.ParseMode`, *optional*): 63 | By default, texts are parsed using 64 | both Markdown and HTML styles. 65 | You can combine both syntaxes together. 66 | Pass "markdown" or "md" to enable 67 | Markdown-style parsing only. 68 | Pass "html" to enable HTML-style parsing only. 69 | Pass None to completely disable style parsing. 70 | 71 | entities (List of :obj:`~pyrogram.types.MessageEntity`): 72 | List of special entities that appear in message text, 73 | which can be specified instead of *parse_mode*. 74 | 75 | disable_web_page_preview (``bool``, *optional*): 76 | Disables link previews for links in this message. 77 | 78 | reply_markup (:obj:`InlineKeyboardMarkup`, *optional*): 79 | An InlineKeyboardMarkup object. 80 | 81 | Returns: 82 | On success, the edited 83 | :obj:`Message` or True is returned. 84 | 85 | Raises: 86 | RPCError: In case of a Telegram RPC error. 87 | """ 88 | msg = await super().edit_message_text(chat_id=chat_id, 89 | message_id=message_id, 90 | text=text, 91 | parse_mode=parse_mode, 92 | entities=entities, 93 | disable_web_page_preview=disable_web_page_preview, 94 | reply_markup=reply_markup) 95 | module = inspect.currentframe().f_back.f_globals['__name__'] 96 | if log: 97 | await self._channel.fwd_msg(msg, module if isinstance(log, bool) else log) 98 | del_in = del_in or config.Dynamic.MSG_DELETE_TIMEOUT 99 | if del_in > 0: 100 | await asyncio.sleep(del_in) 101 | setattr(msg, "_client", self) 102 | return bool(await msg.delete()) 103 | return types.bound.Message.parse(self, msg, module=module) 104 | -------------------------------------------------------------------------------- /userge/core/methods/messages/send_as_file.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['SendAsFile'] 12 | 13 | import inspect 14 | import io 15 | from typing import Union, Optional 16 | 17 | from pyrogram.parser import Parser 18 | 19 | from userge import logging 20 | from ... import types 21 | from ...ext import RawClient 22 | 23 | _LOG = logging.getLogger(__name__) 24 | 25 | 26 | class SendAsFile(RawClient): # pylint: disable=missing-class-docstring 27 | async def send_as_file(self, 28 | chat_id: Union[int, str], 29 | text: str, 30 | as_raw: bool = False, 31 | filename: str = "output.txt", 32 | caption: str = '', 33 | log: Union[bool, str] = False, 34 | reply_to_message_id: Optional[int] = None) -> 'types.bound.Message': 35 | """\nYou can send large outputs as file 36 | 37 | Example: 38 | @userge.send_as_file(chat_id=12345, text="hello") 39 | 40 | Parameters: 41 | chat_id (``int`` | ``str``): 42 | Unique identifier (int) or username (str) of the target chat. 43 | For your personal cloud (Saved Messages) 44 | you can simply use "me" or "self". 45 | For a contact that exists in your Telegram address book 46 | you can use his phone number (str). 47 | 48 | text (``str``): 49 | Text of the message to be sent. 50 | 51 | as_raw (``bool``, *optional*): 52 | If ``False``, the message will be escaped with current parse mode. 53 | default to ``False``. 54 | 55 | filename (``str``, *optional*): 56 | file_name for output file. 57 | 58 | caption (``str``, *optional*): 59 | caption for output file. 60 | 61 | log (``bool`` | ``str``, *optional*): 62 | If ``True``, the message will be forwarded 63 | to the log channel. 64 | If ``str``, the logger name will be updated. 65 | 66 | reply_to_message_id (``int``, *optional*): 67 | If the message is a reply, ID of the original message. 68 | 69 | Returns: 70 | On success, the sent Message is returned. 71 | """ 72 | if not as_raw: 73 | text = (await Parser(self).parse(text)).get("message") 74 | doc = io.BytesIO(text.encode()) 75 | doc.name = filename 76 | 77 | msg = await self.send_document(chat_id=chat_id, 78 | document=doc, 79 | caption=caption[:1024], 80 | disable_notification=True, 81 | reply_to_message_id=reply_to_message_id) 82 | module = inspect.currentframe().f_back.f_globals['__name__'] 83 | if log: 84 | await self._channel.fwd_msg(msg, module if isinstance(log, bool) else log) 85 | return types.bound.Message.parse(self, msg, module=module) 86 | -------------------------------------------------------------------------------- /userge/core/methods/messages/send_message.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['SendMessage'] 12 | 13 | import asyncio 14 | import inspect 15 | from datetime import datetime 16 | from typing import Optional, Union, List 17 | 18 | from pyrogram.types import ( 19 | InlineKeyboardMarkup, ReplyKeyboardMarkup, 20 | ReplyKeyboardRemove, ForceReply, MessageEntity) 21 | from pyrogram import enums 22 | 23 | from userge import config 24 | from ... import types 25 | from ...ext import RawClient 26 | 27 | 28 | class SendMessage(RawClient): # pylint: disable=missing-class-docstring 29 | async def send_message(self, # pylint: disable=arguments-differ 30 | chat_id: Union[int, str], 31 | text: str, 32 | del_in: int = -1, 33 | log: Union[bool, str] = False, 34 | parse_mode: Optional[enums.ParseMode] = None, 35 | entities: List[MessageEntity] = None, 36 | disable_web_page_preview: Optional[bool] = None, 37 | disable_notification: Optional[bool] = None, 38 | reply_to_message_id: Optional[int] = None, 39 | schedule_date: Optional[datetime] = None, 40 | protect_content: Optional[bool] = None, 41 | reply_markup: Union[InlineKeyboardMarkup, 42 | ReplyKeyboardMarkup, 43 | ReplyKeyboardRemove, 44 | ForceReply] = None 45 | ) -> Union['types.bound.Message', bool]: 46 | """\nSend text messages. 47 | 48 | Example: 49 | @userge.send_message(chat_id=12345, text='test') 50 | 51 | Parameters: 52 | chat_id (``int`` | ``str``): 53 | Unique identifier (int) or username (str) of the target chat. 54 | For your personal cloud (Saved Messages) 55 | you can simply use "me" or "self". 56 | For a contact that exists in your Telegram address book 57 | you can use his phone number (str). 58 | 59 | text (``str``): 60 | Text of the message to be sent. 61 | 62 | del_in (``int``): 63 | Time in Seconds for delete that message. 64 | 65 | log (``bool`` | ``str``, *optional*): 66 | If ``True``, the message will be forwarded to the log channel. 67 | If ``str``, the logger name will be updated. 68 | 69 | parse_mode (:obj:`enums.ParseMode`, *optional*): 70 | By default, texts are parsed using both Markdown and HTML styles. 71 | You can combine both syntaxes together. 72 | Pass "markdown" or "md" to enable Markdown-style parsing only. 73 | Pass "html" to enable HTML-style parsing only. 74 | Pass None to completely disable style parsing. 75 | 76 | entities (List of :obj:`~pyrogram.types.MessageEntity`): 77 | List of special entities that appear in message text, 78 | which can be specified instead of *parse_mode*. 79 | 80 | disable_web_page_preview (``bool``, *optional*): 81 | Disables link previews for links in this message. 82 | 83 | disable_notification (``bool``, *optional*): 84 | Sends the message silently. 85 | Users will receive a notification with no sound. 86 | 87 | reply_to_message_id (``int``, *optional*): 88 | If the message is a reply, ID of the original message. 89 | 90 | schedule_date (:py:obj:`~datetime.datetime`, *optional*): 91 | Date when the message will be automatically sent. Unix time. 92 | 93 | protect_content (``bool``, *optional*): 94 | Protects the contents of the sent message from forwarding and saving. 95 | 96 | reply_markup (:obj:`InlineKeyboardMarkup` | :obj:`ReplyKeyboardMarkup` 97 | | :obj:`ReplyKeyboardRemove` | :obj:`ForceReply`, *optional*): 98 | Additional interface options. An object for an inline keyboard, 99 | custom reply keyboard, instructions to remove 100 | reply keyboard or to force a reply from the user. 101 | 102 | Returns: 103 | :obj:`Message`: On success, the sent text message or True is returned. 104 | """ 105 | msg = await super().send_message(chat_id=chat_id, 106 | text=text, 107 | parse_mode=parse_mode, 108 | entities=entities, 109 | disable_web_page_preview=disable_web_page_preview, 110 | disable_notification=disable_notification, 111 | reply_to_message_id=reply_to_message_id, 112 | schedule_date=schedule_date, 113 | protect_content=protect_content, 114 | reply_markup=reply_markup) 115 | module = inspect.currentframe().f_back.f_globals['__name__'] 116 | if log: 117 | await self._channel.fwd_msg(msg, module if isinstance(log, bool) else log) 118 | del_in = del_in or config.Dynamic.MSG_DELETE_TIMEOUT 119 | if del_in > 0: 120 | await asyncio.sleep(del_in) 121 | setattr(msg, "_client", self) 122 | return bool(await msg.delete()) 123 | return types.bound.Message.parse(self, msg, module=module) 124 | -------------------------------------------------------------------------------- /userge/core/methods/users/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['Users'] 12 | 13 | from .get_user_dict import GetUserDict 14 | 15 | 16 | class Users(GetUserDict): 17 | """ methods.users """ 18 | -------------------------------------------------------------------------------- /userge/core/methods/users/get_user_dict.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['GetUserDict'] 12 | 13 | from typing import Dict, Union 14 | 15 | from ...ext import RawClient 16 | 17 | 18 | class GetUserDict(RawClient): # pylint: disable=missing-class-docstring 19 | async def get_user_dict(self, user_id: Union[int, str]) -> Dict[str, str]: 20 | """This will return user `Dict` which contains 21 | `id`(chat id), `fname`(first name), `lname`(last name), 22 | `flname`(full name), `uname`(username) and `mention`. 23 | """ 24 | user_obj = await self.get_users(user_id) 25 | fname = (user_obj.first_name or '').strip() 26 | lname = (user_obj.last_name or '').strip() 27 | username = (user_obj.username or '').strip() 28 | if fname and lname: 29 | full_name = fname + ' ' + lname 30 | elif fname or lname: 31 | full_name = fname or lname 32 | else: 33 | full_name = "user" 34 | mention = f"[{username or full_name}](tg://user?id={user_id})" 35 | return {'id': user_obj.id, 'fname': fname, 'lname': lname, 36 | 'flname': full_name, 'uname': username, 'mention': mention} 37 | -------------------------------------------------------------------------------- /userge/core/methods/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['Utils'] 12 | 13 | from .get_logger import GetLogger 14 | from .get_channel_logger import GetCLogger 15 | from .restart import Restart 16 | from .terminate import Terminate 17 | 18 | 19 | class Utils(GetLogger, GetCLogger, Restart, Terminate): 20 | """ methods.utils """ 21 | -------------------------------------------------------------------------------- /userge/core/methods/utils/get_channel_logger.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['GetCLogger'] 12 | 13 | import inspect 14 | 15 | from ... import types 16 | from ...ext import RawClient 17 | 18 | 19 | class GetCLogger(RawClient): # pylint: disable=missing-class-docstring 20 | # pylint: disable=invalid-name 21 | def getCLogger(self, name: str = '') -> 'types.new.ChannelLogger': 22 | """ This returns new channel logger object """ 23 | if not name: 24 | name = inspect.currentframe().f_back.f_globals['__name__'] 25 | 26 | return types.new.ChannelLogger(self, name) 27 | -------------------------------------------------------------------------------- /userge/core/methods/utils/get_logger.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['GetLogger'] 12 | 13 | import inspect 14 | 15 | from userge import logging 16 | 17 | 18 | class GetLogger: # pylint: disable=missing-class-docstring 19 | @staticmethod 20 | def getLogger(name: str = '') -> logging.Logger: # pylint: disable=invalid-name 21 | """ This returns new logger object """ 22 | if not name: 23 | name = inspect.currentframe().f_back.f_globals['__name__'] 24 | 25 | return logging.getLogger(name) 26 | -------------------------------------------------------------------------------- /userge/core/methods/utils/restart.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['Restart'] 12 | 13 | from loader.userge.api import restart 14 | from userge import logging 15 | from ...ext import RawClient 16 | 17 | _LOG = logging.getLogger(__name__) 18 | 19 | 20 | class Restart(RawClient): # pylint: disable=missing-class-docstring 21 | @staticmethod 22 | async def restart(hard: bool = False, **_) -> None: 23 | """ Restart the Userge """ 24 | _LOG.info(f"Restarting Userge [{'HARD' if hard else 'SOFT'}]") 25 | restart(hard) 26 | -------------------------------------------------------------------------------- /userge/core/methods/utils/terminate.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['Terminate'] 12 | 13 | import asyncio 14 | 15 | from ...ext import RawClient 16 | 17 | 18 | class Terminate(RawClient): # pylint: disable=missing-class-docstring 19 | async def terminate(self) -> None: 20 | """ terminate userge """ 21 | if not self.no_updates: 22 | for _ in range(self.workers): 23 | self.dispatcher.updates_queue.put_nowait(None) 24 | for task in self.dispatcher.handler_worker_tasks: 25 | try: 26 | await asyncio.wait_for(task, timeout=0.3) 27 | except asyncio.TimeoutError: 28 | task.cancel() 29 | self.dispatcher.handler_worker_tasks.clear() 30 | await super().terminate() 31 | -------------------------------------------------------------------------------- /userge/core/types/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | from . import raw, bound, new # noqa 12 | -------------------------------------------------------------------------------- /userge/core/types/bound/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | from .message import Message # noqa 12 | -------------------------------------------------------------------------------- /userge/core/types/new/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | from .channel_logger import ChannelLogger # noqa 12 | from .conversation import Conversation # noqa 13 | from .manager import Manager # noqa 14 | -------------------------------------------------------------------------------- /userge/core/types/new/channel_logger.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['ChannelLogger'] 12 | 13 | import asyncio 14 | from typing import Optional, Union 15 | 16 | from pyrogram.errors import ChatWriteForbidden 17 | from pyrogram.types import Message as RawMessage 18 | from pyrogram.errors.exceptions import MessageTooLong 19 | 20 | from userge import config 21 | from userge.utils import SafeDict, get_file_id_of_media, parse_buttons 22 | from ..bound import message as _message # pylint: disable=unused-import 23 | from ... import client as _client # pylint: disable=unused-import 24 | 25 | 26 | def _gen_string(name: str) -> str: 27 | parts = name.split('.') 28 | 29 | if len(parts) >= 2: 30 | name = parts[-2] 31 | 32 | return "**logger** : #" + name.upper() + "\n\n{}" 33 | 34 | 35 | class ChannelLogger: 36 | """ Channel logger for Userge """ 37 | def __init__(self, client: Union['_client.Userge', '_client.UsergeBot'], name: str) -> None: 38 | self._id = config.LOG_CHANNEL_ID 39 | self._client = client 40 | self._string = _gen_string(name) 41 | 42 | @staticmethod 43 | def get_link(message_id: int) -> str: 44 | """\nreturns link for a specific message. 45 | 46 | Parameters: 47 | message_id (`int`): 48 | Message id of stored message. 49 | 50 | Returns: 51 | str 52 | """ 53 | link = f"https://t.me/c/{str(config.LOG_CHANNEL_ID)[4:]}/{message_id}" 54 | return f"Preview" 55 | 56 | async def log(self, text: str, name: str = '') -> int: 57 | """\nsend text message to log channel. 58 | 59 | Parameters: 60 | text (``str``): 61 | Text of the message to be sent. 62 | 63 | name (``str``, *optional*): 64 | New Name for logger. 65 | 66 | Returns: 67 | message_id on success or None 68 | """ 69 | string = self._string 70 | if name: 71 | string = _gen_string(name) 72 | try: 73 | msg = await self._client.send_message(chat_id=self._id, 74 | text=string.format(text.strip()), 75 | disable_web_page_preview=True) 76 | except MessageTooLong: 77 | msg = await self._client.send_as_file(chat_id=self._id, 78 | text=string.format(text.strip()), 79 | filename="logs.log", 80 | caption=string) 81 | return msg.id 82 | 83 | async def fwd_msg(self, 84 | message: Union['_message.Message', 'RawMessage'], 85 | name: str = '', 86 | as_copy: bool = True) -> None: 87 | """\nforward message to log channel. 88 | 89 | Parameters: 90 | message (`pyrogram.Message`): 91 | pass pyrogram.Message object which want to forward. 92 | 93 | name (``str``, *optional*): 94 | New Name for logger. 95 | 96 | as_copy (`bool`, *optional*): 97 | Pass True to forward messages without the forward header 98 | (i.e.: send a copy of the message content so 99 | that it appears as originally sent by you). 100 | Defaults to True. 101 | 102 | Returns: 103 | None 104 | """ 105 | if isinstance(message, RawMessage): 106 | if message.media: 107 | await self.log("**Forwarding Message...**", name) 108 | try: 109 | if as_copy: 110 | await message.copy(chat_id=self._id) 111 | else: 112 | await message.forward(chat_id=self._id) 113 | except ValueError: 114 | pass 115 | else: 116 | await self.log( 117 | message.text.html if hasattr(message.text, 'html') else message.text, name) 118 | 119 | async def store(self, 120 | message: Optional['_message.Message'], 121 | caption: Optional[str] = '') -> int: 122 | """\nstore message to log channel. 123 | 124 | Parameters: 125 | message (`pyrogram.Message` | `None`): 126 | pass pyrogram.Message object which want to forward. 127 | 128 | caption (`str`, *optional*): 129 | Text or Caption of the message to be sent. 130 | 131 | Returns: 132 | message_id on success or None 133 | """ 134 | file_id = None 135 | caption = caption or '' 136 | if message: 137 | file_id = get_file_id_of_media(message) 138 | if not caption and message.caption: 139 | caption = message.caption.html 140 | if message and message.media and file_id: 141 | if caption: 142 | caption = self._string.format(caption.strip()) 143 | msg = await message.client.send_cached_media(chat_id=self._id, 144 | file_id=file_id, 145 | caption=caption) 146 | message_id = msg.id 147 | else: 148 | message_id = await self.log(caption) 149 | return message_id 150 | 151 | async def forward_stored(self, 152 | client: Union['_client.Userge', '_client.UsergeBot'], 153 | message_id: int, 154 | chat_id: int, 155 | user_id: int, 156 | reply_to_message_id: int, 157 | del_in: int = 0) -> None: 158 | """\nforward stored message from log channel. 159 | 160 | Parameters: 161 | client (`Userge` | `UsergeBot`): 162 | Pass Userge or UsergeBot. 163 | 164 | message_id (`int`): 165 | Message id of stored message. 166 | 167 | chat_id (`int`): 168 | ID of chat (dest) you want to farward. 169 | 170 | user_id (`int`): 171 | ID of user you want to reply. 172 | 173 | reply_to_message_id (`int`): 174 | If the message is a reply, ID of the original message. 175 | 176 | del_in (`int`): 177 | Time in Seconds for delete that message. 178 | 179 | Returns: 180 | None 181 | """ 182 | if not message_id or not isinstance(message_id, int): 183 | return 184 | message = await client.get_messages(chat_id=self._id, 185 | message_ids=message_id) 186 | caption = '' 187 | if message.caption: 188 | caption = message.caption.html.split('\n\n', maxsplit=1)[-1] 189 | elif message.text: 190 | caption = message.text.html.split('\n\n', maxsplit=1)[-1] 191 | if caption: 192 | u_dict = await client.get_user_dict(user_id) 193 | chat = await client.get_chat(chat_id) 194 | u_dict.update({ 195 | 'chat': chat.title if chat.title else "this group", 196 | 'count': chat.members_count}) 197 | caption = caption.format_map(SafeDict(**u_dict)) 198 | file_id = get_file_id_of_media(message) 199 | caption, buttons = parse_buttons(caption) 200 | try: 201 | if message.media and file_id: 202 | msg = await client.send_cached_media( 203 | chat_id=chat_id, 204 | file_id=file_id, 205 | caption=caption, 206 | reply_to_message_id=reply_to_message_id, 207 | reply_markup=buttons if client.is_bot and buttons else None) 208 | else: 209 | msg = await client.send_message( 210 | chat_id=chat_id, 211 | text=caption, 212 | reply_to_message_id=reply_to_message_id, 213 | disable_web_page_preview=True, 214 | reply_markup=buttons if client.is_bot and buttons else None) 215 | if del_in and msg: 216 | await asyncio.sleep(del_in) 217 | await msg.delete() 218 | except ChatWriteForbidden: 219 | pass 220 | -------------------------------------------------------------------------------- /userge/core/types/new/conversation.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['Conversation'] 12 | 13 | import time 14 | import asyncio 15 | import inspect 16 | from typing import Union, Dict, Tuple, Optional 17 | 18 | from pyrogram import filters as _filters 19 | from pyrogram.filters import Filter 20 | from pyrogram.types import Message as RawMessage 21 | from pyrogram.handlers import MessageHandler 22 | from pyrogram import enums 23 | 24 | from userge import logging 25 | from userge.utils.exceptions import StopConversation 26 | from ... import client as _client # pylint: disable=unused-import 27 | 28 | _LOG = logging.getLogger(__name__) 29 | 30 | _CL_TYPE = Union['_client.Userge', '_client.UsergeBot'] 31 | _CONV_DICT: Dict[Tuple[int, _CL_TYPE], Union[asyncio.Queue, Tuple[int, asyncio.Queue]]] = {} 32 | 33 | 34 | class _MsgLimitReached(Exception): 35 | pass 36 | 37 | 38 | class Conversation: 39 | """ Conversation class for userge """ 40 | def __init__(self, 41 | client: _CL_TYPE, 42 | chat: Union[str, int], 43 | user: Union[str, int], 44 | timeout: Union[int, float], 45 | limit: int) -> None: 46 | self._client = client 47 | self._chat = chat 48 | self._user = user 49 | self._timeout = timeout 50 | self._limit = limit 51 | self._chat_id: int 52 | self._user_id: int 53 | self._count = 0 54 | 55 | @property 56 | def chat_id(self) -> int: 57 | """ Returns chat_id """ 58 | return self._chat_id 59 | 60 | async def get_response(self, *, timeout: Union[int, float] = 0, 61 | mark_read: bool = False, 62 | filters: Filter = None) -> RawMessage: 63 | """\nGets the next message that responds to a previous one. 64 | 65 | Parameters: 66 | timeout (``int`` | ``float``, *optional*): 67 | Timeout for waiting. 68 | If present this will override conversation timeout 69 | 70 | mark_read (``bool``, *optional*): 71 | marks response as read. 72 | 73 | filters (``pyrogram.filters.Filter``, *optional*): 74 | filter specific response. 75 | 76 | Returns: 77 | On success, the received Message is returned. 78 | """ 79 | if self._count >= self._limit: 80 | raise _MsgLimitReached 81 | queue = _CONV_DICT[(self._chat_id, self._client)] 82 | if isinstance(queue, tuple): 83 | queue = queue[1] 84 | timeout = timeout or self._timeout 85 | start = time.time() 86 | while True: 87 | diff = time.time() - start 88 | response_ = await asyncio.wait_for(queue.get(), max(0.1, timeout - diff)) 89 | if filters and callable(filters): 90 | found = filters(self._client, response_) 91 | if inspect.iscoroutine(found): 92 | found = await found 93 | if not found: 94 | continue 95 | break 96 | self._count += 1 97 | if mark_read: 98 | await self.mark_read(response_) 99 | return response_ 100 | 101 | async def mark_read(self, message: Optional[RawMessage] = None) -> bool: 102 | """\nMarks message or chat as read. 103 | 104 | Parameters: 105 | message (:obj: `Message`, *optional*): 106 | single message. 107 | 108 | Returns: 109 | On success, True is returned. 110 | """ 111 | return bool( 112 | await self._client.send_read_acknowledge(chat_id=self._chat_id, message=message)) 113 | 114 | async def send_message(self, 115 | text: str, 116 | parse_mode: Optional[enums.ParseMode] = None) -> RawMessage: 117 | """\nSend text messages to the conversation. 118 | 119 | Parameters: 120 | text (``str``): 121 | Text of the message to be sent. 122 | parse_mode (:obj:`enums.ParseMode`, *optional*): 123 | parser to be used to parse text entities. 124 | 125 | Returns: 126 | :obj:`Message`: On success, the sent text message is returned. 127 | """ 128 | return await self._client.send_message(chat_id=self._chat_id, 129 | text=text, parse_mode=parse_mode) 130 | 131 | async def send_document(self, document: str) -> Optional[RawMessage]: 132 | """\nSend documents to the conversation. 133 | 134 | Parameters: 135 | document (``str``): 136 | File to send. 137 | Pass a file_id as string to send a file that 138 | exists on the Telegram servers, 139 | pass an HTTP URL as a string for Telegram 140 | to get a file from the Internet, or 141 | pass a file path as string to upload a new file 142 | that exists on your local machine. 143 | 144 | Returns: 145 | :obj:`Message` | ``None``: On success, the sent 146 | document message is returned, otherwise, in case the upload 147 | is deliberately stopped with 148 | :meth:`~Client.stop_transmission`, None is returned. 149 | """ 150 | return await self._client.send_document(chat_id=self._chat_id, document=document) 151 | 152 | async def forward_message(self, message: RawMessage) -> RawMessage: 153 | """\nForward message to the conversation. 154 | 155 | Parameters: 156 | message (:obj: `Message`): 157 | single message. 158 | 159 | Returns: 160 | On success, forwarded message is returned. 161 | """ 162 | return await self._client.forward_messages(chat_id=self._chat_id, 163 | from_chat_id=message.chat.id, 164 | message_ids=message.id) 165 | 166 | @staticmethod 167 | def init(client: _CL_TYPE) -> None: 168 | """ initialize the conversation method """ 169 | async def _on_conversation(_, msg: RawMessage) -> None: 170 | data = _CONV_DICT[(msg.chat.id, client)] 171 | if isinstance(data, asyncio.Queue): 172 | data.put_nowait(msg) 173 | elif msg.from_user and msg.from_user.id == data[0]: 174 | data[1].put_nowait(msg) 175 | msg.continue_propagation() 176 | client.add_handler( 177 | MessageHandler( 178 | _on_conversation, 179 | _filters.create( 180 | lambda _, __, query: _CONV_DICT and query.chat and ( 181 | query.chat.id, client 182 | ) in _CONV_DICT, "conversation" 183 | ) 184 | ) 185 | ) 186 | 187 | async def __aenter__(self) -> 'Conversation': 188 | self._chat_id = int(self._chat) if isinstance(self._chat, int) else \ 189 | (await self._client.get_chat(self._chat)).id 190 | pack = (self._chat_id, self._client) 191 | if pack in _CONV_DICT: 192 | error = f"already started conversation {self._client} with {self._chat_id} !" 193 | _LOG.error(error) 194 | raise StopConversation(error) 195 | if self._user: 196 | self._user_id = int(self._user) if isinstance(self._user, int) else \ 197 | (await self._client.get_users(self._user)).id 198 | _CONV_DICT[pack] = (self._user_id, asyncio.Queue(self._limit)) 199 | else: 200 | _CONV_DICT[pack] = asyncio.Queue(self._limit) 201 | return self 202 | 203 | async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: 204 | pack = (self._chat_id, self._client) 205 | queue = _CONV_DICT[pack] 206 | if isinstance(queue, tuple): 207 | queue = queue[1] 208 | queue.put_nowait(None) 209 | del _CONV_DICT[pack] 210 | error = '' 211 | if isinstance(exc_val, asyncio.exceptions.TimeoutError): 212 | error = (f"ended conversation {self._client} with {self._chat_id}, " 213 | "timeout reached!") 214 | if isinstance(exc_val, _MsgLimitReached): 215 | error = (f"ended conversation {self._client} with {self._chat_id}, " 216 | "message limit reached!") 217 | if error: 218 | _LOG.error(error) 219 | raise StopConversation(error) 220 | -------------------------------------------------------------------------------- /userge/core/types/new/manager.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['Manager'] 12 | 13 | import asyncio 14 | import logging 15 | from itertools import islice, chain 16 | from typing import Union, List, Dict, Optional 17 | 18 | from userge import config 19 | from ..raw import Filter, Command, Plugin 20 | from ... import client as _client, get_collection # pylint: disable=unused-import 21 | 22 | _LOG = logging.getLogger(__name__) 23 | _FLT = Union[Filter, Command] 24 | 25 | 26 | class Manager: 27 | """ manager for userge """ 28 | def __init__(self, client: '_client.Userge') -> None: 29 | self._client = client 30 | self._event = asyncio.Event() 31 | self.plugins: Dict[str, Plugin] = {} 32 | 33 | @property 34 | def commands(self) -> Dict[str, Command]: 35 | """ returns all commands """ 36 | return {cmd.name: cmd for plg in self.plugins.values() for cmd in plg.commands} 37 | 38 | @property 39 | def filters(self) -> Dict[str, Filter]: 40 | """ returns all filters """ 41 | return {flt.name: flt for plg in self.plugins.values() for flt in plg.filters} 42 | 43 | @property 44 | def loaded_commands(self) -> Dict[str, Command]: 45 | """ returns all loaded commands """ 46 | return {cmd.name: cmd for cmd in self.commands.values() if cmd.loaded} 47 | 48 | @property 49 | def unloaded_commands(self) -> List[Command]: 50 | """ returns all unloaded commands """ 51 | return [cmd for cmd in self.commands.values() if not cmd.loaded] 52 | 53 | @property 54 | def loaded_filters(self) -> List[Filter]: 55 | """returns all loaded filters""" 56 | return [flt for flt in self.filters.values() if flt.loaded] 57 | 58 | @property 59 | def unloaded_filters(self) -> List[Filter]: 60 | """ returns all unloaded filters """ 61 | return [flt for flt in self.filters.values() if not flt.loaded] 62 | 63 | @property 64 | def loaded_plugins(self) -> Dict[str, Plugin]: 65 | """ returns all loaded plugins """ 66 | return {plg.name: plg for plg in self.plugins.values() if plg.loaded} 67 | 68 | @property 69 | def unloaded_plugins(self) -> List[Plugin]: 70 | """returns all unloaded plugins""" 71 | return [plg for plg in self.plugins.values() if not plg.loaded] 72 | 73 | def _get_plugin(self, cat: str, name: str) -> Plugin: 74 | plg = self.plugins[name] = Plugin(self._client, cat, name) 75 | return plg 76 | 77 | def get_plugin(self, module_name: str) -> Plugin: 78 | """ get plugin from manager """ 79 | # __main__ 80 | name = module_name.split('.')[-2] 81 | if name in self.plugins: 82 | return self.plugins[name] 83 | 84 | cat = module_name.split('.')[-3] 85 | 86 | return self._get_plugin(cat, name) 87 | 88 | def update_plugin(self, module_name: str, doc: Optional[str]) -> None: 89 | """ get plugin from name """ 90 | # __init__ 91 | cat, name = module_name.split('.')[-2:] 92 | if name not in self.plugins: 93 | self._get_plugin(cat, name) 94 | 95 | if doc: 96 | self.plugins[name].doc = doc.strip() 97 | 98 | def get_plugins(self) -> Dict[str, List[str]]: 99 | """ returns categorized enabled plugins """ 100 | ret_dict: Dict[str, List[str]] = {} 101 | 102 | for plg in self.loaded_plugins.values(): 103 | if plg.cat not in ret_dict: 104 | ret_dict[plg.cat] = [] 105 | 106 | ret_dict[plg.cat].append(plg.name) 107 | 108 | return ret_dict 109 | 110 | def get_all_plugins(self) -> Dict[str, List[str]]: 111 | """ returns all categorized plugins """ 112 | ret_dict: Dict[str, List[str]] = {} 113 | 114 | for plg in self.plugins.values(): 115 | if plg.cat not in ret_dict: 116 | ret_dict[plg.cat] = [] 117 | 118 | ret_dict[plg.cat].append(plg.name) 119 | 120 | return ret_dict 121 | 122 | async def load_commands(self, commands: List[str]) -> List[str]: 123 | """ load list of commands """ 124 | loaded: List[str] = [] 125 | 126 | for cmd_name in set(commands).intersection(set(self.commands)): 127 | ret = self.commands[cmd_name].load() 128 | if ret: 129 | loaded.append(ret) 130 | 131 | if loaded: 132 | await _load(loaded) 133 | await self._update() 134 | 135 | return loaded 136 | 137 | async def unload_commands(self, commands: List[str]) -> List[str]: 138 | """ unload list of commands """ 139 | unloaded: List[str] = [] 140 | 141 | for cmd_name in set(commands).intersection(set(self.commands)): 142 | ret = self.commands[cmd_name].unload() 143 | if ret: 144 | unloaded.append(ret) 145 | 146 | if unloaded: 147 | await _unload(unloaded) 148 | await self._update() 149 | 150 | return unloaded 151 | 152 | async def load_filters(self, filters: List[str]) -> List[str]: 153 | """ load list of filters """ 154 | loaded: List[str] = [] 155 | 156 | for flt_name in set(filters).intersection(set(self.filters)): 157 | ret = self.filters[flt_name].load() 158 | if ret: 159 | loaded.append(ret) 160 | 161 | if loaded: 162 | await _load(loaded) 163 | await self._update() 164 | 165 | return loaded 166 | 167 | async def unload_filters(self, filters: List[str]) -> List[str]: 168 | """ unload list of filters """ 169 | unloaded: List[str] = [] 170 | 171 | for flt_name in set(filters).intersection(set(self.filters)): 172 | ret = self.filters[flt_name].unload() 173 | if ret: 174 | unloaded.append(ret) 175 | 176 | if unloaded: 177 | await _unload(unloaded) 178 | await self._update() 179 | 180 | return unloaded 181 | 182 | async def load_plugins(self, plugins: List[str]) -> Dict[str, List[str]]: 183 | """ load list of plugins """ 184 | loaded: Dict[str, List[str]] = {} 185 | 186 | for plg_name in set(plugins).intersection(set(self.plugins)): 187 | ret = await self.plugins[plg_name].load() 188 | if ret: 189 | loaded.update({plg_name: ret}) 190 | 191 | to_save = [_ for _ in loaded.values() for _ in _] 192 | if to_save: 193 | await _load(to_save) 194 | 195 | return loaded 196 | 197 | async def unload_plugins(self, plugins: List[str]) -> Dict[str, List[str]]: 198 | """ unload list of plugins """ 199 | unloaded: Dict[str, List[str]] = {} 200 | 201 | for plg_name in set(plugins).intersection(set(self.plugins)): 202 | ret = await self.plugins[plg_name].unload() 203 | if ret: 204 | unloaded.update({plg_name: ret}) 205 | 206 | to_save = [_ for _ in unloaded.values() for _ in _] 207 | if to_save: 208 | await _unload(to_save) 209 | 210 | return unloaded 211 | 212 | async def _update(self) -> None: 213 | for plg in self.plugins.values(): 214 | await plg.update() 215 | 216 | def remove(self, name) -> None: 217 | try: 218 | plg = self.plugins.pop(name) 219 | plg.clear() 220 | except KeyError: 221 | pass 222 | 223 | async def init(self) -> None: 224 | self._event.clear() 225 | await _init_unloaded() 226 | 227 | for plg in self.plugins.values(): 228 | for flt in plg.commands + plg.filters: 229 | if _unloaded(flt.name): 230 | continue 231 | 232 | flt.load() 233 | 234 | @property 235 | def should_wait(self) -> bool: 236 | return not self._event.is_set() 237 | 238 | async def wait(self) -> None: 239 | await self._event.wait() 240 | 241 | async def _do_plugins(self, meth: str) -> None: 242 | loop = asyncio.get_running_loop() 243 | data = iter(self.plugins.values()) 244 | 245 | while True: 246 | chunk = islice(data, config.WORKERS) 247 | 248 | try: 249 | plg = next(chunk) 250 | except StopIteration: 251 | break 252 | 253 | tasks = [] 254 | 255 | for plg in chain((plg,), chunk): 256 | tasks.append((plg, loop.create_task(getattr(plg, meth)()))) 257 | 258 | for plg, task in tasks: 259 | try: 260 | await task 261 | except Exception as i_e: 262 | _LOG.error(f"({meth}) [{plg.cat}/{plg.name}] - {i_e}") 263 | 264 | tasks.clear() 265 | 266 | _LOG.info(f"on_{meth} tasks completed !") 267 | 268 | async def start(self) -> None: 269 | self._event.clear() 270 | await self._do_plugins('start') 271 | self._event.set() 272 | 273 | async def stop(self) -> None: 274 | self._event.clear() 275 | await self._do_plugins('stop') 276 | self._event.set() 277 | 278 | async def exit(self) -> None: 279 | await self._do_plugins('exit') 280 | self.plugins.clear() 281 | 282 | def clear(self) -> None: 283 | for plg in self.plugins.values(): 284 | plg.clear() 285 | 286 | self.plugins.clear() 287 | 288 | @staticmethod 289 | async def clear_unloaded() -> bool: 290 | """ clear all unloaded filters in database """ 291 | return await _clear_db() 292 | 293 | 294 | _UNLOADED_FILTERS = get_collection("UNLOADED_FILTERS") 295 | _UNLOADED: List[str] = [] 296 | _FLAG = False 297 | 298 | 299 | async def _init_unloaded() -> None: 300 | global _FLAG # pylint: disable=global-statement 301 | 302 | if not _FLAG: 303 | async for flt in _UNLOADED_FILTERS.find(): 304 | _UNLOADED.append(flt['filter']) 305 | 306 | _FLAG = True 307 | 308 | 309 | def _unloaded(name: str) -> bool: 310 | return _fix(name) in _UNLOADED 311 | 312 | 313 | def _fix(name: str) -> str: 314 | return name.lstrip(config.CMD_TRIGGER) 315 | 316 | 317 | async def _load(names: List[str]) -> None: 318 | to_delete = [] 319 | 320 | for name in map(_fix, names): 321 | if name in _UNLOADED: 322 | _UNLOADED.remove(name) 323 | to_delete.append(name) 324 | 325 | if to_delete: 326 | await _UNLOADED_FILTERS.delete_many({'filter': {'$in': to_delete}}) 327 | 328 | 329 | async def _unload(names: List[str]) -> None: 330 | to_insert = [] 331 | 332 | for name in map(_fix, names): 333 | if name == "load": 334 | continue 335 | 336 | if name not in _UNLOADED: 337 | _UNLOADED.append(name) 338 | to_insert.append(name) 339 | 340 | if to_insert: 341 | await _UNLOADED_FILTERS.insert_many(map(lambda _: dict(filter=_), to_insert)) 342 | 343 | 344 | async def _clear_db() -> bool: 345 | _UNLOADED.clear() 346 | await _UNLOADED_FILTERS.drop() 347 | 348 | return True 349 | -------------------------------------------------------------------------------- /userge/core/types/raw/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | from .filter import Filter # noqa 12 | from .command import Command # noqa 13 | from .plugin import Plugin # noqa 14 | -------------------------------------------------------------------------------- /userge/core/types/raw/command.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['Command'] 12 | 13 | import re 14 | from typing import Union, Dict, List, Callable 15 | 16 | from pyrogram import filters, enums 17 | from pyrogram.types import Message 18 | 19 | from userge import config 20 | from userge.plugins.builtin import sudo 21 | from .filter import Filter 22 | from ... import client as _client # pylint: disable=unused-import 23 | 24 | 25 | class Command(Filter): 26 | """ command class """ 27 | def __init__(self, about: str, trigger: str, pattern: str, 28 | **kwargs: Union['_client.Userge', int, str, bool]) -> None: 29 | self.about = about 30 | self.trigger = trigger 31 | self.pattern = pattern 32 | super().__init__(**Filter._parse(**kwargs)) # pylint: disable=protected-access 33 | 34 | @classmethod 35 | def parse(cls, command: str, # pylint: disable=arguments-differ 36 | about: Union[str, Dict[str, Union[str, List[str], Dict[str, str]]]], 37 | trigger: str, name: str, filter_me: bool, 38 | **kwargs: Union['_client.Userge', int, bool]) -> 'Command': 39 | """ parse command """ 40 | pattern = '^' 41 | if trigger: 42 | pattern += f"(?:\\{trigger}|\\{config.SUDO_TRIGGER}|\\{config.CMD_TRIGGER})" 43 | pattern += command.lstrip('^') 44 | 45 | if _has_regex(command): 46 | if name: 47 | name = trigger + name 48 | else: 49 | match = re.match("(\\w[\\w_]*)", command) 50 | if match: 51 | name = trigger + match.groups()[0] 52 | else: 53 | if not name: 54 | name = trigger + command 55 | pattern += r"(?:\s([\S\s]+))?$" 56 | 57 | filters_ = filters.regex(pattern=pattern) 58 | if filter_me: 59 | filters_ &= _outgoing_flt(trigger, name) | _incoming_flt(trigger, name) 60 | else: 61 | filters_ &= _public_flt(trigger, name) 62 | 63 | return cls(_format_about(about), trigger, pattern, filters=filters_, name=name, **kwargs) 64 | 65 | def __repr__(self) -> str: 66 | return f"" 67 | 68 | 69 | def _has_regex(command: str) -> bool: 70 | return any(map(command.__contains__, '^()[]+*.\\|?:$')) 71 | 72 | 73 | def _outgoing_flt(trigger: str, name: str) -> filters.Filter: 74 | return _build_filter(_outgoing_logic, trigger, name) 75 | 76 | 77 | def _incoming_flt(trigger: str, name: str) -> filters.Filter: 78 | return _build_filter(_incoming_logic, trigger, name) 79 | 80 | 81 | def _public_flt(trigger: str, name: str) -> filters.Filter: 82 | return _build_filter(_public_logic, trigger, name) 83 | 84 | 85 | def _build_filter(logic: Callable[[Message, str, str], bool], 86 | trigger: str, name: str) -> filters.Filter: 87 | return filters.create( 88 | lambda _, __, m: 89 | m.via_bot is None and not m.scheduled 90 | and not (m.forward_from or m.forward_sender_name) 91 | and m.text and not m.edit_date and logic(m, trigger, name) 92 | ) 93 | 94 | 95 | def _outgoing_logic(m: Message, trigger: str, _) -> bool: 96 | return ( 97 | not (m.from_user and m.from_user.is_bot) 98 | and (m.outgoing or m.from_user and m.from_user.is_self) 99 | and not (m.chat and m.chat.type == enums.ChatType.CHANNEL and m.edit_date) 100 | and (m.text.startswith(trigger) if trigger else True) 101 | ) 102 | 103 | 104 | def _incoming_logic(m: Message, trigger: str, name: str) -> bool: 105 | return ( 106 | not m.outgoing and trigger and m.from_user 107 | and ( 108 | m.from_user.id in config.OWNER_ID or ( 109 | sudo.Dynamic.ENABLED and m.from_user.id in sudo.USERS 110 | and name.lstrip(trigger) in sudo.COMMANDS 111 | ) 112 | ) 113 | and m.text.startswith(config.SUDO_TRIGGER) 114 | ) 115 | 116 | 117 | def _public_logic(m: Message, trigger: str, _) -> bool: 118 | return ( 119 | True 120 | if not trigger 121 | else m.text.startswith(config.CMD_TRIGGER) 122 | if m.from_user and m.from_user.id in config.OWNER_ID 123 | else m.text.startswith(config.SUDO_TRIGGER) 124 | if sudo.Dynamic.ENABLED and m.from_user and m.from_user.id in sudo.USERS 125 | else m.text.startswith(trigger) 126 | ) 127 | 128 | 129 | def _format_about(about: Union[str, Dict[str, Union[str, List[str], Dict[str, str]]]]) -> str: 130 | if not isinstance(about, dict): 131 | return about 132 | 133 | tmp_chelp = '' 134 | 135 | if 'header' in about and isinstance(about['header'], str): 136 | tmp_chelp += f"{about['header'].capitalize()}" 137 | del about['header'] 138 | 139 | if 'description' in about and isinstance(about['description'], str): 140 | tmp_chelp += f"\n\n{about['description'].capitalize()}" 141 | del about['description'] 142 | 143 | if 'flags' in about: 144 | tmp_chelp += "\n\nflags:" 145 | 146 | if isinstance(about['flags'], dict): 147 | for f_n, f_d in about['flags'].items(): 148 | tmp_chelp += f"\n {f_n}: {f_d.lower()}" 149 | else: 150 | tmp_chelp += f"\n {about['flags']}" 151 | 152 | del about['flags'] 153 | 154 | if 'options' in about: 155 | tmp_chelp += "\n\noptions:" 156 | 157 | if isinstance(about['options'], dict): 158 | for o_n, o_d in about['options'].items(): 159 | tmp_chelp += f"\n {o_n}: {o_d.lower()}" 160 | else: 161 | tmp_chelp += f"\n {about['options']}" 162 | 163 | del about['options'] 164 | 165 | if 'types' in about: 166 | tmp_chelp += "\n\ntypes:\n" 167 | 168 | if isinstance(about['types'], list): 169 | for _opt in about['types']: 170 | tmp_chelp += f" {_opt}," 171 | else: 172 | tmp_chelp += f" {about['types']}" 173 | 174 | del about['types'] 175 | 176 | if 'usage' in about: 177 | tmp_chelp += f"\n\nusage:\n{about['usage']}" 178 | del about['usage'] 179 | 180 | if 'examples' in about: 181 | tmp_chelp += "\n\nexamples:" 182 | 183 | if isinstance(about['examples'], list): 184 | for ex_ in about['examples']: 185 | tmp_chelp += f"\n {ex_}" 186 | else: 187 | tmp_chelp += f"\n {about['examples']}" 188 | 189 | del about['examples'] 190 | 191 | if 'others' in about: 192 | tmp_chelp += f"\n\nothers:\n{about['others']}" 193 | del about['others'] 194 | 195 | if about: 196 | for t_n, t_d in about.items(): 197 | tmp_chelp += f"\n\n{t_n.lower()}:" 198 | 199 | if isinstance(t_d, dict): 200 | for o_n, o_d in t_d.items(): 201 | tmp_chelp += f"\n {o_n}: {o_d.lower()}" 202 | elif isinstance(t_d, list): 203 | tmp_chelp += '\n' 204 | for _opt in t_d: 205 | tmp_chelp += f" {_opt}," 206 | else: 207 | tmp_chelp += '\n' 208 | tmp_chelp += t_d 209 | 210 | return tmp_chelp.replace('{tr}', config.CMD_TRIGGER) 211 | -------------------------------------------------------------------------------- /userge/core/types/raw/filter.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['Filter'] 12 | 13 | from typing import List, Dict, Callable, Any, Optional, Union 14 | 15 | from pyrogram import enums 16 | from pyrogram.filters import Filter as RawFilter 17 | from pyrogram.handlers import MessageHandler 18 | from pyrogram.handlers.handler import Handler 19 | 20 | from userge import logging 21 | from ... import client as _client # pylint: disable=unused-import 22 | 23 | _LOG = logging.getLogger(__name__) 24 | 25 | 26 | class Filter: 27 | def __init__(self, 28 | filters: RawFilter, 29 | client: '_client.Userge', 30 | group: int, 31 | scope: List[enums.ChatType], 32 | only_admins: bool, 33 | allow_via_bot: bool, 34 | check_client: bool, 35 | check_downpath: bool, 36 | propagate: Optional[bool], 37 | check_perm: bool, 38 | check_change_info_perm: bool, 39 | check_edit_perm: bool, 40 | check_delete_perm: bool, 41 | check_restrict_perm: bool, 42 | check_promote_perm: bool, 43 | check_invite_perm: bool, 44 | check_pin_perm: bool, 45 | name: str = '') -> None: 46 | self.filters = filters 47 | self.name = name 48 | self.scope = scope 49 | self.only_admins = only_admins 50 | self.allow_via_bot = allow_via_bot 51 | self.check_client = check_client 52 | self.check_downpath = check_downpath 53 | self.propagate = propagate 54 | self.check_perm = check_perm 55 | self.check_change_info_perm = check_change_info_perm 56 | self.check_edit_perm = check_edit_perm 57 | self.check_delete_perm = check_delete_perm 58 | self.check_restrict_perm = check_restrict_perm 59 | self.check_promote_perm = check_promote_perm 60 | self.check_invite_perm = check_invite_perm 61 | self.check_pin_perm = check_pin_perm 62 | self.doc: Optional[str] = None 63 | self.plugin: Optional[str] = None 64 | self._loaded = False 65 | self._client = client 66 | self._group = group if group > -5 else -4 67 | self._func: Optional[Callable[[Any], Any]] = None 68 | self._handler: Optional[Handler] = None 69 | 70 | @classmethod 71 | def parse(cls, filters: RawFilter, **kwargs: Union['_client.Userge', int, bool]) -> 'Filter': 72 | """ parse filter """ 73 | # pylint: disable=protected-access 74 | return cls(**Filter._parse(filters=filters, **kwargs)) 75 | 76 | @staticmethod 77 | def _parse(allow_private: bool, 78 | allow_bots: bool, 79 | allow_groups: bool, 80 | allow_channels: bool, 81 | **kwargs: Union[RawFilter, '_client.Userge', int, bool] 82 | ) -> Dict[str, Union[RawFilter, '_client.Userge', int, bool]]: 83 | kwargs['check_client'] = kwargs['allow_via_bot'] and kwargs['check_client'] 84 | 85 | scope = [] 86 | if allow_bots: 87 | scope.append(enums.ChatType.BOT) 88 | if allow_private: 89 | scope.append(enums.ChatType.PRIVATE) 90 | if allow_channels: 91 | scope.append(enums.ChatType.CHANNEL) 92 | if allow_groups: 93 | scope += [enums.ChatType.GROUP, enums.ChatType.SUPERGROUP] 94 | kwargs['scope'] = scope 95 | 96 | kwargs['check_perm'] = kwargs['check_change_info_perm'] \ 97 | or kwargs['check_edit_perm'] or kwargs['check_delete_perm'] \ 98 | or kwargs['check_restrict_perm'] or kwargs['check_promote_perm'] \ 99 | or kwargs['check_invite_perm'] or kwargs['check_pin_perm'] 100 | 101 | return kwargs 102 | 103 | def __repr__(self) -> str: 104 | return f"" 105 | 106 | @property 107 | def loaded(self) -> bool: 108 | return self._loaded 109 | 110 | def update(self, func: Callable[[Any], Any], template: Callable[[Any], Any]) -> None: 111 | """ update filter """ 112 | self.doc = (func.__doc__ or "undefined").strip() 113 | self.plugin, _ = func.__module__.split('.')[-2:] 114 | 115 | if not self.name: 116 | self.name = '.'.join((self.plugin, func.__name__)) 117 | 118 | self._func = func 119 | self._handler = MessageHandler(template, self.filters) 120 | 121 | def load(self) -> str: 122 | """ load the filter """ 123 | if self._loaded or (self._client.is_bot and not self.allow_via_bot): 124 | return '' 125 | 126 | self._client.add_handler(self._handler, self._group) 127 | # pylint: disable=protected-access 128 | if self.allow_via_bot and self._client._bot is not None: 129 | self._client._bot.add_handler(self._handler, self._group) 130 | 131 | self._loaded = True 132 | return self.name 133 | 134 | def unload(self) -> str: 135 | """ unload the filter """ 136 | if not self._loaded: 137 | return '' 138 | 139 | self._client.remove_handler(self._handler, self._group) 140 | # pylint: disable=protected-access 141 | if self.allow_via_bot and self._client._bot is not None: 142 | self._client._bot.remove_handler(self._handler, self._group) 143 | 144 | self._loaded = False 145 | return self.name 146 | -------------------------------------------------------------------------------- /userge/core/types/raw/plugin.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['Plugin'] 12 | 13 | import asyncio 14 | from typing import Union, List, Optional, Callable, Awaitable, Any 15 | 16 | from userge import logging 17 | from . import command, filter as _filter # pylint: disable=unused-import 18 | from ... import client as _client # pylint: disable=unused-import 19 | 20 | _LOG = logging.getLogger(__name__) 21 | 22 | _LOADED = 0 23 | _UNLOADED = 1 24 | 25 | 26 | class Plugin: 27 | """ plugin class """ 28 | def __init__(self, client: '_client.Userge', cat: str, name: str) -> None: 29 | self._client = client 30 | self.cat = cat 31 | self.name = name 32 | self.doc: str = "undefined" 33 | self.commands: List['command.Command'] = [] 34 | self.filters: List['_filter.Filter'] = [] 35 | 36 | self._state = _UNLOADED 37 | 38 | self._tasks_todo: List[Callable[[], Awaitable[Any]]] = [] 39 | self._running_tasks: List[asyncio.Task] = [] 40 | 41 | self._on_start_callback: Optional[Callable[[], Awaitable[Any]]] = None 42 | self._on_stop_callback: Optional[Callable[[], Awaitable[Any]]] = None 43 | self._on_exit_callback: Optional[Callable[[], Awaitable[Any]]] = None 44 | 45 | def __repr__(self) -> str: 46 | return f"" 47 | 48 | @property 49 | def loaded(self) -> bool: 50 | """ returns load status """ 51 | return any((flt.loaded for flt in self.commands + self.filters)) 52 | 53 | @property 54 | def loaded_commands(self) -> List['command.Command']: 55 | """ returns all loaded commands """ 56 | return [cmd for cmd in self.commands if cmd.loaded] 57 | 58 | @property 59 | def unloaded_commands(self) -> List['command.Command']: 60 | """ returns all unloaded commands """ 61 | return [cmd for cmd in self.commands if not cmd.loaded] 62 | 63 | @property 64 | def loaded_filters(self) -> List['_filter.Filter']: 65 | """ returns all loaded filters """ 66 | return [flt for flt in self.filters if flt.loaded] 67 | 68 | @property 69 | def unloaded_filters(self) -> List['_filter.Filter']: 70 | """ returns all unloaded filters """ 71 | return [flt for flt in self.filters if not flt.loaded] 72 | 73 | def add(self, obj: Union['command.Command', '_filter.Filter']) -> None: 74 | """ add command or filter to plugin """ 75 | if isinstance(obj, command.Command): 76 | type_ = self.commands 77 | else: 78 | type_ = self.filters 79 | 80 | for flt in type_: 81 | if flt.name == obj.name: 82 | type_.remove(flt) 83 | break 84 | 85 | type_.append(obj) 86 | 87 | def get_commands(self) -> List[str]: 88 | """ returns all sorted command names in the plugin """ 89 | return sorted((cmd.name for cmd in self.loaded_commands)) 90 | 91 | async def _start(self) -> None: 92 | if self._on_start_callback: 93 | await self._on_start_callback() 94 | 95 | self._running_tasks.clear() 96 | 97 | for todo in self._tasks_todo: 98 | self._running_tasks.append(asyncio.create_task(todo())) 99 | 100 | self._state = _LOADED 101 | 102 | async def _stop(self) -> None: 103 | for task in self._running_tasks: 104 | task.cancel() 105 | 106 | self._running_tasks.clear() 107 | 108 | if self._on_stop_callback: 109 | await self._on_stop_callback() 110 | 111 | self._state = _UNLOADED 112 | 113 | async def start(self) -> None: 114 | if self.loaded: 115 | await self._start() 116 | 117 | async def stop(self) -> None: 118 | if self._state == _LOADED: 119 | await self._stop() 120 | 121 | async def update(self) -> None: 122 | if self.loaded: 123 | if self._state == _UNLOADED: 124 | await self._start() 125 | else: 126 | if self._state == _LOADED: 127 | await self._stop() 128 | 129 | def clear(self) -> None: 130 | if self._state == _LOADED: 131 | raise AssertionError 132 | 133 | self.commands.clear() 134 | self.filters.clear() 135 | self._tasks_todo.clear() 136 | 137 | self._on_start_callback = None 138 | self._on_stop_callback = None 139 | self._on_exit_callback = None 140 | 141 | async def exit(self) -> None: 142 | if self._state == _LOADED: 143 | raise AssertionError 144 | 145 | if self._on_exit_callback: 146 | await self._on_exit_callback() 147 | 148 | self.clear() 149 | 150 | async def load(self) -> List[str]: 151 | """ load all commands in the plugin """ 152 | if self._state == _LOADED: 153 | return [] 154 | 155 | await self._start() 156 | return _do_it(self, 'load') 157 | 158 | async def unload(self) -> List[str]: 159 | """ unload all commands in the plugin """ 160 | if self._state == _UNLOADED: 161 | return [] 162 | 163 | out = _do_it(self, 'unload') 164 | await self._stop() 165 | 166 | return out 167 | 168 | def add_task(self, task: Callable[[], Awaitable[Any]]) -> None: 169 | self._tasks_todo.append(task) 170 | 171 | def set_on_start_callback(self, callback: Callable[[], Awaitable[Any]]) -> None: 172 | self._on_start_callback = callback 173 | 174 | def set_on_stop_callback(self, callback: Callable[[], Awaitable[Any]]) -> None: 175 | self._on_stop_callback = callback 176 | 177 | def set_on_exit_callback(self, callback: Callable[[], Awaitable[Any]]) -> None: 178 | self._on_exit_callback = callback 179 | 180 | 181 | def _do_it(plg: Plugin, work_type: str) -> List[str]: 182 | done: List[str] = [] 183 | 184 | for flt in plg.commands + plg.filters: 185 | tmp = getattr(flt, work_type)() 186 | if tmp: 187 | done.append(tmp) 188 | 189 | if done: 190 | _LOG.info(f"{work_type.rstrip('e')}ed {plg}") 191 | 192 | return done 193 | -------------------------------------------------------------------------------- /userge/logger.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | __all__ = ['logging'] 12 | 13 | import logging 14 | from logging.handlers import RotatingFileHandler 15 | 16 | logging.basicConfig(level=logging.INFO, 17 | format='[%(asctime)s - %(levelname)s] - %(name)s - %(message)s', 18 | datefmt='%d-%b-%y %H:%M:%S', 19 | handlers=[ 20 | RotatingFileHandler( 21 | "logs/userge.log", maxBytes=20480, backupCount=10), 22 | logging.StreamHandler() 23 | ]) 24 | 25 | logging.getLogger("pyrogram").setLevel(logging.WARNING) 26 | logging.getLogger("pyrogram.parser.html").setLevel(logging.ERROR) 27 | logging.getLogger("pyrogram.session.session").setLevel(logging.ERROR) 28 | -------------------------------------------------------------------------------- /userge/main.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | from pyrogram import StopPropagation 12 | from pyrogram.raw.base import Message 13 | from pyrogram.raw.types import MessageService, MessageActionContactSignUp 14 | 15 | from userge import userge 16 | 17 | 18 | @userge.on_raw_update(-5) 19 | async def _on_raw(_, m: Message, *__) -> None: 20 | if isinstance(m, MessageService) and isinstance(m.action, MessageActionContactSignUp): 21 | raise StopPropagation 22 | -------------------------------------------------------------------------------- /userge/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | -------------------------------------------------------------------------------- /userge/plugins/builtin/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | -------------------------------------------------------------------------------- /userge/plugins/builtin/executor/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 2 | # 3 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 4 | # and is released under the "GNU v3.0 License Agreement". 5 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 6 | # 7 | # All rights reserved. 8 | 9 | """executor services""" 10 | -------------------------------------------------------------------------------- /userge/plugins/builtin/executor/__main__.py: -------------------------------------------------------------------------------- 1 | """ run shell or python command(s) """ 2 | 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | import asyncio 12 | import io 13 | import keyword 14 | import os 15 | import re 16 | import shlex 17 | import sys 18 | import threading 19 | import traceback 20 | from contextlib import contextmanager 21 | from enum import Enum 22 | from getpass import getuser 23 | from shutil import which 24 | from typing import Awaitable, Any, Callable, Dict, Optional, Tuple, Iterable 25 | 26 | import aiofiles 27 | 28 | try: 29 | from os import geteuid, setsid, getpgid, killpg 30 | from signal import SIGKILL 31 | except ImportError: 32 | # pylint: disable=ungrouped-imports 33 | from os import kill as killpg 34 | # pylint: disable=ungrouped-imports 35 | from signal import CTRL_C_EVENT as SIGKILL 36 | 37 | def geteuid() -> int: 38 | return 1 39 | 40 | def getpgid(arg: Any) -> Any: 41 | return arg 42 | 43 | setsid = None 44 | 45 | from pyrogram.types.messages_and_media.message import Str 46 | from pyrogram import enums 47 | 48 | from userge import userge, Message, config, pool 49 | from userge.utils import runcmd 50 | 51 | CHANNEL = userge.getCLogger() 52 | 53 | 54 | def input_checker(func: Callable[[Message], Awaitable[Any]]): 55 | async def wrapper(message: Message) -> None: 56 | replied = message.reply_to_message 57 | 58 | if not message.input_str: 59 | if (func.__name__ == "eval_" 60 | and replied and replied.document 61 | and replied.document.file_name.endswith(('.txt', '.py')) 62 | and replied.document.file_size <= 2097152): 63 | 64 | dl_loc = await replied.download() 65 | async with aiofiles.open(dl_loc) as jv: 66 | message.text += " " + await jv.read() 67 | os.remove(dl_loc) 68 | message.flags.update({'file': True}) 69 | else: 70 | await message.err("No Command Found!") 71 | return 72 | 73 | cmd = message.input_str 74 | 75 | if "config.env" in cmd: 76 | await message.edit("`That's a dangerous operation! Not Permitted!`") 77 | return 78 | await func(message) 79 | return wrapper 80 | 81 | 82 | @userge.on_cmd("exec", about={ 83 | 'header': "run commands in exec", 84 | 'flags': {'-r': "raw text when send as file"}, 85 | 'usage': "{tr}exec [commands]", 86 | 'examples': "{tr}exec echo \"Userge\""}, allow_channels=False) 87 | @input_checker 88 | async def exec_(message: Message): 89 | """ run commands in exec """ 90 | await message.edit("`Executing exec ...`") 91 | cmd = message.filtered_input_str 92 | as_raw = '-r' in message.flags 93 | 94 | try: 95 | out, err, ret, pid = await runcmd(cmd) 96 | except Exception as t_e: # pylint: disable=broad-except 97 | await message.err(str(t_e)) 98 | return 99 | 100 | output = f"**EXEC**:\n\n\ 101 | __Command:__\n`{cmd}`\n__PID:__\n`{pid}`\n__RETURN:__\n`{ret}`\n\n\ 102 | **stderr:**\n`{err or 'no error'}`\n\n**stdout:**\n``{out or 'no output'}`` " 103 | await message.edit_or_send_as_file(text=output, 104 | as_raw=as_raw, 105 | parse_mode=enums.ParseMode.MARKDOWN, 106 | filename="exec.txt", 107 | caption=cmd) 108 | 109 | 110 | _KEY = '_OLD' 111 | _EVAL_TASKS: Dict[asyncio.Future, str] = {} 112 | 113 | 114 | @userge.on_cmd("eval", about={ 115 | 'header': "run python code line | lines", 116 | 'flags': { 117 | '-r': "raw text when send as file", 118 | '-s': "silent mode (hide input code)", 119 | '-p': "run in a private session", 120 | '-n': "spawn new main session and run", 121 | '-l': "list all running eval tasks", 122 | '-c': "cancel specific running eval task", 123 | '-ca': "cancel all running eval tasks" 124 | }, 125 | 'usage': "{tr}eval [flag] [code lines OR reply to .txt | .py file]", 126 | 'examples': [ 127 | "{tr}eval print('Userge')", "{tr}eval -s print('Userge')", 128 | "{tr}eval 5 + 6", "{tr}eval -s 5 + 6", 129 | "{tr}eval -p x = 'private_value'", "{tr}eval -n y = 'new_value'", 130 | "{tr}eval -c2", "{tr}eval -ca", "{tr}eval -l"]}, allow_channels=False) 131 | @input_checker 132 | async def eval_(message: Message): 133 | """ run python code """ 134 | for t in tuple(_EVAL_TASKS): 135 | if t.done(): 136 | del _EVAL_TASKS[t] 137 | 138 | flags = message.flags 139 | 140 | if flags: 141 | if '-l' in flags: 142 | if _EVAL_TASKS: 143 | out = "**Eval Tasks**\n\n" 144 | i = 0 145 | for c in _EVAL_TASKS.values(): 146 | out += f"**{i}** - `{c}`\n" 147 | i += 1 148 | out += f"\nuse `{config.CMD_TRIGGER}eval -c[id]` to Cancel" 149 | await message.edit(out) 150 | else: 151 | await message.edit("No running eval tasks !", del_in=5) 152 | return 153 | 154 | size = len(_EVAL_TASKS) 155 | 156 | if ('-c' in flags or '-ca' in flags) and size == 0: 157 | await message.edit("No running eval tasks !", del_in=5) 158 | return 159 | 160 | if '-ca' in flags: 161 | for t in _EVAL_TASKS: 162 | t.cancel() 163 | await message.edit(f"Canceled all running eval tasks [{size}] !", del_in=5) 164 | return 165 | 166 | if '-c' in flags: 167 | t_id = int(flags.get('-c', -1)) 168 | if t_id < 0 or t_id >= size: 169 | await message.edit(f"Invalid eval task id [{t_id}] !", del_in=5) 170 | return 171 | tuple(_EVAL_TASKS)[t_id].cancel() 172 | await message.edit(f"Canceled eval task [{t_id}] !", del_in=5) 173 | return 174 | 175 | cmd = message.filtered_input_str 176 | if not cmd: 177 | await message.err("Unable to Parse Input!") 178 | return 179 | 180 | msg = message 181 | replied = message.reply_to_message 182 | if (replied and replied.from_user 183 | and replied.from_user.is_self and isinstance(replied.text, Str) 184 | and str(replied.text.html).startswith(">
")):
185 |         msg = replied
186 | 
187 |     await msg.edit("`Executing eval ...`", parse_mode=enums.ParseMode.MARKDOWN)
188 | 
189 |     is_file = replied and replied.document and flags.get("file")
190 |     as_raw = '-r' in flags
191 |     silent_mode = '-s' in flags
192 |     if '-n' in flags:
193 |         context_type = _ContextType.NEW
194 |     elif '-p' in flags:
195 |         context_type = _ContextType.PRIVATE
196 |     else:
197 |         context_type = _ContextType.GLOBAL
198 | 
199 |     async def _callback(output: Optional[str], errored: bool):
200 |         final = ""
201 |         if not silent_mode:
202 |             final += "**>**" + (replied.link if is_file else f"```python\n{cmd}```") + "\n\n"
203 |         if isinstance(output, str):
204 |             output = output.strip()
205 |             if output == '':
206 |                 output = None
207 |         if output is not None:
208 |             final += f"**>>** ```python\n{output}```"
209 |         if errored and message.chat.type in (
210 |                 enums.ChatType.GROUP,
211 |                 enums.ChatType.SUPERGROUP,
212 |                 enums.ChatType.CHANNEL):
213 |             msg_id = await CHANNEL.log(final)
214 |             await msg.edit(f"**Logs**: {CHANNEL.get_link(msg_id)}")
215 |         elif final:
216 |             await msg.edit_or_send_as_file(text=final,
217 |                                            as_raw=as_raw,
218 |                                            parse_mode=enums.ParseMode.MARKDOWN,
219 |                                            disable_web_page_preview=True,
220 |                                            filename="eval.txt",
221 |                                            caption=cmd)
222 |         else:
223 |             await msg.delete()
224 | 
225 |     _g, _l = _context(context_type, userge=userge,
226 |                       message=message, replied=message.reply_to_message)
227 |     l_d = {}
228 |     try:
229 |         # nosec pylint: disable=W0122
230 |         exec(_wrap_code(cmd, _l.keys()), _g, l_d)
231 |     except Exception:  # pylint: disable=broad-except
232 |         _g[_KEY] = _l
233 |         await _callback(traceback.format_exc(), True)
234 |         return
235 | 
236 |     future = asyncio.get_running_loop().create_future()
237 |     pool.submit_thread(
238 |         _run_coro,
239 |         future,
240 |         l_d['__aexec'](
241 |             *_l.values()),
242 |         _callback)
243 |     hint = cmd.split('\n')[0]
244 |     _EVAL_TASKS[future] = hint[:25] + "..." if len(hint) > 25 else hint
245 | 
246 |     with msg.cancel_callback(future.cancel):
247 |         try:
248 |             await future
249 |         except asyncio.CancelledError:
250 |             await asyncio.gather(msg.canceled(),
251 |                                  CHANNEL.log(f"**EVAL Process Canceled!**\n\n```python\n{cmd}```"))
252 |         finally:
253 |             _EVAL_TASKS.pop(future, None)
254 | 
255 | 
256 | @userge.on_cmd("term", about={
257 |     'header': "run commands in shell (terminal)",
258 |     'flags': {'-r': "raw text when send as file"},
259 |     'usage': "{tr}term [commands]",
260 |     'examples': "{tr}term echo \"Userge\""}, allow_channels=False)
261 | @input_checker
262 | async def term_(message: Message):
263 |     """ run commands in shell (terminal with live update) """
264 |     await message.edit("`Executing terminal ...`")
265 |     cmd = message.filtered_input_str
266 |     as_raw = '-r' in message.flags
267 | 
268 |     try:
269 |         parsed_cmd = parse_py_template(cmd, message)
270 |     except Exception as e:  # pylint: disable=broad-except
271 |         await message.err(str(e))
272 |         await CHANNEL.log(f"**Exception**: {type(e).__name__}\n**Message**: " + str(e))
273 |         return
274 |     try:
275 |         t_obj = await Term.execute(parsed_cmd)  # type: Term
276 |     except Exception as t_e:  # pylint: disable=broad-except
277 |         await message.err(str(t_e))
278 |         return
279 | 
280 |     cur_user = getuser()
281 |     uid = geteuid()
282 | 
283 |     prefix = f"{cur_user}:~#" if uid == 0 else f"{cur_user}:~$"
284 |     output = f"{prefix} 
{cmd}
\n" 285 | 286 | with message.cancel_callback(t_obj.cancel): 287 | await t_obj.init() 288 | while not t_obj.finished: 289 | await message.edit(f"{output}
{t_obj.line}
", parse_mode=enums.ParseMode.HTML) 290 | await t_obj.wait(config.Dynamic.EDIT_SLEEP_TIMEOUT) 291 | if t_obj.cancelled: 292 | await message.canceled(reply=True) 293 | return 294 | 295 | out_data = f"{output}
{t_obj.output}
\n{prefix}" 296 | await message.edit_or_send_as_file( 297 | out_data, as_raw=as_raw, parse_mode=enums.ParseMode.HTML, filename="term.txt", caption=cmd) 298 | 299 | 300 | def parse_py_template(cmd: str, msg: Message): 301 | glo, loc = _context(_ContextType.PRIVATE, message=msg, 302 | replied=msg.reply_to_message) 303 | 304 | def replacer(mobj): 305 | # nosec pylint: disable=W0123 306 | return shlex.quote(str(eval(mobj.expand(r"\1"), glo, loc))) 307 | return re.sub(r"{{(.+?)}}", replacer, cmd) 308 | 309 | 310 | class _ContextType(Enum): 311 | GLOBAL = 0 312 | PRIVATE = 1 313 | NEW = 2 314 | 315 | 316 | def _context(context_type: _ContextType, **kwargs) -> Tuple[Dict[str, Any], Dict[str, Any]]: 317 | if context_type == _ContextType.NEW: 318 | try: 319 | del globals()[_KEY] 320 | except KeyError: 321 | pass 322 | if _KEY not in globals(): 323 | globals()[_KEY] = globals().copy() 324 | _g = globals()[_KEY] 325 | if context_type == _ContextType.PRIVATE: 326 | _g = _g.copy() 327 | _l = _g.pop(_KEY, {}) 328 | _l.update(kwargs) 329 | return _g, _l 330 | 331 | 332 | def _wrap_code(code: str, args: Iterable[str]) -> str: 333 | head = "async def __aexec(" + ', '.join(args) + "):\n try:\n " 334 | tail = "\n finally: globals()['" + _KEY + "'] = locals()" 335 | if '\n' in code: 336 | code = code.replace('\n', '\n ') 337 | elif (any(True for k_ in keyword.kwlist if k_ not in ( 338 | 'True', 'False', 'None', 'lambda', 'await') and code.startswith(f"{k_} ")) 339 | or ('=' in code and '==' not in code)): 340 | code = f"\n {code}" 341 | else: 342 | code = f"\n return {code}" 343 | return head + code + tail 344 | 345 | 346 | def _run_coro(future: asyncio.Future, coro: Awaitable[Any], 347 | callback: Callable[[str, bool], Awaitable[Any]]) -> None: 348 | loop = asyncio.new_event_loop() 349 | task = loop.create_task(coro) 350 | userge.loop.call_soon_threadsafe( 351 | future.add_done_callback, lambda _: ( 352 | loop.is_running() and future.cancelled() and loop.call_soon_threadsafe( 353 | task.cancel))) 354 | try: 355 | ret, exc = None, None 356 | with redirect() as out: 357 | try: 358 | ret = loop.run_until_complete(task) 359 | except asyncio.CancelledError: 360 | return 361 | except Exception: # pylint: disable=broad-except 362 | exc = traceback.format_exc().strip() 363 | output = exc or out.getvalue() 364 | if ret is not None: 365 | output += str(ret) 366 | loop.run_until_complete(callback(output, exc is not None)) 367 | finally: 368 | loop.run_until_complete(loop.shutdown_asyncgens()) 369 | loop.close() 370 | userge.loop.call_soon_threadsafe( 371 | lambda: future.done() or future.set_result(None)) 372 | 373 | 374 | _PROXIES = {} 375 | 376 | 377 | class _Wrapper: 378 | def __init__(self, original): 379 | self._original = original 380 | 381 | def __getattr__(self, name: str): 382 | return getattr( 383 | _PROXIES.get( 384 | threading.current_thread().ident, 385 | self._original), 386 | name) 387 | 388 | 389 | sys.stdout = _Wrapper(sys.stdout) 390 | sys.__stdout__ = _Wrapper(sys.__stdout__) 391 | sys.stderr = _Wrapper(sys.stderr) 392 | sys.__stderr__ = _Wrapper(sys.__stderr__) 393 | 394 | 395 | @contextmanager 396 | def redirect() -> io.StringIO: 397 | ident = threading.current_thread().ident 398 | source = io.StringIO() 399 | _PROXIES[ident] = source 400 | try: 401 | yield source 402 | finally: 403 | del _PROXIES[ident] 404 | source.close() 405 | 406 | 407 | class Term: 408 | """ live update term class """ 409 | 410 | def __init__(self, process: asyncio.subprocess.Process) -> None: 411 | self._process = process 412 | self._line = b'' 413 | self._output = b'' 414 | self._init = asyncio.Event() 415 | self._is_init = False 416 | self._cancelled = False 417 | self._finished = False 418 | self._loop = asyncio.get_running_loop() 419 | self._listener = self._loop.create_future() 420 | 421 | @property 422 | def line(self) -> str: 423 | return self._by_to_str(self._line) 424 | 425 | @property 426 | def output(self) -> str: 427 | return self._by_to_str(self._output) 428 | 429 | @staticmethod 430 | def _by_to_str(data: bytes) -> str: 431 | return data.decode('utf-8', 'replace').strip() 432 | 433 | @property 434 | def cancelled(self) -> bool: 435 | return self._cancelled 436 | 437 | @property 438 | def finished(self) -> bool: 439 | return self._finished 440 | 441 | async def init(self) -> None: 442 | await self._init.wait() 443 | 444 | async def wait(self, timeout: int) -> None: 445 | self._check_listener() 446 | try: 447 | await asyncio.wait_for(self._listener, timeout) 448 | except asyncio.TimeoutError: 449 | pass 450 | 451 | def _check_listener(self) -> None: 452 | if self._listener.done(): 453 | self._listener = self._loop.create_future() 454 | 455 | def cancel(self) -> None: 456 | if self._cancelled or self._finished: 457 | return 458 | killpg(getpgid(self._process.pid), SIGKILL) 459 | self._cancelled = True 460 | 461 | @classmethod 462 | async def execute(cls, cmd: str) -> 'Term': 463 | kwargs = dict( 464 | stdout=asyncio.subprocess.PIPE, 465 | stderr=asyncio.subprocess.PIPE) 466 | if setsid: 467 | kwargs['preexec_fn'] = setsid 468 | if sh := which(os.environ.get("USERGE_SHELL", "bash")): 469 | kwargs['executable'] = sh 470 | process = await asyncio.create_subprocess_shell(cmd, **kwargs) 471 | t_obj = cls(process) 472 | t_obj._start() 473 | return t_obj 474 | 475 | def _start(self) -> None: 476 | self._loop.create_task(self._worker()) 477 | 478 | async def _worker(self) -> None: 479 | if self._cancelled or self._finished: 480 | return 481 | await asyncio.wait([self._read_stdout(), self._read_stderr()]) 482 | await self._process.wait() 483 | self._finish() 484 | 485 | async def _read_stdout(self) -> None: 486 | await self._read(self._process.stdout) 487 | 488 | async def _read_stderr(self) -> None: 489 | await self._read(self._process.stderr) 490 | 491 | async def _read(self, reader: asyncio.StreamReader) -> None: 492 | while True: 493 | line = await reader.readline() 494 | if not line: 495 | break 496 | self._append(line) 497 | 498 | def _append(self, line: bytes) -> None: 499 | self._line = line 500 | self._output += line 501 | self._check_init() 502 | 503 | def _check_init(self) -> None: 504 | if self._is_init: 505 | return 506 | self._loop.call_later(1, self._init.set) 507 | self._is_init = True 508 | 509 | def _finish(self) -> None: 510 | if self._finished: 511 | return 512 | self._init.set() 513 | self._finished = True 514 | if not self._listener.done(): 515 | self._listener.set_result(None) 516 | -------------------------------------------------------------------------------- /userge/plugins/builtin/help/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 2 | # 3 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 4 | # and is released under the "GNU v3.0 License Agreement". 5 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 6 | # 7 | # All rights reserved. 8 | 9 | """docs of all commands""" 10 | -------------------------------------------------------------------------------- /userge/plugins/builtin/loader/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 2 | # 3 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 4 | # and is released under the "GNU v3.0 License Agreement". 5 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 6 | # 7 | # All rights reserved. 8 | 9 | """high lvl interface for low lvl loader""" 10 | -------------------------------------------------------------------------------- /userge/plugins/builtin/manage/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 2 | # 3 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 4 | # and is released under the "GNU v3.0 License Agreement". 5 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 6 | # 7 | # All rights reserved. 8 | 9 | """manage commands and plugins dynamically""" 10 | -------------------------------------------------------------------------------- /userge/plugins/builtin/manage/__main__.py: -------------------------------------------------------------------------------- 1 | """ manage your userge :) """ 2 | 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | from userge import userge, Message, config 12 | 13 | 14 | @userge.on_cmd("status", about={ 15 | 'header': "list plugins, commands, filters status", 16 | 'flags': { 17 | '-p': "plugin", 18 | '-c': "command", 19 | '-f': "filter"}, 20 | 'usage': "{tr}status [flags] [name]", 21 | 'examples': [ 22 | "{tr}status", "{tr}status -p", 23 | "{tr}status -p gdrive", "{tr}status -c {tr}gls"]}, del_pre=True, allow_channels=False) 24 | async def status(message: Message) -> None: 25 | """ view current status """ 26 | name_ = message.filtered_input_str 27 | type_ = list(message.flags) 28 | if not type_: 29 | out_str = f"""📊 **--Userge Status--** 📊 30 | 31 | 🗃 **Plugins** : `{len(userge.manager.plugins)}` 32 | ✅ **Loaded** : `{len(userge.manager.loaded_plugins)}` 33 | ❎ **Unloaded** : `{len(userge.manager.unloaded_plugins)}` 34 | 35 | ⚔ **Commands** : `{len(userge.manager.commands)}` 36 | ✅ **Loaded** : `{len(userge.manager.loaded_commands)}` 37 | ❎ **Unloaded** : `{len(userge.manager.unloaded_commands)}` 38 | 39 | ⚖ **Filters** : `{len(userge.manager.filters)}` 40 | ✅ **Loaded** : `{len(userge.manager.loaded_filters)}` 41 | ❎ **Unloaded** : `{len(userge.manager.unloaded_filters)}` 42 | """ 43 | elif 'p' in type_: 44 | if name_: 45 | if name_ in userge.manager.plugins: 46 | plg = userge.manager.plugins[name_] 47 | out_str = f"""🗃 **--Plugin Status--** 🗃 48 | 49 | 🔖 **Name** : `{plg.name}` 50 | 📝 **Doc** : `{plg.doc}` 51 | ✅ **Loaded** : `{plg.is_loaded}` 52 | 53 | ⚔ **Commands** : `{len(plg.commands)}` 54 | `{'`, `'.join((cmd.name for cmd in plg.commands))}` 55 | ✅ **Loaded** : `{len(plg.loaded_commands)}` 56 | ❎ **Unloaded** : `{len(plg.unloaded_commands)}` 57 | `{'`, `'.join((cmd.name for cmd in plg.unloaded_commands))}` 58 | 59 | ⚖ **Filters** : `{len(plg.filters)}` 60 | ✅ **Loaded** : `{len(plg.loaded_filters)}` 61 | ❎ **Unloaded** : `{len(plg.unloaded_filters)}` 62 | `{'`, `'.join((flt.name for flt in plg.unloaded_filters))}` 63 | """ 64 | else: 65 | await message.err(f"plugin : `{name_}` not found!") 66 | return 67 | else: 68 | out_str = f"""🗃 **--Plugins Status--** 🗃 69 | 70 | 🗂 **Total** : `{len(userge.manager.plugins)}` 71 | ✅ **Loaded** : `{len(userge.manager.loaded_plugins)}` 72 | ❎ **Unloaded** : `{len(userge.manager.unloaded_plugins)}` 73 | `{'`, `'.join((cmd.name for cmd in userge.manager.unloaded_plugins))}` 74 | """ 75 | elif 'c' in type_: 76 | if name_: 77 | if not name_.startswith(config.CMD_TRIGGER): 78 | n_name_ = config.CMD_TRIGGER + name_ 79 | if name_ in userge.manager.commands: 80 | cmd = userge.manager.commands[name_] 81 | elif n_name_ in userge.manager.commands: 82 | cmd = userge.manager.commands[n_name_] 83 | else: 84 | await message.err(f"command : {name_} not found!") 85 | return 86 | out_str = f"""⚔ **--Command Status--** ⚔ 87 | 88 | 🔖 **Name** : `{cmd.name}` 89 | 📝 **Doc** : `{cmd.doc}` 90 | 🤖 **Via Bot** : `{cmd.allow_via_bot}` 91 | ✅ **Loaded** : `{cmd.loaded}` 92 | """ 93 | else: 94 | out_str = f"""⚔ **--Commands Status--** ⚔ 95 | 96 | 🗂 **Total** : `{len(userge.manager.commands)}` 97 | ✅ **Loaded** : `{len(userge.manager.loaded_commands)}` 98 | ❎ **Unloaded** : `{len(userge.manager.unloaded_commands)}` 99 | `{'`, `'.join((cmd.name for cmd in userge.manager.unloaded_commands))}` 100 | """ 101 | elif 'f' in type_: 102 | if name_: 103 | if name_ in userge.manager.filters: 104 | flt = userge.manager.filters[name_] 105 | out_str = f"""⚖ **--Filter Status--** ⚖ 106 | 107 | 🔖 **Name** : `{flt.name}` 108 | 📝 **Doc** : `{flt.doc}` 109 | 🤖 **Via Bot** : `{flt.allow_via_bot}` 110 | ✅ **Loaded** : `{flt.loaded}` 111 | """ 112 | else: 113 | await message.err(f"filter : {name_} not found!") 114 | return 115 | else: 116 | out_str = f"""⚖ **--Filters Status--** ⚖ 117 | 118 | 🗂 **Total** : `{len(userge.manager.filters)}` 119 | ✅ **Loaded** : `{len(userge.manager.loaded_filters)}` 120 | ❎ **Unloaded** : `{len(userge.manager.unloaded_filters)}` 121 | `{'`, `'.join((flt.name for flt in userge.manager.unloaded_filters))}` 122 | """ 123 | else: 124 | await message.err("invalid input flag!") 125 | return 126 | await message.edit(out_str.replace(" ``\n", ''), del_in=0) 127 | 128 | 129 | @userge.on_cmd('load', about={ 130 | 'header': "load plugins, commands, filters", 131 | 'flags': { 132 | '-p': "plugin", 133 | '-c': "command", 134 | '-f': "filter"}, 135 | 'usage': "{tr}load [flags] [name | names]", 136 | 'examples': [ 137 | "{tr}load -p gdrive", "{tr}load -c gls gup"]}, del_pre=True, allow_channels=False) 138 | async def load(message: Message) -> None: 139 | """ load plugins, commands, filters """ 140 | if not message.filtered_input_str: 141 | await message.err("name required!") 142 | return 143 | await message.edit("`Loading...`") 144 | names_ = message.filtered_input_str.split(' ') 145 | type_ = list(message.flags) 146 | if 'p' in type_: 147 | found = set(names_).intersection(set(userge.manager.plugins)) 148 | if found: 149 | out = await userge.manager.load_plugins(list(found)) 150 | if out: 151 | out_str = "**--Loaded Plugin(s)--**\n\n" 152 | for plg_name, cmds in out.items(): 153 | out_str += f"**{plg_name}** : `{'`, `'.join(cmds)}`\n" 154 | else: 155 | out_str = f"already loaded! : `{'`, `'.join(names_)}`" 156 | else: 157 | await message.err(f"plugins : {', '.join(names_)} not found!") 158 | return 159 | elif 'c' in type_: 160 | for t_name in names_: 161 | if not t_name.startswith(config.CMD_TRIGGER): 162 | names_.append(config.CMD_TRIGGER + t_name) 163 | found = set(names_).intersection(set(userge.manager.commands)) 164 | if found: 165 | out = await userge.manager.load_commands(list(found)) 166 | if out: 167 | out_str = "**--Loaded Command(s)--**\n\n" 168 | out_str += f"`{'`, `'.join(out)}`" 169 | else: 170 | out_str = f"already loaded! : `{'`, `'.join(names_)}`" 171 | else: 172 | await message.err(f"commands : {', '.join(names_)} not found!") 173 | return 174 | elif 'f' in type_: 175 | found = set(names_).intersection(set(userge.manager.filters)) 176 | if found: 177 | out = await userge.manager.load_filters(list(found)) 178 | if out: 179 | out_str = "**--Loaded Filter(s)--**\n\n" 180 | out_str += f"`{'`, `'.join(out)}`" 181 | else: 182 | out_str = f"already loaded! : `{'`, `'.join(names_)}`" 183 | else: 184 | await message.err(f"filters : {', '.join(names_)} not found!") 185 | return 186 | else: 187 | await message.err("invalid input flag!") 188 | return 189 | await message.edit(out_str, del_in=0, log=True) 190 | 191 | 192 | @userge.on_cmd('unload', about={ 193 | 'header': "unload plugins, commands, filters", 194 | 'flags': { 195 | '-p': "plugin", 196 | '-c': "command", 197 | '-f': "filter"}, 198 | 'usage': "{tr}unload [flags] [name | names]", 199 | 'examples': [ 200 | "{tr}unload -p gdrive", "{tr}unload -c gls gup"]}, del_pre=True, allow_channels=False) 201 | async def unload(message: Message) -> None: 202 | """ unload plugins, commands, filters """ 203 | if not message.flags: 204 | await message.err("flag required!") 205 | return 206 | if not message.filtered_input_str: 207 | await message.err("name required!") 208 | return 209 | await message.edit("`UnLoading...`") 210 | names_ = message.filtered_input_str.split(' ') 211 | type_ = list(message.flags) 212 | if 'p' in type_ and names_: 213 | found = set(names_).intersection(set(userge.manager.plugins)) 214 | if found: 215 | out = await userge.manager.unload_plugins(list(found)) 216 | if out: 217 | out_str = "**--Unloaded Plugin(s)--**\n\n" 218 | for plg_name, cmds in out.items(): 219 | out_str += f"**{plg_name}** : `{'`, `'.join(cmds)}`\n" 220 | else: 221 | out_str = f"already unloaded! : `{'`, `'.join(names_)}`" 222 | else: 223 | await message.err(f"plugins : {', '.join(names_)} not found!") 224 | return 225 | elif 'c' in type_ and names_: 226 | for t_name in names_: 227 | if not t_name.startswith(config.CMD_TRIGGER): 228 | names_.append(config.CMD_TRIGGER + t_name) 229 | found = set(names_).intersection(set(userge.manager.commands)) 230 | if found: 231 | out = await userge.manager.unload_commands(list(found)) 232 | if out: 233 | out_str = "**--Unloaded Command(s)--**\n\n" 234 | out_str += f"`{'`, `'.join(out)}`" 235 | else: 236 | out_str = f"already unloaded! : `{'`, `'.join(names_)}`" 237 | else: 238 | await message.err(f"commands : {', '.join(names_)} not found!") 239 | return 240 | elif 'f' in type_ and names_: 241 | found = set(names_).intersection(set(userge.manager.filters)) 242 | if found: 243 | out = await userge.manager.unload_filters(list(found)) 244 | if out: 245 | out_str = "**--Unloaded Filter(s)--**\n\n" 246 | out_str += f"`{'`, `'.join(out)}`" 247 | else: 248 | out_str = f"already unloaded! : `{'`, `'.join(names_)}`" 249 | else: 250 | await message.err(f"filters : {', '.join(names_)} not found!") 251 | return 252 | else: 253 | await message.err("invalid input flag!") 254 | return 255 | await message.edit(out_str, del_in=0, log=True) 256 | 257 | 258 | @userge.on_cmd('reload', about={'header': "Reload all plugins"}, allow_channels=False) 259 | async def reload_(message: Message) -> None: 260 | """ Reload all plugins """ 261 | await message.edit("`Reloading All Plugins`") 262 | await message.edit( 263 | f"`Reloaded {await userge.reload_plugins()} Plugins`", del_in=3, log=True) 264 | 265 | 266 | @userge.on_cmd('clear_unloaded', about={'header': "clear saved unloaded data in DB"}, 267 | allow_channels=False) 268 | async def clear_(message: Message) -> None: 269 | """ clear all save filters in DB """ 270 | await message.edit("`Clearing Unloaded Data...`") 271 | await message.edit( 272 | f"**Cleared Unloaded** : `{await userge.manager.clear_unloaded()}`", 273 | del_in=3, log=True) 274 | -------------------------------------------------------------------------------- /userge/plugins/builtin/sudo/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 2 | # 3 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 4 | # and is released under the "GNU v3.0 License Agreement". 5 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 6 | # 7 | # All rights reserved. 8 | 9 | """manage sudo cmds and users""" 10 | 11 | 12 | from typing import Set 13 | 14 | USERS: Set[int] = set() 15 | COMMANDS: Set[str] = set() 16 | 17 | 18 | class Dynamic: 19 | ENABLED = False 20 | -------------------------------------------------------------------------------- /userge/plugins/builtin/sudo/__main__.py: -------------------------------------------------------------------------------- 1 | """ setup sudos """ 2 | 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | import asyncio 12 | 13 | from pyrogram.errors.exceptions.bad_request_400 import PeerIdInvalid 14 | 15 | from userge import userge, Message, config, get_collection 16 | from .. import sudo 17 | 18 | SAVED_SETTINGS = get_collection("CONFIGS") 19 | SUDO_USERS_COLLECTION = get_collection("sudo_users") 20 | SUDO_CMDS_COLLECTION = get_collection("sudo_cmds") 21 | 22 | 23 | @userge.on_start 24 | async def _init() -> None: 25 | s_o = await SAVED_SETTINGS.find_one({'_id': 'SUDO_ENABLED'}) 26 | if s_o: 27 | sudo.Dynamic.ENABLED = s_o['data'] 28 | async for i in SUDO_USERS_COLLECTION.find(): 29 | sudo.USERS.add(i['_id']) 30 | async for i in SUDO_CMDS_COLLECTION.find(): 31 | sudo.COMMANDS.add(i['_id']) 32 | 33 | 34 | @userge.on_cmd("sudo", about={'header': "enable / disable sudo access"}, allow_channels=False) 35 | async def sudo_(message: Message): 36 | """ enable / disable sudo access """ 37 | if sudo.Dynamic.ENABLED: 38 | sudo.Dynamic.ENABLED = False 39 | await message.edit("`sudo disabled !`", del_in=3) 40 | else: 41 | sudo.Dynamic.ENABLED = True 42 | await message.edit("`sudo enabled !`", del_in=3) 43 | await SAVED_SETTINGS.update_one( 44 | {'_id': 'SUDO_ENABLED'}, {"$set": {'data': sudo.Dynamic.ENABLED}}, upsert=True) 45 | 46 | 47 | @userge.on_cmd("addsudo", about={ 48 | 'header': "add sudo user", 49 | 'usage': "{tr}addsudo [username | reply to msg]"}, allow_channels=False) 50 | async def add_sudo(message: Message): 51 | """ add sudo user """ 52 | user_id = message.input_str 53 | if message.reply_to_message: 54 | user_id = message.reply_to_message.from_user.id 55 | if not user_id: 56 | await message.err(f'user: `{user_id}` not found!') 57 | return 58 | if isinstance(user_id, str) and user_id.isdigit(): 59 | user_id = int(user_id) 60 | try: 61 | user = await message.client.get_user_dict(user_id) 62 | except PeerIdInvalid as p_e: 63 | await message.err(p_e) 64 | return 65 | if user['id'] in sudo.USERS: 66 | await message.edit(f"user : `{user['id']}` already in **SUDO**!", del_in=5) 67 | else: 68 | sudo.USERS.add(user['id']) 69 | await asyncio.gather( 70 | SUDO_USERS_COLLECTION.insert_one({'_id': user['id'], 'men': user['mention']}), 71 | message.edit(f"user : `{user['id']}` added to **SUDO**!", del_in=5, log=__name__)) 72 | 73 | 74 | @userge.on_cmd("delsudo", about={ 75 | 'header': "delete sudo user", 76 | 'flags': {'-all': "remove all sudo users"}, 77 | 'usage': "{tr}delsudo [user_id | reply to msg]\n{tr}delsudo -all"}, allow_channels=False) 78 | async def del_sudo(message: Message): 79 | """ delete sudo user """ 80 | if '-all' in message.flags: 81 | sudo.USERS.clear() 82 | await asyncio.gather( 83 | SUDO_USERS_COLLECTION.drop(), 84 | message.edit("**SUDO** users cleared!", del_in=5)) 85 | return 86 | user_id = message.filtered_input_str 87 | if message.reply_to_message: 88 | user_id = message.reply_to_message.from_user.id 89 | if not user_id: 90 | await message.err(f'user: `{user_id}` not found!') 91 | return 92 | if isinstance(user_id, str) and user_id.isdigit(): 93 | user_id = int(user_id) 94 | if not isinstance(user_id, int): 95 | await message.err('invalid type!') 96 | return 97 | if user_id not in sudo.USERS: 98 | await message.edit(f"user : `{user_id}` not in **SUDO**!", del_in=5) 99 | else: 100 | sudo.USERS.remove(user_id) 101 | await asyncio.gather( 102 | SUDO_USERS_COLLECTION.delete_one({'_id': user_id}), 103 | message.edit(f"user : `{user_id}` removed from **SUDO**!", del_in=5, log=__name__)) 104 | 105 | 106 | @userge.on_cmd("vsudo", about={'header': "view sudo users"}, allow_channels=False) 107 | async def view_sudo(message: Message): 108 | """ view sudo users """ 109 | if not sudo.USERS: 110 | await message.edit("**SUDO** users not found!", del_in=5) 111 | return 112 | out_str = '🚷 **SUDO USERS** 🚷\n\n' 113 | async for user in SUDO_USERS_COLLECTION.find(): 114 | out_str += f" 🙋‍♂️ {user['men']} 🆔 `{user['_id']}`\n" 115 | await message.edit(out_str, del_in=0) 116 | 117 | 118 | @userge.on_cmd("addscmd", about={ 119 | 'header': "add sudo command", 120 | 'flags': {'-all': "add all commands to sudo"}, 121 | 'usage': "{tr}addscmd [command name]\n{tr}addscmd -all"}, allow_channels=False) 122 | async def add_sudo_cmd(message: Message): 123 | """ add sudo cmd """ 124 | if '-all' in message.flags: 125 | await SUDO_CMDS_COLLECTION.drop() 126 | sudo.COMMANDS.clear() 127 | tmp_ = [] 128 | restricted = ('addsudo', 'addscmd', 'exec', 'eval', 'term', 'load', 'unload') 129 | for c_d in list(userge.manager.loaded_commands): 130 | t_c = c_d.lstrip(config.CMD_TRIGGER) 131 | if t_c in restricted: 132 | continue 133 | tmp_.append({'_id': t_c}) 134 | sudo.COMMANDS.add(t_c) 135 | await asyncio.gather( 136 | SUDO_CMDS_COLLECTION.insert_many(tmp_), 137 | message.edit(f"**Added** all (`{len(tmp_)}`) commands to **SUDO** cmds!", 138 | del_in=5, log=__name__)) 139 | return 140 | cmd = message.input_str 141 | if not cmd: 142 | await message.err('input not found!') 143 | return 144 | cmd = cmd.lstrip(config.CMD_TRIGGER) 145 | if cmd in sudo.COMMANDS: 146 | await message.edit(f"cmd : `{cmd}` already in **SUDO**!", del_in=5) 147 | elif cmd not in (c_d.lstrip(config.CMD_TRIGGER) 148 | for c_d in list(userge.manager.loaded_commands)): 149 | await message.edit(f"cmd : `{cmd}` 🤔, is that a command ?", del_in=5) 150 | else: 151 | sudo.COMMANDS.add(cmd) 152 | await asyncio.gather( 153 | SUDO_CMDS_COLLECTION.insert_one({'_id': cmd}), 154 | message.edit(f"cmd : `{cmd}` added to **SUDO**!", del_in=5, log=__name__)) 155 | 156 | 157 | @userge.on_cmd("delscmd", about={ 158 | 'header': "delete sudo commands", 159 | 'flags': {'-all': "remove all sudo commands"}, 160 | 'usage': "{tr}delscmd [command names separated by space]\n{tr}delscmd -all"}, 161 | allow_channels=False) 162 | async def del_sudo_cmd(message: Message): 163 | """ delete sudo cmd """ 164 | if '-all' in message.flags: 165 | sudo.COMMANDS.clear() 166 | await asyncio.gather( 167 | SUDO_CMDS_COLLECTION.drop(), 168 | message.edit("**SUDO** cmds cleared!", del_in=5)) 169 | return 170 | cmd = message.filtered_input_str 171 | if not cmd: 172 | await message.err('input not found!') 173 | return 174 | NOT_IN_SUDO = [] 175 | IS_REMOVED = [] 176 | for ncmd in cmd.split(" "): 177 | if ncmd not in sudo.COMMANDS: 178 | NOT_IN_SUDO.append(ncmd) 179 | else: 180 | sudo.COMMANDS.remove(ncmd) 181 | IS_REMOVED.append(ncmd) 182 | if IS_REMOVED: 183 | await SUDO_CMDS_COLLECTION.delete_many({'_id': {'$in': IS_REMOVED}}) 184 | await message.edit( 185 | f"cmds : `{' '.join(IS_REMOVED)}` removed from **SUDO**!", 186 | del_in=5, log=__name__) 187 | if NOT_IN_SUDO and not IS_REMOVED: 188 | await message.edit( 189 | f"cmds : `{' '.join(NOT_IN_SUDO)}` not in **SUDO**!", del_in=5) 190 | elif NOT_IN_SUDO: 191 | await message.reply_text( 192 | f"cmds : `{' '.join(NOT_IN_SUDO)}` not in **SUDO**!", del_in=5) 193 | 194 | 195 | @userge.on_cmd("vscmd", about={'header': "view sudo cmds"}, allow_channels=False) 196 | async def view_sudo_cmd(message: Message): 197 | """ view sudo cmds """ 198 | if not sudo.COMMANDS: 199 | await message.edit("**SUDO** cmds not found!", del_in=5) 200 | return 201 | out_str = f"⛔ **SUDO CMDS** ⛔\n\n**trigger** : `{config.SUDO_TRIGGER}`\n\n" 202 | async for cmd in SUDO_CMDS_COLLECTION.find().sort('_id'): 203 | out_str += f"`{cmd['_id']}` " 204 | await message.edit_or_send_as_file(out_str, del_in=0) 205 | -------------------------------------------------------------------------------- /userge/plugins/builtin/system/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 2 | # 3 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 4 | # and is released under the "GNU v3.0 License Agreement". 5 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 6 | # 7 | # All rights reserved. 8 | 9 | """system related commands""" 10 | 11 | 12 | from os import environ, getpid, kill 13 | from typing import Set, Optional 14 | try: 15 | from signal import CTRL_C_EVENT as SIGTERM 16 | except ImportError: 17 | from signal import SIGTERM 18 | 19 | from loader.userge import api 20 | from userge import config 21 | 22 | DISABLED_CHATS: Set[int] = set() 23 | 24 | 25 | class Dynamic: 26 | DISABLED_ALL = False 27 | 28 | RUN_DYNO_SAVER = False 29 | STATUS = None 30 | 31 | 32 | def get_env(key: str) -> Optional[str]: 33 | return environ.get(key) 34 | 35 | 36 | async def set_env(key: str, value: str) -> None: 37 | environ[key] = value 38 | await api.set_env(key, value) 39 | 40 | if config.HEROKU_APP: 41 | config.HEROKU_APP.config()[key] = value 42 | 43 | 44 | async def del_env(key: str) -> Optional[str]: 45 | if key in environ: 46 | val = environ.pop(key) 47 | await api.unset_env(key) 48 | 49 | if config.HEROKU_APP: 50 | del config.HEROKU_APP.config()[key] 51 | 52 | return val 53 | 54 | 55 | def shutdown() -> None: 56 | if config.HEROKU_APP: 57 | config.HEROKU_APP.process_formation()['worker'].scale(0) 58 | 59 | kill(getpid(), SIGTERM) 60 | -------------------------------------------------------------------------------- /userge/plugins/builtin/tools/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 2 | # 3 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 4 | # and is released under the "GNU v3.0 License Agreement". 5 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 6 | # 7 | # All rights reserved. 8 | 9 | """useful tools""" 10 | -------------------------------------------------------------------------------- /userge/plugins/builtin/tools/__main__.py: -------------------------------------------------------------------------------- 1 | """ set or view your timeouts """ 2 | 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | from datetime import datetime 12 | 13 | from pyrogram.raw.functions import Ping 14 | 15 | from userge import userge, Message, logging, config, pool, get_collection 16 | 17 | SAVED_SETTINGS = get_collection("CONFIGS") 18 | 19 | 20 | @userge.on_start 21 | async def _init() -> None: 22 | msg_t = await SAVED_SETTINGS.find_one({'_id': 'MSG_DELETE_TIMEOUT'}) 23 | if msg_t: 24 | config.Dynamic.MSG_DELETE_TIMEOUT = msg_t['data'] 25 | es_t = await SAVED_SETTINGS.find_one({'_id': 'EDIT_SLEEP_TIMEOUT'}) 26 | if es_t: 27 | config.Dynamic.EDIT_SLEEP_TIMEOUT = es_t['data'] 28 | 29 | 30 | @userge.on_cmd("sdelto (\\d+)", about={ 31 | 'header': "Set auto message delete timeout", 32 | 'usage': "{tr}sdelto [timeout in seconds]", 33 | 'examples': "{tr}sdelto 15\n{tr}sdelto 0 : for disable deletion"}) 34 | async def set_delete_timeout(message: Message): 35 | """ set delete timeout """ 36 | await message.edit("`Setting auto message delete timeout...`") 37 | 38 | t_o = int(message.matches[0].group(1)) 39 | config.Dynamic.MSG_DELETE_TIMEOUT = t_o 40 | 41 | await SAVED_SETTINGS.update_one( 42 | {'_id': 'MSG_DELETE_TIMEOUT'}, {"$set": {'data': t_o}}, upsert=True) 43 | 44 | if t_o: 45 | await message.edit( 46 | f"`Set auto message delete timeout as {t_o} seconds!`", del_in=3) 47 | else: 48 | await message.edit("`Auto message deletion disabled!`", del_in=3) 49 | 50 | 51 | @userge.on_cmd("vdelto", about={'header': "View auto message delete timeout"}) 52 | async def view_delete_timeout(message: Message): 53 | """ view delete timeout """ 54 | if config.Dynamic.MSG_DELETE_TIMEOUT: 55 | await message.edit( 56 | f"`Messages will be deleted after {config.Dynamic.MSG_DELETE_TIMEOUT} seconds!`", 57 | del_in=5) 58 | else: 59 | await message.edit("`Auto message deletion disabled!`", del_in=3) 60 | 61 | 62 | @userge.on_cmd("sesto (\\d+)", about={ 63 | 'header': "Set edit sleep timeout", 64 | 'usage': "{tr}sesto [timeout in seconds]", 65 | 'examples': "{tr}sesto 10"}) 66 | async def set_es_timeout(message: Message): 67 | """ set edit sleep timeout """ 68 | t_o = int(message.matches[0].group(1)) 69 | if t_o < 5: 70 | await message.err("too short! (min = 5sec)") 71 | return 72 | 73 | await message.edit("`Setting edit sleep timeout...`") 74 | config.Dynamic.EDIT_SLEEP_TIMEOUT = t_o 75 | 76 | await SAVED_SETTINGS.update_one( 77 | {'_id': 'EDIT_SLEEP_TIMEOUT'}, {"$set": {'data': t_o}}, upsert=True) 78 | 79 | await message.edit( 80 | f"`Set edit sleep timeout as {t_o} seconds!`", del_in=3) 81 | 82 | 83 | @userge.on_cmd("vesto", about={'header': "View edit sleep timeout"}) 84 | async def view_es_timeout(message: Message): 85 | """ view edit sleep timeout """ 86 | await message.edit( 87 | f"`Current edit sleep timeout = {config.Dynamic.EDIT_SLEEP_TIMEOUT} seconds!`", 88 | del_in=5) 89 | 90 | 91 | @userge.on_cmd("cancel", about={ 92 | 'header': "Reply this to message you want to cancel", 93 | 'flags': {'-a': "cancel all tasks"}}) 94 | async def cancel_(message: Message): 95 | """ cancel bg tasks """ 96 | if '-a' in message.flags: 97 | ret = Message._call_all_cancel_callbacks() # pylint: disable=protected-access 98 | if ret == 0: 99 | await message.err("nothing found to cancel", show_help=False) 100 | return 101 | 102 | replied = message.reply_to_message # type: Message 103 | if replied: 104 | if not replied._call_cancel_callbacks(): # pylint: disable=protected-access 105 | await message.err("nothing found to cancel", show_help=False) 106 | else: 107 | await message.err("source not provided !", show_help=False) 108 | 109 | 110 | @userge.on_cmd("json", about={ 111 | 'header': "message object to json", 112 | 'usage': "reply {tr}json to any message"}) 113 | async def jsonify(message: Message): 114 | """ msg to json """ 115 | msg = str(message.reply_to_message) if message.reply_to_message else str(message) 116 | await message.edit_or_send_as_file(text=msg, filename="json.txt", caption="Too Large") 117 | 118 | 119 | @userge.on_cmd("ping", about={ 120 | 'header': "check how long it takes to ping your userbot"}, group=-1) 121 | async def pingme(message: Message): 122 | """ ping tg servers """ 123 | start = datetime.now() 124 | await message.client.invoke(Ping(ping_id=0)) 125 | end = datetime.now() 126 | 127 | m_s = (end - start).microseconds / 1000 128 | await message.edit(f"**Pong!**\n`{m_s} ms`") 129 | 130 | 131 | @userge.on_cmd("s", about={ 132 | 'header': "search commands or plugins", 133 | 'flags': {'-p': "search plugin"}, 134 | 'examples': ["{tr}s wel", "{tr}s -p gdrive"]}, allow_channels=False) 135 | async def search(message: Message): 136 | """ search commands """ 137 | key = message.filtered_input_str 138 | if not key: 139 | await message.err("input not found") 140 | return 141 | 142 | plugins = '-p' in message.flags 143 | data = list(userge.manager.loaded_plugins if plugins else userge.manager.loaded_commands) 144 | 145 | found = [i for i in sorted(data) if key in i] 146 | out_str = ' '.join(found) 147 | 148 | if found: 149 | out = f"**--found {len(found)} {'plugin' if plugins else 'command'}(s) for-- : `{key}`**" 150 | out += f"\n\n`{out_str}`" 151 | else: 152 | out = f"__nothing found for__ : `{key}`" 153 | 154 | await message.edit(text=out, del_in=0) 155 | 156 | 157 | @userge.on_cmd("logs", about={ 158 | 'header': "check userge logs", 159 | 'flags': { 160 | '-h': "get heroku logs (default limit 100)", 161 | '-l': "get loader logs"}, 162 | 'examples': [ 163 | "{tr}logs", "{tr}logs -h", "{tr}logs -h200", "{tr}logs -l"] 164 | }, allow_channels=False) 165 | async def check_logs(message: Message): 166 | """ check logs """ 167 | await message.edit("`checking logs ...`") 168 | 169 | if '-h' in message.flags and config.HEROKU_APP: 170 | limit = int(message.flags.get('-h') or 100) 171 | logs = await pool.run_in_thread(config.HEROKU_APP.get_log)(lines=limit) 172 | 173 | await message.client.send_as_file(chat_id=message.chat.id, 174 | text=logs, 175 | filename='userge-heroku.log', 176 | caption=f'userge-heroku.log [ {limit} lines ]') 177 | else: 178 | filename = f"{'loader' if '-l' in message.flags else 'userge'}.log" 179 | await message.client.send_document(chat_id=message.chat.id, 180 | document=f"logs/{filename}", 181 | caption=filename) 182 | await message.delete() 183 | 184 | 185 | _LEVELS = { 186 | 'debug': logging.DEBUG, 187 | 'info': logging.INFO, 188 | 'warning': logging.WARNING, 189 | 'error': logging.ERROR, 190 | 'critical': logging.CRITICAL 191 | } 192 | 193 | 194 | @userge.on_cmd("setlvl", about={ 195 | 'header': "set logger level, default to info", 196 | 'types': ['debug', 'info', 'warning', 'error', 'critical'], 197 | 'usage': "{tr}setlvl [level]", 198 | 'examples': ["{tr}setlvl info", "{tr}setlvl debug"]}) 199 | async def set_level(message: Message): 200 | """ set logger level """ 201 | await message.edit("`setting logger level ...`") 202 | level = message.input_str.lower() 203 | 204 | if level not in _LEVELS: 205 | await message.err("unknown level !") 206 | return 207 | 208 | for logger in (logging.getLogger(name) for name in logging.root.manager.loggerDict): 209 | logger.setLevel(_LEVELS[level]) 210 | 211 | await message.edit(f"`successfully set logger level as` : **{level.upper()}**", del_in=3) 212 | -------------------------------------------------------------------------------- /userge/sys_tools.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | # 11 | # noqa 12 | # skipcq 13 | 14 | import sys 15 | from os import environ 16 | from typing import Dict, Optional 17 | 18 | 19 | _CACHE: Dict[str, str] = {} 20 | 21 | 22 | def secured_env(key: str, default: Optional[str] = None) -> Optional[str]: 23 | """ get secured env """ 24 | if not key: 25 | raise ValueError 26 | 27 | try: 28 | value = environ.pop(key) 29 | except KeyError: 30 | if key in _CACHE: 31 | return _CACHE[key] 32 | value = default 33 | 34 | ret: Optional[str] = None 35 | 36 | if value: 37 | ret = _CACHE[key] = secured_str(value) 38 | 39 | return ret 40 | 41 | 42 | def secured_str(value: str) -> str: 43 | """ get secured string """ 44 | if not value: 45 | raise ValueError 46 | 47 | if isinstance(value, _SafeStr): 48 | return value 49 | 50 | ret = _SafeStr(_ST) 51 | ret._ = value 52 | 53 | return ret 54 | 55 | 56 | class SafeDict(Dict[str, str]): 57 | """ modded dict """ 58 | def __missing__(self, key: str) -> str: 59 | return '{' + key + '}' 60 | 61 | 62 | class _SafeMeta(type): 63 | def __new__(mcs, *__): 64 | for _ in filter(lambda _: ( 65 | _.startswith('_') and _.__ne__('__new__') 66 | and not __[2].__contains__(_)), __[1][0].__dict__): 67 | __[2][_] = lambda _, *__, ___=_: _._.__getattribute__(___)(*__) 68 | return type.__new__(mcs, *__) 69 | 70 | 71 | class _SafeStr(str, metaclass=_SafeMeta): 72 | def __setattr__(self, *_): 73 | if _[0].__eq__('_') and not hasattr(self, '_'): 74 | super().__setattr__(*_) 75 | 76 | def __delattr__(self, _): 77 | pass 78 | 79 | def __getattribute__(self, _): 80 | ___ = lambda _, __=_: _.__getattribute__(__) if __.__ne__('_') else _ 81 | _ = getattr(sys, '_getframe')(1) 82 | while _: 83 | _f, _n = _.f_code.co_filename, _.f_code.co_name 84 | if _f.__contains__("exec") or _f.__eq__("") and _n.__ne__(""): 85 | return ___(_ST) 86 | if _f.__contains__("asyncio") and _n.__eq__("_run"): 87 | __ = getattr(getattr(_.f_locals['self'], '_callback').__self__, '_coro').cr_frame 88 | _f, _n = __.f_code.co_filename, __.f_code.co_name 89 | if (_f.__contains__("dispatcher") and _n.__eq__("handler_worker") or 90 | (_f.__contains__("client") or _f.__contains__("plugin")) and 91 | ("start", "stop").__contains__(_n)): 92 | break 93 | return ___(_ST) 94 | _ = _.f_back 95 | return ___(super().__getattribute__('_')) 96 | 97 | def __repr__(self): 98 | return self 99 | 100 | def __str__(self): 101 | return self 102 | 103 | 104 | _ST = "[SECURED!]" 105 | -------------------------------------------------------------------------------- /userge/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | from .progress import progress # noqa 12 | from ..sys_tools import SafeDict, secured_env, secured_str # noqa 13 | from .tools import (sort_file_name_key, # noqa 14 | is_url, 15 | get_file_id_of_media, 16 | humanbytes, 17 | time_formatter, 18 | runcmd, 19 | take_screen_shot, 20 | parse_buttons, 21 | is_command, 22 | extract_entities, 23 | get_custom_import_re) 24 | -------------------------------------------------------------------------------- /userge/utils/exceptions.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | 12 | class StopConversation(Exception): 13 | """ raise if conversation has terminated """ 14 | 15 | 16 | class ProcessCanceled(Exception): 17 | """ raise if thread has terminated """ 18 | 19 | 20 | class UsergeBotNotFound(Exception): 21 | """ raise if userge bot not found """ 22 | -------------------------------------------------------------------------------- /userge/utils/progress.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | import time 12 | from math import floor 13 | from typing import Dict, Tuple, Optional 14 | 15 | from pyrogram.errors.exceptions import FloodWait 16 | 17 | import userge 18 | from .tools import humanbytes, time_formatter 19 | from .. import config 20 | 21 | _TASKS: Dict[str, Tuple[float, float]] = {} 22 | 23 | 24 | async def progress(current: int, 25 | total: int, 26 | message: 'userge.Message', 27 | ud_type: str, 28 | file_name: str = '', 29 | delay: Optional[int] = None) -> None: 30 | """ progress function """ 31 | if message.process_is_canceled: 32 | await message.client.stop_transmission() 33 | delay = delay or config.Dynamic.EDIT_SLEEP_TIMEOUT 34 | task_id = f"{message.chat.id}.{message.id}" 35 | if current == total: 36 | if task_id not in _TASKS: 37 | return 38 | del _TASKS[task_id] 39 | try: 40 | await message.edit("`finalizing process ...`") 41 | except FloodWait as f_e: 42 | time.sleep(f_e.value) 43 | return 44 | now = time.time() 45 | if task_id not in _TASKS: 46 | _TASKS[task_id] = (now, now) 47 | start, last = _TASKS[task_id] 48 | elapsed_time = now - start 49 | if (now - last) >= delay: 50 | _TASKS[task_id] = (start, now) 51 | percentage = current * 100 / total 52 | speed = current / elapsed_time 53 | time_to_completion = time_formatter(int((total - current) / speed)) 54 | progress_str = \ 55 | "__{}__ : `{}`\n" + \ 56 | "```\n[{}{}]```\n" + \ 57 | "**Progress** : `{}%`\n" + \ 58 | "**Completed** : `{}`\n" + \ 59 | "**Total** : `{}`\n" + \ 60 | "**Speed** : `{}/s`\n" + \ 61 | "**ETA** : `{}`" 62 | progress_str = progress_str.format( 63 | ud_type, 64 | file_name, 65 | ''.join((userge.config.FINISHED_PROGRESS_STR 66 | for _ in range(floor(percentage / 5)))), 67 | ''.join((userge.config.UNFINISHED_PROGRESS_STR 68 | for _ in range(20 - floor(percentage / 5)))), 69 | round(percentage, 2), 70 | humanbytes(current), 71 | humanbytes(total), 72 | humanbytes(speed), 73 | time_to_completion if time_to_completion else "0 s") 74 | try: 75 | await message.edit(progress_str) 76 | except FloodWait as f_e: 77 | time.sleep(f_e.value) 78 | -------------------------------------------------------------------------------- /userge/utils/tools.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | import asyncio 12 | import importlib 13 | import re 14 | import shlex 15 | from os.path import basename, join, exists 16 | from typing import Tuple, List, Optional, Iterator, Union, Any 17 | 18 | from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message, User 19 | from pyrogram import enums 20 | 21 | import userge 22 | 23 | _LOG = userge.logging.getLogger(__name__) 24 | 25 | _BTN_URL_REGEX = re.compile(r"(\[([^\[]+?)]\[buttonurl:/{0,2}(.+?)(:same)?])") 26 | _PTN_SPLIT = re.compile(r'(\.\d+|\.|\d+)') 27 | _PTN_URL = re.compile(r"(?:https?|ftp)://[^|\s]+\.[^|\s]+") 28 | 29 | 30 | def is_url(url: str) -> bool: 31 | return bool(_PTN_URL.match(url)) 32 | 33 | 34 | def sort_file_name_key(file_name: str) -> tuple: 35 | """ sort key for file names """ 36 | if not isinstance(file_name, str): 37 | file_name = str(file_name) 38 | return tuple(_sort_algo(_PTN_SPLIT.split(file_name.lower()))) 39 | 40 | 41 | # this algo doesn't support signed values 42 | def _sort_algo(data: List[str]) -> Iterator[Union[str, float]]: 43 | """ sort algo for file names """ 44 | p1 = 0.0 45 | for p2 in data: 46 | # skipping null values 47 | if not p2: 48 | continue 49 | 50 | # first letter of the part 51 | c = p2[0] 52 | 53 | # checking c is a digit or not 54 | # if yes, p2 should not contain any non digits 55 | if c.isdigit(): 56 | # p2 should be [0-9]+ 57 | # so c should be 0-9 58 | if c == '0': 59 | # add padding 60 | # this fixes `a1` and `a01` messing 61 | if isinstance(p1, str): 62 | yield 0.0 63 | yield c 64 | 65 | # converting to float 66 | p2 = float(p2) 67 | 68 | # add padding 69 | if isinstance(p1, float): 70 | yield '' 71 | 72 | # checking p2 is `.[0-9]+` or not 73 | elif c == '.' and len(p2) > 1 and p2[1].isdigit(): 74 | # p2 should be `.[0-9]+` 75 | # so converting to float 76 | p2 = float(p2) 77 | 78 | # add padding 79 | if isinstance(p1, str): 80 | yield 0.0 81 | yield c 82 | 83 | # add padding if previous and current both are strings 84 | if isinstance(p1, str) and isinstance(p2, str): 85 | yield 0.0 86 | 87 | yield p2 88 | # saving current value for later use 89 | p1 = p2 90 | 91 | 92 | def get_file_id_of_media(message: 'userge.Message') -> Optional[str]: 93 | """ get file_id """ 94 | file_ = message.audio or message.animation or message.photo \ 95 | or message.sticker or message.voice or message.video_note \ 96 | or message.video or message.document 97 | if file_: 98 | return file_.file_id 99 | return None 100 | 101 | 102 | def humanbytes(size: float) -> str: 103 | """ humanize size """ 104 | if not size: 105 | return "0 B" 106 | power = 1024 107 | t_n = 0 108 | power_dict = { 109 | 0: '', 110 | 1: 'Ki', 111 | 2: 'Mi', 112 | 3: 'Gi', 113 | 4: 'Ti', 114 | 5: 'Pi', 115 | 6: 'Ei', 116 | 7: 'Zi', 117 | 8: 'Yi'} 118 | while size > power: 119 | size /= power 120 | t_n += 1 121 | return "{:.2f} {}B".format(size, power_dict[t_n]) # pylint: disable=consider-using-f-string 122 | 123 | 124 | def time_formatter(seconds: float) -> str: 125 | """ humanize time """ 126 | minutes, seconds = divmod(int(seconds), 60) 127 | hours, minutes = divmod(minutes, 60) 128 | days, hours = divmod(hours, 24) 129 | tmp = ((str(days) + "d, ") if days else "") + \ 130 | ((str(hours) + "h, ") if hours else "") + \ 131 | ((str(minutes) + "m, ") if minutes else "") + \ 132 | ((str(seconds) + "s, ") if seconds else "") 133 | return tmp[:-2] 134 | 135 | 136 | async def runcmd(cmd: str) -> Tuple[str, str, int, int]: 137 | """ run command in terminal """ 138 | args = shlex.split(cmd) 139 | process = await asyncio.create_subprocess_exec(*args, 140 | stdout=asyncio.subprocess.PIPE, 141 | stderr=asyncio.subprocess.PIPE) 142 | stdout, stderr = await process.communicate() 143 | return (stdout.decode('utf-8', 'replace').strip(), 144 | stderr.decode('utf-8', 'replace').strip(), 145 | process.returncode, 146 | process.pid) 147 | 148 | 149 | async def take_screen_shot(video_file: str, duration: int, path: str = '') -> Optional[str]: 150 | """ take a screenshot """ 151 | _LOG.info( 152 | 'Extracting a frame from %s ||| Video duration => %s', 153 | video_file, 154 | duration) 155 | 156 | ttl = duration // 2 157 | thumb_image_path = path or join( 158 | userge.config.Dynamic.DOWN_PATH, 159 | f"{basename(video_file)}.jpg") 160 | command = f'''ffmpeg -ss {ttl} -i "{video_file}" -vframes 1 "{thumb_image_path}"''' 161 | 162 | err = (await runcmd(command))[1] 163 | if err: 164 | _LOG.error(err) 165 | 166 | return thumb_image_path if exists(thumb_image_path) else None 167 | 168 | 169 | def parse_buttons( 170 | markdown_note: str) -> Tuple[str, Optional[InlineKeyboardMarkup]]: 171 | """ markdown_note to string and buttons """ 172 | prev = 0 173 | note_data = "" 174 | buttons: List[Tuple[str, str, bool]] = [] 175 | for match in _BTN_URL_REGEX.finditer(markdown_note): 176 | n_escapes = 0 177 | to_check = match.start(1) - 1 178 | while to_check > 0 and markdown_note[to_check] == "\\": 179 | n_escapes += 1 180 | to_check -= 1 181 | if n_escapes % 2 == 0: 182 | buttons.append( 183 | (match.group(2), 184 | match.group(3), 185 | bool( 186 | match.group(4)))) 187 | note_data += markdown_note[prev:match.start(1)] 188 | prev = match.end(1) 189 | else: 190 | note_data += markdown_note[prev:to_check] 191 | prev = match.start(1) - 1 192 | note_data += markdown_note[prev:] 193 | keyb: List[List[InlineKeyboardButton]] = [] 194 | for btn in buttons: 195 | if btn[2] and keyb: 196 | keyb[-1].append(InlineKeyboardButton(btn[0], url=btn[1])) 197 | else: 198 | keyb.append([InlineKeyboardButton(btn[0], url=btn[1])]) 199 | return note_data.strip(), InlineKeyboardMarkup(keyb) if keyb else None 200 | 201 | 202 | def is_command(cmd: str) -> bool: 203 | commands = userge.userge.manager.loaded_commands 204 | key = userge.config.CMD_TRIGGER + cmd 205 | _key = userge.config.SUDO_TRIGGER + cmd 206 | 207 | is_cmd = False 208 | if cmd in commands: 209 | is_cmd = True 210 | elif key in commands: 211 | is_cmd = True 212 | elif _key in commands: 213 | is_cmd = True 214 | return is_cmd 215 | 216 | 217 | def extract_entities( 218 | message: Message, typeofentity: List[enums.MessageEntityType]) -> List[Union[str, User]]: 219 | """ gets a message and returns a list of entity_type in the message 220 | """ 221 | tero = [] 222 | entities = message.entities or message.caption_entities or [] 223 | text = message.text or message.caption or "" 224 | for entity in entities: 225 | url = None 226 | cet = entity.type 227 | if entity.type in [ 228 | enums.MessageEntityType.URL, 229 | enums.MessageEntityType.MENTION, 230 | enums.MessageEntityType.HASHTAG, 231 | enums.MessageEntityType.CASHTAG, 232 | enums.MessageEntityType.BOT_COMMAND, 233 | enums.MessageEntityType.EMAIL, 234 | enums.MessageEntityType.PHONE_NUMBER, 235 | enums.MessageEntityType.BOLD, 236 | enums.MessageEntityType.ITALIC, 237 | enums.MessageEntityType.UNDERLINE, 238 | enums.MessageEntityType.STRIKETHROUGH, 239 | enums.MessageEntityType.SPOILER, 240 | enums.MessageEntityType.CODE, 241 | enums.MessageEntityType.PRE, 242 | ]: 243 | offset = entity.offset 244 | length = entity.length 245 | url = text[offset:offset + length] 246 | 247 | elif entity.type == enums.MessageEntityType.TEXT_LINK: 248 | url = entity.url 249 | 250 | elif entity.type == enums.MessageEntityType.TEXT_MENTION: 251 | url = entity.user 252 | 253 | if url and cet in typeofentity: 254 | tero.append(url) 255 | return tero 256 | 257 | 258 | def get_custom_import_re(req_module, re_raise=True) -> Any: 259 | """ import custom modules dynamically """ 260 | try: 261 | return importlib.import_module(req_module) 262 | except (ModuleNotFoundError, ImportError): 263 | if re_raise: 264 | raise 265 | 266 | return None 267 | -------------------------------------------------------------------------------- /userge/versions.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | # 3 | # Copyright (C) 2020-2022 by UsergeTeam@Github, < https://github.com/UsergeTeam >. 4 | # 5 | # This file is part of < https://github.com/UsergeTeam/Userge > project, 6 | # and is released under the "GNU v3.0 License Agreement". 7 | # Please see < https://github.com/UsergeTeam/Userge/blob/master/LICENSE > 8 | # 9 | # All rights reserved. 10 | 11 | from sys import version_info 12 | 13 | from pyrogram import __version__ as __pyro_version__ # noqa 14 | 15 | from loader import __version__ as __loader_version__ # noqa 16 | from loader.userge import api 17 | 18 | __major__ = 1 19 | __minor__ = 0 20 | __micro__ = 2 21 | 22 | __python_version__ = f"{version_info[0]}.{version_info[1]}.{version_info[2]}" 23 | __license__ = "[GNU GPL v3.0](https://github.com/UsergeTeam/Userge/blob/master/LICENSE)" 24 | __copyright__ = "[UsergeTeam](https://github.com/UsergeTeam)" 25 | 26 | 27 | def get_version() -> str: 28 | return f"{__major__}.{__minor__}.{__micro__}" 29 | 30 | 31 | async def get_full_version() -> str: 32 | core = await api.get_core() 33 | ver = f"{get_version()}-build.{core.count}" 34 | 35 | return ver + '@' + core.branch 36 | --------------------------------------------------------------------------------