Compare commits

..

11 Commits

Author SHA1 Message Date
a38b55d270 Merge pull request 'v4.0.0' (#169) from dev into main
Some checks failed
Analysis / SonarCloud (push) Successful in 42s
Tests / Build and Test (3.10) (push) Failing after 57s
Tests / Build and Test (3.11) (push) Successful in 1m4s
Tests / Build and Test (3.12) (push) Successful in 1m13s
Tests / Build and Test (3.9) (push) Failing after 57s
Reviewed-on: #169
2024-12-26 19:59:34 +02:00
9907cc50f1 Merge pull request 'v3.3.1' (#160) from dev into main
All checks were successful
Analysis / SonarCloud (push) Successful in 46s
Tests / Build and Test (3.10) (push) Successful in 1m3s
Tests / Build and Test (3.11) (push) Successful in 1m1s
Tests / Build and Test (3.12) (push) Successful in 1m8s
Tests / Build and Test (3.9) (push) Successful in 1m3s
Reviewed-on: #160
2024-12-16 23:57:08 +02:00
1b60257bc5 Merge pull request 'v3.3.0' (#159) from dev into main
All checks were successful
Analysis / SonarCloud (push) Successful in 43s
Tests / Build and Test (3.10) (push) Successful in 1m2s
Tests / Build and Test (3.11) (push) Successful in 1m2s
Tests / Build and Test (3.12) (push) Successful in 1m8s
Tests / Build and Test (3.9) (push) Successful in 1m4s
Reviewed-on: #159
2024-12-16 23:48:07 +02:00
171e36a491 Merge pull request 'v3.2.3' (#118) from dev into main
All checks were successful
Tests / test (3.10) (push) Successful in 1m2s
Tests / test (3.11) (push) Successful in 58s
Tests / test (3.8) (push) Successful in 1m4s
Tests / test (3.9) (push) Successful in 1m1s
Reviewed-on: #118
2024-07-10 00:07:54 +03:00
c419c684aa Merge pull request 'v3.2.2' (#107) from dev into main
All checks were successful
Tests / test (3.10) (push) Successful in 1m10s
Tests / test (3.11) (push) Successful in 1m4s
Tests / test (3.8) (push) Successful in 1m6s
Tests / test (3.9) (push) Successful in 1m7s
Reviewed-on: #107
2024-05-26 22:44:18 +03:00
748b2b2abb Merge pull request 'v3.2.1' (#106) from dev into main
All checks were successful
Tests / test (3.10) (push) Successful in 1m3s
Tests / test (3.11) (push) Successful in 1m23s
Tests / test (3.8) (push) Successful in 1m7s
Tests / test (3.9) (push) Successful in 1m7s
Reviewed-on: #106
2024-05-26 17:53:00 +03:00
52c2e5cc13 Merge pull request 'v3.2.0' (#105) from dev into main
All checks were successful
Tests / test (3.10) (push) Successful in 1m4s
Tests / test (3.11) (push) Successful in 1m2s
Tests / test (3.8) (push) Successful in 1m29s
Tests / test (3.9) (push) Successful in 1m4s
Reviewed-on: #105
2024-05-26 17:29:44 +03:00
55c61e3fce Merge pull request 'v3.1.0' (#102) from dev into main
All checks were successful
Tests / test (3.10) (push) Successful in 55s
Tests / test (3.11) (push) Successful in 55s
Tests / test (3.8) (push) Successful in 57s
Tests / test (3.9) (push) Successful in 1m27s
Reviewed-on: #102
2024-05-19 16:22:17 +03:00
b9550032ba Merge pull request 'Update to 3.0.1' (#98) from dev into main
All checks were successful
Tests / test (3.10) (push) Successful in 57s
Tests / test (3.11) (push) Successful in 54s
Tests / test (3.8) (push) Successful in 1m8s
Tests / test (3.9) (push) Successful in 55s
Reviewed-on: #98
2024-05-15 00:19:03 +03:00
5ba763246b Merge pull request 'Update to 3.0.0' (#52) from dev into main
All checks were successful
Tests / test (3.10) (push) Successful in 1m15s
Tests / test (3.11) (push) Successful in 1m14s
Tests / test (3.8) (push) Successful in 1m14s
Tests / test (3.9) (push) Successful in 1m22s
Reviewed-on: #52
2024-01-04 00:06:50 +02:00
f0ffdf096d Merge pull request 'Pycord support initial release' (#48) from dev into main
All checks were successful
Tests / test (3.10) (push) Successful in 1m8s
Tests / test (3.11) (push) Successful in 1m5s
Tests / test (3.8) (push) Successful in 1m43s
Tests / test (3.9) (push) Successful in 1m3s
Reviewed-on: #48
2023-12-27 15:00:41 +02:00
16 changed files with 106 additions and 217 deletions

View File

@@ -6,19 +6,19 @@ on:
- main
- dev
pull_request:
types: [ opened, synchronize, reopened ]
types: [opened, synchronize, reopened]
jobs:
sonarcloud:
name: SonarCloud
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
container: catthehacker/ubuntu:act-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: SonarActions/cache@v1
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@v4.2.1
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

View File

@@ -1,67 +0,0 @@
name: Upload Python Package
on:
release:
types: [ published ]
permissions:
contents: read
jobs:
release-build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Build release distributions
run: |
python -m pip install build
python -m build
- name: Upload distributions
uses: christopherhx/gitea-upload-artifact@v4
with:
name: release-dists
path: dist/
gitea-publish:
runs-on: ubuntu-24.04
needs: release-build
permissions:
id-token: write
environment:
name: gitea
url: https://git.end-play.xyz/profitroll/-/packages/pypi/libbot
env:
GITHUB_WORKFLOW_REF: ${{ gitea.workflow_ref }}
INPUT_REPOSITORY_URL: https://git.end-play.xyz/api/packages/profitroll/pypi
steps:
- name: Retrieve release distributions
uses: christopherhx/gitea-download-artifact@v4
with:
name: release-dists
path: dist/
- name: Publish package distributions to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_GITEA_API_TOKEN }}
repository-url: https://git.end-play.xyz/api/packages/profitroll/pypi
pypi-publish:
runs-on: ubuntu-24.04
needs: release-build
permissions:
id-token: write
environment:
name: pypi
env:
GITHUB_WORKFLOW_REF: ${{ gitea.workflow_ref }}
steps:
- name: Retrieve release distributions
uses: christopherhx/gitea-download-artifact@v4
with:
name: release-dists
path: dist/
- name: Publish package distributions to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_PYPI_API_TOKEN }}

View File

@@ -11,18 +11,18 @@ on:
jobs:
test:
name: Build and Test
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
container: catthehacker/ubuntu:act-latest
strategy:
matrix:
python-version: [ "3.11", "3.12", "3.13" ]
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
cache-dependency-path: './requirements/*'
env:
AGENT_TOOLSDIRECTORY: /opt/hostedtoolcache
- name: Install dependencies

View File

@@ -36,20 +36,19 @@ pip install libbot[pycord,speed]
### Pyrogram
```python
import sys
from libbot.pyrogram.classes import PyroClient
from libbot.pyrogram import PyroClient
def main():
client: PyroClient = PyroClient()
client = PyroClient(scheduler=scheduler)
try:
client.run()
except KeyboardInterrupt:
print("Shutting down...")
finally:
sys.exit()
if client.scheduler is not None:
client.scheduler.shutdown()
exit()
if __name__ == "__main__":
@@ -59,33 +58,29 @@ if __name__ == "__main__":
### Pycord
```python
import asyncio
from asyncio import AbstractEventLoop
from discord import Intents
from libbot.utils import config_get
from libbot.pycord.classes import PycordBot
from libbot import sync
from libbot.pycord import PycordBot
async def main():
intents: Intents = Intents.default()
bot: PycordBot = PycordBot(intents=intents)
intents = Intents.default()
bot = PycordBot(intents=intents)
bot.load_extension("cogs")
try:
await bot.start(config_get("bot_token", "bot"))
await bot.start(sync.config_get("bot_token", "bot"))
except KeyboardInterrupt:
print("Shutting down...")
logger.warning("Shutting down...")
await bot.close()
if __name__ == "__main__":
loop: AbstractEventLoop = asyncio.get_event_loop()
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
```
## Config examples
For bot config examples please check the examples directory. Without a valid config file, the bot won't start at all, so
you need to make sure the correct config file is used.
For bot config examples please check the examples directory. Without a valid config file, the bot won't start at all, so you need to make sure the correct config file is used.

View File

@@ -1,4 +0,0 @@
# Examples
If you're looking for Pyrogram usage examples, please take a look at
the [PyrogramBotBase](https://git.end-play.xyz/profitroll/PyrogramBotBase) repository.

View File

@@ -1,2 +1 @@
aiofiles>=23.0.0
typing-extensions~=4.12.2
aiofiles>=23.0.0

View File

@@ -1,12 +1,11 @@
black==25.1.0
black==24.10.0
build==1.2.2.post1
isort==5.13.2
mypy==1.15.0
pylint==3.3.4
pytest-asyncio==0.25.3
mypy==1.14.0
pylint==3.3.3
pytest-asyncio==0.25.0
pytest-cov==6.0.0
pytest==8.3.4
tox==4.24.0
twine==6.1.0
tox==4.23.2
types-aiofiles==24.1.0.20241221
types-ujson==5.10.0.20240515

View File

@@ -1,4 +1,4 @@
__version__ = "4.0.2"
__version__ = "4.0.0"
__license__ = "GPL3"
__author__ = "Profitroll"

View File

@@ -1,2 +0,0 @@
# This file is left empty on purpose
# Adding imports here will cause import errors when libbot[pycord] is not installed

View File

@@ -1,13 +1,9 @@
import logging
from logging import Logger
from pathlib import Path
from typing import Any, Dict
from typing_extensions import override
from ...i18n.classes import BotLocale
from ...utils import json_read
try:
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.schedulers.background import BackgroundScheduler
@@ -15,21 +11,32 @@ try:
except ImportError as exc:
raise ImportError("You need to install libbot[pycord] in order to use this class.") from exc
logger: Logger = logging.getLogger(__name__)
try:
from ujson import loads
except ImportError:
from json import loads
from ...i18n.classes import BotLocale
logger = logging.getLogger(__name__)
class PycordBot(Bot):
@override
def __init__(
self,
*args,
config: Dict[str, Any] | None = None,
config_path: str | Path = Path("config.json"),
locales_root: str | Path | None = None,
scheduler: AsyncIOScheduler | BackgroundScheduler | None = None,
**kwargs,
self,
*args,
config: Dict[str, Any] | None = None,
config_path: str | Path = Path("config.json"),
locales_root: str | Path | None = None,
scheduler: AsyncIOScheduler | BackgroundScheduler | None = None,
**kwargs,
):
self.config: Dict[str, Any] = config if config is not None else json_read(config_path)
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__(
debug_guilds=(self.config["bot"]["debug_guilds"] if self.config["debug"] else None),

View File

@@ -1 +0,0 @@
from .color import color_from_hex, hex_from_color

View File

@@ -1,35 +0,0 @@
from discord import Colour
def _int_from_hex(hex_string: str) -> int:
try:
return int(hex_string, base=16)
except Exception as exc:
raise ValueError("Input string must be a valid HEX code.") from exc
def _hex_from_int(color_int: int) -> str:
if not 0 <= color_int <= 0xFFFFFF:
raise ValueError("Color's value must be in the range 0 to 0xFFFFFF.")
return f"#{color_int:06x}"
def color_from_hex(hex_string: str) -> Colour:
"""Convert valid hexadecimal string to discord.Colour.
:param hex_string: Hexadecimal string to convert into Colour object
:type hex_string: str
:return: Colour object
"""
return Colour(_int_from_hex(hex_string))
def hex_from_color(color: Colour) -> str:
"""Convert discord.Colour to hexadecimal string.
:param color: Colour object to convert into the string
:type color: Colour
:return: Hexadecimal string in #XXXXXX format
"""
return _hex_from_int(color.value)

View File

@@ -1,2 +0,0 @@
# This file is left empty on purpose
# Adding imports here will cause import errors when libbot[pyrogram] is not installed

View File

@@ -2,7 +2,6 @@ import asyncio
import logging
import sys
from datetime import datetime, timedelta
from logging import Logger
from os import cpu_count, getpid
from pathlib import Path
from time import time
@@ -10,12 +9,6 @@ from typing import Any, Dict, List
from typing_extensions import override
from .command import PyroCommand
from .commandset import CommandSet
from ...i18n import _
from ...i18n.classes import BotLocale
from ...utils import json_read
try:
import pyrogram
from apscheduler.schedulers.asyncio import AsyncIOScheduler
@@ -42,33 +35,42 @@ try:
except ImportError:
from json import dumps, loads
logger: Logger = logging.getLogger(__name__)
from ...i18n.classes import BotLocale
from ...i18n import _
from .command import PyroCommand
from .commandset import CommandSet
logger = logging.getLogger(__name__)
class PyroClient(Client):
@override
def __init__(
self,
name: str = "bot_client",
owner: int | None = None,
config: Dict[str, Any] | None = None,
config_path: str | Path = Path("config.json"),
api_id: int | None = None,
api_hash: str | None = None,
bot_token: str | None = None,
workers: int = min(32, cpu_count() + 4),
locales_root: str | Path | None = None,
plugins_root: str = "plugins",
plugins_exclude: List[str] | None = None,
sleep_threshold: int = 120,
max_concurrent_transmissions: int = 1,
commands_source: Dict[str, dict] | None = None,
scoped_commands: bool | None = None,
i18n_bot_info: bool = False,
scheduler: AsyncIOScheduler | BackgroundScheduler | None = None,
**kwargs,
self,
name: str = "bot_client",
owner: int | None = None,
config: Dict[str, Any] | None = None,
config_path: str | Path = Path("config.json"),
api_id: int | None = None,
api_hash: str | None = None,
bot_token: str | None = None,
workers: int = min(32, cpu_count() + 4),
locales_root: str | Path | None = None,
plugins_root: str = "plugins",
plugins_exclude: List[str] | None = None,
sleep_threshold: int = 120,
max_concurrent_transmissions: int = 1,
commands_source: Dict[str, dict] | None = None,
scoped_commands: bool | None = None,
i18n_bot_info: bool = False,
scheduler: AsyncIOScheduler | BackgroundScheduler | None = None,
**kwargs,
):
self.config: Dict[str, Any] = config if config is not None else json_read(config_path)
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=name,
@@ -209,7 +211,7 @@ class PyroClient(Client):
@override
async def stop(
self, exit_completely: bool = True, scheduler_shutdown: bool = True, scheduler_wait: bool = True
self, exit_completely: bool = True, scheduler_shutdown: bool = True, scheduler_wait: bool = True
) -> None:
try:
await self.send_message(
@@ -308,7 +310,7 @@ class PyroClient(Client):
# in it, if there are any. Then adds them to self.commands
for handler in self.dispatcher.groups[0]:
if isinstance(handler, MessageHandler) and (
hasattr(handler.filters, "base") or hasattr(handler.filters, "other")
hasattr(handler.filters, "base") or hasattr(handler.filters, "other")
):
for entry in [handler.filters.base, handler.filters.other]:
if hasattr(entry, "commands"):
@@ -319,8 +321,8 @@ class PyroClient(Client):
return command_sets
def add_command(
self,
command: str,
self,
command: str,
) -> None:
"""Add command to the bot's internal commands list

View File

@@ -20,7 +20,7 @@ def config_get(key: str, *path: str, config_file: str | Path = DEFAULT_CONFIG_LO
### Args:
* key (`str`): Key that contains the value
* *path (`str`): Path to the key that contains the value (pass *[] or don't pass anything at all to get on the top/root level)
* *path (`str`): Path to the key that contains the value
* config_file (`str | Path`, *optional*): Path-like object or path as a string of a location of the config file. Defaults to `"config.json"`
### Returns:
@@ -59,7 +59,7 @@ async def config_get(key: str, *path: str, config_file: str | Path = DEFAULT_CON
### Args:
* key (`str`): Key that contains the value
* *path (`str`): Path to the key that contains the value (pass *[] or don't pass anything at all to get on the top/root level)
* *path (`str`): Path to the key that contains the value
* config_file (`str | Path`, *optional*): Path-like object or path as a string of a location of the config file. Defaults to `"config.json"`
### Returns:
@@ -98,7 +98,7 @@ def config_set(key: str, value: Any, *path: str, config_file: str | Path = DEFAU
### Args:
* key (`str`): Key that leads to the value
* value (`Any`): Any JSON serializable data
* *path (`str`): Path to the key of the target (pass *[] or don't pass anything at all to set on the top/root level)
* *path (`str`): Path to the key of the target
* config_file (`str | Path`, *optional*): Path-like object or path as a string of a location of the config file. Defaults to `"config.json"`
### Raises:
@@ -116,7 +116,7 @@ async def config_set(
### Args:
* key (`str`): Key that leads to the value
* value (`Any`): Any JSON serializable data
* *path (`str`): Path to the key of the target (pass *[] or don't pass anything at all to set on the top/root level)
* *path (`str`): Path to the key of the target
* config_file (`str | Path`, *optional*): Path-like object or path as a string of a location of the config file. Defaults to `"config.json"`
### Raises:
@@ -136,7 +136,7 @@ def config_delete(
### Args:
* key (`str`): Key to delete
* *path (`str`): Path to the key of the target (pass *[] or don't pass anything at all to delete on the top/root level)
* *path (`str`): Path to the key of the target
* missing_ok (`bool`): Do not raise an exception if the key is missing. Defaults to `False`
* config_file (`str | Path`, *optional*): Path-like object or path as a string of a location of the config file. Defaults to `"config.json"`
@@ -165,7 +165,7 @@ async def config_delete(
### Args:
* key (`str`): Key to delete
* *path (`str`): Path to the key of the target (pass *[] or don't pass anything at all to delete on the top/root level)
* *path (`str`): Path to the key of the target
* missing_ok (`bool`): Do not raise an exception if the key is missing. Defaults to `False`
* config_file (`str | Path`, *optional*): Path-like object or path as a string of a location of the config file. Defaults to `"config.json"`

View File

@@ -3,11 +3,11 @@ from typing import Any, Dict
from typing import Callable
def supports_argument(func: Callable[..., Any], arg_name: str) -> bool:
def supports_argument(func: Callable, arg_name: str) -> bool:
"""Check whether a function has a specific argument
### Args:
* func (`Callable[..., Any]`): Function to be inspected
* func (`Callable`): Function to be inspected
* arg_name (`str`): Argument to be checked
### Returns:
@@ -24,13 +24,11 @@ def supports_argument(func: Callable[..., Any], arg_name: str) -> bool:
return False
def nested_set(
target: Dict[str, Any], value: Any, *path: str, create_missing: bool = True
) -> Dict[str, Any]:
def nested_set(target: dict, value: Any, *path: str, create_missing=True) -> Dict[str, Any]:
"""Set the key by its path to the value
### Args:
* target (`Dict[str, Any]`): Dictionary to perform modifications on
* target (`dict`): Dictionary to perform modifications on
* value (`Any`): Any data
* *path (`str`): Path to the key of the target
* create_missing (`bool`, *optional*): Create keys on the way if they're missing. Defaults to `True`
@@ -41,29 +39,29 @@ def nested_set(
### Returns:
* `Dict[str, Any]`: Changed dictionary
"""
target_copy: Dict[str, Any] = target
d = target
for key in path[:-1]:
if key in target_copy:
target_copy = target_copy[key]
if key in d:
d = d[key]
elif create_missing:
target_copy = target_copy.setdefault(key, {})
d = d.setdefault(key, {})
else:
raise KeyError(
f"Key '{key}' is not found under path provided ({path}) and create_missing is False"
)
if path[-1] in target_copy or create_missing:
target_copy[path[-1]] = value
if path[-1] in d or create_missing:
d[path[-1]] = value
return target
def nested_delete(target: Dict[str, Any], *path: str) -> Dict[str, Any]:
def nested_delete(target: dict, *path: str) -> Dict[str, Any]:
"""Delete the key by its path
### Args:
* target (`Dict[str, Any]`): Dictionary to perform modifications on
* target (`dict`): Dictionary to perform modifications on
### Raises:
* `KeyError`: Key is not found under path provided
@@ -71,16 +69,16 @@ def nested_delete(target: Dict[str, Any], *path: str) -> Dict[str, Any]:
### Returns:
`Dict[str, Any]`: Changed dictionary
"""
target_copy: Dict[str, Any] = target
d = target
for key in path[:-1]:
if key in target_copy:
target_copy = target_copy[key]
if key in d:
d = d[key]
else:
raise KeyError(f"Key '{key}' is not found under path provided ({path})")
if path[-1] in target_copy:
del target_copy[path[-1]]
if path[-1] in d:
del d[path[-1]]
else:
raise KeyError(f"Key '{path[-1]}' is not found under path provided ({path})")