Compare commits

...

10 Commits
v1.5 ... v1.9

Author SHA1 Message Date
dc107ebdb3 Updated to 1.9 2023-07-26 14:12:13 +02:00
33c33d08e2 Allowed passing kwargs 2023-07-26 14:12:05 +02:00
295e77e403 Updated to 1.8 2023-07-03 10:57:00 +02:00
279a8e9d84 reports.chat_id can now be "owner" 2023-07-03 10:56:24 +02:00
ae374511cd Updated to 1.7 2023-06-30 10:32:16 +02:00
eb23d3e9b6 No more locations.locale in config 2023-06-30 10:31:49 +02:00
f4e74b5bc6 Added trailing slashes 2023-06-29 16:03:49 +02:00
7b8434ae71 Updated to 1.6 2023-06-29 15:59:06 +02:00
8c2054f496 Improved init flexibility 2023-06-29 15:58:50 +02:00
fe9cc3674f Removed duplicate py version 2023-06-29 15:58:20 +02:00
7 changed files with 141 additions and 85 deletions

8
.gitignore vendored
View File

@@ -161,8 +161,8 @@ cython_debug/
#.idea/
# Project
venv
venv_linux
venv_windows
venv/
venv_linux/
venv_windows/
.vscode
.vscode/

View File

@@ -1,5 +1,5 @@
__name__ = "libbot"
__version__ = "1.5"
__version__ = "1.9"
__license__ = "GPL3"
__author__ = "Profitroll"

View File

@@ -1,35 +1,36 @@
from os import listdir
from pathlib import Path
from typing import Any, Dict
from typing import Any, Dict, Union
from libbot import config_get, json_read, sync
from libbot.i18n.classes.bot_locale import BotLocale
async def _(key: str, *args: str, locale: str = sync.config_get("locale")) -> Any:
async def _(
key: str,
*args: str,
locale: str = sync.config_get("locale"),
locales_root: Union[str, Path] = Path("locale"),
) -> Any:
"""Get value of locale string
### Args:
* key (`str`): The last key of the locale's keys path
* *args (`list`): Path to key like: `dict[args][key]`
* locale (`str`): Locale to looked up in. Defaults to config's `"locale"` value
* key (`str`): The last key of the locale's keys path.
* *args (`list`): Path to key like: `dict[args][key]`.
* locale (`str`): Locale to looked up in. Defaults to config's `"locale"` value.
* locales_root (`Union[str, Path]`, *optional*): Folder where locales are located. Defaults to `Path("locale")`.
### Returns:
* `Any`: Value of provided locale key. Is usually `str`, `dict` or `list`
"""
if locale is None:
locale = sync.config_get("locale")
locale = sync.config_get("locale") if locale is None else locale
try:
this_dict = await json_read(
Path(f'{await config_get("locale", "locations")}/{locale}.json')
)
this_dict = await json_read(Path(f"{locales_root}/{locale}.json"))
except FileNotFoundError:
try:
this_dict = await json_read(
Path(
f'{await config_get("locale", "locations")}/{await config_get("locale")}.json'
)
Path(f'{locales_root}/{await config_get("locale")}.json')
)
except FileNotFoundError:
return f'⚠️ Locale in config is invalid: could not get "{key}" in {args} from locale "{locale}"'
@@ -44,26 +45,27 @@ async def _(key: str, *args: str, locale: str = sync.config_get("locale")) -> An
return f'⚠️ Locale in config is invalid: could not get "{key}" in {args} from locale "{locale}"'
async def in_all_locales(key: str, *args: str) -> list:
async def in_all_locales(
key: str, *args: str, locales_root: Union[str, Path] = Path("locale")
) -> list:
"""Get value of the provided key and path in all available locales
### Args:
* key (`str`): The last key of the locale's keys path.
* *args (`list`): Path to key like: `dict[args][key]`.
* locales_root (`Union[str, Path]`, *optional*): Folder where locales are located. Defaults to `Path("locale")`.
### Returns:
* `list`: List of values in all locales
"""
output = []
files_locales = listdir(await config_get("locale", "locations"))
files_locales = listdir(locales_root)
valid_locales = [".".join(entry.split(".")[:-1]) for entry in files_locales]
for lc in valid_locales:
try:
this_dict = await json_read(
Path(f'{await config_get("locale", "locations")}/{lc}.json')
)
this_dict = await json_read(Path(f"{locales_root}/{lc}.json"))
except FileNotFoundError:
continue
@@ -79,26 +81,27 @@ async def in_all_locales(key: str, *args: str) -> list:
return output
async def in_every_locale(key: str, *args: str) -> Dict[str, Any]:
async def in_every_locale(
key: str, *args: str, locales_root: Union[str, Path] = Path("locale")
) -> Dict[str, Any]:
"""Get value of the provided key and path in every available locale with locale tag
### Args:
* key (`str`): The last key of the locale's keys path.
* *args (`list`): Path to key like: `dict[args][key]`.
* locales_root (`Union[str, Path]`, *optional*): Folder where locales are located. Defaults to `Path("locale")`.
### Returns:
* `Dict[str, Any]`: Locale is a key and it's value from locale file is a value
"""
output = {}
files_locales = listdir(await config_get("locale", "locations"))
files_locales = listdir(locales_root)
valid_locales = [".".join(entry.split(".")[:-1]) for entry in files_locales]
for lc in valid_locales:
try:
this_dict = await json_read(
Path(f'{await config_get("locale", "locations")}/{lc}.json')
)
this_dict = await json_read(Path(f"{locales_root}/{lc}.json"))
except FileNotFoundError:
continue

View File

@@ -8,15 +8,16 @@ from libbot import sync
class BotLocale:
"""Small addon that can be used by bot clients' classes in order to minimize I/O"""
def __init__(self, locales_folder: Union[str, Path, None] = None) -> None:
if locales_folder is None:
locales_folder = Path(sync.config_get("locale", "locations"))
elif isinstance(locales_folder, str):
locales_folder = Path(locales_folder)
elif not isinstance(locales_folder, Path):
raise TypeError("'locales_folder' must be a valid path or path-like object")
def __init__(
self,
locales_root: Union[str, Path] = Path("locale"),
) -> None:
if isinstance(locales_root, str):
locales_root = Path(locales_root)
elif not isinstance(locales_root, Path):
raise TypeError("'locales_root' must be a valid path or path-like object")
files_locales: list = listdir(locales_folder)
files_locales: list = listdir(locales_root)
valid_locales: list = [
".".join(entry.split(".")[:-1]) for entry in files_locales
@@ -26,7 +27,7 @@ class BotLocale:
self.locales: dict = {}
for lc in valid_locales:
self.locales[lc] = sync.json_read(Path(f"{locales_folder}/{lc}.json"))
self.locales[lc] = sync.json_read(Path(f"{locales_root}/{lc}.json"))
def _(self, key: str, *args: str, locale: Union[str, None] = None) -> Any:
"""Get value of locale string

View File

@@ -1,18 +1,24 @@
from os import listdir
from pathlib import Path
from typing import Any, Dict
from typing import Any, Dict, Union
from libbot import sync
from libbot.sync import config_get, json_read
def _(key: str, *args: str, locale: str = sync.config_get("locale")) -> Any:
def _(
key: str,
*args: str,
locale: str = sync.config_get("locale"),
locales_root: Union[str, Path] = Path("locale"),
) -> Any:
"""Get value of locale string
### Args:
* key (`str`): The last key of the locale's keys path
* *args (`list`): Path to key like: `dict[args][key]`
* locale (`str`): Locale to looked up in. Defaults to config's `"locale"` value
* key (`str`): The last key of the locale's keys path.
* *args (`list`): Path to key like: `dict[args][key]`.
* locale (`str`): Locale to looked up in. Defaults to config's `"locale"` value.
* locales_root (`Union[str, Path]`, *optional*): Folder where locales are located. Defaults to `Path("locale")`.
### Returns:
* `Any`: Value of provided locale key. Is usually `str`, `dict` or `list`
@@ -21,14 +27,10 @@ def _(key: str, *args: str, locale: str = sync.config_get("locale")) -> Any:
locale = sync.config_get("locale")
try:
this_dict = json_read(
Path(f'{config_get("locale", "locations")}/{locale}.json')
)
this_dict = json_read(Path(f"{locales_root}/{locale}.json"))
except FileNotFoundError:
try:
this_dict = json_read(
Path(f'{config_get("locale", "locations")}/{config_get("locale")}.json')
)
this_dict = json_read(Path(f'{locales_root}/{config_get("locale")}.json'))
except FileNotFoundError:
return f'⚠️ Locale in config is invalid: could not get "{key}" in {args} from locale "{locale}"'
@@ -42,26 +44,27 @@ def _(key: str, *args: str, locale: str = sync.config_get("locale")) -> Any:
return f'⚠️ Locale in config is invalid: could not get "{key}" in {args} from locale "{locale}"'
def in_all_locales(key: str, *args: str) -> list:
def in_all_locales(
key: str, *args: str, locales_root: Union[str, Path] = Path("locale")
) -> list:
"""Get value of the provided key and path in all available locales
### Args:
* key (`str`): The last key of the locale's keys path.
* *args (`list`): Path to key like: `dict[args][key]`.
* locales_root (`Union[str, Path]`, *optional*): Folder where locales are located. Defaults to `Path("locale")`.
### Returns:
* `list`: List of values in all locales
"""
output = []
files_locales = listdir(config_get("locale", "locations"))
files_locales = listdir(locales_root)
valid_locales = [".".join(entry.split(".")[:-1]) for entry in files_locales]
for lc in valid_locales:
try:
this_dict = json_read(
Path(f'{config_get("locale", "locations")}/{lc}.json')
)
this_dict = json_read(Path(f"{locales_root}/{lc}.json"))
except FileNotFoundError:
continue
@@ -77,26 +80,27 @@ def in_all_locales(key: str, *args: str) -> list:
return output
def in_every_locale(key: str, *args: str) -> Dict[str, Any]:
def in_every_locale(
key: str, *args: str, locales_root: Union[str, Path] = Path("locale")
) -> Dict[str, Any]:
"""Get value of the provided key and path in every available locale with locale tag
### Args:
* key (`str`): The last key of the locale's keys path.
* *args (`list`): Path to key like: `dict[args][key]`.
* locales_root (`Union[str, Path]`, *optional*): Folder where locales are located. Defaults to `Path("locale")`.
### Returns:
* `Dict[str, Any]`: Locale is a key and it's value from locale file is a value
"""
output = {}
files_locales = listdir(config_get("locale", "locations"))
files_locales = listdir(locales_root)
valid_locales = [".".join(entry.split(".")[:-1]) for entry in files_locales]
for lc in valid_locales:
try:
this_dict = json_read(
Path(f'{config_get("locale", "locations")}/{lc}.json')
)
this_dict = json_read(Path(f"{locales_root}/{lc}.json"))
except FileNotFoundError:
continue

View File

@@ -1,9 +1,10 @@
import asyncio
import logging
from datetime import datetime, timedelta
from os import cpu_count, getpid
from pathlib import Path
from time import time
from typing import Dict, List, Union
from typing import Any, Dict, List, Union
try:
import pyrogram
@@ -43,34 +44,66 @@ logger = logging.getLogger(__name__)
class PyroClient(Client):
def __init__(
self, scheduler: Union[AsyncIOScheduler, BackgroundScheduler, None] = None
self,
name: str = "bot_client",
config: Union[Dict[str, Any], None] = None,
config_path: Union[str, Path] = Path("config.json"),
api_id: Union[int, None] = None,
api_hash: Union[str, None] = None,
bot_token: Union[str, None] = None,
workers: int = min(32, cpu_count() + 4),
locales_root: Union[str, Path, None] = None,
plugins_root: str = "plugins",
plugins_exclude: Union[List[str], None] = None,
sleep_threshold: int = 120,
max_concurrent_transmissions: int = 1,
commands_source: Union[Dict[str, dict], None] = None,
scheduler: Union[AsyncIOScheduler, BackgroundScheduler, None] = None,
**kwargs,
):
with open("config.json", "r", encoding="utf-8") as f:
self.config: dict = loads(f.read())
if config is None:
with open(config_path, "r", encoding="utf-8") as f:
self.config: dict = loads(f.read())
else:
self.config = config
super().__init__(
name="bot_client",
api_id=self.config["bot"]["api_id"],
api_hash=self.config["bot"]["api_hash"],
bot_token=self.config["bot"]["bot_token"],
# Workers should be commented when using convopyro, otherwise
name=name,
api_id=self.config["bot"]["api_id"] if api_id is None else api_id,
api_hash=self.config["bot"]["api_hash"] if api_hash is None else api_hash,
bot_token=self.config["bot"]["bot_token"]
if bot_token is None
else bot_token,
# Workers should be `min(32, cpu_count() + 4)`, otherwise
# handlers land in another event loop and you won't see them
workers=self.config["bot"]["workers"]
if "workers" in self.config["bot"]
else min(32, cpu_count() + 4),
plugins=dict(root="plugins", exclude=self.config["disabled_plugins"]),
sleep_threshold=120,
else workers,
plugins=dict(
root=plugins_root,
exclude=self.config["disabled_plugins"]
if plugins_exclude is None
else plugins_exclude,
),
sleep_threshold=sleep_threshold,
max_concurrent_transmissions=self.config["bot"][
"max_concurrent_transmissions"
]
if "max_concurrent_transmissions" in self.config["bot"]
else 1,
else max_concurrent_transmissions,
**kwargs,
)
self.owner: int = self.config["bot"]["owner"]
self.commands: List[PyroCommand] = []
self.commands_source: Dict[str, dict] = (
self.config["commands"] if commands_source is None else commands_source
)
self.scoped_commands: bool = self.config["bot"]["scoped_commands"]
self.start_time: float = 0
self.bot_locale: BotLocale = BotLocale(Path(self.config["locations"]["locale"]))
self.bot_locale: BotLocale = BotLocale(
(Path("locale") if locales_root is None else locales_root)
)
self.default_locale: str = self.bot_locale.default
self.locales: dict = self.bot_locale.locales
@@ -82,7 +115,7 @@ class PyroClient(Client):
self.scopes_placeholders: Dict[str, int] = {"owner": self.owner}
async def start(self):
async def start(self, register_commands: bool = True):
await super().start()
self.start_time = time()
@@ -97,35 +130,50 @@ class PyroClient(Client):
try:
await self.send_message(
chat_id=self.config["reports"]["chat_id"],
chat_id=self.owner
if self.config["reports"]["chat_id"] == "owner"
else self.config["reports"]["chat_id"],
text=f"Bot started PID `{getpid()}`",
)
if self.scheduler is None:
return
self.scheduler.add_job(
self.register_commands,
trigger="date",
run_date=datetime.now() + timedelta(seconds=5),
kwargs={"command_sets": await self.collect_commands()},
)
if register_commands:
self.scheduler.add_job(
self.register_commands,
trigger="date",
run_date=datetime.now() + timedelta(seconds=5),
kwargs={"command_sets": await self.collect_commands()},
)
self.scheduler.start()
except BadRequest:
logger.warning("Unable to send message to report chat.")
async def stop(self):
async def stop(self, exit_completely: bool = True):
try:
await self.send_message(
chat_id=self.config["reports"]["chat_id"],
chat_id=self.owner
if self.config["reports"]["chat_id"] == "owner"
else self.config["reports"]["chat_id"],
text=f"Bot stopped with PID `{getpid()}`",
)
await asyncio.sleep(0.5)
except BadRequest:
logger.warning("Unable to send message to report chat.")
await super().stop()
logger.warning("Bot stopped with PID %s.", getpid())
if exit_completely:
try:
exit()
except SystemExit as exp:
raise SystemExit(
"Bot has been shut down, this is not an application error!"
) from exp
async def collect_commands(self) -> Union[List[CommandSet], None]:
"""Gather list of the bot's commands
@@ -141,7 +189,7 @@ class PyroClient(Client):
command_sets = []
# Iterate through all commands in config
for command, contents in self.config["commands"].items():
for command, contents in self.commands_source.items():
# Iterate through all scopes of a command
for scope in contents["scopes"]:
if dumps(scope) not in scopes:

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "libbot"
version = "1.5"
version = "1.9"
authors = [{ name = "Profitroll" }]
description = "Universal bot library with functions needed for basic Discord/Telegram bot development."
readme = "README.md"
@@ -48,7 +48,7 @@ packages = [
[tool.setuptools_scm]
[tool.black]
target-version = ['py38', 'py38', 'py39', 'py310', 'py311']
target-version = ['py38', 'py39', 'py310', 'py311']
[tool.isort]
profile = "black"