From c3e77fa5e645b0e6df17d7d791817dbc542cfda5 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Tue, 4 Feb 2020 00:15:33 +0100 Subject: [PATCH 01/13] Start work on 5.5 --- poetry.lock | 41 ++++++++++- pycharm_templates/table.vm | 1 + pyproject.toml | 5 +- royalnet/backpack/commands/__init__.py | 12 --- royalnet/backpack/commands/exception.py | 11 --- royalnet/backpack/commands/excevent.py | 12 --- royalnet/backpack/commands/keyboardtest.py | 28 ------- royalnet/backpack/commands/link.py | 86 ++++++++++++++++++++++ royalnet/backpack/tables/aliases.py | 14 ++-- royalnet/backpack/tables/discord.py | 12 +-- royalnet/backpack/tables/telegram.py | 10 +-- royalnet/backpack/tables/users.py | 12 +++ royalnet/commands/commanddata.py | 12 +++ royalnet/version.py | 2 +- 14 files changed, 173 insertions(+), 85 deletions(-) delete mode 100644 royalnet/backpack/commands/exception.py delete mode 100644 royalnet/backpack/commands/excevent.py delete mode 100644 royalnet/backpack/commands/keyboardtest.py create mode 100644 royalnet/backpack/commands/link.py diff --git a/poetry.lock b/poetry.lock index 4fa9e774..c7f408d5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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"}, diff --git a/pycharm_templates/table.vm b/pycharm_templates/table.vm index c6134933..90eb3d5a 100644 --- a/pycharm_templates/table.vm +++ b/pycharm_templates/table.vm @@ -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}" diff --git a/pyproject.toml b/pyproject.toml index a47f0fff..706b1cdf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/royalnet/backpack/commands/__init__.py b/royalnet/backpack/commands/__init__.py index 3b99caa5..50c1aa68 100644 --- a/royalnet/backpack/commands/__init__.py +++ b/royalnet/backpack/commands/__init__.py @@ -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] diff --git a/royalnet/backpack/commands/exception.py b/royalnet/backpack/commands/exception.py deleted file mode 100644 index 5419b0b3..00000000 --- a/royalnet/backpack/commands/exception.py +++ /dev/null @@ -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") diff --git a/royalnet/backpack/commands/excevent.py b/royalnet/backpack/commands/excevent.py deleted file mode 100644 index 38220942..00000000 --- a/royalnet/backpack/commands/excevent.py +++ /dev/null @@ -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!") diff --git a/royalnet/backpack/commands/keyboardtest.py b/royalnet/backpack/commands/keyboardtest.py deleted file mode 100644 index 4f24fade..00000000 --- a/royalnet/backpack/commands/keyboardtest.py +++ /dev/null @@ -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.") diff --git a/royalnet/backpack/commands/link.py b/royalnet/backpack/commands/link.py new file mode 100644 index 00000000..30c38730 --- /dev/null +++ b/royalnet/backpack/commands/link.py @@ -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}") diff --git a/royalnet/backpack/tables/aliases.py b/royalnet/backpack/tables/aliases.py index 0f4ec1f8..544c78f3 100644 --- a/royalnet/backpack/tables/aliases.py +++ b/royalnet/backpack/tables/aliases.py @@ -10,26 +10,30 @@ 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): + def find_user(cls, alchemy, session, alias: str): result = session.query(alchemy.get(cls)).filter_by(alias=alias.lower()).one_or_none() if result is not None: result = result.royal return result + def __init__(self, user: str, alias: str): + self.user = user + self.alias = alias.lower() + def __repr__(self): return f"" def __str__(self): - return f"{self.alias}->{self.royal_id}" + return f"{self.alias}->{self.user_id}" diff --git a/royalnet/backpack/tables/discord.py b/royalnet/backpack/tables/discord.py index 2de86e7a..4f92dbdf 100644 --- a/royalnet/backpack/tables/discord.py +++ b/royalnet/backpack/tables/discord.py @@ -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"" diff --git a/royalnet/backpack/tables/telegram.py b/royalnet/backpack/tables/telegram.py index d3798f6e..54de8090 100644 --- a/royalnet/backpack/tables/telegram.py +++ b/royalnet/backpack/tables/telegram.py @@ -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"" diff --git a/royalnet/backpack/tables/users.py b/royalnet/backpack/tables/users.py index 88ca5f87..d63a33a5 100644 --- a/royalnet/backpack/tables/users.py +++ b/royalnet/backpack/tables/users.py @@ -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}>" diff --git a/royalnet/commands/commanddata.py b/royalnet/commands/commanddata.py index 93ee1213..4b966ca2 100644 --- a/royalnet/commands/commanddata.py +++ b/royalnet/commands/commanddata.py @@ -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__) @@ -64,6 +66,16 @@ 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 ru.asyncify( + Alias.find_user(self._interface.alchemy, self.session, alias) + ) + + @contextlib.asynccontextmanager async def keyboard(self, text, keys: List["KeyboardKey"]): yield diff --git a/royalnet/version.py b/royalnet/version.py index 03217183..63bf6999 100644 --- a/royalnet/version.py +++ b/royalnet/version.py @@ -1 +1 @@ -semantic = "5.4" +semantic = "5.5" From 353b94f1ba3db15d23d43446456c8fd78b5311d9 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Tue, 4 Feb 2020 18:27:05 +0100 Subject: [PATCH 02/13] A lot of progress on a lot of things --- royalnet/backpack/__init__.py | 18 ---- royalnet/backpack/stars/__init__.py | 7 +- .../backpack/stars/api_royalnet_version.py | 18 ++-- royalnet/backpack/tables/tokens.py | 28 ++++++ royalnet/constellation/__init__.py | 12 ++- royalnet/constellation/apistar.py | 26 +++++ royalnet/constellation/constellation.py | 41 ++------ royalnet/constellation/jsonapi.py | 32 +++++++ royalnet/constellation/pagestar.py | 34 +++++++ royalnet/constellation/shoot.py | 13 --- royalnet/constellation/star.py | 60 ------------ royalnet/generate.py | 96 ++----------------- royalnet/serf/serf.py | 17 ++-- 13 files changed, 164 insertions(+), 238 deletions(-) create mode 100644 royalnet/backpack/tables/tokens.py create mode 100644 royalnet/constellation/apistar.py create mode 100644 royalnet/constellation/jsonapi.py create mode 100644 royalnet/constellation/pagestar.py delete mode 100644 royalnet/constellation/shoot.py diff --git a/royalnet/backpack/__init__.py b/royalnet/backpack/__init__.py index a51af2ed..3d320ba5 100644 --- a/royalnet/backpack/__init__.py +++ b/royalnet/backpack/__init__.py @@ -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", -] diff --git a/royalnet/backpack/stars/__init__.py b/royalnet/backpack/stars/__init__.py index 49192e58..129ed92b 100644 --- a/royalnet/backpack/stars/__init__.py +++ b/royalnet/backpack/stars/__init__.py @@ -7,10 +7,5 @@ available_page_stars = [ ApiRoyalnetVersionStar, ] -# Enter the ExceptionStars of your Pack here! -available_exception_stars = [ - -] - # 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] diff --git a/royalnet/backpack/stars/api_royalnet_version.py b/royalnet/backpack/stars/api_royalnet_version.py index 26f3b9d0..a842ddfe 100644 --- a/royalnet/backpack/stars/api_royalnet_version.py +++ b/royalnet/backpack/stars/api_royalnet_version.py @@ -1,15 +1,11 @@ -import royalnet -from starlette.requests import Request -from starlette.responses import * -from royalnet.constellation import PageStar +import royalnet.version as rv +from royalnet.constellation import ApiStar -class ApiRoyalnetVersionStar(PageStar): +class ApiRoyalnetVersionStar(ApiStar): path = "/api/royalnet/version" - async def page(self, request: Request) -> JSONResponse: - return JSONResponse({ - "version": { - "semantic": royalnet.__version__, - } - }) + async def api(self, data: dict) -> dict: + return { + "semantic": rv.semantic + } diff --git a/royalnet/backpack/tables/tokens.py b/royalnet/backpack/tables/tokens.py new file mode 100644 index 00000000..3d87e1ef --- /dev/null +++ b/royalnet/backpack/tables/tokens.py @@ -0,0 +1,28 @@ +import datetime +from sqlalchemy import * +from sqlalchemy.orm import * +from sqlalchemy.ext.declarative import declared_attr + + +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 diff --git a/royalnet/constellation/__init__.py b/royalnet/constellation/__init__.py index 69c62af0..d9438019 100644 --- a/royalnet/constellation/__init__.py +++ b/royalnet/constellation/__init__.py @@ -15,13 +15,17 @@ 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 +from .apistar import ApiStar +from .jsonapi import api_response, api_success, api_error __all__ = [ "Constellation", "Star", "PageStar", - "ExceptionStar", - "shoot", + "ApiStar", + "api_response", + "api_success", + "api_error", ] diff --git a/royalnet/constellation/apistar.py b/royalnet/constellation/apistar.py new file mode 100644 index 00000000..372af97e --- /dev/null +++ b/royalnet/constellation/apistar.py @@ -0,0 +1,26 @@ +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 + + +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 = {} + try: + response = await self.api(data) + except Exception as e: + return api_error(e) + return api_success(response) + + async def api(self, data: dict) -> dict: + raise NotImplementedError() diff --git a/royalnet/constellation/constellation.py b/royalnet/constellation/constellation.py index 6fad39b9..2cdc94e9 100644 --- a/royalnet/constellation/constellation.py +++ b/royalnet/constellation/constellation.py @@ -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() diff --git a/royalnet/constellation/jsonapi.py b/royalnet/constellation/jsonapi.py new file mode 100644 index 00000000..e46b3d28 --- /dev/null +++ b/royalnet/constellation/jsonapi.py @@ -0,0 +1,32 @@ +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) + } + return api_response(result, code=code) diff --git a/royalnet/constellation/pagestar.py b/royalnet/constellation/pagestar.py new file mode 100644 index 00000000..79da65ab --- /dev/null +++ b/royalnet/constellation/pagestar.py @@ -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}>" diff --git a/royalnet/constellation/shoot.py b/royalnet/constellation/shoot.py deleted file mode 100644 index f5c8471e..00000000 --- a/royalnet/constellation/shoot.py +++ /dev/null @@ -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) diff --git a/royalnet/constellation/star.py b/royalnet/constellation/star.py index 657968dc..c56fa27e 100644 --- a/royalnet/constellation/star.py +++ b/royalnet/constellation/star.py @@ -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}>" diff --git a/royalnet/generate.py b/royalnet/generate.py index 5462d22d..80e18944 100644 --- a/royalnet/generate.py +++ b/royalnet/generate.py @@ -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("") - 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") diff --git a/royalnet/serf/serf.py b/royalnet/serf/serf.py index 39c5c3d6..022763ff 100644 --- a/royalnet/serf/serf.py +++ b/royalnet/serf/serf.py @@ -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,7 +39,12 @@ 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())}") @@ -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: From 741b9f50db6bda0a9718ed5817d71c6dd18d439c Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Thu, 6 Feb 2020 18:15:12 +0100 Subject: [PATCH 03/13] Add basic login token creation --- royalnet/backpack/stars/__init__.py | 2 ++ royalnet/backpack/stars/api_royalnet_login.py | 32 +++++++++++++++++++ .../backpack/stars/api_royalnet_version.py | 6 ++-- royalnet/backpack/tables/__init__.py | 4 ++- royalnet/backpack/tables/tokens.py | 14 ++++++++ royalnet/constellation/__init__.py | 6 ---- royalnet/constellation/api/__init__.py | 16 ++++++++++ royalnet/constellation/api/apidatadict.py | 6 ++++ royalnet/constellation/api/apierrors.py | 14 ++++++++ royalnet/constellation/{ => api}/apistar.py | 12 +++++-- royalnet/constellation/{ => api}/jsonapi.py | 0 11 files changed, 99 insertions(+), 13 deletions(-) create mode 100644 royalnet/backpack/stars/api_royalnet_login.py create mode 100644 royalnet/constellation/api/__init__.py create mode 100644 royalnet/constellation/api/apidatadict.py create mode 100644 royalnet/constellation/api/apierrors.py rename royalnet/constellation/{ => api}/apistar.py (64%) rename royalnet/constellation/{ => api}/jsonapi.py (100%) diff --git a/royalnet/backpack/stars/__init__.py b/royalnet/backpack/stars/__init__.py index 129ed92b..4ca5f3fa 100644 --- a/royalnet/backpack/stars/__init__.py +++ b/royalnet/backpack/stars/__init__.py @@ -1,10 +1,12 @@ # Imports go here! from .api_royalnet_version import ApiRoyalnetVersionStar +from .api_royalnet_login import ApiRoyalnetLoginStar # Enter the PageStars of your Pack here! available_page_stars = [ ApiRoyalnetVersionStar, + ApiRoyalnetLoginStar, ] # Don't change this, it should automatically generate __all__ diff --git a/royalnet/backpack/stars/api_royalnet_login.py b/royalnet/backpack/stars/api_royalnet_login.py new file mode 100644 index 00000000..c20d14b3 --- /dev/null +++ b/royalnet/backpack/stars/api_royalnet_login.py @@ -0,0 +1,32 @@ +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 ApiRoyalnetLoginStar(ApiStar): + path = "/api/royalnet/login/v1" + + async def api(self, data: ApiDataDict) -> dict: + 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 NotFoundException("User not found") + pswd_check = user.test_password(password) + if not pswd_check: + raise ApiError("Invalid password") + token: Token = TokenT.generate(user=user, expiration_delta=datetime.timedelta(days=7)) + session.add(token) + await ru.asyncify(session.commit) + response = token.json() + + return response diff --git a/royalnet/backpack/stars/api_royalnet_version.py b/royalnet/backpack/stars/api_royalnet_version.py index a842ddfe..de77030a 100644 --- a/royalnet/backpack/stars/api_royalnet_version.py +++ b/royalnet/backpack/stars/api_royalnet_version.py @@ -1,11 +1,11 @@ import royalnet.version as rv -from royalnet.constellation import ApiStar +from royalnet.constellation.api import * class ApiRoyalnetVersionStar(ApiStar): - path = "/api/royalnet/version" + path = "/api/royalnet/version/v1" - async def api(self, data: dict) -> dict: + async def api(self, data: ApiDataDict) -> dict: return { "semantic": rv.semantic } diff --git a/royalnet/backpack/tables/__init__.py b/royalnet/backpack/tables/__init__.py index d4c750e9..3a18b96f 100644 --- a/royalnet/backpack/tables/__init__.py +++ b/royalnet/backpack/tables/__init__.py @@ -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__ diff --git a/royalnet/backpack/tables/tokens.py b/royalnet/backpack/tables/tokens.py index 3d87e1ef..7b332f84 100644 --- a/royalnet/backpack/tables/tokens.py +++ b/royalnet/backpack/tables/tokens.py @@ -1,4 +1,5 @@ import datetime +import secrets from sqlalchemy import * from sqlalchemy.orm import * from sqlalchemy.ext.declarative import declared_attr @@ -26,3 +27,16 @@ class Token: @property def expired(self): return datetime.datetime.now() > self.expiration + + @classmethod + def generate(cls, user, expiration_delta: datetime.timedelta): + # noinspection PyArgumentList + token = cls(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() + } diff --git a/royalnet/constellation/__init__.py b/royalnet/constellation/__init__.py index d9438019..c1e373a5 100644 --- a/royalnet/constellation/__init__.py +++ b/royalnet/constellation/__init__.py @@ -17,15 +17,9 @@ You can install them with: :: from .constellation import Constellation from .star import Star from .pagestar import PageStar -from .apistar import ApiStar -from .jsonapi import api_response, api_success, api_error __all__ = [ "Constellation", "Star", "PageStar", - "ApiStar", - "api_response", - "api_success", - "api_error", ] diff --git a/royalnet/constellation/api/__init__.py b/royalnet/constellation/api/__init__.py new file mode 100644 index 00000000..a10f1f7d --- /dev/null +++ b/royalnet/constellation/api/__init__.py @@ -0,0 +1,16 @@ +from .apistar import ApiStar +from .jsonapi import api_response, api_success, api_error +from .apidatadict import ApiDataDict +from .apierrors import ApiError, MissingParameterException, NotFoundException, UnauthorizedException + + +__all__ = [ + "ApiStar", + "api_response", + "api_success", + "api_error", + "ApiDataDict", + "ApiError", + "MissingParameterException", + "NotFoundException", +] diff --git a/royalnet/constellation/api/apidatadict.py b/royalnet/constellation/api/apidatadict.py new file mode 100644 index 00000000..f9c7d217 --- /dev/null +++ b/royalnet/constellation/api/apidatadict.py @@ -0,0 +1,6 @@ +from .apierrors import MissingParameterException + + +class ApiDataDict(dict): + def __missing__(self, key): + raise MissingParameterException(f"Missing '{key}'") diff --git a/royalnet/constellation/api/apierrors.py b/royalnet/constellation/api/apierrors.py new file mode 100644 index 00000000..040ccc7b --- /dev/null +++ b/royalnet/constellation/api/apierrors.py @@ -0,0 +1,14 @@ +class ApiError(Exception): + pass + + +class NotFoundException(ApiError): + pass + + +class UnauthorizedException(ApiError): + pass + + +class MissingParameterException(ApiError): + pass diff --git a/royalnet/constellation/apistar.py b/royalnet/constellation/api/apistar.py similarity index 64% rename from royalnet/constellation/apistar.py rename to royalnet/constellation/api/apistar.py index 372af97e..1d7b33e2 100644 --- a/royalnet/constellation/apistar.py +++ b/royalnet/constellation/api/apistar.py @@ -3,8 +3,10 @@ from json import JSONDecodeError from abc import * from starlette.requests import Request from starlette.responses import JSONResponse -from .pagestar import PageStar +from ..pagestar import PageStar from .jsonapi import api_error, api_success +from .apidatadict import ApiDataDict +from .apierrors import ApiError, NotFoundException class ApiStar(PageStar, ABC): @@ -17,9 +19,13 @@ class ApiStar(PageStar, ABC): except JSONDecodeError: data = {} try: - response = await self.api(data) + response = await self.api(ApiDataDict(data)) + except NotFoundException as e: + return api_error(e, code=404) + except ApiError as e: + return api_error(e, code=400) except Exception as e: - return api_error(e) + return api_error(e, code=500) return api_success(response) async def api(self, data: dict) -> dict: diff --git a/royalnet/constellation/jsonapi.py b/royalnet/constellation/api/jsonapi.py similarity index 100% rename from royalnet/constellation/jsonapi.py rename to royalnet/constellation/api/jsonapi.py From bb3aa1fd8580d6dd296367281089cb684f06e6b2 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Fri, 7 Feb 2020 15:36:19 +0100 Subject: [PATCH 04/13] API login routes completed --- royalnet/backpack/stars/__init__.py | 6 ++-- ...oyalnet_login.py => api_login_royalnet.py} | 8 ++--- .../backpack/stars/api_royalnet_version.py | 2 +- royalnet/backpack/stars/api_token_info.py | 13 ++++++++ royalnet/backpack/tables/aliases.py | 7 +++-- royalnet/backpack/tables/tokens.py | 6 ++++ royalnet/commands/commanddata.py | 1 - royalnet/constellation/api/__init__.py | 10 +++--- royalnet/constellation/api/apidata.py | 31 +++++++++++++++++++ royalnet/constellation/api/apidatadict.py | 6 ---- royalnet/constellation/api/apierrors.py | 14 +++++++-- royalnet/constellation/api/apistar.py | 12 ++++--- royalnet/constellation/api/jsonapi.py | 3 +- 13 files changed, 89 insertions(+), 30 deletions(-) rename royalnet/backpack/stars/{api_royalnet_login.py => api_login_royalnet.py} (83%) create mode 100644 royalnet/backpack/stars/api_token_info.py create mode 100644 royalnet/constellation/api/apidata.py delete mode 100644 royalnet/constellation/api/apidatadict.py diff --git a/royalnet/backpack/stars/__init__.py b/royalnet/backpack/stars/__init__.py index 4ca5f3fa..b9fbd55a 100644 --- a/royalnet/backpack/stars/__init__.py +++ b/royalnet/backpack/stars/__init__.py @@ -1,12 +1,14 @@ # Imports go here! from .api_royalnet_version import ApiRoyalnetVersionStar -from .api_royalnet_login import ApiRoyalnetLoginStar +from .api_login_royalnet import ApiLoginRoyalnetStar +from .api_token_info import ApiTokenInfoStar # Enter the PageStars of your Pack here! available_page_stars = [ ApiRoyalnetVersionStar, - ApiRoyalnetLoginStar, + ApiLoginRoyalnetStar, + ApiTokenInfoStar, ] # Don't change this, it should automatically generate __all__ diff --git a/royalnet/backpack/stars/api_royalnet_login.py b/royalnet/backpack/stars/api_login_royalnet.py similarity index 83% rename from royalnet/backpack/stars/api_royalnet_login.py rename to royalnet/backpack/stars/api_login_royalnet.py index c20d14b3..a8b03e4c 100644 --- a/royalnet/backpack/stars/api_royalnet_login.py +++ b/royalnet/backpack/stars/api_login_royalnet.py @@ -6,10 +6,10 @@ from ..tables.aliases import Alias from ..tables.tokens import Token -class ApiRoyalnetLoginStar(ApiStar): - path = "/api/royalnet/login/v1" +class ApiLoginRoyalnetStar(ApiStar): + path = "/api/login/royalnet/v1" - async def api(self, data: ApiDataDict) -> dict: + async def api(self, data: ApiData) -> dict: TokenT = self.alchemy.get(Token) UserT = self.alchemy.get(User) AliasT = self.alchemy.get(Alias) @@ -20,7 +20,7 @@ class ApiRoyalnetLoginStar(ApiStar): 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 NotFoundException("User not found") + raise NotFoundError("User not found") pswd_check = user.test_password(password) if not pswd_check: raise ApiError("Invalid password") diff --git a/royalnet/backpack/stars/api_royalnet_version.py b/royalnet/backpack/stars/api_royalnet_version.py index de77030a..65a17425 100644 --- a/royalnet/backpack/stars/api_royalnet_version.py +++ b/royalnet/backpack/stars/api_royalnet_version.py @@ -5,7 +5,7 @@ from royalnet.constellation.api import * class ApiRoyalnetVersionStar(ApiStar): path = "/api/royalnet/version/v1" - async def api(self, data: ApiDataDict) -> dict: + async def api(self, data: ApiData) -> dict: return { "semantic": rv.semantic } diff --git a/royalnet/backpack/stars/api_token_info.py b/royalnet/backpack/stars/api_token_info.py new file mode 100644 index 00000000..3a5e888f --- /dev/null +++ b/royalnet/backpack/stars/api_token_info.py @@ -0,0 +1,13 @@ +import datetime +import royalnet.utils as ru +from royalnet.constellation.api import * +from ..tables.users import User +from ..tables.aliases import Alias + + +class ApiTokenInfoStar(ApiStar): + path = "/api/token/info/v1" + + async def api(self, data: ApiData) -> dict: + token = await data.token() + return token.json() diff --git a/royalnet/backpack/tables/aliases.py b/royalnet/backpack/tables/aliases.py index 544c78f3..d153698a 100644 --- a/royalnet/backpack/tables/aliases.py +++ b/royalnet/backpack/tables/aliases.py @@ -4,6 +4,7 @@ from sqlalchemy import Column, \ ForeignKey from sqlalchemy.orm import relationship from sqlalchemy.ext.declarative import declared_attr +import royalnet.utils as ru class Alias: @@ -22,10 +23,10 @@ class Alias: return relationship("User", backref="aliases") @classmethod - def find_user(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): diff --git a/royalnet/backpack/tables/tokens.py b/royalnet/backpack/tables/tokens.py index 7b332f84..acada45e 100644 --- a/royalnet/backpack/tables/tokens.py +++ b/royalnet/backpack/tables/tokens.py @@ -3,6 +3,8 @@ import secrets from sqlalchemy import * from sqlalchemy.orm import * from sqlalchemy.ext.declarative import declared_attr +import royalnet.utils as ru +from .users import User class Token: @@ -40,3 +42,7 @@ class Token: "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) diff --git a/royalnet/commands/commanddata.py b/royalnet/commands/commanddata.py index 4b966ca2..fa7b1956 100644 --- a/royalnet/commands/commanddata.py +++ b/royalnet/commands/commanddata.py @@ -75,7 +75,6 @@ class CommandData: Alias.find_user(self._interface.alchemy, self.session, alias) ) - @contextlib.asynccontextmanager async def keyboard(self, text, keys: List["KeyboardKey"]): yield diff --git a/royalnet/constellation/api/__init__.py b/royalnet/constellation/api/__init__.py index a10f1f7d..bff3af0b 100644 --- a/royalnet/constellation/api/__init__.py +++ b/royalnet/constellation/api/__init__.py @@ -1,7 +1,7 @@ from .apistar import ApiStar from .jsonapi import api_response, api_success, api_error -from .apidatadict import ApiDataDict -from .apierrors import ApiError, MissingParameterException, NotFoundException, UnauthorizedException +from .apidata import ApiData +from .apierrors import ApiError, MissingParameterError, NotFoundError, ForbiddenError __all__ = [ @@ -9,8 +9,8 @@ __all__ = [ "api_response", "api_success", "api_error", - "ApiDataDict", + "ApiData", "ApiError", - "MissingParameterException", - "NotFoundException", + "MissingParameterError", + "NotFoundError", ] diff --git a/royalnet/constellation/api/apidata.py b/royalnet/constellation/api/apidata.py new file mode 100644 index 00000000..c20fee4f --- /dev/null +++ b/royalnet/constellation/api/apidata.py @@ -0,0 +1,31 @@ +from .apierrors import MissingParameterError +from royalnet.backpack.tables.tokens import Token +from royalnet.backpack.tables.users import User +from .apierrors import * + + +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) -> Token: + 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") + self._session = self.star.alchemy.Session() + return self._session diff --git a/royalnet/constellation/api/apidatadict.py b/royalnet/constellation/api/apidatadict.py deleted file mode 100644 index f9c7d217..00000000 --- a/royalnet/constellation/api/apidatadict.py +++ /dev/null @@ -1,6 +0,0 @@ -from .apierrors import MissingParameterException - - -class ApiDataDict(dict): - def __missing__(self, key): - raise MissingParameterException(f"Missing '{key}'") diff --git a/royalnet/constellation/api/apierrors.py b/royalnet/constellation/api/apierrors.py index 040ccc7b..b09fadeb 100644 --- a/royalnet/constellation/api/apierrors.py +++ b/royalnet/constellation/api/apierrors.py @@ -2,13 +2,21 @@ class ApiError(Exception): pass -class NotFoundException(ApiError): +class NotFoundError(ApiError): pass -class UnauthorizedException(ApiError): +class ForbiddenError(ApiError): pass -class MissingParameterException(ApiError): +class MissingParameterError(ApiError): + pass + + +class NotImplementedError(ApiError): + pass + + +class UnsupportedError(NotImplementedError): pass diff --git a/royalnet/constellation/api/apistar.py b/royalnet/constellation/api/apistar.py index 1d7b33e2..d6ea4770 100644 --- a/royalnet/constellation/api/apistar.py +++ b/royalnet/constellation/api/apistar.py @@ -5,8 +5,8 @@ from starlette.requests import Request from starlette.responses import JSONResponse from ..pagestar import PageStar from .jsonapi import api_error, api_success -from .apidatadict import ApiDataDict -from .apierrors import ApiError, NotFoundException +from .apidata import ApiData +from .apierrors import * class ApiStar(PageStar, ABC): @@ -19,9 +19,13 @@ class ApiStar(PageStar, ABC): except JSONDecodeError: data = {} try: - response = await self.api(ApiDataDict(data)) - except NotFoundException as e: + response = await self.api(ApiData(data, self)) + 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 ApiError as e: return api_error(e, code=400) except Exception as e: diff --git a/royalnet/constellation/api/jsonapi.py b/royalnet/constellation/api/jsonapi.py index e46b3d28..da540729 100644 --- a/royalnet/constellation/api/jsonapi.py +++ b/royalnet/constellation/api/jsonapi.py @@ -27,6 +27,7 @@ def api_error(error: Exception, code: int = 500) -> JSONResponse: result = { "success": False, "error_type": error.__class__.__qualname__, - "error_args": list(error.args) + "error_args": list(error.args), + "error_code": code, } return api_response(result, code=code) From 56cbd5b5c61144514bfefa73e989ae4aad192271 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Fri, 7 Feb 2020 16:47:52 +0100 Subject: [PATCH 05/13] Do some token things --- royalnet/alchemy/alchemy.py | 8 ++--- royalnet/backpack/stars/__init__.py | 5 +++- royalnet/backpack/stars/api_token_create.py | 21 +++++++++++++ royalnet/backpack/stars/api_token_passwd.py | 33 +++++++++++++++++++++ royalnet/backpack/tables/tokens.py | 13 ++++++-- royalnet/commands/commanddata.py | 7 +++-- royalnet/constellation/api/__init__.py | 20 ++++++++++++- royalnet/constellation/api/apidata.py | 21 ++++++++++++- royalnet/constellation/api/apierrors.py | 14 ++++++++- royalnet/constellation/api/apistar.py | 12 ++++++-- royalnet/serf/serf.py | 2 +- 11 files changed, 138 insertions(+), 18 deletions(-) create mode 100644 royalnet/backpack/stars/api_token_create.py create mode 100644 royalnet/backpack/stars/api_token_passwd.py diff --git a/royalnet/alchemy/alchemy.py b/royalnet/alchemy/alchemy.py index 27704632..b4c1c2aa 100644 --- a/royalnet/alchemy/alchemy.py +++ b/royalnet/alchemy/alchemy.py @@ -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: diff --git a/royalnet/backpack/stars/__init__.py b/royalnet/backpack/stars/__init__.py index b9fbd55a..3c869821 100644 --- a/royalnet/backpack/stars/__init__.py +++ b/royalnet/backpack/stars/__init__.py @@ -2,13 +2,16 @@ 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, ApiLoginRoyalnetStar, ApiTokenInfoStar, + ApiTokenPasswdStar, + ApiTokenCreateStar, ] # Don't change this, it should automatically generate __all__ diff --git a/royalnet/backpack/stars/api_token_create.py b/royalnet/backpack/stars/api_token_create.py new file mode 100644 index 00000000..5465b468 --- /dev/null +++ b/royalnet/backpack/stars/api_token_create.py @@ -0,0 +1,21 @@ +from typing import * +import datetime +import royalnet.utils as ru +from royalnet.constellation.api import * +from ..tables.tokens import Token +from sqlalchemy import and_ + + +class ApiTokenCreateStar(ApiStar): + path = "/api/token/create/v1" + + async def api(self, data: ApiData) -> dict: + 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() diff --git a/royalnet/backpack/stars/api_token_passwd.py b/royalnet/backpack/stars/api_token_passwd.py new file mode 100644 index 00000000..22a63720 --- /dev/null +++ b/royalnet/backpack/stars/api_token_passwd.py @@ -0,0 +1,33 @@ +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" + + async def api(self, data: ApiData) -> dict: + 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 + } diff --git a/royalnet/backpack/tables/tokens.py b/royalnet/backpack/tables/tokens.py index acada45e..f9e80cd8 100644 --- a/royalnet/backpack/tables/tokens.py +++ b/royalnet/backpack/tables/tokens.py @@ -4,7 +4,6 @@ from sqlalchemy import * from sqlalchemy.orm import * from sqlalchemy.ext.declarative import declared_attr import royalnet.utils as ru -from .users import User class Token: @@ -30,10 +29,18 @@ class Token: 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, user, expiration_delta: datetime.timedelta): + def generate(cls, alchemy, user, expiration_delta: datetime.timedelta): # noinspection PyArgumentList - token = cls(user=user, expiration=datetime.datetime.now() + expiration_delta, token=secrets.token_urlsafe()) + TokenT = alchemy.get(cls) + token = TokenT(user=user, expiration=datetime.datetime.now() + expiration_delta, token=secrets.token_urlsafe()) return token def json(self) -> dict: diff --git a/royalnet/commands/commanddata.py b/royalnet/commands/commanddata.py index fa7b1956..bee9d885 100644 --- a/royalnet/commands/commanddata.py +++ b/royalnet/commands/commanddata.py @@ -26,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 @@ -34,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: @@ -71,9 +74,7 @@ class CommandData: Parameters: alias: the Alias to search for.""" - return await ru.asyncify( - Alias.find_user(self._interface.alchemy, self.session, alias) - ) + return await Alias.find_user(self._interface.alchemy, self.session, alias) @contextlib.asynccontextmanager async def keyboard(self, text, keys: List["KeyboardKey"]): diff --git a/royalnet/constellation/api/__init__.py b/royalnet/constellation/api/__init__.py index bff3af0b..fd04cb9e 100644 --- a/royalnet/constellation/api/__init__.py +++ b/royalnet/constellation/api/__init__.py @@ -1,7 +1,16 @@ from .apistar import ApiStar from .jsonapi import api_response, api_success, api_error from .apidata import ApiData -from .apierrors import ApiError, MissingParameterError, NotFoundError, ForbiddenError +from .apierrors import \ + ApiError, \ + NotFoundError, \ + ForbiddenError, \ + BadRequestError, \ + ParameterError, \ + MissingParameterError, \ + InvalidParameterError, \ + NotImplementedError, \ + UnsupportedError __all__ = [ @@ -13,4 +22,13 @@ __all__ = [ "ApiError", "MissingParameterError", "NotFoundError", + "ApiError", + "NotFoundError", + "ForbiddenError", + "BadRequestError", + "ParameterError", + "MissingParameterError", + "InvalidParameterError", + "NotImplementedError", + "UnsupportedError", ] diff --git a/royalnet/constellation/api/apidata.py b/royalnet/constellation/api/apidata.py index c20fee4f..a8915c50 100644 --- a/royalnet/constellation/api/apidata.py +++ b/royalnet/constellation/api/apidata.py @@ -1,7 +1,11 @@ +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): @@ -19,7 +23,7 @@ class ApiData(dict): raise ForbiddenError("'token' is invalid") return token - async def user(self) -> Token: + async def user(self) -> User: return (await self.token()).user @property @@ -27,5 +31,20 @@ class ApiData(dict): 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) diff --git a/royalnet/constellation/api/apierrors.py b/royalnet/constellation/api/apierrors.py index b09fadeb..b566a602 100644 --- a/royalnet/constellation/api/apierrors.py +++ b/royalnet/constellation/api/apierrors.py @@ -10,7 +10,19 @@ class ForbiddenError(ApiError): pass -class MissingParameterError(ApiError): +class BadRequestError(ApiError): + pass + + +class ParameterError(BadRequestError): + pass + + +class MissingParameterError(ParameterError): + pass + + +class InvalidParameterError(ParameterError): pass diff --git a/royalnet/constellation/api/apistar.py b/royalnet/constellation/api/apistar.py index d6ea4770..2d9a6c4e 100644 --- a/royalnet/constellation/api/apistar.py +++ b/royalnet/constellation/api/apistar.py @@ -7,6 +7,7 @@ from ..pagestar import PageStar from .jsonapi import api_error, api_success from .apidata import ApiData from .apierrors import * +from royalnet.utils import sentry_exc class ApiStar(PageStar, ABC): @@ -18,19 +19,24 @@ class ApiStar(PageStar, ABC): data = await request.json() except JSONDecodeError: data = {} + apidata = ApiData(data, self) try: - response = await self.api(ApiData(data, self)) + 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 ApiError as e: + except BadRequestError as e: return api_error(e, code=400) except Exception as e: + sentry_exc(e) return api_error(e, code=500) - return api_success(response) + else: + return api_success(response) + finally: + await apidata.session_close() async def api(self, data: dict) -> dict: raise NotImplementedError() diff --git a/royalnet/serf/serf.py b/royalnet/serf/serf.py index 022763ff..db22afca 100644 --- a/royalnet/serf/serf.py +++ b/royalnet/serf/serf.py @@ -47,7 +47,7 @@ class Serf: } 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 From 4cd8ea3c52c716e307caa3037b983f11915f03b7 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 8 Feb 2020 00:58:28 +0100 Subject: [PATCH 06/13] Bump version (alpha) --- pyproject.toml | 2 +- royalnet/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 706b1cdf..cf5766b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "royalnet" - version = "5.4" + version = "5.5a1" description = "A multipurpose bot and web framework" authors = ["Stefano Pigozzi "] license = "AGPL-3.0+" diff --git a/royalnet/version.py b/royalnet/version.py index 63bf6999..79a82ecf 100644 --- a/royalnet/version.py +++ b/royalnet/version.py @@ -1 +1 @@ -semantic = "5.5" +semantic = "5.5a1" From 2238229a6e57894de0412fa91e5c50ee6e42fa08 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 8 Feb 2020 01:22:31 +0100 Subject: [PATCH 07/13] publish: 5.5a2 --- publish.bat | 1 + pyproject.toml | 2 +- royalnet/serf/discord/voiceplayer.py | 1 - royalnet/version.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 publish.bat diff --git a/publish.bat b/publish.bat new file mode 100644 index 00000000..46fa4128 --- /dev/null +++ b/publish.bat @@ -0,0 +1 @@ +git commit -am "publish: %1" && git push && poetry build && poetry publish && hub release create "%1" -m "Royalnet %1" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index cf5766b3..26cfeaa0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "royalnet" - version = "5.5a1" + version = "5.5a2" description = "A multipurpose bot and web framework" authors = ["Stefano Pigozzi "] license = "AGPL-3.0+" diff --git a/royalnet/serf/discord/voiceplayer.py b/royalnet/serf/discord/voiceplayer.py index b365e805..cbcf583d 100644 --- a/royalnet/serf/discord/voiceplayer.py +++ b/royalnet/serf/discord/voiceplayer.py @@ -87,7 +87,6 @@ class VoicePlayer: raise PlayerNotConnectedError() if self.voice_client.is_playing(): raise PlayerAlreadyPlaying() - self.playing = None log.debug("Getting next AudioSource...") next_source: Optional["discord.AudioSource"] = await self.playing.next() if next_source is None: diff --git a/royalnet/version.py b/royalnet/version.py index 79a82ecf..1db30019 100644 --- a/royalnet/version.py +++ b/royalnet/version.py @@ -1 +1 @@ -semantic = "5.5a1" +semantic = "5.5a2" From eb0030a6b3d2a556f62714caad4c0b38e377f33f Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 8 Feb 2020 01:23:03 +0100 Subject: [PATCH 08/13] publish: 5.5a3 --- pyproject.toml | 2 +- royalnet/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 26cfeaa0..f98d3eaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "royalnet" - version = "5.5a2" + version = "5.5a3" description = "A multipurpose bot and web framework" authors = ["Stefano Pigozzi "] license = "AGPL-3.0+" diff --git a/royalnet/version.py b/royalnet/version.py index 1db30019..a65bd8c3 100644 --- a/royalnet/version.py +++ b/royalnet/version.py @@ -1 +1 @@ -semantic = "5.5a2" +semantic = "5.5a3" From b489b9c554b0624f553d0125c2ebd0916d7e451e Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Tue, 11 Feb 2020 18:55:03 +0100 Subject: [PATCH 09/13] publish: 5.5a4 --- pyproject.toml | 2 +- royalnet/backpack/stars/api_login_royalnet.py | 4 +++- royalnet/backpack/stars/api_royalnet_version.py | 3 ++- royalnet/backpack/stars/api_token_create.py | 5 +++-- royalnet/backpack/stars/api_token_info.py | 5 +---- royalnet/backpack/stars/api_token_passwd.py | 4 +++- royalnet/constellation/api/apistar.py | 6 +++--- royalnet/utils/__init__.py | 2 ++ royalnet/utils/royaltyping.py | 3 +++ royalnet/version.py | 2 +- 10 files changed, 22 insertions(+), 14 deletions(-) create mode 100644 royalnet/utils/royaltyping.py diff --git a/pyproject.toml b/pyproject.toml index f98d3eaf..2de10a5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "royalnet" - version = "5.5a3" + version = "5.5a4" description = "A multipurpose bot and web framework" authors = ["Stefano Pigozzi "] license = "AGPL-3.0+" diff --git a/royalnet/backpack/stars/api_login_royalnet.py b/royalnet/backpack/stars/api_login_royalnet.py index a8b03e4c..2340d427 100644 --- a/royalnet/backpack/stars/api_login_royalnet.py +++ b/royalnet/backpack/stars/api_login_royalnet.py @@ -9,7 +9,9 @@ from ..tables.tokens import Token class ApiLoginRoyalnetStar(ApiStar): path = "/api/login/royalnet/v1" - async def api(self, data: ApiData) -> dict: + 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) diff --git a/royalnet/backpack/stars/api_royalnet_version.py b/royalnet/backpack/stars/api_royalnet_version.py index 65a17425..74092984 100644 --- a/royalnet/backpack/stars/api_royalnet_version.py +++ b/royalnet/backpack/stars/api_royalnet_version.py @@ -1,11 +1,12 @@ import royalnet.version as rv from royalnet.constellation.api import * +import royalnet.utils as ru class ApiRoyalnetVersionStar(ApiStar): path = "/api/royalnet/version/v1" - async def api(self, data: ApiData) -> dict: + async def api(self, data: ApiData) -> ru.JSON: return { "semantic": rv.semantic } diff --git a/royalnet/backpack/stars/api_token_create.py b/royalnet/backpack/stars/api_token_create.py index 5465b468..4c1f6211 100644 --- a/royalnet/backpack/stars/api_token_create.py +++ b/royalnet/backpack/stars/api_token_create.py @@ -3,13 +3,14 @@ import datetime import royalnet.utils as ru from royalnet.constellation.api import * from ..tables.tokens import Token -from sqlalchemy import and_ class ApiTokenCreateStar(ApiStar): path = "/api/token/create/v1" - async def api(self, data: ApiData) -> dict: + methods = ["POST"] + + async def api(self, data: ApiData) -> ru.JSON: user = await data.user() try: duration = int(data["duration"]) diff --git a/royalnet/backpack/stars/api_token_info.py b/royalnet/backpack/stars/api_token_info.py index 3a5e888f..7174df36 100644 --- a/royalnet/backpack/stars/api_token_info.py +++ b/royalnet/backpack/stars/api_token_info.py @@ -1,13 +1,10 @@ -import datetime import royalnet.utils as ru from royalnet.constellation.api import * -from ..tables.users import User -from ..tables.aliases import Alias class ApiTokenInfoStar(ApiStar): path = "/api/token/info/v1" - async def api(self, data: ApiData) -> dict: + async def api(self, data: ApiData) -> ru.JSON: token = await data.token() return token.json() diff --git a/royalnet/backpack/stars/api_token_passwd.py b/royalnet/backpack/stars/api_token_passwd.py index 22a63720..323dd930 100644 --- a/royalnet/backpack/stars/api_token_passwd.py +++ b/royalnet/backpack/stars/api_token_passwd.py @@ -9,7 +9,9 @@ from ..tables.tokens import Token class ApiTokenPasswdStar(ApiStar): path = "/api/token/passwd/v1" - async def api(self, data: ApiData) -> dict: + methods = ["POST"] + + async def api(self, data: ApiData) -> ru.JSON: TokenT = self.alchemy.get(Token) token = await data.token() user = token.user diff --git a/royalnet/constellation/api/apistar.py b/royalnet/constellation/api/apistar.py index 2d9a6c4e..afa58fc2 100644 --- a/royalnet/constellation/api/apistar.py +++ b/royalnet/constellation/api/apistar.py @@ -7,7 +7,7 @@ from ..pagestar import PageStar from .jsonapi import api_error, api_success from .apidata import ApiData from .apierrors import * -from royalnet.utils import sentry_exc +import royalnet.utils as ru class ApiStar(PageStar, ABC): @@ -31,12 +31,12 @@ class ApiStar(PageStar, ABC): except BadRequestError as e: return api_error(e, code=400) except Exception as e: - sentry_exc(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: dict) -> dict: + async def api(self, data: ApiData) -> ru.JSON: raise NotImplementedError() diff --git a/royalnet/utils/__init__.py b/royalnet/utils/__init__.py index deb9539b..88893c75 100644 --- a/royalnet/utils/__init__.py +++ b/royalnet/utils/__init__.py @@ -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", ] diff --git a/royalnet/utils/royaltyping.py b/royalnet/utils/royaltyping.py new file mode 100644 index 00000000..93eca407 --- /dev/null +++ b/royalnet/utils/royaltyping.py @@ -0,0 +1,3 @@ +from typing import * + +JSON = Union[None, int, str, List["JSON"], Dict[str, "JSON"]] diff --git a/royalnet/version.py b/royalnet/version.py index a65bd8c3..384e124e 100644 --- a/royalnet/version.py +++ b/royalnet/version.py @@ -1 +1 @@ -semantic = "5.5a3" +semantic = "5.5a4" From a7965e62829ac8a7abb4175f8df1d7185416aacd Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Tue, 11 Feb 2020 18:56:23 +0100 Subject: [PATCH 10/13] publish: 5.5a5 --- pyproject.toml | 2 +- royalnet/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2de10a5a..6f229d79 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "royalnet" - version = "5.5a4" + version = "5.5a5" description = "A multipurpose bot and web framework" authors = ["Stefano Pigozzi "] license = "AGPL-3.0+" diff --git a/royalnet/version.py b/royalnet/version.py index 384e124e..4ebf3c24 100644 --- a/royalnet/version.py +++ b/royalnet/version.py @@ -1 +1 @@ -semantic = "5.5a4" +semantic = "5.5a5" From b7dc55e0315fe8cff043de3edda9c40d6f4f6bf5 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Tue, 11 Feb 2020 19:05:44 +0100 Subject: [PATCH 11/13] publish: 5.5a6 --- pyproject.toml | 2 +- royalnet/backpack/stars/api_login_royalnet.py | 2 +- royalnet/version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6f229d79..ee52c336 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "royalnet" - version = "5.5a5" + version = "5.5a6" description = "A multipurpose bot and web framework" authors = ["Stefano Pigozzi "] license = "AGPL-3.0+" diff --git a/royalnet/backpack/stars/api_login_royalnet.py b/royalnet/backpack/stars/api_login_royalnet.py index 2340d427..32c04d28 100644 --- a/royalnet/backpack/stars/api_login_royalnet.py +++ b/royalnet/backpack/stars/api_login_royalnet.py @@ -26,7 +26,7 @@ class ApiLoginRoyalnetStar(ApiStar): pswd_check = user.test_password(password) if not pswd_check: raise ApiError("Invalid password") - token: Token = TokenT.generate(user=user, expiration_delta=datetime.timedelta(days=7)) + 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() diff --git a/royalnet/version.py b/royalnet/version.py index 4ebf3c24..3571209e 100644 --- a/royalnet/version.py +++ b/royalnet/version.py @@ -1 +1 @@ -semantic = "5.5a5" +semantic = "5.5a6" From 715ae32cea2f8b19b7af4b9ba5f623d7c33dd9c8 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Tue, 11 Feb 2020 23:03:14 +0100 Subject: [PATCH 12/13] Done? --- pycharm_templates/command.vm | 1 - pycharm_templates/event.vm | 1 - 2 files changed, 2 deletions(-) diff --git a/pycharm_templates/command.vm b/pycharm_templates/command.vm index e96b214a..af1a69a3 100644 --- a/pycharm_templates/command.vm +++ b/pycharm_templates/command.vm @@ -1,5 +1,4 @@ from typing import * -import royalnet import royalnet.commands as rc diff --git a/pycharm_templates/event.vm b/pycharm_templates/event.vm index 1182ac76..495b8150 100644 --- a/pycharm_templates/event.vm +++ b/pycharm_templates/event.vm @@ -1,5 +1,4 @@ from typing import * -import royalnet import royalnet.commands as rc From f314465b37d6fe9e8cd568890c1306188f756ca0 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Tue, 11 Feb 2020 23:04:36 +0100 Subject: [PATCH 13/13] Bump version --- pyproject.toml | 2 +- royalnet/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ee52c336..33b91041 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "royalnet" - version = "5.5a6" + version = "5.5" description = "A multipurpose bot and web framework" authors = ["Stefano Pigozzi "] license = "AGPL-3.0+" diff --git a/royalnet/version.py b/royalnet/version.py index 3571209e..63bf6999 100644 --- a/royalnet/version.py +++ b/royalnet/version.py @@ -1 +1 @@ -semantic = "5.5a6" +semantic = "5.5"