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

BREAKING: Drop matrix support (for now)

This commit is contained in:
Steffo 2020-08-19 00:22:08 +02:00
parent 207c2d4dcc
commit d38400521f
19 changed files with 38 additions and 256 deletions

View file

@ -16,9 +16,8 @@ Commands using the Royalnet Serf API share their code between chat platforms: ea
- [Telegram](https://core.telegram.org/bots) - [Telegram](https://core.telegram.org/bots)
- [Discord](https://discordapp.com/developers/docs/) - [Discord](https://discordapp.com/developers/docs/)
- [Matrix](https://matrix.org/) (alpha)
More can easily be added by creating a new serf! More can easily be added by implementing a new serf!
### [Alchemy](royalnet/alchemy) ### [Alchemy](royalnet/alchemy)

View file

@ -48,6 +48,7 @@ intersphinx_mapping = {
} }
# noinspection PyUnusedLocal
def skip(app, what, name: str, obj, would_skip, options): def skip(app, what, name: str, obj, would_skip, options):
if name == "__init__" or name == "__getitem__" or name == "__getattr__": if name == "__init__" or name == "__getitem__" or name == "__getattr__":
return not bool(obj.__doc__) return not bool(obj.__doc__)

View file

@ -1,7 +1,7 @@
# Remember to run `poetry update` editing this file! # Remember to run `poetry update` editing this file!
# Install everything with # Install everything with
# poetry install -E telegram -E discord -E matrix -E alchemy_easy -E constellation -E sentry -E herald -E coloredlogs # poetry install -E telegram -E discord -E alchemy_easy -E constellation -E sentry -E herald -E coloredlogs
[tool.poetry] [tool.poetry]
name = "royalnet" name = "royalnet"
@ -32,9 +32,6 @@ python_telegram_bot = { version = "^12.2.0", optional = true }
"discord.py" = { version = "^1.3.1", optional = true } "discord.py" = { version = "^1.3.1", optional = true }
pynacl = { version = "^1.3.0", optional = true } # This requires libffi-dev and python3.*-dev to be installed on Linux systems pynacl = { version = "^1.3.0", optional = true } # This requires libffi-dev and python3.*-dev to be installed on Linux systems
# matrix
matrix-nio = { version = "^0.6", optional = true }
# alchemy # alchemy
sqlalchemy = { version = "^1.3.18", optional = true } sqlalchemy = { version = "^1.3.18", optional = true }
psycopg2 = { version = "^2.8.4", optional = true } # Requires quite a bit of stuff http://initd.org/psycopg/docs/install.html#install-from-source psycopg2 = { version = "^2.8.4", optional = true } # Requires quite a bit of stuff http://initd.org/psycopg/docs/install.html#install-from-source
@ -66,7 +63,6 @@ sphinx_rtd_theme = "^0.4.3"
[tool.poetry.extras] [tool.poetry.extras]
telegram = ["python_telegram_bot"] telegram = ["python_telegram_bot"]
discord = ["discord.py", "pynacl", "lavalink", "aiohttp", "cchardet"] discord = ["discord.py", "pynacl", "lavalink", "aiohttp", "cchardet"]
matrix = ["matrix-nio"]
alchemy_easy = ["sqlalchemy", "psycopg2_binary", "bcrypt"] alchemy_easy = ["sqlalchemy", "psycopg2_binary", "bcrypt"]
alchemy_hard = ["sqlalchemy", "psycopg2", "bcrypt"] alchemy_hard = ["sqlalchemy", "psycopg2", "bcrypt"]
constellation = ["starlette", "uvicorn", "python-multipart"] constellation = ["starlette", "uvicorn", "python-multipart"]

View file

@ -19,11 +19,6 @@ try:
except ImportError: except ImportError:
rsd = None rsd = None
try:
import royalnet.serf.matrix as rsm
except ImportError:
rsm = None
try: try:
import royalnet.constellation as rc import royalnet.constellation as rc
except ImportError: except ImportError:
@ -100,18 +95,18 @@ def run(config_file: str):
else: else:
log.debug("__serfs__: Configured") log.debug("__serfs__: Configured")
def configure_serf(name: str, module, class_: Type[rs.Serf]): def configure_serf(n: str, module, class_: Type[rs.Serf]):
serf_cfg = serfs_cfg.get(name) serf_cfg = serfs_cfg.get(n)
if module is None: if module is None:
log.info(f"Serf.{name}: Not installed") log.info(f"Serf.{n}: Not installed")
elif serf_cfg is None: elif serf_cfg is None:
log.warning(f"Serf.{name}: Not configured") log.warning(f"Serf.{n}: Not configured")
elif not serf_cfg["enabled"]: elif not serf_cfg["enabled"]:
log.info(f"Serf.{name}: Disabled") log.info(f"Serf.{n}: Disabled")
else: else:
def serf_constructor() -> multiprocessing.Process: def serf_constructor() -> multiprocessing.Process:
return multiprocessing.Process( return multiprocessing.Process(
name=f"Serf.{name}", name=f"Serf.{n}",
target=class_.run_process, target=class_.run_process,
daemon=True, daemon=True,
kwargs={ kwargs={
@ -124,12 +119,11 @@ def run(config_file: str):
} }
) )
processes[f"Serf.{name}"] = ru.RoyalnetProcess(serf_constructor, None) processes[f"Serf.{n}"] = ru.RoyalnetProcess(serf_constructor, None)
log.info(f"Serf.{name}: Enabled") log.info(f"Serf.{n}: Enabled")
configure_serf("Telegram", rst, rst.TelegramSerf) configure_serf("Telegram", rst, rst.TelegramSerf)
configure_serf("Discord", rsd, rsd.DiscordSerf) configure_serf("Discord", rsd, rsd.DiscordSerf)
configure_serf("Matrix", rsm, rsm.MatrixSerf)
# Constellation # Constellation
constellation_cfg = config.get("Constellation") constellation_cfg = config.get("Constellation")

View file

@ -5,7 +5,6 @@ from typing import *
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.exc import ProgrammingError from sqlalchemy.exc import ProgrammingError
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.declarative.api import DeclarativeMeta
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
from sqlalchemy.schema import Table from sqlalchemy.schema import Table
@ -52,7 +51,7 @@ class Alchemy:
except ProgrammingError: except ProgrammingError:
log.warning("Skipping table creation, as it is probably being created by a different process.") log.warning("Skipping table creation, as it is probably being created by a different process.")
def get(self, table: Union[str, type]) -> DeclarativeMeta: def get(self, table: Union[str, type]) -> Any:
"""Get the table with a specified name or class. """Get the table with a specified name or class.
Args: Args:

View file

@ -11,7 +11,8 @@ class RoyalnetaliasesCommand(rc.Command):
async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
if name := args.optional(0) is not None: if name := args.optional(0) is not None:
user = await User.find(alchemy=self.alchemy, session=data.session, identifier=name) async with data.session_acm() as session:
user = await User.find(alchemy=self.alchemy, session=session, identifier=name)
else: else:
user = await data.get_author(error_if_none=True) user = await data.get_author(error_if_none=True)

View file

@ -11,7 +11,8 @@ class RoyalnetrolesCommand(rc.Command):
async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
if name := args.optional(0) is not None: if name := args.optional(0) is not None:
user = await User.find(alchemy=self.alchemy, session=data.session, identifier=name) async with data.session_acm() as session:
user = await User.find(alchemy=self.alchemy, session=session, identifier=name)
else: else:
user = await data.get_author(error_if_none=True) user = await data.get_author(error_if_none=True)

View file

@ -37,11 +37,7 @@ class ApiUserPasswd(rca.ApiStar):
tokens: List[Token] = await ru.asyncify( tokens: List[Token] = await ru.asyncify(
data.session data.session
.query(self.alchemy.get(Token)) .query(self.alchemy.get(Token))
.filter( .filter(and_(TokenT.user == user, TokenT.expiration >= datetime.datetime.now()))
and_(
TokenT.user == user,
TokenT.expiration >= datetime.datetime.now()
))
.all .all
) )
for t in tokens: for t in tokens:

View file

@ -45,7 +45,8 @@ class DocsStar(PageStar):
<html lang="en"> <html lang="en">
<head> <head>
<title>Royalnet Docs</title> <title>Royalnet Docs</title>
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@3.23.4/swagger-ui.css"> <link rel="stylesheet"
type="text/css" href="https://unpkg.com/swagger-ui-dist@3.23.4/swagger-ui.css">
<script src="https://unpkg.com/swagger-ui-dist@3.23.4/swagger-ui-bundle.js"></script> <script src="https://unpkg.com/swagger-ui-dist@3.23.4/swagger-ui-bundle.js"></script>
<script src="https://unpkg.com/swagger-ui-dist@3.23.4/swagger-ui-standalone-preset.js"></script> <script src="https://unpkg.com/swagger-ui-dist@3.23.4/swagger-ui-standalone-preset.js"></script>
</head> </head>

View file

@ -1,44 +0,0 @@
import re
from sqlalchemy import *
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import relationship
# noinspection PyUnresolvedReferences
from .users import User
class Matrix:
__tablename__ = "matrix"
@declared_attr
def user_id(self):
return Column(Integer, ForeignKey("users.uid"), nullable=False)
@declared_attr
def user(self):
return relationship("User", backref="matrix")
@declared_attr
def matrix_id(self):
return Column(String, nullable=False, primary_key=True)
@property
def username(self):
match = re.match("^@(.+):.+$", self.matrix_id)
result = match.group(1)
assert result is not None
return result
@property
def homeserver(self):
match = re.match("^@.+:(.+)$", self.matrix_id)
result = match.group(1)
assert result is not None
return result
def __repr__(self):
return f"<Matrix {str(self)}>"
def __str__(self):
return f"[c]matrix:{self.matrix_id}[/c]"

View file

@ -1,9 +1,9 @@
import datetime import datetime
import secrets import secrets
from sqlalchemy import * import sqlalchemy as s
from sqlalchemy.ext.declarative import declared_attr import sqlalchemy.ext.declarative as sed
from sqlalchemy.orm import * import sqlalchemy.orm as so
import royalnet.utils as ru import royalnet.utils as ru
@ -12,21 +12,21 @@ import royalnet.utils as ru
class Token: class Token:
__tablename__ = "tokens" __tablename__ = "tokens"
@declared_attr @sed.declared_attr
def token(self): def token(self):
return Column(String, primary_key=True) return s.Column(s.String, primary_key=True)
@declared_attr @sed.declared_attr
def user_id(self): def user_id(self):
return Column(Integer, ForeignKey("users.uid"), nullable=False) return s.Column(s.Integer, s.ForeignKey("users.uid"), nullable=False)
@declared_attr @sed.declared_attr
def user(self): def user(self):
return relationship("User", backref="tokens") return so.relationship("User", backref="tokens")
@declared_attr @sed.declared_attr
def expiration(self): def expiration(self):
return Column(DateTime, nullable=False) return s.Column(s.DateTime, nullable=False)
@property @property
def expired(self): def expired(self):

View file

@ -74,6 +74,7 @@ class User:
@property @property
def roles(self) -> list: def roles(self) -> list:
# noinspection PyUnresolvedReferences
return list(map(lambda a: a.role, self._roles)) return list(map(lambda a: a.role, self._roles))
def add_role(self, alchemy, role: str) -> None: def add_role(self, alchemy, role: str) -> None:
@ -90,6 +91,7 @@ class User:
@property @property
def aliases(self) -> list: def aliases(self) -> list:
# noinspection PyUnresolvedReferences
return list(map(lambda a: a.alias, self._aliases)) return list(map(lambda a: a.alias, self._aliases))
def add_alias(self, alchemy, alias: str) -> None: def add_alias(self, alchemy, alias: str) -> None:

View file

@ -69,6 +69,7 @@ class Constellation:
tables = set() tables = set()
for pack in packs.values(): for pack in packs.values():
try: try:
# noinspection PyUnresolvedReferences
tables = tables.union(pack["tables"].available_tables) tables = tables.union(pack["tables"].available_tables)
except AttributeError: except AttributeError:
log.warning(f"Pack `{pack}` does not have the `available_tables` attribute.") log.warning(f"Pack `{pack}` does not have the `available_tables` attribute.")
@ -108,6 +109,7 @@ class Constellation:
pack = packs[pack_name] pack = packs[pack_name]
pack_cfg = packs_cfg.get(pack_name, {}) pack_cfg = packs_cfg.get(pack_name, {})
try: try:
# noinspection PyUnresolvedReferences
events = pack["events"].available_events events = pack["events"].available_events
except AttributeError: except AttributeError:
log.warning(f"Pack `{pack}` does not have the `available_events` attribute.") log.warning(f"Pack `{pack}` does not have the `available_events` attribute.")
@ -127,6 +129,7 @@ class Constellation:
pack = packs[pack_name] pack = packs[pack_name]
pack_cfg = packs_cfg.get(pack_name, {}) pack_cfg = packs_cfg.get(pack_name, {})
try: try:
# noinspection PyUnresolvedReferences
page_stars = pack["stars"].available_page_stars page_stars = pack["stars"].available_page_stars
except AttributeError: except AttributeError:
log.warning(f"Pack `{pack}` does not have the `available_page_stars` attribute.") log.warning(f"Pack `{pack}` does not have the `available_page_stars` attribute.")
@ -248,7 +251,7 @@ class Constellation:
return page_star.path, f, page_star.methods() return page_star.path, f, page_star.methods()
def register_page_stars(self, page_stars: List[Type[PageStar]], pack_cfg: Dict[str, Any]): def register_page_stars(self, page_stars: List[Type[PageStar]], pack_cfg: rc.ConfigDict):
for SelectedPageStar in page_stars: for SelectedPageStar in page_stars:
log.debug(f"Registering: {SelectedPageStar.path} -> {SelectedPageStar.__qualname__}") log.debug(f"Registering: {SelectedPageStar.path} -> {SelectedPageStar.__qualname__}")
try: try:

View file

@ -36,6 +36,7 @@ def run(config_filename, file_format):
lines = [] lines = []
try: try:
# noinspection PyUnresolvedReferences
commands = pack["commands"].available_commands commands = pack["commands"].available_commands
except AttributeError: except AttributeError:
p(f"Pack `{pack}` does not have the `available_commands` attribute.", err=True) p(f"Pack `{pack}` does not have the `available_commands` attribute.", err=True)

View file

@ -60,6 +60,7 @@ class Server:
matching = [client for client in self.identified_clients if client.link_type == link_type] matching = [client for client in self.identified_clients if client.link_type == link_type]
return matching or [] return matching or []
# noinspection PyUnusedLocal
async def listener(self, websocket: "websockets.server.WebSocketServerProtocol", path): async def listener(self, websocket: "websockets.server.WebSocketServerProtocol", path):
connected_client = ConnectedClient(websocket) connected_client = ConnectedClient(websocket)
# Wait for identification # Wait for identification

View file

@ -1,10 +0,0 @@
# `royalnet.serf.matrix`
A `Serf` implementation for Matrix.
It requires (obviously) the `matrix` extra to be installed.
Install it with:
```
pip install royalnet[matrix]
```

View file

@ -1,17 +0,0 @@
"""A :class:`Serf` implementation for Matrix.
It requires (obviously) the ``matrix`` extra to be installed.
Install it with: ::
pip install royalnet[matrix]
"""
from .escape import escape
from .matrixserf import MatrixSerf
__all__ = [
"MatrixSerf",
"escape",
]

View file

@ -1,15 +0,0 @@
def escape(string: str) -> str:
"""Escape a string to be sent through Matrix, and format it using RoyalCode.
Underlines are currently unsupported.
Warning:
Currently escapes everything, even items in code blocks."""
return string.replace("[b]", "**") \
.replace("[/b]", "**") \
.replace("[i]", "_") \
.replace("[/i]", "_") \
.replace("[c]", "`") \
.replace("[/c]", "`") \
.replace("[p]", "```") \
.replace("[/p]", "```")

View file

@ -1,127 +0,0 @@
import asyncio as aio
import datetime
import logging
from typing import *
import nio
import royalnet.backpack as rb
import royalnet.commands as rc
import royalnet.utils as ru
from .escape import escape
from ..serf import Serf
log = logging.getLogger(__name__)
class MatrixSerf(Serf):
"""A serf that connects to `Matrix <https://matrix.org/>`_ as an user."""
interface_name = "matrix"
prefix = "!"
_identity_table = rb.tables.Matrix
_identity_column = "matrix_id"
def __init__(self,
loop: aio.AbstractEventLoop,
alchemy_cfg: rc.ConfigDict,
herald_cfg: rc.ConfigDict,
sentry_cfg: rc.ConfigDict,
packs_cfg: rc.ConfigDict,
serf_cfg: rc.ConfigDict,
**_):
if nio is None:
raise ImportError("'matrix' extra is not installed")
super().__init__(loop=loop,
alchemy_cfg=alchemy_cfg,
herald_cfg=herald_cfg,
sentry_cfg=sentry_cfg,
packs_cfg=packs_cfg,
serf_cfg=serf_cfg)
self.client: Optional[nio.AsyncClient] = None
self.homeserver: str = serf_cfg["homeserver"]
self.matrix_id: str = serf_cfg["matrix_id"]
self.password: str = serf_cfg["password"]
self._started_timestamp: Optional[int] = None
self.Data: Type[rc.CommandData] = self.data_factory()
def data_factory(self) -> Type[rc.CommandData]:
# noinspection PyMethodParameters,PyAbstractClass
class MatrixData(rc.CommandData):
def __init__(data,
command: rc.Command,
room: nio.MatrixRoom,
event: nio.Event):
super().__init__(command=command)
data.room: nio.MatrixRoom = room
data.event: nio.Event = event
async def reply(data, text: str):
await self.client.room_send(room_id=data.room.room_id, message_type="m.room.message", content={
"msgtype": "m.text",
"body": escape(text)
})
async def get_author(data, error_if_none=False):
user: str = data.event.sender
query = data.session.query(self.master_table)
for link in self.identity_chain:
query = query.join(link.mapper.class_)
query = query.filter(self.identity_column == user)
result = await ru.asyncify(query.one_or_none)
if result is None and error_if_none:
raise rc.CommandError("You must be registered to use this command.")
return result
# Delete invoking does not really make sense on Matrix
return MatrixData
async def handle_message(self, room: "nio.MatrixRoom", event: "nio.RoomMessageText"):
# Skip events happened before the startup of the Serf
if event.server_timestamp < self._started_timestamp:
return
# Find the text in the event
text = event.body
# Skip non-command events
if not text.startswith("!"):
return
# Find and clean parameters
command_text, *parameters = text.split(" ")
# Don't use a case-sensitive command name
command_name = command_text.lower()
# Find the command
try:
command = self.commands[command_name]
except KeyError:
# Skip the message
return
# Send typing
await self.client.room_typing(room_id=room.room_id, typing_state=True)
# Open an alchemy session, if available
if self.alchemy is not None:
session = await ru.asyncify(self.alchemy.Session)
else:
session = None
# Prepare data
# noinspection PyArgumentList
data = self.Data(command=command, room=room, event=event)
# Call the command
await self.call(command, data, parameters)
# Close the alchemy session
if session is not None:
await ru.asyncify(session.close)
async def run(self):
self.client = nio.AsyncClient(self.homeserver, self.matrix_id)
await self.client.login(self.password)
self._started_timestamp = int(datetime.datetime.now().timestamp() * 1000)
# matrix-nio type annotations are wrong for asyncclients
# noinspection PyTypeChecker
self.client.add_event_callback(self.handle_message, (nio.RoomMessageText,))
await self.client.sync_forever()