1
Fork 0
mirror of https://github.com/RYGhub/royalnet.git synced 2024-11-23 19:44:20 +00:00

Make the bot runnable

This commit is contained in:
Steffo 2019-11-15 20:47:32 +01:00
parent ef645ab143
commit 192b4cf3d6
29 changed files with 247 additions and 315 deletions

View file

@ -3,6 +3,7 @@
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/royalnet" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/royalnet.egg-info" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.8 (royalnet-1MWM6-kd-py3.8)" jdkType="Python SDK" />

39
poetry.lock generated
View file

@ -82,7 +82,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "7.0"
[[package]]
category = "dev"
category = "main"
description = "Cross-platform colored terminal text."
marker = "sys_platform == \"win32\""
name = "colorama"
@ -90,6 +90,17 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "0.4.1"
[[package]]
category = "main"
description = "Log formatting with colors!"
name = "colorlog"
optional = true
python-versions = "*"
version = "4.0.2"
[package.dependencies]
colorama = "*"
[[package]]
category = "main"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
@ -142,7 +153,7 @@ voice = ["PyNaCl (1.3.0)"]
[package.source]
reference = "09a08f9a9f126aa1f55c2444eb70508d1d52f8d9"
type = "git"
url = "https://github.com/Steffo99/discord.py.git"
url = "https://github.com/Steffo99/discord.py"
[[package]]
category = "main"
description = "Discover and load entry points from installed packages."
@ -328,7 +339,7 @@ description = "pytest: simple powerful testing with Python"
name = "pytest"
optional = false
python-versions = ">=3.5"
version = "5.2.2"
version = "5.2.3"
[package.dependencies]
atomicwrites = ">=1.0"
@ -562,6 +573,7 @@ version = "2019.11.5"
alchemy_easy = ["sqlalchemy", "psycopg2_binary"]
alchemy_hard = ["sqlalchemy", "psycopg2"]
bard = ["ffmpeg_python", "youtube_dl"]
colorlog = ["colorlog"]
constellation = ["starlette", "uvicorn"]
discord = ["discord.py", "pynacl"]
herald = ["websockets"]
@ -569,7 +581,7 @@ sentry = ["sentry_sdk"]
telegram = ["python_telegram_bot"]
[metadata]
content-hash = "a51bc903341dd7fb6c2923e4f0a45654ccc9c011182ad9a67c2c80d24f62fb26"
content-hash = "48fd4f6a0a25ffaf89999db4a999774b8e22794e07488b032ac0f244049caa5f"
python-versions = "^3.8"
[metadata.files]
@ -660,6 +672,10 @@ colorama = [
{file = "colorama-0.4.1-py2.py3-none-any.whl", hash = "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"},
{file = "colorama-0.4.1.tar.gz", hash = "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d"},
]
colorlog = [
{file = "colorlog-4.0.2-py2.py3-none-any.whl", hash = "sha256:450f52ea2a2b6ebb308f034ea9a9b15cea51e65650593dca1da3eb792e4e4981"},
{file = "colorlog-4.0.2.tar.gz", hash = "sha256:3cf31b25cbc8f86ec01fef582ef3b840950dea414084ed19ab922c8b493f9b42"},
]
cryptography = [
{file = "cryptography-2.8-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8"},
{file = "cryptography-2.8-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2"},
@ -845,8 +861,8 @@ pyparsing = [
{file = "pyparsing-2.4.5.tar.gz", hash = "sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a"},
]
pytest = [
{file = "pytest-5.2.2-py3-none-any.whl", hash = "sha256:58cee9e09242937e136dbb3dab466116ba20d6b7828c7620f23947f37eb4dae4"},
{file = "pytest-5.2.2.tar.gz", hash = "sha256:27abc3fef618a01bebb1f0d6d303d2816a99aa87a5968ebc32fe971be91eb1e6"},
{file = "pytest-5.2.3-py3-none-any.whl", hash = "sha256:b6cf7ad9064049ee486586b3a0ddd70dc5136c40e1147e7d286efd77ba66c5eb"},
{file = "pytest-5.2.3.tar.gz", hash = "sha256:15837d2880cb94821087bc07476892ea740696b20e90288fd6c19e44b435abdb"},
]
python-dateutil = [
{file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
@ -936,24 +952,13 @@ websockets = [
{file = "websockets-8.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c"},
{file = "websockets-8.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170"},
{file = "websockets-8.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8"},
{file = "websockets-8.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb"},
{file = "websockets-8.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5"},
{file = "websockets-8.1-cp36-cp36m-win32.whl", hash = "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a"},
{file = "websockets-8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5"},
{file = "websockets-8.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989"},
{file = "websockets-8.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d"},
{file = "websockets-8.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779"},
{file = "websockets-8.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8"},
{file = "websockets-8.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422"},
{file = "websockets-8.1-cp37-cp37m-win32.whl", hash = "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc"},
{file = "websockets-8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308"},
{file = "websockets-8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092"},
{file = "websockets-8.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485"},
{file = "websockets-8.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1"},
{file = "websockets-8.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55"},
{file = "websockets-8.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824"},
{file = "websockets-8.1-cp38-cp38-win32.whl", hash = "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36"},
{file = "websockets-8.1-cp38-cp38-win_amd64.whl", hash = "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"},
{file = "websockets-8.1.tar.gz", hash = "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f"},
]
yarl = [

View file

@ -24,7 +24,7 @@
# telegram
python_telegram_bot = {version="^12.2.0", optional=true}
# discord
"discord.py" = {git="https://github.com/Steffo99/discord.py.git", optional=true} # discord.py 1.2.4 is missing Go Live related methods
"discord.py" = {git="https://github.com/Steffo99/discord.py", optional=true} # discord.py 1.2.4 is missing Go Live related methods
pynacl = {version="^1.3.0", optional=true} # This requires libffi-dev and python3.*-dev to be installed on Linux systems
# bard
ffmpeg_python = {version="~0.2.0", optional=true}
@ -40,6 +40,8 @@
sentry_sdk = {version="~0.13.2", optional=true}
# herald
websockets = {version="^8.1", optional=true}
# colorlog
colorlog = {version="^4.0.2", optional=true}
# Development dependencies
[tool.poetry.dev-dependencies]
@ -55,7 +57,7 @@
constellation = ["starlette", "uvicorn"]
sentry = ["sentry_sdk"]
herald = ["websockets"]
colorlog = ["colorlog"]
[build-system]
requires = ["poetry>=0.12"]

View file

@ -1 +1,14 @@
__version__ = "5.1a1"
from . import alchemy, bard, commands, constellation, herald, backpack, serf, utils
__all__ = [
"alchemy",
"bard",
"commands",
"constellation",
"herald",
"serf",
"utils",
"backpack",
]

View file

@ -2,10 +2,9 @@ import click
import typing
import importlib
import royalnet as r
import royalherald as rh
import multiprocessing
import keyring
import logging
from logging import Formatter, StreamHandler, getLogger, Logger
@click.command()
@ -13,55 +12,53 @@ import logging
help="Enable/disable the Telegram bot.")
@click.option("--discord/--no-discord", default=None,
help="Enable/disable the Discord bot.")
@click.option("--webserver/--no-webserver", default=None,
help="Enable/disable the Web server.")
@click.option("--webserver-port", default=8001,
help="The port on which the web server will listen on.")
@click.option("-d", "--database", type=str, default=None,
help="The PostgreSQL database path.")
@click.option("-p", "--packs", type=str, multiple=True, default=[],
help="The names of the Packs that should be used.")
@click.option("-n", "--network-address", type=str, default=None,
help="The Network server URL to connect to.")
@click.option("-l", "--local-network-server", is_flag=True, default=False,
help="Locally run a Network server and bind it to port 44444. Overrides -n.")
@click.option("--local-network-server-port", type=int, default=44444,
help="The port on which the local network will be ran.")
@click.option("--constellation/--no-constellation", default=None,
help="Enable/disable the Constellation web server.")
@click.option("--herald/--no-herald", default=None,
help="Enable/disable the integrated Herald server."
" If turned off, Royalnet will try to connect to another server.")
@click.option("--remote-herald-address", type=str, default=None,
help="If --no-herald is specified, connect to the Herald server at this URL instead.")
@click.option("-c", "--constellation-port", default=44445,
help="The port on which the Constellation will serve webpages on.")
@click.option("-a", "--alchemy-url", type=str, default=None,
help="The Alchemy database path.")
@click.option("-h", "--herald-port", type=int, default=44444,
help="The port on which the Herald should be running.")
@click.option("-p", "--pack", type=str, multiple=True, default=tuple(),
help="Import the pack with the specified name and use it in the Royalnet instance.")
@click.option("-s", "--secrets-name", type=str, default="__default__",
help="The name in the keyring that the secrets are stored with.")
@click.option("-v", "--verbose", is_flag=True, default=False,
help="Print all possible debug information.")
@click.option("-l", "--log-level", type=str, default="INFO",
help="Select how much information you want to be printed on the console."
" Valid log levels are: FATAL/ERROR/WARNING/INFO/DEBUG")
def run(telegram: typing.Optional[bool],
discord: typing.Optional[bool],
webserver: typing.Optional[bool],
webserver_port: typing.Optional[int],
database: typing.Optional[str],
packs: typing.Tuple[str],
network_address: typing.Optional[str],
local_network_server: bool,
local_network_server_port: int,
constellation: typing.Optional[bool],
herald: typing.Optional[bool],
remote_herald_address: typing.Optional[str],
constellation_port: int,
alchemy_url: typing.Optional[str],
herald_port: int,
pack: typing.Tuple[str],
secrets_name: str,
verbose: bool):
# Setup logging
if verbose:
core_logger = logging.root
core_logger.setLevel(logging.DEBUG)
stream_handler = logging.StreamHandler()
stream_handler.formatter = logging.Formatter("{asctime}\t{name}\t{levelname}\t{message}", style="{")
core_logger.addHandler(stream_handler)
core_logger.debug("Logging setup complete.")
log_level: str):
# Initialize logging
royalnet_log: Logger = getLogger("royalnet")
royalnet_log.setLevel(log_level)
stream_handler = StreamHandler()
stream_handler.formatter = Formatter("{asctime}\t{name}\t{levelname}\t{message}", style="{")
royalnet_log.addHandler(stream_handler)
# Get the network password
network_password = keyring.get_password(f"Royalnet/{secrets_name}", "network")
# Get the sentry dsn
sentry_dsn = keyring.get_password(f"Royalnet/{secrets_name}", "sentry")
def get_secret(username: str):
return keyring.get_password(f"Royalnet/{secrets_name}", username)
# Enable / Disable interfaces
interfaces = {
"telegram": telegram,
"discord": discord,
"webserver": webserver
"herald": herald,
"constellation": constellation,
}
# If any interface is True, then the undefined ones should be False
if any(interfaces[name] is True for name in interfaces):
@ -79,36 +76,30 @@ def run(telegram: typing.Optional[bool],
for name in interfaces:
interfaces[name] = True
server_process: typing.Optional[multiprocessing.Process] = None
# Start the network server
if local_network_server:
server_process = multiprocessing.Process(name="Network Server",
target=rh.Server("0.0.0.0", local_network_server_port, network_password).run_blocking,
herald_process: typing.Optional[multiprocessing.Process] = None
# Start the Herald server
if interfaces["herald"]:
herald_config = r.herald.Config(name="<server>",
address="127.0.0.1",
port=herald_port,
secret=get_secret("herald"),
secure=False,
path="/")
herald_process = multiprocessing.Process(name="Herald",
target=r.herald.Server(config=herald_config).run_blocking,
daemon=True)
server_process.start()
network_address = f"ws://127.0.0.1:{local_network_server_port}/"
# Create a Royalnet configuration
network_config: typing.Optional[rh.Config] = None
if network_address is not None:
network_config = rh.Config(network_address, network_password)
# Create a Alchemy configuration
telegram_db_config: typing.Optional[r.alchemy.DatabaseConfig] = None
discord_db_config: typing.Optional[r.alchemy.DatabaseConfig] = None
if database is not None:
telegram_db_config = r.alchemy.DatabaseConfig(database,
r.packs.common.tables.User,
r.packs.common.tables.Telegram,
"tg_id")
discord_db_config = r.alchemy.DatabaseConfig(database,
r.packs.common.tables.User,
r.packs.common.tables.Discord,
"discord_id")
herald_process.start()
else:
herald_config = r.herald.Config(name=...,
address=remote_herald_address,
port=herald_port,
secret=get_secret("herald"),
secure=False,
path="/")
# Import command and star packs
packs: typing.List[str] = list(packs)
packs.append("royalnet.packs.common") # common pack is always imported
packs: typing.List[str] = list(pack)
packs.append("royalnet.backpack") # backpack is always imported
enabled_commands = []
enabled_page_stars = []
enabled_exception_stars = []
@ -132,62 +123,66 @@ def run(telegram: typing.Optional[bool],
telegram_process: typing.Optional[multiprocessing.Process] = None
if interfaces["telegram"]:
click.echo("\n@BotFather Commands String")
for command in enabled_commands:
click.echo(f"{command.name} - {command.description}")
click.echo("")
telegram_bot = r.interfaces.TelegramBot(network_config=network_config,
database_config=telegram_db_config,
sentry_dsn=sentry_dsn,
commands=enabled_commands,
secrets_name=secrets_name)
telegram_process = multiprocessing.Process(name="Telegram Interface",
target=telegram_bot.run_blocking,
args=(verbose,),
telegram_db_config = r.serf.AlchemyConfig(database_url=alchemy_url,
master_table=r.backpack.tables.User,
identity_table=r.backpack.tables.Telegram,
identity_column="tg_id")
telegram_serf_kwargs = {
'alchemy_config': telegram_db_config,
'commands': enabled_commands,
'network_config': herald_config.copy(name="telegram"),
'secrets_name': secrets_name
}
telegram_process = multiprocessing.Process(name="Telegram Serf",
target=r.serf.telegram.TelegramSerf.run_process,
kwargs=telegram_serf_kwargs,
daemon=True)
telegram_process.start()
discord_process: typing.Optional[multiprocessing.Process] = None
if interfaces["discord"]:
discord_bot = r.interfaces.DiscordBot(network_config=network_config,
database_config=discord_db_config,
sentry_dsn=sentry_dsn,
commands=enabled_commands,
secrets_name=secrets_name)
discord_process = multiprocessing.Process(name="Discord Interface",
target=discord_bot.run_blocking,
args=(verbose,),
discord_db_config = r.serf.AlchemyConfig(database_url=alchemy_url,
master_table=r.backpack.tables.User,
identity_table=r.backpack.tables.Discord,
identity_column="discord_id")
discord_serf_kwargs = {
'alchemy_config': discord_db_config,
'commands': enabled_commands,
'network_config': herald_config.copy(name="discord"),
'secrets_name': secrets_name
}
discord_process = multiprocessing.Process(name="Discord Serf",
target=r.serf.discord.DiscordSerf.run_process,
kwargs=discord_serf_kwargs,
daemon=True)
discord_process.start()
webserver_process: typing.Optional[multiprocessing.Process] = None
if interfaces["webserver"]:
# Common tables are always included
constellation_tables = set(r.packs.common.available_tables)
# Find the required tables
for star in [*enabled_page_stars, *enabled_exception_stars]:
constellation_tables = constellation_tables.union(star.tables)
constellation_process: typing.Optional[multiprocessing.Process] = None
if interfaces["constellation"]:
# Create the Constellation
constellation = r.web.Constellation(page_stars=enabled_page_stars,
exc_stars=enabled_exception_stars,
secrets_name=secrets_name,
database_uri=database,
tables=constellation_tables)
webserver_process = multiprocessing.Process(name="Constellation Webserver",
target=constellation.run_blocking,
args=("0.0.0.0", webserver_port, verbose,),
constellation_kwargs = {
'address': "127.0.0.1",
'port': constellation_port,
'secrets_name': secrets_name,
'database_uri': alchemy_url,
'page_stars': enabled_page_stars,
'exc_stars': enabled_exception_stars,
}
constellation_process = multiprocessing.Process(name="Constellation",
target=r.constellation.Constellation.run_process,
kwargs=constellation_kwargs,
daemon=True)
webserver_process.start()
constellation_process.start()
click.echo("Royalnet processes have been started. You can force-quit by pressing Ctrl+C.")
if server_process is not None:
server_process.join()
click.echo("Royalnet is now running! You can stop its execution by pressing Ctrl+C at any time.")
if herald_process is not None:
herald_process.join()
if telegram_process is not None:
telegram_process.join()
if discord_process is not None:
discord_process.join()
if webserver_process is not None:
webserver_process.join()
if constellation_process is not None:
constellation_process.join()
if __name__ == "__main__":

View file

@ -0,0 +1,3 @@
# `backpack`
A Pack that is imported by default by all `royalnet` instances.

View file

@ -1,4 +1,4 @@
# This is a template Pack __init__. You can use this without changing anything in other packages too!
"""A Pack that is imported by default by all :mod:`royalnet` instances."""
from . import commands, tables, stars
from .commands import available_commands

View file

@ -1,5 +1,5 @@
import royalnet
from royalnet.commands import *
from royalnet.version import semantic
class VersionCommand(Command):
@ -8,7 +8,7 @@ class VersionCommand(Command):
description: str = "Get the current Royalnet version."
async def run(self, args: CommandArgs, data: CommandData) -> None:
message = f" Royalnet {semantic}\n"
message = f" Royalnet {royalnet.__version__}\n"
if "69" in message:
message += "(Nice.)"
await data.reply(message)

View file

@ -1,15 +1,18 @@
import royalnet
from starlette.requests import Request
from starlette.responses import *
from royalnet.web import PageStar
from royalnet.constellation import PageStar
from ..tables import available_tables
class ApiRoyalnetVersionStar(PageStar):
path = "/api/royalnet/version"
tables = set(available_tables)
async def page(self, request: Request) -> JSONResponse:
return JSONResponse({
"version": {
"semantic": royalnet.version.semantic
"semantic": royalnet.__version__,
}
})

View file

@ -34,7 +34,7 @@ class YtdlMp3:
)
self.mp3_filename = destination_filename
def delete_asap(self) -> None:
async def delete_asap(self) -> None:
"""Delete the mp3 file."""
if self.is_converted:
async with self.lock.exclusive():

View file

@ -1,3 +1,4 @@
from asyncio import AbstractEventLoop
from typing import Optional, TYPE_CHECKING
from .errors import UnsupportedError
from .commandinterface import CommandInterface
@ -8,9 +9,10 @@ if TYPE_CHECKING:
class CommandData:
def __init__(self, interface: CommandInterface, session: Optional["Session"]):
def __init__(self, interface: CommandInterface, session: Optional["Session"], loop: AbstractEventLoop):
self._interface: CommandInterface = interface
self._session: Optional["Session"] = session
self.loop: AbstractEventLoop = loop
@property
def session(self) -> "Session":

View file

@ -6,9 +6,9 @@ import keyring
def run():
click.echo("Welcome to the Royalnet configuration creator!")
secrets_name = click.prompt("Desired secrets name", default="__default__")
network = click.prompt("Network password", default="")
network = click.prompt("Herald password", default="")
if network:
keyring.set_password(f"Royalnet/{secrets_name}", "network", network)
keyring.set_password(f"Royalnet/{secrets_name}", "herald", network)
telegram = click.prompt("Telegram Bot API token", default="")
if telegram:
keyring.set_password(f"Royalnet/{secrets_name}", "telegram", telegram)
@ -21,9 +21,6 @@ def run():
sentry = click.prompt("Sentry DSN", default="")
if sentry:
keyring.set_password(f"Royalnet/{secrets_name}", "sentry", sentry)
leagueoflegends = click.prompt("League of Legends API Token", default="")
if leagueoflegends:
keyring.set_password(f"Royalnet/{secrets_name}", "leagueoflegends", leagueoflegends)
if __name__ == "__main__":

View file

@ -2,7 +2,6 @@ import typing
import logging
import royalnet
import keyring
from royalnet.alchemy import Alchemy
from .star import PageStar, ExceptionStar
try:
@ -60,7 +59,7 @@ class Constellation:
"""The :class:`Starlette` app."""
log.debug("Finding required Tables...")
tables = set()
tables = set(royalnet.backpack.available_tables)
for SelectedPageStar in page_stars:
tables = tables.union(SelectedPageStar.tables)
for SelectedExcStar in exc_stars:
@ -68,7 +67,7 @@ class Constellation:
log.debug(f"Found Tables: {' '.join([table.__name__ for table in tables])}")
log.info(f"Creating Alchemy...")
self.alchemy: Alchemy = Alchemy(database_uri=database_uri, tables=tables)
self.alchemy: royalnet.alchemy.Alchemy = royalnet.alchemy.Alchemy(database_uri=database_uri, tables=tables)
"""The :class:`Alchemy: of this Constellation."""
log.info("Registering PageStars...")
@ -98,19 +97,34 @@ class Constellation:
username: the name of the secret that should be retrieved."""
return keyring.get_password(f"Royalnet/{self.secrets_name}", username)
def run_blocking(self, address: str, port: int):
"""Blockingly run the Constellation.
@classmethod
def run_process(cls,
address: str,
port: int,
secrets_name: str,
database_uri: str,
page_stars: typing.List[typing.Type[PageStar]] = None,
exc_stars: typing.List[typing.Type[ExceptionStar]] = None,
*,
debug: bool = __debug__,):
"""Blockingly create and run the Constellation.
This should be used as the target of a :class:`multiprocessing.Process`.
Args:
address: The IP address this Constellation should bind to.
port: The port this Constellation should listen for requests on."""
constellation = cls(secrets_name=secrets_name,
database_uri=database_uri,
page_stars=page_stars,
exc_stars=exc_stars,
debug=debug)
# Initialize Sentry on the process
if sentry_sdk is None:
log.info("Sentry: not installed")
else:
sentry_dsn = self.get_secret("sentry")
sentry_dsn = constellation.get_secret("sentry")
if not sentry_dsn:
log.info("Sentry: disabled")
else:
@ -128,11 +142,11 @@ class Constellation:
log.info(f"Sentry: enabled (Royalnet {release})")
# Run the server
log.info(f"Running Constellation on {address}:{port}...")
self.running = True
constellation.running = True
try:
uvicorn.run(self.starlette, host=address, port=port)
uvicorn.run(constellation.starlette, host=address, port=port)
finally:
self.running = False
constellation.running = False
def __repr__(self):
return f"<{self.__class__.__qualname__}: {'running' if self.running else 'inactive'}>"

View file

@ -1,3 +1,6 @@
from typing import Optional
class Config:
def __init__(self,
name: str,
@ -30,5 +33,20 @@ class Config:
def url(self):
return f"ws{'s' if self.secure else ''}://{self.address}:{self.port}{self.path}"
def copy(self,
name: Optional[str] = None,
address: Optional[str] = None,
port: Optional[int] = None,
secret: Optional[str] = None,
secure: Optional[bool] = None,
path: Optional[str] = None):
"""Create an exact copy of this configuration, but with different parameters."""
return self.__class__(name=name if name else self.name,
address=address if address else self.address,
port=port if port else self.port,
secret=secret if secret else self.secret,
secure=secure if secure else self.secure,
path=path if path else self.path)
def __repr__(self):
return f"<HeraldConfig for {self.url}>"

View file

@ -1,7 +1,10 @@
from .serf import Serf
from .alchemyconfig import AlchemyConfig
from . import telegram, discord
__all__ = [
"Serf",
"AlchemyConfig"
"AlchemyConfig",
"telegram",
"discord",
]

View file

@ -1,18 +1,16 @@
from typing import Type, TYPE_CHECKING
if TYPE_CHECKING:
from sqlalchemy.schema import Table
from typing import TYPE_CHECKING
class AlchemyConfig:
"""A helper class to configure :class:`Alchemy` in a :class:`Serf`."""
def __init__(self,
database_url: str,
master_table: Type[Table],
identity_table: Type[Table],
master_table: type,
identity_table: type,
identity_column: str):
self.database_url: str = database_url
self.master_table: Type[Table] = master_table
self.identity_table: Type[Table] = identity_table
self.master_table: type = master_table
self.identity_table: type = identity_table
self.identity_column: str = identity_column
def __repr__(self):

View file

@ -1,3 +1,4 @@
import asyncio
import logging
from typing import Type, Optional, List, Union
from royalnet.commands import Command, CommandInterface, CommandData, CommandArgs, CommandError, InvalidInputError, \
@ -63,8 +64,12 @@ class DiscordSerf(Serf):
def data_factory(self) -> Type[CommandData]:
# noinspection PyMethodParameters,PyAbstractClass
class DiscordData(CommandData):
def __init__(data, interface: CommandInterface, session, message: discord.Message):
super().__init__(interface=interface, session=session)
def __init__(data,
interface: CommandInterface,
session,
loop: asyncio.AbstractEventLoop,
message: discord.Message):
super().__init__(interface=interface, session=session, loop=loop)
data.message = message
async def reply(data, text: str):
@ -118,7 +123,7 @@ class DiscordSerf(Serf):
else:
session = None
# Prepare data
data = self.Data(interface=command.interface, session=session, message=message)
data = self.Data(interface=command.interface, session=session, loop=self.loop, message=message)
try:
# Run the command
await command.run(CommandArgs(parameters), data)
@ -196,6 +201,7 @@ class DiscordSerf(Serf):
return DiscordClient
async def run(self):
await super().run()
token = self.get_secret("discord")
await self.client.login(token)
await self.client.connect()

View file

@ -1,5 +1,5 @@
import logging
from asyncio import Task, AbstractEventLoop
from asyncio import Task, AbstractEventLoop, get_event_loop
from typing import Type, Optional, Awaitable, Dict, List, Any, Callable, Union, Set
from keyring import get_password
from sqlalchemy.schema import Table
@ -130,10 +130,10 @@ class Serf:
self.alchemy = Alchemy(alchemy_config.database_url, tables)
self._master_table = self.alchemy.get(alchemy_config.master_table)
self._identity_table = self.alchemy.get(alchemy_config.identity_table)
# FIXME: this MAY break
self._identity_column = self._identity_table.__getattribute__(alchemy_config.identity_column)
# self._identity_column = self._identity_table.__getattribute__(self._identity_table,
# alchemy_config.identity_column)
# This is fine, as Pycharm doesn't know that identity_table is a class and not an object
# noinspection PyArgumentList
self._identity_column = self._identity_table.__getattribute__(self._identity_table,
alchemy_config.identity_column)
@property
def _identity_chain(self) -> tuple:
@ -147,7 +147,6 @@ class Serf:
class GenericInterface(CommandInterface):
alchemy: Alchemy = self.alchemy
bot: "Serf" = self
loop: AbstractEventLoop = self.loop
def register_herald_action(ci,
event_name: str,
@ -232,10 +231,9 @@ class Serf:
def init_network(self, config: HeraldConfig):
"""Create a :py:class:`Link`, and run it as a :py:class:`asyncio.Task`."""
log.debug(f"Initializing herald...")
self.herald: Link = Link(config, self._network_handler)
self.herald_task = self.loop.create_task(self.herald.run())
self.herald: Link = Link(config, self.network_handler)
async def _network_handler(self, message: Union[Request, Broadcast]) -> Response:
async def network_handler(self, message: Union[Request, Broadcast]) -> Response:
try:
network_handler = self.herald_handlers[message.handler]
except KeyError:
@ -288,19 +286,24 @@ class Serf:
async def run(self):
"""A coroutine that starts the event loop and handles command calls."""
raise NotImplementedError()
self.herald_task = self.loop.create_task(self.herald.run())
# OVERRIDE THIS METHOD!
def run_blocking(self):
"""Blockingly run the Serf.
@classmethod
def run_process(cls, *args, **kwargs):
"""Blockingly create and run the Serf.
This should be used as the target of a :class:`multiprocessing.Process`."""
serf = cls(*args, **kwargs)
if sentry_sdk is None:
log.info("Sentry: not installed")
else:
sentry_dsn = self.get_secret("sentry")
sentry_dsn = serf.get_secret("sentry")
if sentry_dsn is None:
log.info("Sentry: disabled")
else:
self.init_sentry(sentry_dsn)
serf.init_sentry(sentry_dsn)
self.loop.run_until_complete(self.run())
serf.loop = get_event_loop()
serf.loop.run_until_complete(serf.run())

View file

@ -99,8 +99,12 @@ class TelegramSerf(Serf):
def data_factory(self) -> Type[CommandData]:
# noinspection PyMethodParameters
class TelegramData(CommandData):
def __init__(data, interface: CommandInterface, session, update: telegram.Update):
super().__init__(interface=interface, session=session)
def __init__(data,
interface: CommandInterface,
session,
loop: asyncio.AbstractEventLoop,
update: telegram.Update):
super().__init__(interface=interface, session=session, loop=loop)
data.update = update
async def reply(data, text: str):
@ -192,7 +196,7 @@ class TelegramSerf(Serf):
session = None
try:
# Create the command data
data = self.Data(interface=command.interface, session=session, update=update)
data = self.Data(interface=command.interface, session=session, loop=self.loop, update=update)
try:
# Run the command
await command.run(CommandArgs(parameters), data)
@ -240,6 +244,7 @@ class TelegramSerf(Serf):
pass
async def run(self):
await super().run()
while True:
# Get the latest 100 updates
last_updates: List[telegram.Update] = await self.api_call(self.client.get_updates,

View file

@ -1,11 +1,7 @@
"""Miscellaneous useful functions and classes."""
from .asyncify import asyncify
from .escaping import telegram_escape, discord_escape
from .safeformat import safeformat
from .classdictjanitor import cdj
from .sleep_until import sleep_until
from .formatters import andformat, plusformat, underscorize, ytdldateformat, numberemojiformat, splitstring, ordinalformat
from .formatters import andformat, underscorize, ytdldateformat, numberemojiformat, splitstring, ordinalformat
from .urluuid import to_urluuid, from_urluuid
from .multilock import MultiLock

View file

@ -1,95 +0,0 @@
import typing
import uvicorn
import logging
import sentry_sdk
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
from sentry_sdk.integrations.logging import LoggingIntegration
import royalnet
import keyring
from starlette.applications import Starlette
from .star import PageStar, ExceptionStar
log = logging.getLogger(__name__)
class Constellation:
def __init__(self,
secrets_name: str,
database_uri: str,
tables: set,
page_stars: typing.List[typing.Type[PageStar]] = None,
exc_stars: typing.List[typing.Type[ExceptionStar]] = None,
*,
debug: bool = __debug__,):
if page_stars is None:
page_stars = []
if exc_stars is None:
exc_stars = []
self.secrets_name: str = secrets_name
log.info("Creating starlette app...")
self.starlette = Starlette(debug=debug)
log.info(f"Creating alchemy with tables: {' '.join([table.__name__ for table in tables])}")
self.alchemy: royalnet.alchemy.Alchemy = royalnet.alchemy.Alchemy(database_uri=database_uri, tables=tables)
log.info("Registering page_stars...")
for SelectedPageStar in page_stars:
try:
page_star_instance = SelectedPageStar(constellation=self)
except Exception as e:
log.error(f"{e.__class__.__qualname__} during the registration of {SelectedPageStar.__qualname__}")
sentry_sdk.capture_exception(e)
continue
log.info(f"Registering: {page_star_instance.path} -> {page_star_instance.__class__.__name__}")
self.starlette.add_route(page_star_instance.path, page_star_instance.page, page_star_instance.methods)
log.info("Registering exc_stars...")
for SelectedExcStar in exc_stars:
try:
exc_star_instance = SelectedExcStar(constellation=self)
except Exception as e:
log.error(f"{e.__class__.__qualname__} during the registration of {SelectedExcStar.__qualname__}")
sentry_sdk.capture_exception(e)
continue
log.info(f"Registering: {exc_star_instance.error} -> {exc_star_instance.__class__.__name__}")
self.starlette.add_exception_handler(exc_star_instance.error, exc_star_instance.page)
def _init_sentry(self):
sentry_dsn = self.get_secret("sentry")
if sentry_dsn:
# noinspection PyUnreachableCode
if __debug__:
release = "DEV"
else:
release = royalnet.version.semantic
log.info(f"Sentry: enabled (Royalnet {release})")
self.sentry = sentry_sdk.init(sentry_dsn,
integrations=[AioHttpIntegration(),
SqlalchemyIntegration(),
LoggingIntegration(event_level=None)],
release=release)
else:
log.info("Sentry: disabled")
def get_secret(self, username: str):
return keyring.get_password(f"Royalnet/{self.secrets_name}", username)
def set_secret(self, username: str, password: str):
return keyring.set_password(f"Royalnet/{self.secrets_name}", username, password)
def run_blocking(self, address: str, port: int, verbose: bool):
if verbose:
core_logger = logging.root
core_logger.setLevel(logging.DEBUG)
stream_handler = logging.StreamHandler()
stream_handler.formatter = logging.Formatter("{asctime}\t{name}\t{levelname}\t{message}", style="{")
core_logger.addHandler(stream_handler)
core_logger.debug("Logging setup complete.")
self._init_sentry()
log.info(f"Running constellation server on {address}:{port}...")
uvicorn.run(self.starlette, host=address, port=port)

View file

@ -1,37 +0,0 @@
import typing
from starlette.requests import Request
from starlette.responses import Response
if typing.TYPE_CHECKING:
from .constellation import Constellation
class Star:
tables: set = {}
def __init__(self, constellation: "Constellation"):
self.constellation: "Constellation" = constellation
async def page(self, request: Request) -> Response:
raise NotImplementedError()
@property
def alchemy(self):
return self.constellation.alchemy
@property
def Session(self):
return self.constellation.alchemy._Session
@property
def session_acm(self):
return self.constellation.alchemy.session_acm
class PageStar(Star):
path: str = NotImplemented
methods: typing.List[str] = ["GET"]
class ExceptionStar(Star):
error: typing.Union[typing.Type[Exception], int]