Compare commits

...

10 Commits
v1.4 ... v1.8

Author SHA1 Message Date
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
c71a7555f9 Updated to 1.5 2023-06-26 13:29:47 +02:00
cb755faa9a Added scopes_placeholders 2023-06-26 13:29:26 +02:00
7 changed files with 144 additions and 87 deletions

8
.gitignore vendored
View File

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

View File

@@ -1,5 +1,5 @@
__name__ = "libbot" __name__ = "libbot"
__version__ = "1.4" __version__ = "1.8"
__license__ = "GPL3" __license__ = "GPL3"
__author__ = "Profitroll" __author__ = "Profitroll"

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,10 @@
import asyncio
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from os import cpu_count, getpid from os import cpu_count, getpid
from pathlib import Path from pathlib import Path
from time import time from time import time
from typing import List, Union from typing import Any, Dict, List, Union
try: try:
import pyrogram import pyrogram
@@ -43,34 +44,64 @@ logger = logging.getLogger(__name__)
class PyroClient(Client): class PyroClient(Client):
def __init__( 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,
): ):
with open("config.json", "r", encoding="utf-8") as f: if config is None:
self.config: dict = loads(f.read()) with open(config_path, "r", encoding="utf-8") as f:
self.config: dict = loads(f.read())
else:
self.config = config
super().__init__( super().__init__(
name="bot_client", name=name,
api_id=self.config["bot"]["api_id"], api_id=self.config["bot"]["api_id"] if api_id is None else api_id,
api_hash=self.config["bot"]["api_hash"], api_hash=self.config["bot"]["api_hash"] if api_hash is None else api_hash,
bot_token=self.config["bot"]["bot_token"], bot_token=self.config["bot"]["bot_token"]
# Workers should be commented when using convopyro, otherwise 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 # handlers land in another event loop and you won't see them
workers=self.config["bot"]["workers"] workers=self.config["bot"]["workers"]
if "workers" in self.config["bot"] if "workers" in self.config["bot"]
else min(32, cpu_count() + 4), else workers,
plugins=dict(root="plugins", exclude=self.config["disabled_plugins"]), plugins=dict(
sleep_threshold=120, 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=self.config["bot"][
"max_concurrent_transmissions" "max_concurrent_transmissions"
] ]
if "max_concurrent_transmissions" in self.config["bot"] if "max_concurrent_transmissions" in self.config["bot"]
else 1, else max_concurrent_transmissions,
) )
self.owner: int = self.config["bot"]["owner"] self.owner: int = self.config["bot"]["owner"]
self.commands: List[PyroCommand] = [] 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.scoped_commands: bool = self.config["bot"]["scoped_commands"]
self.start_time: float = 0 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.default_locale: str = self.bot_locale.default
self.locales: dict = self.bot_locale.locales self.locales: dict = self.bot_locale.locales
@@ -80,7 +111,9 @@ class PyroClient(Client):
self.scheduler: Union[AsyncIOScheduler, BackgroundScheduler, None] = scheduler self.scheduler: Union[AsyncIOScheduler, BackgroundScheduler, None] = scheduler
async def start(self): self.scopes_placeholders: Dict[str, int] = {"owner": self.owner}
async def start(self, register_commands: bool = True):
await super().start() await super().start()
self.start_time = time() self.start_time = time()
@@ -95,35 +128,50 @@ class PyroClient(Client):
try: try:
await self.send_message( 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()}`", text=f"Bot started PID `{getpid()}`",
) )
if self.scheduler is None: if self.scheduler is None:
return return
self.scheduler.add_job( if register_commands:
self.register_commands, self.scheduler.add_job(
trigger="date", self.register_commands,
run_date=datetime.now() + timedelta(seconds=5), trigger="date",
kwargs={"command_sets": await self.collect_commands()}, run_date=datetime.now() + timedelta(seconds=5),
) kwargs={"command_sets": await self.collect_commands()},
)
self.scheduler.start() self.scheduler.start()
except BadRequest: except BadRequest:
logger.warning("Unable to send message to report chat.") logger.warning("Unable to send message to report chat.")
async def stop(self): async def stop(self, exit_completely: bool = True):
try: try:
await self.send_message( 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()}`", text=f"Bot stopped with PID `{getpid()}`",
) )
await asyncio.sleep(0.5)
except BadRequest: except BadRequest:
logger.warning("Unable to send message to report chat.") logger.warning("Unable to send message to report chat.")
await super().stop() await super().stop()
logger.warning("Bot stopped with PID %s.", getpid()) 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]: async def collect_commands(self) -> Union[List[CommandSet], None]:
"""Gather list of the bot's commands """Gather list of the bot's commands
@@ -139,7 +187,7 @@ class PyroClient(Client):
command_sets = [] command_sets = []
# Iterate through all commands in config # 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 # Iterate through all scopes of a command
for scope in contents["scopes"]: for scope in contents["scopes"]:
if dumps(scope) not in scopes: if dumps(scope) not in scopes:
@@ -164,8 +212,9 @@ class PyroClient(Client):
scope_dict = loads(scope) scope_dict = loads(scope)
# Replace "owner" in the bot scope with owner's id # Replace "owner" in the bot scope with owner's id
if "chat_id" in scope_dict and scope_dict["chat_id"] == "owner": for placeholder, chat_id in self.scopes_placeholders.items():
scope_dict["chat_id"] = self.owner if "chat_id" in scope_dict and scope_dict["chat_id"] == placeholder:
scope_dict["chat_id"] = chat_id
# Create object with the same name and args from the dict # Create object with the same name and args from the dict
try: try:

View File

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