mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-23 19:44:20 +00:00
Merge branch '5.4-optional' into 5.4
# Conflicts: # royalnet/commands/commandinterface.py
This commit is contained in:
commit
026f1e5b6e
46 changed files with 311 additions and 318 deletions
|
@ -1,14 +1,5 @@
|
||||||
from . import alchemy, bard, commands, constellation, herald, backpack, serf, utils, version
|
from .version import semantic
|
||||||
|
|
||||||
__version__ = version.semantic
|
__version__ = semantic
|
||||||
|
|
||||||
__all__ = [
|
__all__ = []
|
||||||
"alchemy",
|
|
||||||
"bard",
|
|
||||||
"commands",
|
|
||||||
"constellation",
|
|
||||||
"herald",
|
|
||||||
"serf",
|
|
||||||
"utils",
|
|
||||||
"backpack",
|
|
||||||
]
|
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import click
|
import click
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import royalnet.constellation as rc
|
|
||||||
import royalnet.serf as rs
|
|
||||||
import royalnet.utils as ru
|
|
||||||
import royalnet.herald as rh
|
|
||||||
import toml
|
import toml
|
||||||
import logging
|
import logging
|
||||||
|
import royalnet.constellation as rc
|
||||||
|
import royalnet.serf.telegram as rst
|
||||||
|
import royalnet.serf.discord as rsd
|
||||||
|
import royalnet.serf.matrix as rsm
|
||||||
|
import royalnet.utils as ru
|
||||||
|
import royalnet.herald as rh
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import coloredlogs
|
import coloredlogs
|
||||||
|
@ -60,7 +62,7 @@ def run(config_filename: str):
|
||||||
telegram_process = None
|
telegram_process = None
|
||||||
if "Telegram" in config["Serfs"] and config["Serfs"]["Telegram"]["enabled"]:
|
if "Telegram" in config["Serfs"] and config["Serfs"]["Telegram"]["enabled"]:
|
||||||
telegram_process = multiprocessing.Process(name="Serf.Telegram",
|
telegram_process = multiprocessing.Process(name="Serf.Telegram",
|
||||||
target=rs.telegram.TelegramSerf.run_process,
|
target=rst.TelegramSerf.run_process,
|
||||||
daemon=True,
|
daemon=True,
|
||||||
kwargs={
|
kwargs={
|
||||||
"alchemy_cfg": config["Alchemy"],
|
"alchemy_cfg": config["Alchemy"],
|
||||||
|
@ -78,7 +80,7 @@ def run(config_filename: str):
|
||||||
discord_process = None
|
discord_process = None
|
||||||
if "Discord" in config["Serfs"] and config["Serfs"]["Discord"]["enabled"]:
|
if "Discord" in config["Serfs"] and config["Serfs"]["Discord"]["enabled"]:
|
||||||
discord_process = multiprocessing.Process(name="Serf.Discord",
|
discord_process = multiprocessing.Process(name="Serf.Discord",
|
||||||
target=rs.discord.DiscordSerf.run_process,
|
target=rsd.DiscordSerf.run_process,
|
||||||
daemon=True,
|
daemon=True,
|
||||||
kwargs={
|
kwargs={
|
||||||
"alchemy_cfg": config["Alchemy"],
|
"alchemy_cfg": config["Alchemy"],
|
||||||
|
@ -96,7 +98,7 @@ def run(config_filename: str):
|
||||||
matrix_process = None
|
matrix_process = None
|
||||||
if "Matrix" in config["Serfs"] and config["Serfs"]["Matrix"]["enabled"]:
|
if "Matrix" in config["Serfs"] and config["Serfs"]["Matrix"]["enabled"]:
|
||||||
matrix_process = multiprocessing.Process(name="Serf.Matrix",
|
matrix_process = multiprocessing.Process(name="Serf.Matrix",
|
||||||
target=rs.matrix.MatrixSerf.run_process,
|
target=rsm.MatrixSerf.run_process,
|
||||||
daemon=True,
|
daemon=True,
|
||||||
kwargs={
|
kwargs={
|
||||||
"alchemy_cfg": config["Alchemy"],
|
"alchemy_cfg": config["Alchemy"],
|
||||||
|
|
15
royalnet/alchemy/README.md
Normal file
15
royalnet/alchemy/README.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# `royalnet.alchemy`
|
||||||
|
|
||||||
|
The subpackage providing all functions and classes related to databases and tables.
|
||||||
|
|
||||||
|
It requires either the `alchemy_easy` or the `alchemy_hard` extras to be installed.
|
||||||
|
|
||||||
|
You can install `alchemy_easy` with:
|
||||||
|
```
|
||||||
|
pip install royalnet[alchemy_easy]
|
||||||
|
```
|
||||||
|
To install `alchemy_hard`, refer to the [`psycopg2`](https://pypi.org/project/psycopg2/) installation instructions,
|
||||||
|
then run:
|
||||||
|
```
|
||||||
|
pip install royalnet[alchemy_hard]
|
||||||
|
```
|
|
@ -1,4 +1,17 @@
|
||||||
"""Relational database classes and methods."""
|
"""The subpackage providing all functions and classes related to databases and tables.
|
||||||
|
|
||||||
|
It requires either the ``alchemy_easy`` or the ``alchemy_hard`` extras to be installed.
|
||||||
|
|
||||||
|
You can install ``alchemy_easy`` with: ::
|
||||||
|
|
||||||
|
pip install royalnet[alchemy_easy]
|
||||||
|
|
||||||
|
To install ``alchemy_hard``, refer to the `psycopg2 <https://pypi.org/project/psycopg2/>}`_ installation instructions,
|
||||||
|
then run: ::
|
||||||
|
|
||||||
|
pip install royalnet[alchemy_hard]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
from .alchemy import Alchemy
|
from .alchemy import Alchemy
|
||||||
from .table_dfs import table_dfs
|
from .table_dfs import table_dfs
|
||||||
|
@ -8,5 +21,5 @@ __all__ = [
|
||||||
"Alchemy",
|
"Alchemy",
|
||||||
"table_dfs",
|
"table_dfs",
|
||||||
"AlchemyException",
|
"AlchemyException",
|
||||||
"TableNotFoundError"
|
"TableNotFoundError",
|
||||||
]
|
]
|
||||||
|
|
|
@ -2,21 +2,12 @@ from typing import Set, Dict, Union
|
||||||
from contextlib import contextmanager, asynccontextmanager
|
from contextlib import contextmanager, asynccontextmanager
|
||||||
from royalnet.utils import asyncify
|
from royalnet.utils import asyncify
|
||||||
from royalnet.alchemy.errors import TableNotFoundError
|
from royalnet.alchemy.errors import TableNotFoundError
|
||||||
|
from sqlalchemy import create_engine
|
||||||
try:
|
from sqlalchemy.engine import Engine
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy.schema import Table
|
||||||
from sqlalchemy.engine import Engine
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.schema import Table
|
from sqlalchemy.ext.declarative.api import DeclarativeMeta
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.orm import sessionmaker
|
||||||
from sqlalchemy.ext.declarative.api import DeclarativeMeta
|
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
except ImportError:
|
|
||||||
create_engine = None
|
|
||||||
Engine = None
|
|
||||||
Table = None
|
|
||||||
declarative_base = None
|
|
||||||
DeclarativeMeta = None
|
|
||||||
sessionmaker = None
|
|
||||||
|
|
||||||
|
|
||||||
class Alchemy:
|
class Alchemy:
|
||||||
|
@ -31,9 +22,6 @@ class Alchemy:
|
||||||
tables: The :class:`set` of tables to be created and used in the selected database.
|
tables: The :class:`set` of tables to be created and used in the selected database.
|
||||||
Check the tables submodule for more details.
|
Check the tables submodule for more details.
|
||||||
"""
|
"""
|
||||||
if create_engine is None:
|
|
||||||
raise ImportError("'alchemy' extra is not installed")
|
|
||||||
|
|
||||||
if database_uri.startswith("sqlite"):
|
if database_uri.startswith("sqlite"):
|
||||||
raise NotImplementedError("sqlite databases aren't supported, as they can't be used in multithreaded"
|
raise NotImplementedError("sqlite databases aren't supported, as they can't be used in multithreaded"
|
||||||
" applications")
|
" applications")
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
try:
|
from sqlalchemy.inspection import inspect
|
||||||
from sqlalchemy.inspection import inspect
|
from sqlalchemy.schema import Table
|
||||||
from sqlalchemy.schema import Table
|
|
||||||
except ImportError:
|
|
||||||
inspect = None
|
|
||||||
Table = None
|
|
||||||
|
|
||||||
|
|
||||||
def table_dfs(starting_table: Table, ending_table: Table) -> tuple:
|
def table_dfs(starting_table: Table, ending_table: Table) -> tuple:
|
||||||
|
@ -11,9 +7,6 @@ def table_dfs(starting_table: Table, ending_table: Table) -> tuple:
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A :class:`tuple` containing the path, starting from the starting table and ending at the ending table."""
|
A :class:`tuple` containing the path, starting from the starting table and ending at the ending table."""
|
||||||
if inspect is None:
|
|
||||||
raise ImportError("'alchemy' extra is not installed")
|
|
||||||
|
|
||||||
inspected = set()
|
inspected = set()
|
||||||
|
|
||||||
def search(_mapper, chain):
|
def search(_mapper, chain):
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
"""A Pack that is imported by default by all Royalnet instances.
|
"""A Pack that is imported by default by all Royalnet instances."""
|
||||||
|
|
||||||
Keep things here to a minimum!"""
|
|
||||||
|
|
||||||
from . import commands, tables, stars, events
|
from . import commands, tables, stars, events
|
||||||
from .commands import available_commands
|
from .commands import available_commands
|
||||||
|
|
10
royalnet/bard/README.md
Normal file
10
royalnet/bard/README.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# `royalnet.bard`
|
||||||
|
|
||||||
|
The subpackage providing all classes related to music files.
|
||||||
|
|
||||||
|
It requires the `bard` extra to be installed (the [`ffmpeg_python`](https://pypi.org/project/ffmpeg-python/), [`youtube_dl`](https://pypi.org/project/youtube_dl/) and [`eyed3`](https://pypi.org/project/eyeD3/) packages).
|
||||||
|
|
||||||
|
You can install it with:
|
||||||
|
```
|
||||||
|
pip install royalnet[bard]
|
||||||
|
```
|
|
@ -1,18 +1,29 @@
|
||||||
from .ytdlinfo import YtdlInfo
|
"""The subpackage providing all classes related to music files.
|
||||||
from .ytdlfile import YtdlFile
|
|
||||||
from .ytdlmp3 import YtdlMp3
|
It requires the ``bard`` extra to be installed (the :mod:`ffmpeg_python`, :mod:`youtube_dl` and :mod:`eyed3` packages).
|
||||||
from .ytdldiscord import YtdlDiscord
|
|
||||||
|
You can install it with: ::
|
||||||
|
|
||||||
|
pip install royalnet[bard]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .fileaudiosource import FileAudioSource
|
import ffmpeg
|
||||||
|
import youtube_dl
|
||||||
|
import eyed3
|
||||||
except ImportError:
|
except ImportError:
|
||||||
FileAudioSource = None
|
raise ImportError("The `bard` extra is not installed. Please install it with `pip install royalnet[bard]`.")
|
||||||
|
|
||||||
|
from .ytdlinfo import YtdlInfo
|
||||||
|
from .ytdlfile import YtdlFile
|
||||||
|
from .errors import BardError, YtdlError, NotFoundError, MultipleFilesError
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"YtdlInfo",
|
"YtdlInfo",
|
||||||
"YtdlFile",
|
"YtdlFile",
|
||||||
"YtdlMp3",
|
"BardError",
|
||||||
"YtdlDiscord",
|
"YtdlError",
|
||||||
"FileAudioSource",
|
"NotFoundError",
|
||||||
|
"MultipleFilesError",
|
||||||
]
|
]
|
||||||
|
|
10
royalnet/bard/discord/README.md
Normal file
10
royalnet/bard/discord/README.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# `royalnet.bard.discord`
|
||||||
|
|
||||||
|
The subpackage providing all functions and classes related to music playback on Discord.
|
||||||
|
|
||||||
|
It requires the both the ``bard`` and ``discord`` extras to be installed.
|
||||||
|
|
||||||
|
You can install them with:
|
||||||
|
```
|
||||||
|
pip install royalnet[bard,discord]
|
||||||
|
```
|
17
royalnet/bard/discord/__init__.py
Normal file
17
royalnet/bard/discord/__init__.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
"""The subpackage providing all functions and classes related to music playback on Discord.
|
||||||
|
|
||||||
|
It requires the both the ``bard`` and ``discord`` extras to be installed.
|
||||||
|
|
||||||
|
You can install them with: ::
|
||||||
|
|
||||||
|
pip install royalnet[bard,discord]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .ytdldiscord import YtdlDiscord
|
||||||
|
from .fileaudiosource import FileAudioSource
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"YtdlDiscord",
|
||||||
|
"FileAudioSource",
|
||||||
|
]
|
|
@ -1,7 +1,4 @@
|
||||||
try:
|
import discord
|
||||||
import discord
|
|
||||||
except ImportError:
|
|
||||||
discord = None
|
|
||||||
|
|
||||||
|
|
||||||
class FileAudioSource(discord.AudioSource):
|
class FileAudioSource(discord.AudioSource):
|
|
@ -2,19 +2,12 @@ import typing
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
import ffmpeg
|
||||||
|
import discord
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from royalnet.utils import asyncify, MultiLock, FileAudioSource
|
from royalnet.utils import asyncify, MultiLock
|
||||||
from royalnet.bard import YtdlInfo, YtdlFile
|
from royalnet.bard import YtdlInfo, YtdlFile
|
||||||
|
from .fileaudiosource import FileAudioSource
|
||||||
try:
|
|
||||||
import ffmpeg
|
|
||||||
except ImportError:
|
|
||||||
ffmpeg = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
import discord
|
|
||||||
except ImportError:
|
|
||||||
discord = None
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -44,8 +37,6 @@ class YtdlDiscord:
|
||||||
|
|
||||||
async def convert_to_pcm(self) -> None:
|
async def convert_to_pcm(self) -> None:
|
||||||
"""Convert the file to pcm with :mod:`ffmpeg`."""
|
"""Convert the file to pcm with :mod:`ffmpeg`."""
|
||||||
if ffmpeg is None:
|
|
||||||
raise ImportError("'bard' extra is not installed")
|
|
||||||
await self.ytdl_file.download_file()
|
await self.ytdl_file.download_file()
|
||||||
if self.pcm_filename is None:
|
if self.pcm_filename is None:
|
||||||
async with self.ytdl_file.lock.normal():
|
async with self.ytdl_file.lock.normal():
|
||||||
|
@ -87,8 +78,6 @@ class YtdlDiscord:
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def spawn_audiosource(self):
|
async def spawn_audiosource(self):
|
||||||
log.debug(f"Spawning audio_source for: {self}")
|
log.debug(f"Spawning audio_source for: {self}")
|
||||||
if FileAudioSource is None:
|
|
||||||
raise ImportError("'discord' extra is not installed")
|
|
||||||
await self.convert_to_pcm()
|
await self.convert_to_pcm()
|
||||||
async with self.lock.normal():
|
async with self.lock.normal():
|
||||||
with open(self.pcm_filename, "rb") as stream:
|
with open(self.pcm_filename, "rb") as stream:
|
||||||
|
@ -97,8 +86,6 @@ class YtdlDiscord:
|
||||||
|
|
||||||
def embed(self) -> "discord.Embed":
|
def embed(self) -> "discord.Embed":
|
||||||
"""Return this info as a :py:class:`discord.Embed`."""
|
"""Return this info as a :py:class:`discord.Embed`."""
|
||||||
if discord is None:
|
|
||||||
raise ImportError("'discord' extra is not installed")
|
|
||||||
colors = {
|
colors = {
|
||||||
"youtube": 0xCC0000,
|
"youtube": 0xCC0000,
|
||||||
"soundcloud": 0xFF5400,
|
"soundcloud": 0xFF5400,
|
|
@ -8,11 +8,7 @@ from royalnet.utils import *
|
||||||
from asyncio import AbstractEventLoop, get_event_loop
|
from asyncio import AbstractEventLoop, get_event_loop
|
||||||
from .ytdlinfo import YtdlInfo
|
from .ytdlinfo import YtdlInfo
|
||||||
from .errors import NotFoundError, MultipleFilesError
|
from .errors import NotFoundError, MultipleFilesError
|
||||||
|
from youtube_dl import YoutubeDL
|
||||||
try:
|
|
||||||
from youtube_dl import YoutubeDL
|
|
||||||
except ImportError:
|
|
||||||
YoutubeDL = None
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -78,9 +74,6 @@ class YtdlFile:
|
||||||
|
|
||||||
async def download_file(self) -> None:
|
async def download_file(self) -> None:
|
||||||
"""Download the file."""
|
"""Download the file."""
|
||||||
if YoutubeDL is None:
|
|
||||||
raise ImportError("'bard' extra is not installed")
|
|
||||||
|
|
||||||
def download():
|
def download():
|
||||||
"""Download function block to be asyncified."""
|
"""Download function block to be asyncified."""
|
||||||
with YoutubeDL(self.ytdl_args) as ytdl:
|
with YoutubeDL(self.ytdl_args) as ytdl:
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
from asyncio import AbstractEventLoop, get_event_loop
|
from typing import *
|
||||||
from typing import Optional, Dict, List, Any
|
import asyncio as aio
|
||||||
from datetime import datetime, timedelta
|
import datetime
|
||||||
import dateparser
|
import dateparser
|
||||||
import logging
|
import logging
|
||||||
from royalnet.utils import ytdldateformat, asyncify
|
import royalnet.utils as ru
|
||||||
|
import youtube_dl
|
||||||
try:
|
|
||||||
from youtube_dl import YoutubeDL
|
|
||||||
except ImportError:
|
|
||||||
YoutubeDL = None
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -36,7 +32,7 @@ class YtdlInfo:
|
||||||
self.uploader_url: Optional[str] = info.get("uploader_url")
|
self.uploader_url: Optional[str] = info.get("uploader_url")
|
||||||
self.channel_id: Optional[str] = info.get("channel_id")
|
self.channel_id: Optional[str] = info.get("channel_id")
|
||||||
self.channel_url: Optional[str] = info.get("channel_url")
|
self.channel_url: Optional[str] = info.get("channel_url")
|
||||||
self.upload_date: Optional[datetime] = dateparser.parse(ytdldateformat(info.get("upload_date")))
|
self.upload_date: Optional[datetime.datetime] = dateparser.parse(ru.ytdldateformat(info.get("upload_date")))
|
||||||
self.license: Optional[str] = info.get("license")
|
self.license: Optional[str] = info.get("license")
|
||||||
self.creator: Optional[...] = info.get("creator")
|
self.creator: Optional[...] = info.get("creator")
|
||||||
self.title: Optional[str] = info.get("title")
|
self.title: Optional[str] = info.get("title")
|
||||||
|
@ -47,7 +43,7 @@ class YtdlInfo:
|
||||||
self.tags: Optional[List[str]] = info.get("tags")
|
self.tags: Optional[List[str]] = info.get("tags")
|
||||||
self.subtitles: Optional[Dict[str, List[Dict[str, str]]]] = info.get("subtitles")
|
self.subtitles: Optional[Dict[str, List[Dict[str, str]]]] = info.get("subtitles")
|
||||||
self.automatic_captions: Optional[dict] = info.get("automatic_captions")
|
self.automatic_captions: Optional[dict] = info.get("automatic_captions")
|
||||||
self.duration: Optional[timedelta] = timedelta(seconds=info.get("duration", 0))
|
self.duration: Optional[datetime.timedelta] = datetime.timedelta(seconds=info.get("duration", 0))
|
||||||
self.age_limit: Optional[int] = info.get("age_limit")
|
self.age_limit: Optional[int] = info.get("age_limit")
|
||||||
self.annotations: Optional[...] = info.get("annotations")
|
self.annotations: Optional[...] = info.get("annotations")
|
||||||
self.chapters: Optional[...] = info.get("chapters")
|
self.chapters: Optional[...] = info.get("chapters")
|
||||||
|
@ -90,20 +86,17 @@ class YtdlInfo:
|
||||||
self.album: Optional[str] = None
|
self.album: Optional[str] = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def from_url(cls, url, loop: Optional[AbstractEventLoop] = None, **ytdl_args) -> List["YtdlInfo"]:
|
async def from_url(cls, url, loop: Optional[aio.AbstractEventLoop] = None, **ytdl_args) -> List["YtdlInfo"]:
|
||||||
"""Fetch the info for an url through :class:`YoutubeDL`.
|
"""Fetch the info for an url through :class:`YoutubeDL`.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A :class:`list` containing the infos for the requested videos."""
|
A :class:`list` containing the infos for the requested videos."""
|
||||||
if YoutubeDL is None:
|
|
||||||
raise ImportError("'bard' extra is not installed")
|
|
||||||
|
|
||||||
if loop is None:
|
if loop is None:
|
||||||
loop: AbstractEventLoop = get_event_loop()
|
loop: aio.AbstractEventLoop = aio.get_event_loop()
|
||||||
# So many redundant options!
|
# So many redundant options!
|
||||||
log.debug(f"Fetching info: {url}")
|
log.debug(f"Fetching info: {url}")
|
||||||
with YoutubeDL({**cls._default_ytdl_args, **ytdl_args}) as ytdl:
|
with youtube_dl.YoutubeDL({**cls._default_ytdl_args, **ytdl_args}) as ytdl:
|
||||||
first_info = await asyncify(ytdl.extract_info, loop=loop, url=url, download=False)
|
first_info = await ru.asyncify(ytdl.extract_info, loop=loop, url=url, download=False)
|
||||||
# No video was found
|
# No video was found
|
||||||
if first_info is None:
|
if first_info is None:
|
||||||
return []
|
return []
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
import typing
|
|
||||||
import re
|
|
||||||
import os
|
|
||||||
from royalnet.utils import asyncify, MultiLock
|
|
||||||
from .ytdlinfo import YtdlInfo
|
|
||||||
from .ytdlfile import YtdlFile
|
|
||||||
|
|
||||||
try:
|
|
||||||
import ffmpeg
|
|
||||||
except ImportError:
|
|
||||||
ffmpeg = None
|
|
||||||
|
|
||||||
|
|
||||||
class YtdlMp3:
|
|
||||||
"""A representation of a :class:`YtdlFile` conversion to mp3."""
|
|
||||||
def __init__(self, ytdl_file: YtdlFile):
|
|
||||||
self.ytdl_file: YtdlFile = ytdl_file
|
|
||||||
self.mp3_filename: typing.Optional[str] = None
|
|
||||||
self.lock: MultiLock = MultiLock()
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
if not self.ytdl_file.has_info:
|
|
||||||
return f"<{self.__class__.__qualname__} without info>"
|
|
||||||
elif not self.ytdl_file.is_downloaded:
|
|
||||||
return f"<{self.__class__.__qualname__} not downloaded>"
|
|
||||||
elif not self.is_converted:
|
|
||||||
return f"<{self.__class__.__qualname__} at '{self.ytdl_file.filename}' not converted>"
|
|
||||||
else:
|
|
||||||
return f"<{self.__class__.__qualname__} at '{self.mp3_filename}'>"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_converted(self):
|
|
||||||
"""Has the file been converted?"""
|
|
||||||
return self.mp3_filename is not None
|
|
||||||
|
|
||||||
async def convert_to_mp3(self) -> None:
|
|
||||||
"""Convert the file to mp3 with :mod:`ffmpeg`."""
|
|
||||||
if ffmpeg is None:
|
|
||||||
raise ImportError("'bard' extra is not installed")
|
|
||||||
await self.ytdl_file.download_file()
|
|
||||||
if self.mp3_filename is None:
|
|
||||||
async with self.ytdl_file.lock.normal():
|
|
||||||
destination_filename = re.sub(r"\.[^.]+$", ".mp3", self.ytdl_file.filename)
|
|
||||||
async with self.lock.exclusive():
|
|
||||||
await asyncify(
|
|
||||||
ffmpeg.input(self.ytdl_file.filename)
|
|
||||||
.output(destination_filename, format="mp3")
|
|
||||||
.overwrite_output()
|
|
||||||
.run
|
|
||||||
)
|
|
||||||
self.mp3_filename = destination_filename
|
|
||||||
|
|
||||||
async def delete_asap(self) -> None:
|
|
||||||
"""Delete the mp3 file."""
|
|
||||||
if self.is_converted:
|
|
||||||
async with self.lock.exclusive():
|
|
||||||
os.remove(self.mp3_filename)
|
|
||||||
self.mp3_filename = None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def from_url(cls, url, **ytdl_args) -> typing.List["YtdlMp3"]:
|
|
||||||
"""Create a :class:`list` of :class:`YtdlMp3` from a URL."""
|
|
||||||
files = await YtdlFile.from_url(url, **ytdl_args)
|
|
||||||
dfiles = []
|
|
||||||
for file in files:
|
|
||||||
dfile = YtdlMp3(file)
|
|
||||||
dfiles.append(dfile)
|
|
||||||
return dfiles
|
|
||||||
|
|
||||||
@property
|
|
||||||
def info(self) -> typing.Optional[YtdlInfo]:
|
|
||||||
"""Shortcut to get the :class:`YtdlInfo` of the object."""
|
|
||||||
return self.ytdl_file.info
|
|
3
royalnet/commands/README.md
Normal file
3
royalnet/commands/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# `royalnet.commands`
|
||||||
|
|
||||||
|
The subpackage providing all classes related to Royalnet commands.
|
|
@ -1,15 +1,12 @@
|
||||||
|
"""The subpackage providing all classes related to Royalnet commands."""
|
||||||
|
|
||||||
from .commandinterface import CommandInterface
|
from .commandinterface import CommandInterface
|
||||||
from .command import Command
|
from .command import Command
|
||||||
from .commanddata import CommandData
|
from .commanddata import CommandData
|
||||||
from .commandargs import CommandArgs
|
from .commandargs import CommandArgs
|
||||||
from .event import Event
|
from .event import Event
|
||||||
from .errors import CommandError, \
|
from .errors import \
|
||||||
InvalidInputError, \
|
CommandError, InvalidInputError, UnsupportedError, ConfigurationError, ExternalError, UserError, ProgramError
|
||||||
UnsupportedError, \
|
|
||||||
ConfigurationError, \
|
|
||||||
ExternalError, \
|
|
||||||
UserError, \
|
|
||||||
ProgramError
|
|
||||||
from .keyboardkey import KeyboardKey
|
from .keyboardkey import KeyboardKey
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import typing
|
from typing import *
|
||||||
from .commandinterface import CommandInterface
|
from .commandinterface import CommandInterface
|
||||||
from .commandargs import CommandArgs
|
from .commandargs import CommandArgs
|
||||||
from .commanddata import CommandData
|
from .commanddata import CommandData
|
||||||
|
@ -11,7 +11,7 @@ class Command:
|
||||||
Example:
|
Example:
|
||||||
To be able to call ``/example`` on Telegram, the name should be ``"example"``."""
|
To be able to call ``/example`` on Telegram, the name should be ``"example"``."""
|
||||||
|
|
||||||
aliases: typing.List[str] = []
|
aliases: List[str] = []
|
||||||
"""A list of possible aliases for a command.
|
"""A list of possible aliases for a command.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import re
|
import re
|
||||||
from typing import Pattern, AnyStr, Optional, Sequence, Union
|
import typing
|
||||||
from .errors import InvalidInputError
|
from .errors import InvalidInputError
|
||||||
|
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ class CommandArgs(list):
|
||||||
raise InvalidInputError(f"Not enough arguments specified (minimum is {require_at_least}).")
|
raise InvalidInputError(f"Not enough arguments specified (minimum is {require_at_least}).")
|
||||||
return " ".join(self)
|
return " ".join(self)
|
||||||
|
|
||||||
def match(self, pattern: Union[str, Pattern], *flags) -> Sequence[AnyStr]:
|
def match(self, pattern: typing.Union[str, typing.Pattern], *flags) -> typing.Sequence[typing.AnyStr]:
|
||||||
"""Match the :meth:`.joined` string to a :class:`re.Pattern`-like object.
|
"""Match the :meth:`.joined` string to a :class:`re.Pattern`-like object.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
|
@ -83,7 +83,7 @@ class CommandArgs(list):
|
||||||
raise InvalidInputError("Invalid syntax.")
|
raise InvalidInputError("Invalid syntax.")
|
||||||
return match.groups()
|
return match.groups()
|
||||||
|
|
||||||
def optional(self, index: int, default=None) -> Optional[str]:
|
def optional(self, index: int, default=None) -> typing.Optional[str]:
|
||||||
"""Get the argument at a specific index, but don't raise an error if nothing is found, instead returning the
|
"""Get the argument at a specific index, but don't raise an error if nothing is found, instead returning the
|
||||||
``default`` value.
|
``default`` value.
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
|
from typing import *
|
||||||
import contextlib
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
import asyncio as aio
|
import asyncio as aio
|
||||||
from typing import *
|
import royalnet.utils as ru
|
||||||
from sqlalchemy.orm.session import Session
|
|
||||||
from .errors import UnsupportedError
|
from .errors import UnsupportedError
|
||||||
from .commandinterface import CommandInterface
|
from .commandinterface import CommandInterface
|
||||||
import royalnet.utils as ru
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .keyboardkey import KeyboardKey
|
from .keyboardkey import KeyboardKey
|
||||||
|
|
||||||
|
@ -14,26 +14,28 @@ log = logging.getLogger(__name__)
|
||||||
|
|
||||||
class CommandData:
|
class CommandData:
|
||||||
def __init__(self, interface: CommandInterface, loop: aio.AbstractEventLoop):
|
def __init__(self, interface: CommandInterface, loop: aio.AbstractEventLoop):
|
||||||
self._interface: CommandInterface = interface
|
|
||||||
self.loop: aio.AbstractEventLoop = loop
|
self.loop: aio.AbstractEventLoop = loop
|
||||||
|
self._interface: CommandInterface = interface
|
||||||
self._session = None
|
self._session = None
|
||||||
|
|
||||||
|
# TODO: make this asyncronous... somehow?
|
||||||
@property
|
@property
|
||||||
def session(self):
|
def session(self):
|
||||||
if self._session is None:
|
if self._session is None:
|
||||||
if self._interface.alchemy is None:
|
if self._interface.alchemy is None:
|
||||||
raise UnsupportedError("'alchemy' is not enabled on this Royalnet instance")
|
raise UnsupportedError("'alchemy' is not enabled on this Royalnet instance")
|
||||||
# FIXME: this may take a while
|
|
||||||
self._session = self._interface.alchemy.Session()
|
self._session = self._interface.alchemy.Session()
|
||||||
return self._session
|
return self._session
|
||||||
|
|
||||||
async def session_commit(self):
|
async def session_commit(self):
|
||||||
|
"""Asyncronously commit the :attr:`.session` of this object."""
|
||||||
if self._session:
|
if self._session:
|
||||||
log.warning("Session had to be created to be committed")
|
log.warning("Session had to be created to be committed")
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
await ru.asyncify(self.session.commit)
|
await ru.asyncify(self.session.commit)
|
||||||
|
|
||||||
async def session_close(self):
|
async def session_close(self):
|
||||||
|
"""Asyncronously close the :attr:`.session` of this object."""
|
||||||
if self._session is not None:
|
if self._session is not None:
|
||||||
await ru.asyncify(self._session.close)
|
await ru.asyncify(self._session.close)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from typing import *
|
from typing import *
|
||||||
import asyncio as aio
|
import asyncio as aio
|
||||||
from .errors import *
|
from .errors import UnsupportedError
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .event import Event
|
from .event import Event
|
||||||
from .command import Command
|
from .command import Command
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
import asyncio as aio
|
||||||
from .commandinterface import CommandInterface
|
from .commandinterface import CommandInterface
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from serf import Serf
|
from serf import Serf
|
||||||
|
|
||||||
|
@ -16,7 +18,7 @@ class Event:
|
||||||
"""The :class:`CommandInterface` available to this :class:`Event`."""
|
"""The :class:`CommandInterface` available to this :class:`Event`."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serf(self):
|
def serf(self) -> "Serf":
|
||||||
"""A shortcut for :attr:`.interface.serf`."""
|
"""A shortcut for :attr:`.interface.serf`."""
|
||||||
return self.interface.serf
|
return self.interface.serf
|
||||||
|
|
||||||
|
@ -26,12 +28,12 @@ class Event:
|
||||||
return self.interface.alchemy
|
return self.interface.alchemy
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def loop(self):
|
def loop(self) -> aio.AbstractEventLoop:
|
||||||
"""A shortcut for :attr:`.interface.loop`."""
|
"""A shortcut for :attr:`.interface.loop`."""
|
||||||
return self.interface.loop
|
return self.interface.loop
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config(self):
|
def config(self) -> dict:
|
||||||
"""A shortcut for :attr:`.interface.config`."""
|
"""A shortcut for :attr:`.interface.config`."""
|
||||||
return self.interface.config
|
return self.interface.config
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
# `royalnet.constellation`
|
# `royalnet.constellation`
|
||||||
|
|
||||||
The part of `royalnet` that handles the webserver and webpages.
|
The subpackage providing all functions and classes that handle the webserver and the webpages.
|
||||||
|
|
||||||
It uses many features of [`starlette`](https://www.starlette.io).
|
It requires the `constellation` extra to be installed ([`starlette`](https://github.com/encode/starlette)).
|
||||||
|
|
||||||
## Hierarchy
|
You can install it with:
|
||||||
|
```
|
||||||
|
pip install royalnet[constellation]
|
||||||
|
```
|
||||||
|
|
||||||
- `constellation`
|
It optionally uses the `sentry` extra.
|
||||||
- `star`
|
|
||||||
- `shoot`
|
You can install it with:
|
||||||
|
```
|
||||||
|
pip install royalnet[constellation,sentry]
|
||||||
|
```
|
|
@ -1,6 +1,18 @@
|
||||||
"""The part of :mod:`royalnet` that handles the webserver and webpages.
|
"""The subpackage providing all functions and classes that handle the webserver and the webpages.
|
||||||
|
|
||||||
It uses many features of :mod:`starlette`."""
|
It requires the ``constellation`` extra to be installed (:mod:`starlette`).
|
||||||
|
|
||||||
|
You can install it with: ::
|
||||||
|
|
||||||
|
pip install royalnet[constellation]
|
||||||
|
|
||||||
|
It optionally uses the ``sentry`` extra for error reporting.
|
||||||
|
|
||||||
|
You can install them with: ::
|
||||||
|
|
||||||
|
pip install royalnet[constellation,sentry]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
from .constellation import Constellation
|
from .constellation import Constellation
|
||||||
from .star import Star, PageStar, ExceptionStar
|
from .star import Star, PageStar, ExceptionStar
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
from typing import *
|
||||||
|
import asyncio as aio
|
||||||
import logging
|
import logging
|
||||||
import importlib
|
import importlib
|
||||||
import asyncio as aio
|
import uvicorn
|
||||||
from typing import *
|
import starlette.applications
|
||||||
import royalnet.alchemy as ra
|
import royalnet.alchemy as ra
|
||||||
import royalnet.herald as rh
|
import royalnet.herald as rh
|
||||||
import royalnet.utils as ru
|
import royalnet.utils as ru
|
||||||
|
@ -9,26 +11,6 @@ import royalnet.commands as rc
|
||||||
from .star import PageStar, ExceptionStar
|
from .star import PageStar, ExceptionStar
|
||||||
from ..utils import init_logging
|
from ..utils import init_logging
|
||||||
|
|
||||||
try:
|
|
||||||
import uvicorn
|
|
||||||
from starlette.applications import Starlette
|
|
||||||
except ImportError:
|
|
||||||
uvicorn = None
|
|
||||||
Starlette = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
import sentry_sdk
|
|
||||||
except ImportError:
|
|
||||||
sentry_sdk = None
|
|
||||||
AioHttpIntegration = None
|
|
||||||
SqlalchemyIntegration = None
|
|
||||||
LoggingIntegration = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
import coloredlogs
|
|
||||||
except ImportError:
|
|
||||||
coloredlogs = None
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -54,9 +36,6 @@ class Constellation:
|
||||||
constellation_cfg: Dict[str, Any],
|
constellation_cfg: Dict[str, Any],
|
||||||
logging_cfg: Dict[str, Any]
|
logging_cfg: Dict[str, Any]
|
||||||
):
|
):
|
||||||
if Starlette is None:
|
|
||||||
raise ImportError("`constellation` extra is not installed")
|
|
||||||
|
|
||||||
# Import packs
|
# Import packs
|
||||||
pack_names = packs_cfg["active"]
|
pack_names = packs_cfg["active"]
|
||||||
packs = {}
|
packs = {}
|
||||||
|
@ -112,7 +91,7 @@ class Constellation:
|
||||||
self.events: Dict[str, rc.Event] = {}
|
self.events: Dict[str, rc.Event] = {}
|
||||||
"""A dictionary containing all :class:`~rc.Event` that can be handled by this :class:`Constellation`."""
|
"""A dictionary containing all :class:`~rc.Event` that can be handled by this :class:`Constellation`."""
|
||||||
|
|
||||||
self.starlette = Starlette(debug=__debug__)
|
self.starlette = starlette.applications.Starlette(debug=__debug__)
|
||||||
"""The :class:`~starlette.Starlette` app."""
|
"""The :class:`~starlette.Starlette` app."""
|
||||||
|
|
||||||
# Register Events
|
# Register Events
|
||||||
|
|
12
royalnet/herald/README.md
Normal file
12
royalnet/herald/README.md
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# `royalnet.herald`
|
||||||
|
|
||||||
|
The subpackage providing all functions and classes to handle communication between process (even over the Internet).
|
||||||
|
|
||||||
|
It is based on [`websockets`](https://github.com/websockets).
|
||||||
|
|
||||||
|
It requires the `herald` extra to be installed.
|
||||||
|
|
||||||
|
You can install it with:
|
||||||
|
```
|
||||||
|
pip install royalnet[herald]
|
||||||
|
```
|
|
@ -1,5 +1,17 @@
|
||||||
|
"""The subpackage providing all functions and classes to handle communication between process (even over the Internet).
|
||||||
|
|
||||||
|
It is based on :mod:`websockets`.
|
||||||
|
|
||||||
|
It requires the ``herald`` extra to be installed.
|
||||||
|
|
||||||
|
You can install it with: ::
|
||||||
|
|
||||||
|
pip install royalnet[herald]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .errors import HeraldError, ConnectionClosedError, LinkError, InvalidServerResponseError, ServerError
|
from .errors import *
|
||||||
from .link import Link
|
from .link import Link
|
||||||
from .package import Package
|
from .package import Package
|
||||||
from .request import Request
|
from .request import Request
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import typing
|
from typing import *
|
||||||
|
|
||||||
|
|
||||||
class Broadcast:
|
class Broadcast:
|
||||||
def __init__(self, handler: str, data: dict, msg_type: typing.Optional[str] = None):
|
def __init__(self, handler: str, data: dict, msg_type: Optional[str] = None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if msg_type is not None:
|
if msg_type is not None:
|
||||||
assert msg_type == self.__class__.__name__
|
assert msg_type == self.__class__.__name__
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import asyncio
|
from typing import *
|
||||||
|
import asyncio as aio
|
||||||
import uuid
|
import uuid
|
||||||
import functools
|
import functools
|
||||||
import logging as _logging
|
import logging
|
||||||
import typing
|
import websockets
|
||||||
from .package import Package
|
from .package import Package
|
||||||
from .request import Request
|
from .request import Request
|
||||||
from .response import Response, ResponseSuccess, ResponseFailure
|
from .response import Response, ResponseSuccess, ResponseFailure
|
||||||
|
@ -10,23 +11,18 @@ from .broadcast import Broadcast
|
||||||
from .errors import ConnectionClosedError, InvalidServerResponseError
|
from .errors import ConnectionClosedError, InvalidServerResponseError
|
||||||
from .config import Config
|
from .config import Config
|
||||||
|
|
||||||
try:
|
|
||||||
import websockets
|
|
||||||
except ImportError:
|
|
||||||
websockets = None
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
log = _logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class PendingRequest:
|
class PendingRequest:
|
||||||
def __init__(self, *, loop: asyncio.AbstractEventLoop = None):
|
def __init__(self, *, loop: aio.AbstractEventLoop = None):
|
||||||
if loop is None:
|
if loop is None:
|
||||||
self.loop = asyncio.get_event_loop()
|
self.loop = aio.get_event_loop()
|
||||||
else:
|
else:
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
self.event: asyncio.Event = asyncio.Event(loop=loop)
|
self.event: aio.Event = aio.Event(loop=loop)
|
||||||
self.data: typing.Optional[dict] = None
|
self.data: Optional[dict] = None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self.event.is_set():
|
if self.event.is_set():
|
||||||
|
@ -56,22 +52,20 @@ def requires_identification(func):
|
||||||
|
|
||||||
class Link:
|
class Link:
|
||||||
def __init__(self, config: Config, request_handler, *,
|
def __init__(self, config: Config, request_handler, *,
|
||||||
loop: asyncio.AbstractEventLoop = None):
|
loop: aio.AbstractEventLoop = None):
|
||||||
if websockets is None:
|
|
||||||
raise ImportError("'websockets' extra is not installed")
|
|
||||||
self.config: Config = config
|
self.config: Config = config
|
||||||
self.nid: str = str(uuid.uuid4())
|
self.nid: str = str(uuid.uuid4())
|
||||||
self.websocket: typing.Optional["websockets.WebSocketClientProtocol"] = None
|
self.websocket: Optional["websockets.WebSocketClientProtocol"] = None
|
||||||
self.request_handler: typing.Callable[[typing.Union[Request, Broadcast]],
|
self.request_handler: Callable[[Union[Request, Broadcast]],
|
||||||
typing.Awaitable[Response]] = request_handler
|
Awaitable[Response]] = request_handler
|
||||||
self._pending_requests: typing.Dict[str, PendingRequest] = {}
|
self._pending_requests: Dict[str, PendingRequest] = {}
|
||||||
if loop is None:
|
if loop is None:
|
||||||
self._loop = asyncio.get_event_loop()
|
self._loop = aio.get_event_loop()
|
||||||
else:
|
else:
|
||||||
self._loop = loop
|
self._loop = loop
|
||||||
self.error_event: asyncio.Event = asyncio.Event(loop=self._loop)
|
self.error_event: aio.Event = aio.Event(loop=self._loop)
|
||||||
self.connect_event: asyncio.Event = asyncio.Event(loop=self._loop)
|
self.connect_event: aio.Event = aio.Event(loop=self._loop)
|
||||||
self.identify_event: asyncio.Event = asyncio.Event(loop=self._loop)
|
self.identify_event: aio.Event = aio.Event(loop=self._loop)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self.identify_event.is_set():
|
if self.identify_event.is_set():
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
import typing
|
from typing import *
|
||||||
|
|
||||||
|
|
||||||
class Package:
|
class Package:
|
||||||
|
@ -14,8 +14,8 @@ class Package:
|
||||||
*,
|
*,
|
||||||
source: str,
|
source: str,
|
||||||
destination: str,
|
destination: str,
|
||||||
source_conv_id: typing.Optional[str] = None,
|
source_conv_id: Optional[str] = None,
|
||||||
destination_conv_id: typing.Optional[str] = None):
|
destination_conv_id: Optional[str] = None):
|
||||||
"""Create a Package.
|
"""Create a Package.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
|
@ -30,7 +30,7 @@ class Package:
|
||||||
self.source: str = source
|
self.source: str = source
|
||||||
self.source_conv_id: str = source_conv_id or str(uuid.uuid4())
|
self.source_conv_id: str = source_conv_id or str(uuid.uuid4())
|
||||||
self.destination: str = destination
|
self.destination: str = destination
|
||||||
self.destination_conv_id: typing.Optional[str] = destination_conv_id
|
self.destination_conv_id: Optional[str] = destination_conv_id
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__qualname__} {self.source} » {self.destination}>"
|
return f"<{self.__class__.__qualname__} {self.source} » {self.destination}>"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import typing
|
from typing import *
|
||||||
|
|
||||||
|
|
||||||
class Request:
|
class Request:
|
||||||
|
@ -6,7 +6,7 @@ class Request:
|
||||||
|
|
||||||
It contains the name of the requested handler, in addition to the data."""
|
It contains the name of the requested handler, in addition to the data."""
|
||||||
|
|
||||||
def __init__(self, handler: str, data: dict, msg_type: typing.Optional[str] = None):
|
def __init__(self, handler: str, data: dict, msg_type: Optional[str] = None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if msg_type is not None:
|
if msg_type is not None:
|
||||||
assert msg_type == self.__class__.__name__
|
assert msg_type == self.__class__.__name__
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import typing
|
from typing import *
|
||||||
|
|
||||||
|
|
||||||
class Response:
|
class Response:
|
||||||
|
@ -28,7 +28,7 @@ class Response:
|
||||||
class ResponseSuccess(Response):
|
class ResponseSuccess(Response):
|
||||||
"""A response to a successful :py:class:`Request`."""
|
"""A response to a successful :py:class:`Request`."""
|
||||||
|
|
||||||
def __init__(self, data: typing.Optional[dict] = None):
|
def __init__(self, data: Optional[dict] = None):
|
||||||
if data is None:
|
if data is None:
|
||||||
self.data = {}
|
self.data = {}
|
||||||
else:
|
else:
|
||||||
|
@ -41,10 +41,10 @@ class ResponseSuccess(Response):
|
||||||
class ResponseFailure(Response):
|
class ResponseFailure(Response):
|
||||||
"""A response to a invalid :py:class:`Request`."""
|
"""A response to a invalid :py:class:`Request`."""
|
||||||
|
|
||||||
def __init__(self, name: str, description: str, extra_info: typing.Optional[dict] = None):
|
def __init__(self, name: str, description: str, extra_info: Optional[dict] = None):
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
self.description: str = description
|
self.description: str = description
|
||||||
self.extra_info: typing.Optional[dict] = extra_info
|
self.extra_info: Optional[dict] = extra_info
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"{self.__class__.__qualname__}(name={self.name}, description={self.description}, extra_info={self.extra_info})"
|
return f"{self.__class__.__qualname__}(name={self.name}, description={self.description}, extra_info={self.extra_info})"
|
||||||
|
|
|
@ -1,25 +1,16 @@
|
||||||
from typing import *
|
from typing import *
|
||||||
|
import asyncio as aio
|
||||||
import re
|
import re
|
||||||
import datetime
|
import datetime
|
||||||
import uuid
|
import uuid
|
||||||
import asyncio
|
import logging
|
||||||
import logging as _logging
|
import websockets
|
||||||
import royalnet.utils as ru
|
import royalnet.utils as ru
|
||||||
from .package import Package
|
from .package import Package
|
||||||
from .config import Config
|
from .config import Config
|
||||||
|
|
||||||
try:
|
|
||||||
import coloredlogs
|
|
||||||
except ImportError:
|
|
||||||
coloredlogs = None
|
|
||||||
|
|
||||||
try:
|
log = logging.getLogger(__name__)
|
||||||
import websockets
|
|
||||||
except ImportError:
|
|
||||||
websockets = None
|
|
||||||
|
|
||||||
|
|
||||||
log = _logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ConnectedClient:
|
class ConnectedClient:
|
||||||
|
@ -49,7 +40,7 @@ class ConnectedClient:
|
||||||
|
|
||||||
|
|
||||||
class Server:
|
class Server:
|
||||||
def __init__(self, config: Config, *, loop: asyncio.AbstractEventLoop = None):
|
def __init__(self, config: Config, *, loop: aio.AbstractEventLoop = None):
|
||||||
self.config: Config = config
|
self.config: Config = config
|
||||||
self.identified_clients: List[ConnectedClient] = []
|
self.identified_clients: List[ConnectedClient] = []
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
|
@ -165,5 +156,5 @@ class Server:
|
||||||
def run_blocking(self, logging_cfg: Dict[str, Any]):
|
def run_blocking(self, logging_cfg: Dict[str, Any]):
|
||||||
ru.init_logging(logging_cfg)
|
ru.init_logging(logging_cfg)
|
||||||
if self.loop is None:
|
if self.loop is None:
|
||||||
self.loop = asyncio.get_event_loop()
|
self.loop = aio.get_event_loop()
|
||||||
self.serve()
|
self.serve()
|
||||||
|
|
3
royalnet/serf/README.md
Normal file
3
royalnet/serf/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# `royalnet.serf`
|
||||||
|
|
||||||
|
The subpackage providing all Serf implementations.
|
|
@ -1,11 +1,9 @@
|
||||||
|
"""The subpackage providing all Serf implementations."""
|
||||||
|
|
||||||
from .serf import Serf
|
from .serf import Serf
|
||||||
from .errors import SerfError
|
from .errors import SerfError
|
||||||
from . import telegram, discord, matrix
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Serf",
|
"Serf",
|
||||||
"SerfError",
|
"SerfError",
|
||||||
"telegram",
|
|
||||||
"discord",
|
|
||||||
"matrix",
|
|
||||||
]
|
]
|
||||||
|
|
10
royalnet/serf/discord/README.md
Normal file
10
royalnet/serf/discord/README.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# `royalnet.serf.discord`
|
||||||
|
|
||||||
|
A `Serf` implementation for Discord.
|
||||||
|
|
||||||
|
It requires (obviously) the `discord` extra to be installed.
|
||||||
|
|
||||||
|
Install it with:
|
||||||
|
```
|
||||||
|
pip install royalnet[discord]
|
||||||
|
```
|
|
@ -1,6 +1,12 @@
|
||||||
"""A :class:`Serf` implementation for Discord.
|
"""A :class:`Serf` implementation for Discord.
|
||||||
|
|
||||||
It is pretty unstable, compared to the rest of the bot, but it *should* work."""
|
It requires (obviously) the ``discord`` extra to be installed.
|
||||||
|
|
||||||
|
Install it with: ::
|
||||||
|
|
||||||
|
pip install royalnet[discord]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
from .escape import escape
|
from .escape import escape
|
||||||
from .discordserf import DiscordSerf
|
from .discordserf import DiscordSerf
|
||||||
|
|
|
@ -87,6 +87,7 @@ class VoicePlayer:
|
||||||
raise PlayerNotConnectedError()
|
raise PlayerNotConnectedError()
|
||||||
if self.voice_client.is_playing():
|
if self.voice_client.is_playing():
|
||||||
raise PlayerAlreadyPlaying()
|
raise PlayerAlreadyPlaying()
|
||||||
|
self.playing = None
|
||||||
log.debug("Getting next AudioSource...")
|
log.debug("Getting next AudioSource...")
|
||||||
next_source: Optional["discord.AudioSource"] = await self.playing.next()
|
next_source: Optional["discord.AudioSource"] = await self.playing.next()
|
||||||
if next_source is None:
|
if next_source is None:
|
||||||
|
@ -97,7 +98,6 @@ class VoicePlayer:
|
||||||
self.loop.create_task(self._playback_check())
|
self.loop.create_task(self._playback_check())
|
||||||
|
|
||||||
async def _playback_check(self):
|
async def _playback_check(self):
|
||||||
# FIXME: quite spaghetti
|
|
||||||
while True:
|
while True:
|
||||||
if self._playback_ended_event.is_set():
|
if self._playback_ended_event.is_set():
|
||||||
self._playback_ended_event.clear()
|
self._playback_ended_event.clear()
|
||||||
|
|
10
royalnet/serf/matrix/README.md
Normal file
10
royalnet/serf/matrix/README.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# `royalnet.serf.matrix`
|
||||||
|
|
||||||
|
A `Serf` implementation for Matrix.
|
||||||
|
|
||||||
|
It requires (obviously) the `matrix` extra to be installed.
|
||||||
|
|
||||||
|
Install it with:
|
||||||
|
```
|
||||||
|
pip install royalnet[matrix]
|
||||||
|
```
|
|
@ -1,3 +1,13 @@
|
||||||
|
"""A :class:`Serf` implementation for Matrix.
|
||||||
|
|
||||||
|
It requires (obviously) the ``matrix`` extra to be installed.
|
||||||
|
|
||||||
|
Install it with: ::
|
||||||
|
|
||||||
|
pip install royalnet[matrix]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
from .matrixserf import MatrixSerf
|
from .matrixserf import MatrixSerf
|
||||||
from .escape import escape
|
from .escape import escape
|
||||||
|
|
||||||
|
|
|
@ -2,30 +2,14 @@ import logging
|
||||||
import importlib
|
import importlib
|
||||||
import asyncio as aio
|
import asyncio as aio
|
||||||
from typing import *
|
from typing import *
|
||||||
|
|
||||||
from sqlalchemy.schema import Table
|
from sqlalchemy.schema import Table
|
||||||
|
|
||||||
from royalnet.commands import *
|
from royalnet.commands import *
|
||||||
import royalnet.utils as ru
|
import royalnet.utils as ru
|
||||||
import royalnet.alchemy as ra
|
import royalnet.alchemy as ra
|
||||||
import royalnet.backpack as rb
|
import royalnet.backpack as rb
|
||||||
import royalnet.herald as rh
|
import royalnet.herald as rh
|
||||||
|
import traceback
|
||||||
|
|
||||||
try:
|
|
||||||
import sentry_sdk
|
|
||||||
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
|
|
||||||
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
|
|
||||||
from sentry_sdk.integrations.logging import LoggingIntegration
|
|
||||||
except ImportError:
|
|
||||||
sentry_sdk = None
|
|
||||||
SqlalchemyIntegration = None
|
|
||||||
AioHttpIntegration = None
|
|
||||||
LoggingIntegration = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
import coloredlogs
|
|
||||||
except ImportError:
|
|
||||||
coloredlogs = None
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -56,7 +40,7 @@ class Serf:
|
||||||
try:
|
try:
|
||||||
packs[pack_name] = importlib.import_module(pack_name)
|
packs[pack_name] = importlib.import_module(pack_name)
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
log.error(f"Error during the import of {pack_name}: {e}")
|
log.error(f"{e.__class__.__name__} during the import of {pack_name}: {e}")
|
||||||
log.info(f"Packs: {len(packs)} imported")
|
log.info(f"Packs: {len(packs)} imported")
|
||||||
|
|
||||||
self.alchemy: Optional[ra.Alchemy] = None
|
self.alchemy: Optional[ra.Alchemy] = None
|
||||||
|
@ -317,8 +301,7 @@ class Serf:
|
||||||
try:
|
try:
|
||||||
await key.press(data)
|
await key.press(data)
|
||||||
except InvalidInputError as e:
|
except InvalidInputError as e:
|
||||||
await data.reply(f"⚠️ {e.message}\n"
|
await data.reply(f"⚠️ {e.message}")
|
||||||
f"Syntax: [c]{command.interface.prefix}{command.name} {command.syntax}[/c]")
|
|
||||||
except UserError as e:
|
except UserError as e:
|
||||||
await data.reply(f"⚠️ {e.message}")
|
await data.reply(f"⚠️ {e.message}")
|
||||||
except UnsupportedError as e:
|
except UnsupportedError as e:
|
||||||
|
@ -337,7 +320,6 @@ class Serf:
|
||||||
finally:
|
finally:
|
||||||
await data.session_close()
|
await data.session_close()
|
||||||
|
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
"""A coroutine that starts the event loop and handles command calls."""
|
"""A coroutine that starts the event loop and handles command calls."""
|
||||||
self.herald_task = self.loop.create_task(self.herald.run())
|
self.herald_task = self.loop.create_task(self.herald.run())
|
||||||
|
|
10
royalnet/serf/telegram/README.md
Normal file
10
royalnet/serf/telegram/README.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# `royalnet.serf.matrix`
|
||||||
|
|
||||||
|
A `Serf` implementation for Telegram.
|
||||||
|
|
||||||
|
It requires (obviously) the `telegram` extra to be installed.
|
||||||
|
|
||||||
|
Install it with:
|
||||||
|
```
|
||||||
|
pip install royalnet[telegram]
|
||||||
|
```
|
|
@ -1,3 +1,13 @@
|
||||||
|
"""A :class:`Serf` implementation for Telegram.
|
||||||
|
|
||||||
|
It requires (obviously) the ``telegram`` extra to be installed.
|
||||||
|
|
||||||
|
Install it with: ::
|
||||||
|
|
||||||
|
pip install royalnet[telegram]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
from .escape import escape
|
from .escape import escape
|
||||||
from .telegramserf import TelegramSerf
|
from .telegramserf import TelegramSerf
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ from .sleep_until import sleep_until
|
||||||
from .formatters import andformat, underscorize, ytdldateformat, numberemojiformat, ordinalformat
|
from .formatters import andformat, underscorize, ytdldateformat, numberemojiformat, ordinalformat
|
||||||
from .urluuid import to_urluuid, from_urluuid
|
from .urluuid import to_urluuid, from_urluuid
|
||||||
from .multilock import MultiLock
|
from .multilock import MultiLock
|
||||||
from .fileaudiosource import FileAudioSource
|
|
||||||
from .sentry import init_sentry, sentry_exc
|
from .sentry import init_sentry, sentry_exc
|
||||||
from .log import init_logging
|
from .log import init_logging
|
||||||
|
|
||||||
|
@ -18,7 +17,6 @@ __all__ = [
|
||||||
"to_urluuid",
|
"to_urluuid",
|
||||||
"from_urluuid",
|
"from_urluuid",
|
||||||
"MultiLock",
|
"MultiLock",
|
||||||
"FileAudioSource",
|
|
||||||
"init_sentry",
|
"init_sentry",
|
||||||
"sentry_exc",
|
"sentry_exc",
|
||||||
"init_logging",
|
"init_logging",
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
semantic = "5.3.4"
|
semantic = "5.4"
|
||||||
|
|
Loading…
Reference in a new issue