1
Fork 0
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:
Steffo 2020-02-11 23:05:19 +01:00
commit 6db81c689c
41 changed files with 622 additions and 336 deletions

41
poetry.lock generated
View file

@ -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
View file

@ -0,0 +1 @@
git commit -am "publish: %1" && git push && poetry build && poetry publish && hub release create "%1" -m "Royalnet %1"

View file

@ -1,5 +1,4 @@
from typing import * from typing import *
import royalnet
import royalnet.commands as rc import royalnet.commands as rc

View file

@ -1,5 +1,4 @@
from typing import * from typing import *
import royalnet
import royalnet.commands as rc import royalnet.commands as rc

View file

@ -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}"

View file

@ -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"]

View file

@ -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:

View file

@ -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",
]

View file

@ -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]

View file

@ -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")

View file

@ -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!")

View file

@ -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.")

View 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}")

View file

@ -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]

View 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

View file

@ -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__, }
}
})

View 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()

View 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()

View 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
}

View file

@ -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__

View file

@ -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}"

View file

@ -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)}>"

View file

@ -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)}>"

View 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)

View file

@ -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}>"

View file

@ -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

View file

@ -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",
] ]

View 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",
]

View 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)

View 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

View 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()

View 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)

View file

@ -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()

View 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}>"

View file

@ -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)

View file

@ -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}>"

View file

@ -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")

View file

@ -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:

View file

@ -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",
] ]

View file

@ -0,0 +1,3 @@
from typing import *
JSON = Union[None, int, str, List["JSON"], Dict[str, "JSON"]]

View file

@ -1 +1 @@
semantic = "5.4.1" semantic = "5.5"