mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-23 19:44:20 +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]
|
||||
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]]
|
||||
category = "main"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
|
@ -913,8 +928,8 @@ python-versions = "*"
|
|||
version = "2020.1.24"
|
||||
|
||||
[extras]
|
||||
alchemy_easy = ["sqlalchemy", "psycopg2_binary"]
|
||||
alchemy_hard = ["sqlalchemy", "psycopg2"]
|
||||
alchemy_easy = ["sqlalchemy", "psycopg2_binary", "bcrypt"]
|
||||
alchemy_hard = ["sqlalchemy", "psycopg2", "bcrypt"]
|
||||
bard = ["ffmpeg_python", "youtube_dl", "eyed3"]
|
||||
coloredlogs = ["coloredlogs"]
|
||||
constellation = ["starlette", "uvicorn", "python-multipart"]
|
||||
|
@ -925,7 +940,7 @@ sentry = ["sentry_sdk"]
|
|||
telegram = ["python_telegram_bot"]
|
||||
|
||||
[metadata]
|
||||
content-hash = "f275cd948fe28423a90d37d2825eabfec97e8ac0cdf52ee2d20f803d61987b40"
|
||||
content-hash = "218f4a253a7ef17bb871abf541ae7182f1626cd43d55417de12f9561c58ca0e9"
|
||||
python-versions = "^3.8"
|
||||
|
||||
[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.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 = [
|
||||
{file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"},
|
||||
{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 *
|
||||
import royalnet
|
||||
import royalnet.commands as rc
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from typing import *
|
||||
import royalnet
|
||||
import royalnet.commands as rc
|
||||
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ from sqlalchemy.orm import *
|
|||
from sqlalchemy.ext.declarative import declared_attr
|
||||
|
||||
|
||||
# noinspection PyAttributeOutsideInit
|
||||
#set($CAPITALIZED_NAME = $NAME.substring(0,1).toUpperCase() + $NAME.substring(1))
|
||||
class ${CAPITALIZED_NAME}:
|
||||
__tablename__ = "${NAME}"
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
[tool.poetry]
|
||||
name = "royalnet"
|
||||
version = "5.4.1"
|
||||
version = "5.5"
|
||||
description = "A multipurpose bot and web framework"
|
||||
authors = ["Stefano Pigozzi <ste.pigozzi@gmail.com>"]
|
||||
license = "AGPL-3.0+"
|
||||
|
@ -44,6 +44,7 @@
|
|||
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_binary = {version="^2.8.4", optional=true} # Prebuilt alternative to psycopg2, not recommended
|
||||
bcrypt = {version="^3.1.7", optional=true}
|
||||
|
||||
# constellation
|
||||
starlette = {version="^0.12.13", optional=true}
|
||||
|
@ -71,8 +72,8 @@
|
|||
telegram = ["python_telegram_bot"]
|
||||
discord = ["discord.py", "pynacl"]
|
||||
matrix = ["matrix-nio"]
|
||||
alchemy_easy = ["sqlalchemy", "psycopg2_binary"]
|
||||
alchemy_hard = ["sqlalchemy", "psycopg2"]
|
||||
alchemy_easy = ["sqlalchemy", "psycopg2_binary", "bcrypt"]
|
||||
alchemy_hard = ["sqlalchemy", "psycopg2", "bcrypt"]
|
||||
bard = ["ffmpeg_python", "youtube_dl", "eyed3"]
|
||||
constellation = ["starlette", "uvicorn", "python-multipart"]
|
||||
sentry = ["sentry_sdk"]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Set, Dict, Union
|
||||
from typing import *
|
||||
from contextlib import contextmanager, asynccontextmanager
|
||||
from royalnet.utils import asyncify
|
||||
from royalnet.alchemy.errors import TableNotFoundError
|
||||
|
@ -6,7 +6,7 @@ from sqlalchemy import create_engine
|
|||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.schema import Table
|
||||
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
|
||||
|
||||
|
||||
|
@ -26,7 +26,7 @@ class Alchemy:
|
|||
raise NotImplementedError("sqlite databases aren't supported, as they can't be used in multithreaded"
|
||||
" applications")
|
||||
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._tables: Dict[str, Table] = {}
|
||||
for table in tables:
|
||||
|
@ -38,7 +38,7 @@ class Alchemy:
|
|||
self._tables[name] = bound_table
|
||||
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.
|
||||
|
||||
Args:
|
||||
|
|
|
@ -1,19 +1 @@
|
|||
"""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!
|
||||
from .version import VersionCommand
|
||||
from .exception import ExceptionCommand
|
||||
from .excevent import ExceventCommand
|
||||
from .keyboardtest import KeyboardtestCommand
|
||||
|
||||
# Enter the commands of your Pack here!
|
||||
available_commands = [
|
||||
VersionCommand,
|
||||
]
|
||||
|
||||
# noinspection PyUnreachableCode
|
||||
if __debug__:
|
||||
available_commands = [
|
||||
*available_commands,
|
||||
ExceptionCommand,
|
||||
ExceventCommand,
|
||||
KeyboardtestCommand,
|
||||
]
|
||||
|
||||
# Don't change this, it should automatically generate __all__
|
||||
__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!
|
||||
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!
|
||||
available_page_stars = [
|
||||
ApiRoyalnetVersionStar,
|
||||
]
|
||||
|
||||
# Enter the ExceptionStars of your Pack here!
|
||||
available_exception_stars = [
|
||||
|
||||
ApiLoginRoyalnetStar,
|
||||
ApiTokenInfoStar,
|
||||
ApiTokenPasswdStar,
|
||||
ApiTokenCreateStar,
|
||||
]
|
||||
|
||||
# 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
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import *
|
||||
from royalnet.constellation import PageStar
|
||||
import royalnet.version as rv
|
||||
from royalnet.constellation.api import *
|
||||
import royalnet.utils as ru
|
||||
|
||||
|
||||
class ApiRoyalnetVersionStar(PageStar):
|
||||
path = "/api/royalnet/version"
|
||||
class ApiRoyalnetVersionStar(ApiStar):
|
||||
path = "/api/royalnet/version/v1"
|
||||
|
||||
async def page(self, request: Request) -> JSONResponse:
|
||||
return JSONResponse({
|
||||
"version": {
|
||||
"semantic": royalnet.__version__,
|
||||
}
|
||||
})
|
||||
async def api(self, data: ApiData) -> ru.JSON:
|
||||
return {
|
||||
"semantic": rv.semantic
|
||||
}
|
||||
|
|
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 .matrix import Matrix
|
||||
from .aliases import Alias
|
||||
from .tokens import Token
|
||||
|
||||
# Enter the tables of your Pack here!
|
||||
available_tables = {
|
||||
|
@ -11,7 +12,8 @@ available_tables = {
|
|||
Telegram,
|
||||
Discord,
|
||||
Matrix,
|
||||
Alias
|
||||
Alias,
|
||||
Token,
|
||||
}
|
||||
|
||||
# Don't change this, it should automatically generate __all__
|
||||
|
|
|
@ -4,32 +4,37 @@ from sqlalchemy import Column, \
|
|||
ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.ext.declarative import declared_attr
|
||||
import royalnet.utils as ru
|
||||
|
||||
|
||||
class Alias:
|
||||
__tablename__ = "aliases"
|
||||
|
||||
@declared_attr
|
||||
def royal_id(self):
|
||||
return Column(Integer, ForeignKey("users.uid"))
|
||||
def user_id(self):
|
||||
return Column(Integer, ForeignKey("users.uid"), primary_key=True)
|
||||
|
||||
@declared_attr
|
||||
def alias(self):
|
||||
return Column(String, primary_key=True)
|
||||
|
||||
@declared_attr
|
||||
def royal(self):
|
||||
def user(self):
|
||||
return relationship("User", backref="aliases")
|
||||
|
||||
@classmethod
|
||||
def find_by_alias(cls, alchemy, session, alias: str):
|
||||
result = session.query(alchemy.get(cls)).filter_by(alias=alias.lower()).one_or_none()
|
||||
async def find_user(cls, alchemy, session, alias: str):
|
||||
result = await ru.asyncify(session.query(alchemy.get(cls)).filter_by(alias=alias.lower()).one_or_none)
|
||||
if result is not None:
|
||||
result = result.royal
|
||||
result = result.user
|
||||
return result
|
||||
|
||||
def __init__(self, user: str, alias: str):
|
||||
self.user = user
|
||||
self.alias = alias.lower()
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Alias {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"
|
||||
|
||||
@declared_attr
|
||||
def royal_id(self):
|
||||
def user_id(self):
|
||||
return Column(Integer, ForeignKey("users.uid"))
|
||||
|
||||
@declared_attr
|
||||
def user(self):
|
||||
return relationship("User", backref="discord")
|
||||
|
||||
@declared_attr
|
||||
def discord_id(self):
|
||||
return Column(BigInteger, primary_key=True)
|
||||
|
@ -29,13 +33,9 @@ class Discord:
|
|||
return Column(String)
|
||||
|
||||
@declared_attr
|
||||
def avatar_hash(self):
|
||||
def avatar_url(self):
|
||||
return Column(String)
|
||||
|
||||
@declared_attr
|
||||
def royal(self):
|
||||
return relationship("User", backref="discord")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Discord {str(self)}>"
|
||||
|
||||
|
|
|
@ -13,9 +13,13 @@ class Telegram:
|
|||
__tablename__ = "telegram"
|
||||
|
||||
@declared_attr
|
||||
def royal_id(self):
|
||||
def user_id(self):
|
||||
return Column(Integer, ForeignKey("users.uid"))
|
||||
|
||||
@declared_attr
|
||||
def user(self):
|
||||
return relationship("User", backref="telegram")
|
||||
|
||||
@declared_attr
|
||||
def tg_id(self):
|
||||
return Column(BigInteger, primary_key=True)
|
||||
|
@ -32,10 +36,6 @@ class Telegram:
|
|||
def username(self):
|
||||
return Column(String)
|
||||
|
||||
@declared_attr
|
||||
def royal(self):
|
||||
return relationship("User", backref="telegram")
|
||||
|
||||
def __repr__(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, \
|
||||
Integer, \
|
||||
String, \
|
||||
|
@ -5,6 +6,7 @@ from sqlalchemy import Column, \
|
|||
from sqlalchemy.ext.declarative import declared_attr
|
||||
|
||||
|
||||
# noinspection PyAttributeOutsideInit
|
||||
class User:
|
||||
__tablename__ = "users"
|
||||
|
||||
|
@ -36,6 +38,16 @@ class User:
|
|||
"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):
|
||||
return f"<{self.__class__.__qualname__} {self.username}>"
|
||||
|
||||
|
|
|
@ -5,9 +5,11 @@ import asyncio as aio
|
|||
import royalnet.utils as ru
|
||||
from .errors import UnsupportedError
|
||||
from .commandinterface import CommandInterface
|
||||
from royalnet.backpack.tables.aliases import Alias
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .keyboardkey import KeyboardKey
|
||||
from royalnet.backpack.tables.users import User
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
@ -24,6 +26,7 @@ class CommandData:
|
|||
if self._session is None:
|
||||
if self._interface.alchemy is None:
|
||||
raise UnsupportedError("'alchemy' is not enabled on this Royalnet instance")
|
||||
log.debug("Creating Session...")
|
||||
self._session = self._interface.alchemy.Session()
|
||||
return self._session
|
||||
|
||||
|
@ -32,11 +35,13 @@ class CommandData:
|
|||
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)
|
||||
|
||||
async def reply(self, text: str) -> None:
|
||||
|
@ -64,6 +69,13 @@ class CommandData:
|
|||
if error_if_unavailable:
|
||||
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
|
||||
async def keyboard(self, text, keys: List["KeyboardKey"]):
|
||||
yield
|
||||
|
|
|
@ -15,13 +15,11 @@ You can install them with: ::
|
|||
"""
|
||||
|
||||
from .constellation import Constellation
|
||||
from .star import Star, PageStar, ExceptionStar
|
||||
from .shoot import shoot
|
||||
from .star import Star
|
||||
from .pagestar import PageStar
|
||||
|
||||
__all__ = [
|
||||
"Constellation",
|
||||
"Star",
|
||||
"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.utils as ru
|
||||
import royalnet.commands as rc
|
||||
from .star import PageStar, ExceptionStar
|
||||
from .pagestar import PageStar
|
||||
from ..utils import init_logging
|
||||
|
||||
|
||||
|
@ -42,7 +42,12 @@ class Constellation:
|
|||
for pack_name in pack_names:
|
||||
log.debug(f"Importing pack: {pack_name}")
|
||||
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:
|
||||
log.error(f"Error during the import of {pack_name}: {e}")
|
||||
log.info(f"Packs: {len(packs)} imported")
|
||||
|
@ -60,7 +65,7 @@ class Constellation:
|
|||
tables = set()
|
||||
for pack in packs.values():
|
||||
try:
|
||||
tables = tables.union(pack.available_tables)
|
||||
tables = tables.union(pack["tables"].available_tables)
|
||||
except AttributeError:
|
||||
log.warning(f"Pack `{pack}` does not have the `available_tables` attribute.")
|
||||
continue
|
||||
|
@ -99,7 +104,7 @@ class Constellation:
|
|||
pack = packs[pack_name]
|
||||
pack_cfg = packs_cfg.get(pack_name, {})
|
||||
try:
|
||||
events = pack.available_events
|
||||
events = pack["events"].available_events
|
||||
except AttributeError:
|
||||
log.warning(f"Pack `{pack}` does not have the `available_events` attribute.")
|
||||
else:
|
||||
|
@ -118,17 +123,11 @@ class Constellation:
|
|||
pack = packs[pack_name]
|
||||
pack_cfg = packs_cfg.get(pack_name, {})
|
||||
try:
|
||||
page_stars = pack.available_page_stars
|
||||
page_stars = pack["stars"].available_page_stars
|
||||
except AttributeError:
|
||||
log.warning(f"Pack `{pack}` does not have the `available_page_stars` attribute.")
|
||||
else:
|
||||
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"ExceptionStars: {len(self.starlette.exception_handlers)} stars")
|
||||
|
||||
|
@ -259,14 +258,6 @@ class Constellation:
|
|||
|
||||
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]):
|
||||
for SelectedPageStar in page_stars:
|
||||
log.debug(f"Registering: {SelectedPageStar.path} -> {SelectedPageStar.__qualname__}")
|
||||
|
@ -279,18 +270,6 @@ class Constellation:
|
|||
continue
|
||||
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):
|
||||
log.info(f"Running Constellation on https://{self.address}:{self.port}/...")
|
||||
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):
|
||||
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 = {}
|
||||
for pack_name in pack_names:
|
||||
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:
|
||||
p(f"Skipping `{pack_name}`: {e}", err=True)
|
||||
continue
|
||||
|
@ -30,7 +35,7 @@ def run(config_filename, file_format):
|
|||
lines = []
|
||||
|
||||
try:
|
||||
commands = pack.available_commands
|
||||
commands = pack["commands"].available_commands
|
||||
except AttributeError:
|
||||
p(f"Pack `{pack}` does not have the `available_commands` attribute.", err=True)
|
||||
continue
|
||||
|
@ -41,93 +46,6 @@ def run(config_filename, file_format):
|
|||
for line in lines:
|
||||
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:
|
||||
raise click.ClickException("Unknown format")
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ from sqlalchemy.schema import Table
|
|||
from royalnet.commands import *
|
||||
import royalnet.utils as ru
|
||||
import royalnet.alchemy as ra
|
||||
import royalnet.backpack as rb
|
||||
import royalnet.backpack.tables as rbt
|
||||
import royalnet.herald as rh
|
||||
import traceback
|
||||
|
||||
|
@ -20,7 +20,7 @@ class Serf:
|
|||
Discord)."""
|
||||
interface_name = NotImplemented
|
||||
|
||||
_master_table: type = rb.tables.User
|
||||
_master_table: type = rbt.User
|
||||
_identity_table: type = NotImplemented
|
||||
_identity_column: str = NotImplemented
|
||||
|
||||
|
@ -39,10 +39,15 @@ class Serf:
|
|||
for pack_name in pack_names:
|
||||
log.debug(f"Importing pack: {pack_name}")
|
||||
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:
|
||||
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")
|
||||
|
||||
self.alchemy: Optional[ra.Alchemy] = None
|
||||
|
@ -68,7 +73,7 @@ class Serf:
|
|||
tables = set()
|
||||
for pack in packs.values():
|
||||
try:
|
||||
tables = tables.union(pack.available_tables)
|
||||
tables = tables.union(pack["tables"].available_tables)
|
||||
except AttributeError:
|
||||
log.warning(f"Pack `{pack}` does not have the `available_tables` attribute.")
|
||||
continue
|
||||
|
@ -95,13 +100,13 @@ class Serf:
|
|||
pack = packs[pack_name]
|
||||
pack_cfg = packs_cfg.get(pack_name, {})
|
||||
try:
|
||||
events = pack.available_events
|
||||
events = pack["events"].available_events
|
||||
except AttributeError:
|
||||
log.warning(f"Pack `{pack}` does not have the `available_events` attribute.")
|
||||
else:
|
||||
self.register_events(events, pack_cfg)
|
||||
try:
|
||||
commands = pack.available_commands
|
||||
commands = pack["commands"].available_commands
|
||||
except AttributeError:
|
||||
log.warning(f"Pack `{pack}` does not have the `available_commands` attribute.")
|
||||
else:
|
||||
|
|
|
@ -5,6 +5,7 @@ from .urluuid import to_urluuid, from_urluuid
|
|||
from .multilock import MultiLock
|
||||
from .sentry import init_sentry, sentry_exc
|
||||
from .log import init_logging
|
||||
from .royaltyping import JSON
|
||||
|
||||
__all__ = [
|
||||
"asyncify",
|
||||
|
@ -20,4 +21,5 @@ __all__ = [
|
|||
"init_sentry",
|
||||
"sentry_exc",
|
||||
"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