mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-27 13:34:28 +00:00
Merge branch '5.5'
# Conflicts: # pyproject.toml # royalnet/version.py
This commit is contained in:
commit
6db81c689c
41 changed files with 622 additions and 336 deletions
41
poetry.lock
generated
41
poetry.lock
generated
|
@ -66,6 +66,21 @@ version = "2.8.0"
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
pytz = ">=2015.7"
|
pytz = ">=2015.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "main"
|
||||||
|
description = "Modern password hashing for your software and your servers"
|
||||||
|
name = "bcrypt"
|
||||||
|
optional = true
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
version = "3.1.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cffi = ">=1.1"
|
||||||
|
six = ">=1.4.1"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
tests = ["pytest (>=3.2.1,<3.3.0 || >3.3.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "main"
|
category = "main"
|
||||||
description = "Python package for providing Mozilla's CA Bundle."
|
description = "Python package for providing Mozilla's CA Bundle."
|
||||||
|
@ -913,8 +928,8 @@ python-versions = "*"
|
||||||
version = "2020.1.24"
|
version = "2020.1.24"
|
||||||
|
|
||||||
[extras]
|
[extras]
|
||||||
alchemy_easy = ["sqlalchemy", "psycopg2_binary"]
|
alchemy_easy = ["sqlalchemy", "psycopg2_binary", "bcrypt"]
|
||||||
alchemy_hard = ["sqlalchemy", "psycopg2"]
|
alchemy_hard = ["sqlalchemy", "psycopg2", "bcrypt"]
|
||||||
bard = ["ffmpeg_python", "youtube_dl", "eyed3"]
|
bard = ["ffmpeg_python", "youtube_dl", "eyed3"]
|
||||||
coloredlogs = ["coloredlogs"]
|
coloredlogs = ["coloredlogs"]
|
||||||
constellation = ["starlette", "uvicorn", "python-multipart"]
|
constellation = ["starlette", "uvicorn", "python-multipart"]
|
||||||
|
@ -925,7 +940,7 @@ sentry = ["sentry_sdk"]
|
||||||
telegram = ["python_telegram_bot"]
|
telegram = ["python_telegram_bot"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
content-hash = "f275cd948fe28423a90d37d2825eabfec97e8ac0cdf52ee2d20f803d61987b40"
|
content-hash = "218f4a253a7ef17bb871abf541ae7182f1626cd43d55417de12f9561c58ca0e9"
|
||||||
python-versions = "^3.8"
|
python-versions = "^3.8"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
|
@ -963,6 +978,26 @@ babel = [
|
||||||
{file = "Babel-2.8.0-py2.py3-none-any.whl", hash = "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"},
|
{file = "Babel-2.8.0-py2.py3-none-any.whl", hash = "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"},
|
||||||
{file = "Babel-2.8.0.tar.gz", hash = "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38"},
|
{file = "Babel-2.8.0.tar.gz", hash = "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38"},
|
||||||
]
|
]
|
||||||
|
bcrypt = [
|
||||||
|
{file = "bcrypt-3.1.7-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7"},
|
||||||
|
{file = "bcrypt-3.1.7-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31"},
|
||||||
|
{file = "bcrypt-3.1.7-cp27-cp27m-win32.whl", hash = "sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161"},
|
||||||
|
{file = "bcrypt-3.1.7-cp27-cp27m-win_amd64.whl", hash = "sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e"},
|
||||||
|
{file = "bcrypt-3.1.7-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0"},
|
||||||
|
{file = "bcrypt-3.1.7-cp34-abi3-macosx_10_6_intel.whl", hash = "sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052"},
|
||||||
|
{file = "bcrypt-3.1.7-cp34-abi3-manylinux1_x86_64.whl", hash = "sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105"},
|
||||||
|
{file = "bcrypt-3.1.7-cp34-cp34m-win32.whl", hash = "sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de"},
|
||||||
|
{file = "bcrypt-3.1.7-cp34-cp34m-win_amd64.whl", hash = "sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133"},
|
||||||
|
{file = "bcrypt-3.1.7-cp35-cp35m-win32.whl", hash = "sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5"},
|
||||||
|
{file = "bcrypt-3.1.7-cp35-cp35m-win_amd64.whl", hash = "sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09"},
|
||||||
|
{file = "bcrypt-3.1.7-cp36-cp36m-win32.whl", hash = "sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c"},
|
||||||
|
{file = "bcrypt-3.1.7-cp36-cp36m-win_amd64.whl", hash = "sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89"},
|
||||||
|
{file = "bcrypt-3.1.7-cp37-cp37m-win32.whl", hash = "sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294"},
|
||||||
|
{file = "bcrypt-3.1.7-cp37-cp37m-win_amd64.whl", hash = "sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc"},
|
||||||
|
{file = "bcrypt-3.1.7-cp38-cp38-win32.whl", hash = "sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1"},
|
||||||
|
{file = "bcrypt-3.1.7-cp38-cp38-win_amd64.whl", hash = "sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752"},
|
||||||
|
{file = "bcrypt-3.1.7.tar.gz", hash = "sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42"},
|
||||||
|
]
|
||||||
certifi = [
|
certifi = [
|
||||||
{file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"},
|
{file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"},
|
||||||
{file = "certifi-2019.11.28.tar.gz", hash = "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"},
|
{file = "certifi-2019.11.28.tar.gz", hash = "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"},
|
||||||
|
|
1
publish.bat
Normal file
1
publish.bat
Normal file
|
@ -0,0 +1 @@
|
||||||
|
git commit -am "publish: %1" && git push && poetry build && poetry publish && hub release create "%1" -m "Royalnet %1"
|
|
@ -1,5 +1,4 @@
|
||||||
from typing import *
|
from typing import *
|
||||||
import royalnet
|
|
||||||
import royalnet.commands as rc
|
import royalnet.commands as rc
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from typing import *
|
from typing import *
|
||||||
import royalnet
|
|
||||||
import royalnet.commands as rc
|
import royalnet.commands as rc
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ from sqlalchemy.orm import *
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyAttributeOutsideInit
|
||||||
#set($CAPITALIZED_NAME = $NAME.substring(0,1).toUpperCase() + $NAME.substring(1))
|
#set($CAPITALIZED_NAME = $NAME.substring(0,1).toUpperCase() + $NAME.substring(1))
|
||||||
class ${CAPITALIZED_NAME}:
|
class ${CAPITALIZED_NAME}:
|
||||||
__tablename__ = "${NAME}"
|
__tablename__ = "${NAME}"
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "royalnet"
|
name = "royalnet"
|
||||||
version = "5.4.1"
|
version = "5.5"
|
||||||
description = "A multipurpose bot and web framework"
|
description = "A multipurpose bot and web framework"
|
||||||
authors = ["Stefano Pigozzi <ste.pigozzi@gmail.com>"]
|
authors = ["Stefano Pigozzi <ste.pigozzi@gmail.com>"]
|
||||||
license = "AGPL-3.0+"
|
license = "AGPL-3.0+"
|
||||||
|
@ -44,6 +44,7 @@
|
||||||
sqlalchemy = {version="^1.3.10", optional=true}
|
sqlalchemy = {version="^1.3.10", optional=true}
|
||||||
psycopg2 = {version="^2.8.4", optional=true} # Requires quite a bit of stuff http://initd.org/psycopg/docs/install.html#install-from-source
|
psycopg2 = {version="^2.8.4", optional=true} # Requires quite a bit of stuff http://initd.org/psycopg/docs/install.html#install-from-source
|
||||||
psycopg2_binary = {version="^2.8.4", optional=true} # Prebuilt alternative to psycopg2, not recommended
|
psycopg2_binary = {version="^2.8.4", optional=true} # Prebuilt alternative to psycopg2, not recommended
|
||||||
|
bcrypt = {version="^3.1.7", optional=true}
|
||||||
|
|
||||||
# constellation
|
# constellation
|
||||||
starlette = {version="^0.12.13", optional=true}
|
starlette = {version="^0.12.13", optional=true}
|
||||||
|
@ -71,8 +72,8 @@
|
||||||
telegram = ["python_telegram_bot"]
|
telegram = ["python_telegram_bot"]
|
||||||
discord = ["discord.py", "pynacl"]
|
discord = ["discord.py", "pynacl"]
|
||||||
matrix = ["matrix-nio"]
|
matrix = ["matrix-nio"]
|
||||||
alchemy_easy = ["sqlalchemy", "psycopg2_binary"]
|
alchemy_easy = ["sqlalchemy", "psycopg2_binary", "bcrypt"]
|
||||||
alchemy_hard = ["sqlalchemy", "psycopg2"]
|
alchemy_hard = ["sqlalchemy", "psycopg2", "bcrypt"]
|
||||||
bard = ["ffmpeg_python", "youtube_dl", "eyed3"]
|
bard = ["ffmpeg_python", "youtube_dl", "eyed3"]
|
||||||
constellation = ["starlette", "uvicorn", "python-multipart"]
|
constellation = ["starlette", "uvicorn", "python-multipart"]
|
||||||
sentry = ["sentry_sdk"]
|
sentry = ["sentry_sdk"]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Set, Dict, Union
|
from typing import *
|
||||||
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
|
||||||
|
@ -6,7 +6,7 @@ from sqlalchemy import create_engine
|
||||||
from sqlalchemy.engine import Engine
|
from sqlalchemy.engine import Engine
|
||||||
from sqlalchemy.schema import Table
|
from sqlalchemy.schema import Table
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.ext.declarative.api import DeclarativeMeta
|
from sqlalchemy.ext.declarative.api import DeclarativeMeta, AbstractConcreteBase
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ class Alchemy:
|
||||||
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")
|
||||||
self._engine: Engine = create_engine(database_uri)
|
self._engine: Engine = create_engine(database_uri)
|
||||||
self._Base: DeclarativeMeta = declarative_base(bind=self._engine)
|
self._Base = declarative_base(bind=self._engine)
|
||||||
self.Session: sessionmaker = sessionmaker(bind=self._engine)
|
self.Session: sessionmaker = sessionmaker(bind=self._engine)
|
||||||
self._tables: Dict[str, Table] = {}
|
self._tables: Dict[str, Table] = {}
|
||||||
for table in tables:
|
for table in tables:
|
||||||
|
@ -38,7 +38,7 @@ class Alchemy:
|
||||||
self._tables[name] = bound_table
|
self._tables[name] = bound_table
|
||||||
self._Base.metadata.create_all()
|
self._Base.metadata.create_all()
|
||||||
|
|
||||||
def get(self, table: Union[str, type]) -> Table:
|
def get(self, table: Union[str, type]) -> DeclarativeMeta:
|
||||||
"""Get the table with a specified name or class.
|
"""Get the table with a specified name or class.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
|
@ -1,19 +1 @@
|
||||||
"""A Pack that is imported by default by all Royalnet instances."""
|
"""A Pack that is imported by default by all Royalnet instances."""
|
||||||
|
|
||||||
from . import commands, tables, stars, events
|
|
||||||
from .commands import available_commands
|
|
||||||
from .tables import available_tables
|
|
||||||
from .stars import available_page_stars, available_exception_stars
|
|
||||||
from .events import available_events
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"commands",
|
|
||||||
"tables",
|
|
||||||
"stars",
|
|
||||||
"events",
|
|
||||||
"available_commands",
|
|
||||||
"available_tables",
|
|
||||||
"available_page_stars",
|
|
||||||
"available_exception_stars",
|
|
||||||
"available_events",
|
|
||||||
]
|
|
||||||
|
|
|
@ -1,22 +1,10 @@
|
||||||
# Imports go here!
|
# Imports go here!
|
||||||
from .version import VersionCommand
|
from .version import VersionCommand
|
||||||
from .exception import ExceptionCommand
|
|
||||||
from .excevent import ExceventCommand
|
|
||||||
from .keyboardtest import KeyboardtestCommand
|
|
||||||
|
|
||||||
# Enter the commands of your Pack here!
|
# Enter the commands of your Pack here!
|
||||||
available_commands = [
|
available_commands = [
|
||||||
VersionCommand,
|
VersionCommand,
|
||||||
]
|
]
|
||||||
|
|
||||||
# noinspection PyUnreachableCode
|
|
||||||
if __debug__:
|
|
||||||
available_commands = [
|
|
||||||
*available_commands,
|
|
||||||
ExceptionCommand,
|
|
||||||
ExceventCommand,
|
|
||||||
KeyboardtestCommand,
|
|
||||||
]
|
|
||||||
|
|
||||||
# Don't change this, it should automatically generate __all__
|
# Don't change this, it should automatically generate __all__
|
||||||
__all__ = [command.__name__ for command in available_commands]
|
__all__ = [command.__name__ for command in available_commands]
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
import royalnet
|
|
||||||
from royalnet.commands import *
|
|
||||||
|
|
||||||
|
|
||||||
class ExceptionCommand(Command):
|
|
||||||
name: str = "exception"
|
|
||||||
|
|
||||||
description: str = "Raise an exception in the command."
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
raise Exception(f"{self.interface.prefix}{self.name} was called")
|
|
|
@ -1,12 +0,0 @@
|
||||||
import royalnet
|
|
||||||
from royalnet.commands import *
|
|
||||||
|
|
||||||
|
|
||||||
class ExceventCommand(Command):
|
|
||||||
name: str = "excevent"
|
|
||||||
|
|
||||||
description: str = "Call an event that raises an exception."
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
await self.interface.call_herald_event(self.interface.name, "exception")
|
|
||||||
await data.reply("✅ Event called!")
|
|
|
@ -1,28 +0,0 @@
|
||||||
from typing import *
|
|
||||||
from royalnet.commands import *
|
|
||||||
import functools
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
|
|
||||||
class KeyboardtestCommand(Command):
|
|
||||||
name: str = "keyboardtest"
|
|
||||||
|
|
||||||
description: str = "Create a new keyboard with the specified keys."
|
|
||||||
|
|
||||||
syntax: str = "{keys}+"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def echo(data: CommandData, echo: str):
|
|
||||||
await data.reply(echo)
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
keys = []
|
|
||||||
for arg in args:
|
|
||||||
# noinspection PyTypeChecker
|
|
||||||
keys.append(KeyboardKey(interface=self.interface,
|
|
||||||
short=arg[0],
|
|
||||||
text=arg,
|
|
||||||
callback=functools.partial(self.echo, echo=arg)))
|
|
||||||
async with data.keyboard("This is a test keyboard.", keys):
|
|
||||||
await asyncio.sleep(10)
|
|
||||||
await data.reply("The keyboard is no longer in scope.")
|
|
86
royalnet/backpack/commands/link.py
Normal file
86
royalnet/backpack/commands/link.py
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
from typing import *
|
||||||
|
import royalnet
|
||||||
|
import royalnet.commands as rc
|
||||||
|
import royalnet.utils as ru
|
||||||
|
from ..tables.telegram import Telegram
|
||||||
|
from ..tables.discord import Discord
|
||||||
|
|
||||||
|
|
||||||
|
class SyncCommand(rc.Command):
|
||||||
|
name: str = "sync"
|
||||||
|
|
||||||
|
description: str = "Connect your chat account to Royalnet!"
|
||||||
|
|
||||||
|
syntax: str = "{username} {password}"
|
||||||
|
|
||||||
|
async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
|
||||||
|
username = args[0]
|
||||||
|
password = " ".join(args[1:])
|
||||||
|
|
||||||
|
author = await data.get_author(error_if_none=True)
|
||||||
|
|
||||||
|
user = await data.find_user(username)
|
||||||
|
try:
|
||||||
|
successful = user.test_password(password)
|
||||||
|
except ValueError:
|
||||||
|
raise rc.UserError(f"User {user} has no password set!")
|
||||||
|
if not successful:
|
||||||
|
raise rc.InvalidInputError(f"Invalid password!")
|
||||||
|
|
||||||
|
if self.interface.name == "telegram":
|
||||||
|
import telegram
|
||||||
|
message: telegram.Message = data.message
|
||||||
|
from_user: telegram.User = message.from_user
|
||||||
|
TelegramT = self.alchemy.get(Telegram)
|
||||||
|
tg_user: Telegram = await ru.asyncify(
|
||||||
|
data.session.query(TelegramT).filter_by(tg_id=from_user.id).one_or_none
|
||||||
|
)
|
||||||
|
if tg_user is None:
|
||||||
|
# Create
|
||||||
|
tg_user = TelegramT(
|
||||||
|
user=author,
|
||||||
|
tg_id=from_user.id,
|
||||||
|
first_name=from_user.first_name,
|
||||||
|
last_name=from_user.last_name,
|
||||||
|
username=from_user.username
|
||||||
|
)
|
||||||
|
data.session.add(tg_user)
|
||||||
|
else:
|
||||||
|
# Edit
|
||||||
|
tg_user.first_name = from_user.first_name
|
||||||
|
tg_user.last_name = from_user.last_name
|
||||||
|
tg_user.username = from_user.username
|
||||||
|
await data.session_commit()
|
||||||
|
await data.reply(f"↔️ Account {tg_user} synced to {author}!")
|
||||||
|
|
||||||
|
elif self.interface.name == "discord":
|
||||||
|
import discord
|
||||||
|
message: discord.Message = data.message
|
||||||
|
author: discord.User = message.author
|
||||||
|
DiscordT = self.alchemy.get(Discord)
|
||||||
|
ds_user: Discord = await ru.asyncify(
|
||||||
|
data.session.query(DiscordT).filter_by(discord_id=author.id).one_or_none
|
||||||
|
)
|
||||||
|
if ds_user is None:
|
||||||
|
# Create
|
||||||
|
ds_user = DiscordT(
|
||||||
|
user=author,
|
||||||
|
discord_id=author.id,
|
||||||
|
username=author.name,
|
||||||
|
discriminator=author.discriminator,
|
||||||
|
avatar_url=author.avatar_url
|
||||||
|
)
|
||||||
|
data.session.add(ds_user)
|
||||||
|
else:
|
||||||
|
# Edit
|
||||||
|
ds_user.username = author.name
|
||||||
|
ds_user.discriminator = author.discriminator
|
||||||
|
ds_user.avatar_url = author.avatar_url
|
||||||
|
await data.session_commit()
|
||||||
|
await data.reply(f"↔️ Account {ds_user} synced to {author}!")
|
||||||
|
|
||||||
|
elif self.interface.name == "matrix":
|
||||||
|
raise rc.UnsupportedError(f"{self} hasn't been implemented for Matrix yet")
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise rc.UnsupportedError(f"Unknown interface: {self.interface.name}")
|
|
@ -1,16 +1,18 @@
|
||||||
# Imports go here!
|
# Imports go here!
|
||||||
from .api_royalnet_version import ApiRoyalnetVersionStar
|
from .api_royalnet_version import ApiRoyalnetVersionStar
|
||||||
|
from .api_login_royalnet import ApiLoginRoyalnetStar
|
||||||
|
from .api_token_info import ApiTokenInfoStar
|
||||||
|
from .api_token_passwd import ApiTokenPasswdStar
|
||||||
|
from .api_token_create import ApiTokenCreateStar
|
||||||
|
|
||||||
# Enter the PageStars of your Pack here!
|
# Enter the PageStars of your Pack here!
|
||||||
available_page_stars = [
|
available_page_stars = [
|
||||||
ApiRoyalnetVersionStar,
|
ApiRoyalnetVersionStar,
|
||||||
]
|
ApiLoginRoyalnetStar,
|
||||||
|
ApiTokenInfoStar,
|
||||||
# Enter the ExceptionStars of your Pack here!
|
ApiTokenPasswdStar,
|
||||||
available_exception_stars = [
|
ApiTokenCreateStar,
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Don't change this, it should automatically generate __all__
|
# Don't change this, it should automatically generate __all__
|
||||||
__all__ = [star.__name__ for star in [*available_page_stars, *available_exception_stars]]
|
__all__ = [star.__name__ for star in available_page_stars]
|
||||||
|
|
34
royalnet/backpack/stars/api_login_royalnet.py
Normal file
34
royalnet/backpack/stars/api_login_royalnet.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import datetime
|
||||||
|
import royalnet.utils as ru
|
||||||
|
from royalnet.constellation.api import *
|
||||||
|
from ..tables.users import User
|
||||||
|
from ..tables.aliases import Alias
|
||||||
|
from ..tables.tokens import Token
|
||||||
|
|
||||||
|
|
||||||
|
class ApiLoginRoyalnetStar(ApiStar):
|
||||||
|
path = "/api/login/royalnet/v1"
|
||||||
|
|
||||||
|
methods = ["POST"]
|
||||||
|
|
||||||
|
async def api(self, data: ApiData) -> ru.JSON:
|
||||||
|
TokenT = self.alchemy.get(Token)
|
||||||
|
UserT = self.alchemy.get(User)
|
||||||
|
AliasT = self.alchemy.get(Alias)
|
||||||
|
|
||||||
|
username = data["username"]
|
||||||
|
password = data["password"]
|
||||||
|
|
||||||
|
async with self.session_acm() as session:
|
||||||
|
user: User = await ru.asyncify(session.query(UserT).filter_by(username=username).one_or_none)
|
||||||
|
if user is None:
|
||||||
|
raise NotFoundError("User not found")
|
||||||
|
pswd_check = user.test_password(password)
|
||||||
|
if not pswd_check:
|
||||||
|
raise ApiError("Invalid password")
|
||||||
|
token: Token = TokenT.generate(alchemy=self.alchemy, user=user, expiration_delta=datetime.timedelta(days=7))
|
||||||
|
session.add(token)
|
||||||
|
await ru.asyncify(session.commit)
|
||||||
|
response = token.json()
|
||||||
|
|
||||||
|
return response
|
|
@ -1,15 +1,12 @@
|
||||||
import royalnet
|
import royalnet.version as rv
|
||||||
from starlette.requests import Request
|
from royalnet.constellation.api import *
|
||||||
from starlette.responses import *
|
import royalnet.utils as ru
|
||||||
from royalnet.constellation import PageStar
|
|
||||||
|
|
||||||
|
|
||||||
class ApiRoyalnetVersionStar(PageStar):
|
class ApiRoyalnetVersionStar(ApiStar):
|
||||||
path = "/api/royalnet/version"
|
path = "/api/royalnet/version/v1"
|
||||||
|
|
||||||
async def page(self, request: Request) -> JSONResponse:
|
async def api(self, data: ApiData) -> ru.JSON:
|
||||||
return JSONResponse({
|
return {
|
||||||
"version": {
|
"semantic": rv.semantic
|
||||||
"semantic": royalnet.__version__,
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
22
royalnet/backpack/stars/api_token_create.py
Normal file
22
royalnet/backpack/stars/api_token_create.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
from typing import *
|
||||||
|
import datetime
|
||||||
|
import royalnet.utils as ru
|
||||||
|
from royalnet.constellation.api import *
|
||||||
|
from ..tables.tokens import Token
|
||||||
|
|
||||||
|
|
||||||
|
class ApiTokenCreateStar(ApiStar):
|
||||||
|
path = "/api/token/create/v1"
|
||||||
|
|
||||||
|
methods = ["POST"]
|
||||||
|
|
||||||
|
async def api(self, data: ApiData) -> ru.JSON:
|
||||||
|
user = await data.user()
|
||||||
|
try:
|
||||||
|
duration = int(data["duration"])
|
||||||
|
except ValueError:
|
||||||
|
raise InvalidParameterError("Duration is not a valid integer")
|
||||||
|
new_token = Token.generate(self.alchemy, user, datetime.timedelta(seconds=duration))
|
||||||
|
data.session.add(new_token)
|
||||||
|
await data.session_commit()
|
||||||
|
return new_token.json()
|
10
royalnet/backpack/stars/api_token_info.py
Normal file
10
royalnet/backpack/stars/api_token_info.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import royalnet.utils as ru
|
||||||
|
from royalnet.constellation.api import *
|
||||||
|
|
||||||
|
|
||||||
|
class ApiTokenInfoStar(ApiStar):
|
||||||
|
path = "/api/token/info/v1"
|
||||||
|
|
||||||
|
async def api(self, data: ApiData) -> ru.JSON:
|
||||||
|
token = await data.token()
|
||||||
|
return token.json()
|
35
royalnet/backpack/stars/api_token_passwd.py
Normal file
35
royalnet/backpack/stars/api_token_passwd.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
from typing import *
|
||||||
|
import datetime
|
||||||
|
import royalnet.utils as ru
|
||||||
|
from royalnet.constellation.api import *
|
||||||
|
from sqlalchemy import and_
|
||||||
|
from ..tables.tokens import Token
|
||||||
|
|
||||||
|
|
||||||
|
class ApiTokenPasswdStar(ApiStar):
|
||||||
|
path = "/api/token/passwd/v1"
|
||||||
|
|
||||||
|
methods = ["POST"]
|
||||||
|
|
||||||
|
async def api(self, data: ApiData) -> ru.JSON:
|
||||||
|
TokenT = self.alchemy.get(Token)
|
||||||
|
token = await data.token()
|
||||||
|
user = token.user
|
||||||
|
user.set_password(data["new_password"])
|
||||||
|
tokens: List[Token] = await ru.asyncify(
|
||||||
|
data.session
|
||||||
|
.query(self.alchemy.get(Token))
|
||||||
|
.filter(
|
||||||
|
and_(
|
||||||
|
TokenT.user == user,
|
||||||
|
TokenT.expiration >= datetime.datetime.now()
|
||||||
|
))
|
||||||
|
.all
|
||||||
|
)
|
||||||
|
for t in tokens:
|
||||||
|
if t.token != token.token:
|
||||||
|
t.expired = True
|
||||||
|
await data.session_commit()
|
||||||
|
return {
|
||||||
|
"revoked_tokens": len(tokens) - 1
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ from .telegram import Telegram
|
||||||
from .discord import Discord
|
from .discord import Discord
|
||||||
from .matrix import Matrix
|
from .matrix import Matrix
|
||||||
from .aliases import Alias
|
from .aliases import Alias
|
||||||
|
from .tokens import Token
|
||||||
|
|
||||||
# Enter the tables of your Pack here!
|
# Enter the tables of your Pack here!
|
||||||
available_tables = {
|
available_tables = {
|
||||||
|
@ -11,7 +12,8 @@ available_tables = {
|
||||||
Telegram,
|
Telegram,
|
||||||
Discord,
|
Discord,
|
||||||
Matrix,
|
Matrix,
|
||||||
Alias
|
Alias,
|
||||||
|
Token,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Don't change this, it should automatically generate __all__
|
# Don't change this, it should automatically generate __all__
|
||||||
|
|
|
@ -4,32 +4,37 @@ from sqlalchemy import Column, \
|
||||||
ForeignKey
|
ForeignKey
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
|
import royalnet.utils as ru
|
||||||
|
|
||||||
|
|
||||||
class Alias:
|
class Alias:
|
||||||
__tablename__ = "aliases"
|
__tablename__ = "aliases"
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
def royal_id(self):
|
def user_id(self):
|
||||||
return Column(Integer, ForeignKey("users.uid"))
|
return Column(Integer, ForeignKey("users.uid"), primary_key=True)
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
def alias(self):
|
def alias(self):
|
||||||
return Column(String, primary_key=True)
|
return Column(String, primary_key=True)
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
def royal(self):
|
def user(self):
|
||||||
return relationship("User", backref="aliases")
|
return relationship("User", backref="aliases")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_by_alias(cls, alchemy, session, alias: str):
|
async def find_user(cls, alchemy, session, alias: str):
|
||||||
result = session.query(alchemy.get(cls)).filter_by(alias=alias.lower()).one_or_none()
|
result = await ru.asyncify(session.query(alchemy.get(cls)).filter_by(alias=alias.lower()).one_or_none)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
result = result.royal
|
result = result.user
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def __init__(self, user: str, alias: str):
|
||||||
|
self.user = user
|
||||||
|
self.alias = alias.lower()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Alias {str(self)}>"
|
return f"<Alias {str(self)}>"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.alias}->{self.royal_id}"
|
return f"{self.alias}->{self.user_id}"
|
||||||
|
|
|
@ -13,9 +13,13 @@ class Discord:
|
||||||
__tablename__ = "discord"
|
__tablename__ = "discord"
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
def royal_id(self):
|
def user_id(self):
|
||||||
return Column(Integer, ForeignKey("users.uid"))
|
return Column(Integer, ForeignKey("users.uid"))
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def user(self):
|
||||||
|
return relationship("User", backref="discord")
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
def discord_id(self):
|
def discord_id(self):
|
||||||
return Column(BigInteger, primary_key=True)
|
return Column(BigInteger, primary_key=True)
|
||||||
|
@ -29,13 +33,9 @@ class Discord:
|
||||||
return Column(String)
|
return Column(String)
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
def avatar_hash(self):
|
def avatar_url(self):
|
||||||
return Column(String)
|
return Column(String)
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def royal(self):
|
|
||||||
return relationship("User", backref="discord")
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Discord {str(self)}>"
|
return f"<Discord {str(self)}>"
|
||||||
|
|
||||||
|
|
|
@ -13,9 +13,13 @@ class Telegram:
|
||||||
__tablename__ = "telegram"
|
__tablename__ = "telegram"
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
def royal_id(self):
|
def user_id(self):
|
||||||
return Column(Integer, ForeignKey("users.uid"))
|
return Column(Integer, ForeignKey("users.uid"))
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def user(self):
|
||||||
|
return relationship("User", backref="telegram")
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
def tg_id(self):
|
def tg_id(self):
|
||||||
return Column(BigInteger, primary_key=True)
|
return Column(BigInteger, primary_key=True)
|
||||||
|
@ -32,10 +36,6 @@ class Telegram:
|
||||||
def username(self):
|
def username(self):
|
||||||
return Column(String)
|
return Column(String)
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def royal(self):
|
|
||||||
return relationship("User", backref="telegram")
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Telegram {str(self)}>"
|
return f"<Telegram {str(self)}>"
|
||||||
|
|
||||||
|
|
55
royalnet/backpack/tables/tokens.py
Normal file
55
royalnet/backpack/tables/tokens.py
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import datetime
|
||||||
|
import secrets
|
||||||
|
from sqlalchemy import *
|
||||||
|
from sqlalchemy.orm import *
|
||||||
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
|
import royalnet.utils as ru
|
||||||
|
|
||||||
|
|
||||||
|
class Token:
|
||||||
|
__tablename__ = "tokens"
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def token(self):
|
||||||
|
return Column(String, primary_key=True)
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def user_id(self):
|
||||||
|
return Column(Integer, ForeignKey("users.uid"), nullable=False)
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def user(self):
|
||||||
|
return relationship("User", backref="tokens")
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def expiration(self):
|
||||||
|
return Column(DateTime, nullable=False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expired(self):
|
||||||
|
return datetime.datetime.now() > self.expiration
|
||||||
|
|
||||||
|
@expired.setter
|
||||||
|
def expired(self, value):
|
||||||
|
if value is True:
|
||||||
|
self.expiration = datetime.datetime.fromtimestamp(0)
|
||||||
|
else:
|
||||||
|
raise ValueError("'expired' can only be set to True.")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate(cls, alchemy, user, expiration_delta: datetime.timedelta):
|
||||||
|
# noinspection PyArgumentList
|
||||||
|
TokenT = alchemy.get(cls)
|
||||||
|
token = TokenT(user=user, expiration=datetime.datetime.now() + expiration_delta, token=secrets.token_urlsafe())
|
||||||
|
return token
|
||||||
|
|
||||||
|
def json(self) -> dict:
|
||||||
|
return {
|
||||||
|
"user": self.user.json(),
|
||||||
|
"token": self.token,
|
||||||
|
"expiration": self.expiration.isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def authenticate(cls, alchemy, session, token: str) -> "Token":
|
||||||
|
return await ru.asyncify(session.query(alchemy.get(cls)).filter_by(token=token).one_or_none)
|
|
@ -1,3 +1,4 @@
|
||||||
|
import bcrypt
|
||||||
from sqlalchemy import Column, \
|
from sqlalchemy import Column, \
|
||||||
Integer, \
|
Integer, \
|
||||||
String, \
|
String, \
|
||||||
|
@ -5,6 +6,7 @@ from sqlalchemy import Column, \
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyAttributeOutsideInit
|
||||||
class User:
|
class User:
|
||||||
__tablename__ = "users"
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
@ -36,6 +38,16 @@ class User:
|
||||||
"avatar": self.avatar
|
"avatar": self.avatar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def set_password(self, password: str):
|
||||||
|
byte_password: bytes = bytes(password, encoding="UTF8")
|
||||||
|
self.password = bcrypt.hashpw(byte_password, bcrypt.gensalt(14))
|
||||||
|
|
||||||
|
def test_password(self, password: str):
|
||||||
|
if self.password is None:
|
||||||
|
raise ValueError("No password is set")
|
||||||
|
byte_password: bytes = bytes(password, encoding="UTF8")
|
||||||
|
return bcrypt.checkpw(byte_password, self.password)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__qualname__} {self.username}>"
|
return f"<{self.__class__.__qualname__} {self.username}>"
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,11 @@ import asyncio as aio
|
||||||
import royalnet.utils as ru
|
import royalnet.utils as ru
|
||||||
from .errors import UnsupportedError
|
from .errors import UnsupportedError
|
||||||
from .commandinterface import CommandInterface
|
from .commandinterface import CommandInterface
|
||||||
|
from royalnet.backpack.tables.aliases import Alias
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .keyboardkey import KeyboardKey
|
from .keyboardkey import KeyboardKey
|
||||||
|
from royalnet.backpack.tables.users import User
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -24,6 +26,7 @@ class CommandData:
|
||||||
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")
|
||||||
|
log.debug("Creating Session...")
|
||||||
self._session = self._interface.alchemy.Session()
|
self._session = self._interface.alchemy.Session()
|
||||||
return self._session
|
return self._session
|
||||||
|
|
||||||
|
@ -32,11 +35,13 @@ class CommandData:
|
||||||
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
|
||||||
|
log.debug("Committing Session...")
|
||||||
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."""
|
"""Asyncronously close the :attr:`.session` of this object."""
|
||||||
if self._session is not None:
|
if self._session is not None:
|
||||||
|
log.debug("Closing Session...")
|
||||||
await ru.asyncify(self._session.close)
|
await ru.asyncify(self._session.close)
|
||||||
|
|
||||||
async def reply(self, text: str) -> None:
|
async def reply(self, text: str) -> None:
|
||||||
|
@ -64,6 +69,13 @@ class CommandData:
|
||||||
if error_if_unavailable:
|
if error_if_unavailable:
|
||||||
raise UnsupportedError(f"'{self.delete_invoking.__name__}' is not supported")
|
raise UnsupportedError(f"'{self.delete_invoking.__name__}' is not supported")
|
||||||
|
|
||||||
|
async def find_user(self, alias: str) -> Optional["User"]:
|
||||||
|
"""Find the User having a specific Alias.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
alias: the Alias to search for."""
|
||||||
|
return await Alias.find_user(self._interface.alchemy, self.session, alias)
|
||||||
|
|
||||||
@contextlib.asynccontextmanager
|
@contextlib.asynccontextmanager
|
||||||
async def keyboard(self, text, keys: List["KeyboardKey"]):
|
async def keyboard(self, text, keys: List["KeyboardKey"]):
|
||||||
yield
|
yield
|
||||||
|
|
|
@ -15,13 +15,11 @@ You can install them with: ::
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .constellation import Constellation
|
from .constellation import Constellation
|
||||||
from .star import Star, PageStar, ExceptionStar
|
from .star import Star
|
||||||
from .shoot import shoot
|
from .pagestar import PageStar
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Constellation",
|
"Constellation",
|
||||||
"Star",
|
"Star",
|
||||||
"PageStar",
|
"PageStar",
|
||||||
"ExceptionStar",
|
|
||||||
"shoot",
|
|
||||||
]
|
]
|
||||||
|
|
34
royalnet/constellation/api/__init__.py
Normal file
34
royalnet/constellation/api/__init__.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
from .apistar import ApiStar
|
||||||
|
from .jsonapi import api_response, api_success, api_error
|
||||||
|
from .apidata import ApiData
|
||||||
|
from .apierrors import \
|
||||||
|
ApiError, \
|
||||||
|
NotFoundError, \
|
||||||
|
ForbiddenError, \
|
||||||
|
BadRequestError, \
|
||||||
|
ParameterError, \
|
||||||
|
MissingParameterError, \
|
||||||
|
InvalidParameterError, \
|
||||||
|
NotImplementedError, \
|
||||||
|
UnsupportedError
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ApiStar",
|
||||||
|
"api_response",
|
||||||
|
"api_success",
|
||||||
|
"api_error",
|
||||||
|
"ApiData",
|
||||||
|
"ApiError",
|
||||||
|
"MissingParameterError",
|
||||||
|
"NotFoundError",
|
||||||
|
"ApiError",
|
||||||
|
"NotFoundError",
|
||||||
|
"ForbiddenError",
|
||||||
|
"BadRequestError",
|
||||||
|
"ParameterError",
|
||||||
|
"MissingParameterError",
|
||||||
|
"InvalidParameterError",
|
||||||
|
"NotImplementedError",
|
||||||
|
"UnsupportedError",
|
||||||
|
]
|
50
royalnet/constellation/api/apidata.py
Normal file
50
royalnet/constellation/api/apidata.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import logging
|
||||||
|
from .apierrors import MissingParameterError
|
||||||
|
from royalnet.backpack.tables.tokens import Token
|
||||||
|
from royalnet.backpack.tables.users import User
|
||||||
|
from .apierrors import *
|
||||||
|
import royalnet.utils as ru
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ApiData(dict):
|
||||||
|
def __init__(self, data, star):
|
||||||
|
super().__init__(data)
|
||||||
|
self.star = star
|
||||||
|
self._session = None
|
||||||
|
|
||||||
|
def __missing__(self, key):
|
||||||
|
raise MissingParameterError(f"Missing '{key}'")
|
||||||
|
|
||||||
|
async def token(self) -> Token:
|
||||||
|
token = await Token.authenticate(self.star.alchemy, self.session, self["token"])
|
||||||
|
if token is None:
|
||||||
|
raise ForbiddenError("'token' is invalid")
|
||||||
|
return token
|
||||||
|
|
||||||
|
async def user(self) -> User:
|
||||||
|
return (await self.token()).user
|
||||||
|
|
||||||
|
@property
|
||||||
|
def session(self):
|
||||||
|
if self._session is None:
|
||||||
|
if self.star.alchemy is None:
|
||||||
|
raise UnsupportedError("'alchemy' is not enabled on this Royalnet instance")
|
||||||
|
log.debug("Creating Session...")
|
||||||
|
self._session = self.star.alchemy.Session()
|
||||||
|
return self._session
|
||||||
|
|
||||||
|
async def session_commit(self):
|
||||||
|
"""Asyncronously commit the :attr:`.session` of this object."""
|
||||||
|
if self._session:
|
||||||
|
log.warning("Session had to be created to be committed")
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
log.debug("Committing Session...")
|
||||||
|
await ru.asyncify(self.session.commit)
|
||||||
|
|
||||||
|
async def session_close(self):
|
||||||
|
"""Asyncronously close the :attr:`.session` of this object."""
|
||||||
|
if self._session is not None:
|
||||||
|
log.debug("Closing Session...")
|
||||||
|
await ru.asyncify(self._session.close)
|
34
royalnet/constellation/api/apierrors.py
Normal file
34
royalnet/constellation/api/apierrors.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
class ApiError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotFoundError(ApiError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ForbiddenError(ApiError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BadRequestError(ApiError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ParameterError(BadRequestError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MissingParameterError(ParameterError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidParameterError(ParameterError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotImplementedError(ApiError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedError(NotImplementedError):
|
||||||
|
pass
|
42
royalnet/constellation/api/apistar.py
Normal file
42
royalnet/constellation/api/apistar.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
from typing import *
|
||||||
|
from json import JSONDecodeError
|
||||||
|
from abc import *
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.responses import JSONResponse
|
||||||
|
from ..pagestar import PageStar
|
||||||
|
from .jsonapi import api_error, api_success
|
||||||
|
from .apidata import ApiData
|
||||||
|
from .apierrors import *
|
||||||
|
import royalnet.utils as ru
|
||||||
|
|
||||||
|
|
||||||
|
class ApiStar(PageStar, ABC):
|
||||||
|
async def page(self, request: Request) -> JSONResponse:
|
||||||
|
if request.query_params:
|
||||||
|
data = request.query_params
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
except JSONDecodeError:
|
||||||
|
data = {}
|
||||||
|
apidata = ApiData(data, self)
|
||||||
|
try:
|
||||||
|
response = await self.api(apidata)
|
||||||
|
except NotFoundError as e:
|
||||||
|
return api_error(e, code=404)
|
||||||
|
except ForbiddenError as e:
|
||||||
|
return api_error(e, code=403)
|
||||||
|
except NotImplementedError as e:
|
||||||
|
return api_error(e, code=501)
|
||||||
|
except BadRequestError as e:
|
||||||
|
return api_error(e, code=400)
|
||||||
|
except Exception as e:
|
||||||
|
ru.sentry_exc(e)
|
||||||
|
return api_error(e, code=500)
|
||||||
|
else:
|
||||||
|
return api_success(response)
|
||||||
|
finally:
|
||||||
|
await apidata.session_close()
|
||||||
|
|
||||||
|
async def api(self, data: ApiData) -> ru.JSON:
|
||||||
|
raise NotImplementedError()
|
33
royalnet/constellation/api/jsonapi.py
Normal file
33
royalnet/constellation/api/jsonapi.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
from typing import *
|
||||||
|
try:
|
||||||
|
from starlette.responses import JSONResponse
|
||||||
|
except ImportError:
|
||||||
|
JSONResponse = None
|
||||||
|
|
||||||
|
|
||||||
|
def api_response(data: dict, code: int, headers: dict = None) -> JSONResponse:
|
||||||
|
if headers is None:
|
||||||
|
headers = {}
|
||||||
|
full_headers = {
|
||||||
|
**headers,
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
}
|
||||||
|
return JSONResponse(data, status_code=code, headers=full_headers)
|
||||||
|
|
||||||
|
|
||||||
|
def api_success(data: dict) -> JSONResponse:
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"data": data
|
||||||
|
}
|
||||||
|
return api_response(result, code=200)
|
||||||
|
|
||||||
|
|
||||||
|
def api_error(error: Exception, code: int = 500) -> JSONResponse:
|
||||||
|
result = {
|
||||||
|
"success": False,
|
||||||
|
"error_type": error.__class__.__qualname__,
|
||||||
|
"error_args": list(error.args),
|
||||||
|
"error_code": code,
|
||||||
|
}
|
||||||
|
return api_response(result, code=code)
|
|
@ -8,7 +8,7 @@ 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
|
||||||
import royalnet.commands as rc
|
import royalnet.commands as rc
|
||||||
from .star import PageStar, ExceptionStar
|
from .pagestar import PageStar
|
||||||
from ..utils import init_logging
|
from ..utils import init_logging
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,7 +42,12 @@ class Constellation:
|
||||||
for pack_name in pack_names:
|
for pack_name in pack_names:
|
||||||
log.debug(f"Importing pack: {pack_name}")
|
log.debug(f"Importing pack: {pack_name}")
|
||||||
try:
|
try:
|
||||||
packs[pack_name] = importlib.import_module(pack_name)
|
packs[pack_name] = {
|
||||||
|
"commands": importlib.import_module(f"{pack_name}.commands"),
|
||||||
|
"events": importlib.import_module(f"{pack_name}.events"),
|
||||||
|
"stars": importlib.import_module(f"{pack_name}.stars"),
|
||||||
|
"tables": importlib.import_module(f"{pack_name}.tables"),
|
||||||
|
}
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
log.error(f"Error during the import of {pack_name}: {e}")
|
log.error(f"Error during the import of {pack_name}: {e}")
|
||||||
log.info(f"Packs: {len(packs)} imported")
|
log.info(f"Packs: {len(packs)} imported")
|
||||||
|
@ -60,7 +65,7 @@ class Constellation:
|
||||||
tables = set()
|
tables = set()
|
||||||
for pack in packs.values():
|
for pack in packs.values():
|
||||||
try:
|
try:
|
||||||
tables = tables.union(pack.available_tables)
|
tables = tables.union(pack["tables"].available_tables)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
log.warning(f"Pack `{pack}` does not have the `available_tables` attribute.")
|
log.warning(f"Pack `{pack}` does not have the `available_tables` attribute.")
|
||||||
continue
|
continue
|
||||||
|
@ -99,7 +104,7 @@ class Constellation:
|
||||||
pack = packs[pack_name]
|
pack = packs[pack_name]
|
||||||
pack_cfg = packs_cfg.get(pack_name, {})
|
pack_cfg = packs_cfg.get(pack_name, {})
|
||||||
try:
|
try:
|
||||||
events = pack.available_events
|
events = pack["events"].available_events
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
log.warning(f"Pack `{pack}` does not have the `available_events` attribute.")
|
log.warning(f"Pack `{pack}` does not have the `available_events` attribute.")
|
||||||
else:
|
else:
|
||||||
|
@ -118,17 +123,11 @@ class Constellation:
|
||||||
pack = packs[pack_name]
|
pack = packs[pack_name]
|
||||||
pack_cfg = packs_cfg.get(pack_name, {})
|
pack_cfg = packs_cfg.get(pack_name, {})
|
||||||
try:
|
try:
|
||||||
page_stars = pack.available_page_stars
|
page_stars = pack["stars"].available_page_stars
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
log.warning(f"Pack `{pack}` does not have the `available_page_stars` attribute.")
|
log.warning(f"Pack `{pack}` does not have the `available_page_stars` attribute.")
|
||||||
else:
|
else:
|
||||||
self.register_page_stars(page_stars, pack_cfg)
|
self.register_page_stars(page_stars, pack_cfg)
|
||||||
try:
|
|
||||||
exc_stars = pack.available_exception_stars
|
|
||||||
except AttributeError:
|
|
||||||
log.warning(f"Pack `{pack}` does not have the `available_exception_stars` attribute.")
|
|
||||||
else:
|
|
||||||
self.register_exc_stars(exc_stars, pack_cfg)
|
|
||||||
log.info(f"PageStars: {len(self.starlette.routes)} stars")
|
log.info(f"PageStars: {len(self.starlette.routes)} stars")
|
||||||
log.info(f"ExceptionStars: {len(self.starlette.exception_handlers)} stars")
|
log.info(f"ExceptionStars: {len(self.starlette.exception_handlers)} stars")
|
||||||
|
|
||||||
|
@ -259,14 +258,6 @@ class Constellation:
|
||||||
|
|
||||||
return page_star.path, f, page_star.methods
|
return page_star.path, f, page_star.methods
|
||||||
|
|
||||||
def _exc_star_wrapper(self, exc_star: ExceptionStar):
|
|
||||||
async def f(request):
|
|
||||||
self._first_page_check()
|
|
||||||
log.info(f"Running {exc_star}")
|
|
||||||
return await exc_star.page(request)
|
|
||||||
|
|
||||||
return exc_star.error, f
|
|
||||||
|
|
||||||
def register_page_stars(self, page_stars: List[Type[PageStar]], pack_cfg: Dict[str, Any]):
|
def register_page_stars(self, page_stars: List[Type[PageStar]], pack_cfg: Dict[str, Any]):
|
||||||
for SelectedPageStar in page_stars:
|
for SelectedPageStar in page_stars:
|
||||||
log.debug(f"Registering: {SelectedPageStar.path} -> {SelectedPageStar.__qualname__}")
|
log.debug(f"Registering: {SelectedPageStar.path} -> {SelectedPageStar.__qualname__}")
|
||||||
|
@ -279,18 +270,6 @@ class Constellation:
|
||||||
continue
|
continue
|
||||||
self.starlette.add_route(*self._page_star_wrapper(page_star_instance))
|
self.starlette.add_route(*self._page_star_wrapper(page_star_instance))
|
||||||
|
|
||||||
def register_exc_stars(self, exc_stars: List[Type[ExceptionStar]], pack_cfg: Dict[str, Any]):
|
|
||||||
for SelectedExcStar in exc_stars:
|
|
||||||
log.debug(f"Registering: {SelectedExcStar.error} -> {SelectedExcStar.__qualname__}")
|
|
||||||
try:
|
|
||||||
exc_star_instance = SelectedExcStar(interface=self.Interface(pack_cfg))
|
|
||||||
except Exception as e:
|
|
||||||
log.error(f"Skipping: "
|
|
||||||
f"{SelectedExcStar.__qualname__} - {e.__class__.__qualname__} in the initialization.")
|
|
||||||
ru.sentry_exc(e)
|
|
||||||
continue
|
|
||||||
self.starlette.add_exception_handler(*self._exc_star_wrapper(exc_star_instance))
|
|
||||||
|
|
||||||
def run_blocking(self):
|
def run_blocking(self):
|
||||||
log.info(f"Running Constellation on https://{self.address}:{self.port}/...")
|
log.info(f"Running Constellation on https://{self.address}:{self.port}/...")
|
||||||
loop: aio.AbstractEventLoop = aio.get_event_loop()
|
loop: aio.AbstractEventLoop = aio.get_event_loop()
|
||||||
|
|
34
royalnet/constellation/pagestar.py
Normal file
34
royalnet/constellation/pagestar.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
from typing import *
|
||||||
|
from .star import Star
|
||||||
|
|
||||||
|
|
||||||
|
class PageStar(Star):
|
||||||
|
"""A PageStar is a class representing a single route of the website (for example, ``/api/user/get``).
|
||||||
|
|
||||||
|
To create a new website route you should create a new class inheriting from this class with a function overriding
|
||||||
|
:meth:`.page`, :attr:`.path` and optionally :attr:`.methods`."""
|
||||||
|
|
||||||
|
path: str = NotImplemented
|
||||||
|
"""The route of the star.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
::
|
||||||
|
|
||||||
|
path: str = '/api/user/get'
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
methods: List[str] = ["GET"]
|
||||||
|
"""The HTTP methods supported by the Star, in form of a list.
|
||||||
|
|
||||||
|
By default, a Star only supports the ``GET`` method, but more can be added.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
::
|
||||||
|
|
||||||
|
methods: List[str] = ["GET", "POST", "PUT", "DELETE"]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<{self.__class__.__qualname__}: {self.path}>"
|
|
@ -1,13 +0,0 @@
|
||||||
try:
|
|
||||||
from starlette.responses import JSONResponse
|
|
||||||
except ImportError:
|
|
||||||
JSONResponse = None
|
|
||||||
|
|
||||||
|
|
||||||
def shoot(code: int, description: str) -> JSONResponse:
|
|
||||||
"""Create a error :class:`~starlette.response.JSONResponse` with the passed error code and description."""
|
|
||||||
if JSONResponse is None:
|
|
||||||
raise ImportError("'constellation' extra is not installed")
|
|
||||||
return JSONResponse({
|
|
||||||
"error": description
|
|
||||||
}, status_code=code)
|
|
|
@ -48,63 +48,3 @@ class Star:
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__qualname__}>"
|
return f"<{self.__class__.__qualname__}>"
|
||||||
|
|
||||||
|
|
||||||
class PageStar(Star):
|
|
||||||
"""A PageStar is a class representing a single route of the website (for example, ``/api/user/get``).
|
|
||||||
|
|
||||||
To create a new website route you should create a new class inheriting from this class with a function overriding
|
|
||||||
:meth:`.page` and changing the values of :attr:`.path` and optionally :attr:`.methods`."""
|
|
||||||
path: str = NotImplemented
|
|
||||||
"""The route of the star.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
::
|
|
||||||
|
|
||||||
path: str = '/api/user/get'
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
methods: List[str] = ["GET"]
|
|
||||||
"""The HTTP methods supported by the Star, in form of a list.
|
|
||||||
|
|
||||||
By default, a Star only supports the ``GET`` method, but more can be added.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
::
|
|
||||||
|
|
||||||
methods: List[str] = ["GET", "POST", "PUT", "DELETE"]
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<{self.__class__.__qualname__}: {self.path}>"
|
|
||||||
|
|
||||||
|
|
||||||
class ExceptionStar(Star):
|
|
||||||
"""An ExceptionStar is a class that handles an :class:`Exception` raised by another star by returning a different
|
|
||||||
response than the one originally intended.
|
|
||||||
|
|
||||||
The handled exception type is specified in the :attr:`.error`.
|
|
||||||
|
|
||||||
It can also handle standard webserver errors, such as ``404 Not Found``:
|
|
||||||
to handle them, set :attr:`.error` to an :class:`int` of the corresponding error code.
|
|
||||||
|
|
||||||
To create a new exception handler you should create a new class inheriting from this class with a function
|
|
||||||
overriding :meth:`.page` and changing the value of :attr:`.error`."""
|
|
||||||
error: Union[Type[Exception], int]
|
|
||||||
"""The error that should be handled by this star. It should be either a subclass of :exc:`Exception`,
|
|
||||||
or the :class:`int` of an HTTP error code.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
::
|
|
||||||
|
|
||||||
error: int = 404
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
error: Type[Exception] = ValueError
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<{self.__class__.__qualname__}: handles {self.error}>"
|
|
||||||
|
|
|
@ -19,7 +19,12 @@ def run(config_filename, file_format):
|
||||||
packs = {}
|
packs = {}
|
||||||
for pack_name in pack_names:
|
for pack_name in pack_names:
|
||||||
try:
|
try:
|
||||||
packs[pack_name] = importlib.import_module(pack_name)
|
packs[pack_name] = {
|
||||||
|
"commands": importlib.import_module(f"{pack_name}.commands"),
|
||||||
|
"events": importlib.import_module(f"{pack_name}.events"),
|
||||||
|
"stars": importlib.import_module(f"{pack_name}.stars"),
|
||||||
|
"tables": importlib.import_module(f"{pack_name}.tables"),
|
||||||
|
}
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
p(f"Skipping `{pack_name}`: {e}", err=True)
|
p(f"Skipping `{pack_name}`: {e}", err=True)
|
||||||
continue
|
continue
|
||||||
|
@ -30,7 +35,7 @@ def run(config_filename, file_format):
|
||||||
lines = []
|
lines = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
commands = pack.available_commands
|
commands = pack["commands"].available_commands
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
p(f"Pack `{pack}` does not have the `available_commands` attribute.", err=True)
|
p(f"Pack `{pack}` does not have the `available_commands` attribute.", err=True)
|
||||||
continue
|
continue
|
||||||
|
@ -41,93 +46,6 @@ def run(config_filename, file_format):
|
||||||
for line in lines:
|
for line in lines:
|
||||||
p(line)
|
p(line)
|
||||||
|
|
||||||
elif file_format == "markdown":
|
|
||||||
p("<!--This documentation was autogenerated with `python -m royalnet.generate -f markdown`.-->")
|
|
||||||
p("")
|
|
||||||
for pack_name in packs:
|
|
||||||
pack = packs[pack_name]
|
|
||||||
p(f"# `{pack_name}`")
|
|
||||||
p("")
|
|
||||||
if pack.__doc__:
|
|
||||||
p(f"{pack.__doc__}")
|
|
||||||
p("")
|
|
||||||
|
|
||||||
try:
|
|
||||||
commands = pack.available_commands
|
|
||||||
except AttributeError:
|
|
||||||
p(f"Pack `{pack}` does not have the `available_commands` attribute.", err=True)
|
|
||||||
else:
|
|
||||||
p(f"## Commands")
|
|
||||||
p("")
|
|
||||||
for command in commands:
|
|
||||||
p(f"### `{command.name}`")
|
|
||||||
p("")
|
|
||||||
p(f"{command.description}")
|
|
||||||
p("")
|
|
||||||
if command.__doc__:
|
|
||||||
p(f"{command.__doc__}")
|
|
||||||
p("")
|
|
||||||
if len(command.aliases) > 0:
|
|
||||||
p(f"> Aliases: {''.join(['`' + alias + '` ' for alias in command.aliases])}")
|
|
||||||
p("")
|
|
||||||
|
|
||||||
try:
|
|
||||||
events = pack.available_events
|
|
||||||
except AttributeError:
|
|
||||||
p(f"Pack `{pack}` does not have the `available_events` attribute.", err=True)
|
|
||||||
else:
|
|
||||||
p(f"## Events")
|
|
||||||
p("")
|
|
||||||
for event in events:
|
|
||||||
p(f"### `{event.name}`")
|
|
||||||
p("")
|
|
||||||
if event.__doc__:
|
|
||||||
p(f"{event.__doc__}")
|
|
||||||
p("")
|
|
||||||
|
|
||||||
try:
|
|
||||||
page_stars = pack.available_page_stars
|
|
||||||
except AttributeError:
|
|
||||||
p(f"Pack `{pack}` does not have the `available_page_stars` attribute.", err=True)
|
|
||||||
else:
|
|
||||||
p(f"## Page Stars")
|
|
||||||
p("")
|
|
||||||
for page_star in page_stars:
|
|
||||||
p(f"### `{page_star.path}`")
|
|
||||||
p("")
|
|
||||||
if page_star.__doc__:
|
|
||||||
p(f"{page_star.__doc__}")
|
|
||||||
p("")
|
|
||||||
|
|
||||||
try:
|
|
||||||
exc_stars = pack.available_exception_stars
|
|
||||||
except AttributeError:
|
|
||||||
p(f"Pack `{pack}` does not have the `available_exception_stars` attribute.", err=True)
|
|
||||||
else:
|
|
||||||
p(f"## Exception Stars")
|
|
||||||
p("")
|
|
||||||
for exc_star in exc_stars:
|
|
||||||
p(f"### `{exc_star.error}`")
|
|
||||||
p("")
|
|
||||||
if exc_star.__doc__:
|
|
||||||
p(f"{exc_star.__doc__}")
|
|
||||||
p("")
|
|
||||||
|
|
||||||
try:
|
|
||||||
tables = pack.available_tables
|
|
||||||
except AttributeError:
|
|
||||||
p(f"Pack `{pack}` does not have the `available_tables` attribute.", err=True)
|
|
||||||
else:
|
|
||||||
p(f"## Tables")
|
|
||||||
p("")
|
|
||||||
for table in tables:
|
|
||||||
p(f"### `{table.__tablename__}`")
|
|
||||||
p("")
|
|
||||||
# TODO: list columns
|
|
||||||
if table.__doc__:
|
|
||||||
p(f"{table.__doc__}")
|
|
||||||
p("")
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise click.ClickException("Unknown format")
|
raise click.ClickException("Unknown format")
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ 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.tables as rbt
|
||||||
import royalnet.herald as rh
|
import royalnet.herald as rh
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ class Serf:
|
||||||
Discord)."""
|
Discord)."""
|
||||||
interface_name = NotImplemented
|
interface_name = NotImplemented
|
||||||
|
|
||||||
_master_table: type = rb.tables.User
|
_master_table: type = rbt.User
|
||||||
_identity_table: type = NotImplemented
|
_identity_table: type = NotImplemented
|
||||||
_identity_column: str = NotImplemented
|
_identity_column: str = NotImplemented
|
||||||
|
|
||||||
|
@ -39,10 +39,15 @@ class Serf:
|
||||||
for pack_name in pack_names:
|
for pack_name in pack_names:
|
||||||
log.debug(f"Importing pack: {pack_name}")
|
log.debug(f"Importing pack: {pack_name}")
|
||||||
try:
|
try:
|
||||||
packs[pack_name] = importlib.import_module(pack_name)
|
packs[pack_name] = {
|
||||||
|
"commands": importlib.import_module(f"{pack_name}.commands"),
|
||||||
|
"events": importlib.import_module(f"{pack_name}.events"),
|
||||||
|
"stars": importlib.import_module(f"{pack_name}.stars"),
|
||||||
|
"tables": importlib.import_module(f"{pack_name}.tables"),
|
||||||
|
}
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
log.error(f"{e.__class__.__name__} during the import of {pack_name}:\n"
|
log.error(f"{e.__class__.__name__} during the import of {pack_name}:\n"
|
||||||
f"{traceback.format_exception(*sys.exc_info())}")
|
f"{''.join(traceback.format_exception(*sys.exc_info()))}")
|
||||||
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
|
||||||
|
@ -68,7 +73,7 @@ class Serf:
|
||||||
tables = set()
|
tables = set()
|
||||||
for pack in packs.values():
|
for pack in packs.values():
|
||||||
try:
|
try:
|
||||||
tables = tables.union(pack.available_tables)
|
tables = tables.union(pack["tables"].available_tables)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
log.warning(f"Pack `{pack}` does not have the `available_tables` attribute.")
|
log.warning(f"Pack `{pack}` does not have the `available_tables` attribute.")
|
||||||
continue
|
continue
|
||||||
|
@ -95,13 +100,13 @@ class Serf:
|
||||||
pack = packs[pack_name]
|
pack = packs[pack_name]
|
||||||
pack_cfg = packs_cfg.get(pack_name, {})
|
pack_cfg = packs_cfg.get(pack_name, {})
|
||||||
try:
|
try:
|
||||||
events = pack.available_events
|
events = pack["events"].available_events
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
log.warning(f"Pack `{pack}` does not have the `available_events` attribute.")
|
log.warning(f"Pack `{pack}` does not have the `available_events` attribute.")
|
||||||
else:
|
else:
|
||||||
self.register_events(events, pack_cfg)
|
self.register_events(events, pack_cfg)
|
||||||
try:
|
try:
|
||||||
commands = pack.available_commands
|
commands = pack["commands"].available_commands
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
log.warning(f"Pack `{pack}` does not have the `available_commands` attribute.")
|
log.warning(f"Pack `{pack}` does not have the `available_commands` attribute.")
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -5,6 +5,7 @@ from .urluuid import to_urluuid, from_urluuid
|
||||||
from .multilock import MultiLock
|
from .multilock import MultiLock
|
||||||
from .sentry import init_sentry, sentry_exc
|
from .sentry import init_sentry, sentry_exc
|
||||||
from .log import init_logging
|
from .log import init_logging
|
||||||
|
from .royaltyping import JSON
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"asyncify",
|
"asyncify",
|
||||||
|
@ -20,4 +21,5 @@ __all__ = [
|
||||||
"init_sentry",
|
"init_sentry",
|
||||||
"sentry_exc",
|
"sentry_exc",
|
||||||
"init_logging",
|
"init_logging",
|
||||||
|
"JSON",
|
||||||
]
|
]
|
||||||
|
|
3
royalnet/utils/royaltyping.py
Normal file
3
royalnet/utils/royaltyping.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from typing import *
|
||||||
|
|
||||||
|
JSON = Union[None, int, str, List["JSON"], Dict[str, "JSON"]]
|
|
@ -1 +1 @@
|
||||||
semantic = "5.4.1"
|
semantic = "5.5"
|
||||||
|
|
Loading…
Reference in a new issue