mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-27 05:24:20 +00:00
BREAKING: Drop matrix support (for now)
This commit is contained in:
parent
207c2d4dcc
commit
d38400521f
19 changed files with 38 additions and 256 deletions
|
@ -16,9 +16,8 @@ Commands using the Royalnet Serf API share their code between chat platforms: ea
|
|||
|
||||
- [Telegram](https://core.telegram.org/bots)
|
||||
- [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)
|
||||
|
||||
|
|
|
@ -48,6 +48,7 @@ intersphinx_mapping = {
|
|||
}
|
||||
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
def skip(app, what, name: str, obj, would_skip, options):
|
||||
if name == "__init__" or name == "__getitem__" or name == "__getattr__":
|
||||
return not bool(obj.__doc__)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Remember to run `poetry update` editing this file!
|
||||
|
||||
# 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]
|
||||
name = "royalnet"
|
||||
|
@ -32,9 +32,6 @@ python_telegram_bot = { version = "^12.2.0", 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
|
||||
|
||||
# matrix
|
||||
matrix-nio = { version = "^0.6", optional = true }
|
||||
|
||||
# alchemy
|
||||
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
|
||||
|
@ -66,7 +63,6 @@ sphinx_rtd_theme = "^0.4.3"
|
|||
[tool.poetry.extras]
|
||||
telegram = ["python_telegram_bot"]
|
||||
discord = ["discord.py", "pynacl", "lavalink", "aiohttp", "cchardet"]
|
||||
matrix = ["matrix-nio"]
|
||||
alchemy_easy = ["sqlalchemy", "psycopg2_binary", "bcrypt"]
|
||||
alchemy_hard = ["sqlalchemy", "psycopg2", "bcrypt"]
|
||||
constellation = ["starlette", "uvicorn", "python-multipart"]
|
||||
|
|
|
@ -19,11 +19,6 @@ try:
|
|||
except ImportError:
|
||||
rsd = None
|
||||
|
||||
try:
|
||||
import royalnet.serf.matrix as rsm
|
||||
except ImportError:
|
||||
rsm = None
|
||||
|
||||
try:
|
||||
import royalnet.constellation as rc
|
||||
except ImportError:
|
||||
|
@ -100,18 +95,18 @@ def run(config_file: str):
|
|||
else:
|
||||
log.debug("__serfs__: Configured")
|
||||
|
||||
def configure_serf(name: str, module, class_: Type[rs.Serf]):
|
||||
serf_cfg = serfs_cfg.get(name)
|
||||
def configure_serf(n: str, module, class_: Type[rs.Serf]):
|
||||
serf_cfg = serfs_cfg.get(n)
|
||||
if module is None:
|
||||
log.info(f"Serf.{name}: Not installed")
|
||||
log.info(f"Serf.{n}: Not installed")
|
||||
elif serf_cfg is None:
|
||||
log.warning(f"Serf.{name}: Not configured")
|
||||
log.warning(f"Serf.{n}: Not configured")
|
||||
elif not serf_cfg["enabled"]:
|
||||
log.info(f"Serf.{name}: Disabled")
|
||||
log.info(f"Serf.{n}: Disabled")
|
||||
else:
|
||||
def serf_constructor() -> multiprocessing.Process:
|
||||
return multiprocessing.Process(
|
||||
name=f"Serf.{name}",
|
||||
name=f"Serf.{n}",
|
||||
target=class_.run_process,
|
||||
daemon=True,
|
||||
kwargs={
|
||||
|
@ -124,12 +119,11 @@ def run(config_file: str):
|
|||
}
|
||||
)
|
||||
|
||||
processes[f"Serf.{name}"] = ru.RoyalnetProcess(serf_constructor, None)
|
||||
log.info(f"Serf.{name}: Enabled")
|
||||
processes[f"Serf.{n}"] = ru.RoyalnetProcess(serf_constructor, None)
|
||||
log.info(f"Serf.{n}: Enabled")
|
||||
|
||||
configure_serf("Telegram", rst, rst.TelegramSerf)
|
||||
configure_serf("Discord", rsd, rsd.DiscordSerf)
|
||||
configure_serf("Matrix", rsm, rsm.MatrixSerf)
|
||||
|
||||
# Constellation
|
||||
constellation_cfg = config.get("Constellation")
|
||||
|
|
|
@ -5,7 +5,6 @@ from typing import *
|
|||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.exc import ProgrammingError
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.ext.declarative.api import DeclarativeMeta
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.orm.session import Session
|
||||
from sqlalchemy.schema import Table
|
||||
|
@ -52,7 +51,7 @@ class Alchemy:
|
|||
except ProgrammingError:
|
||||
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.
|
||||
|
||||
Args:
|
||||
|
|
|
@ -11,7 +11,8 @@ class RoyalnetaliasesCommand(rc.Command):
|
|||
|
||||
async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> 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:
|
||||
user = await data.get_author(error_if_none=True)
|
||||
|
||||
|
|
|
@ -11,7 +11,8 @@ class RoyalnetrolesCommand(rc.Command):
|
|||
|
||||
async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> 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:
|
||||
user = await data.get_author(error_if_none=True)
|
||||
|
||||
|
|
|
@ -37,11 +37,7 @@ class ApiUserPasswd(rca.ApiStar):
|
|||
tokens: List[Token] = await ru.asyncify(
|
||||
data.session
|
||||
.query(self.alchemy.get(Token))
|
||||
.filter(
|
||||
and_(
|
||||
TokenT.user == user,
|
||||
TokenT.expiration >= datetime.datetime.now()
|
||||
))
|
||||
.filter(and_(TokenT.user == user, TokenT.expiration >= datetime.datetime.now()))
|
||||
.all
|
||||
)
|
||||
for t in tokens:
|
||||
|
|
|
@ -45,7 +45,8 @@ class DocsStar(PageStar):
|
|||
<html lang="en">
|
||||
<head>
|
||||
<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-standalone-preset.js"></script>
|
||||
</head>
|
||||
|
|
|
@ -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]"
|
|
@ -1,9 +1,9 @@
|
|||
import datetime
|
||||
import secrets
|
||||
|
||||
from sqlalchemy import *
|
||||
from sqlalchemy.ext.declarative import declared_attr
|
||||
from sqlalchemy.orm import *
|
||||
import sqlalchemy as s
|
||||
import sqlalchemy.ext.declarative as sed
|
||||
import sqlalchemy.orm as so
|
||||
|
||||
import royalnet.utils as ru
|
||||
|
||||
|
@ -12,21 +12,21 @@ import royalnet.utils as ru
|
|||
class Token:
|
||||
__tablename__ = "tokens"
|
||||
|
||||
@declared_attr
|
||||
@sed.declared_attr
|
||||
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):
|
||||
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):
|
||||
return relationship("User", backref="tokens")
|
||||
return so.relationship("User", backref="tokens")
|
||||
|
||||
@declared_attr
|
||||
@sed.declared_attr
|
||||
def expiration(self):
|
||||
return Column(DateTime, nullable=False)
|
||||
return s.Column(s.DateTime, nullable=False)
|
||||
|
||||
@property
|
||||
def expired(self):
|
||||
|
|
|
@ -74,6 +74,7 @@ class User:
|
|||
|
||||
@property
|
||||
def roles(self) -> list:
|
||||
# noinspection PyUnresolvedReferences
|
||||
return list(map(lambda a: a.role, self._roles))
|
||||
|
||||
def add_role(self, alchemy, role: str) -> None:
|
||||
|
@ -90,6 +91,7 @@ class User:
|
|||
|
||||
@property
|
||||
def aliases(self) -> list:
|
||||
# noinspection PyUnresolvedReferences
|
||||
return list(map(lambda a: a.alias, self._aliases))
|
||||
|
||||
def add_alias(self, alchemy, alias: str) -> None:
|
||||
|
|
|
@ -69,6 +69,7 @@ class Constellation:
|
|||
tables = set()
|
||||
for pack in packs.values():
|
||||
try:
|
||||
# noinspection PyUnresolvedReferences
|
||||
tables = tables.union(pack["tables"].available_tables)
|
||||
except AttributeError:
|
||||
log.warning(f"Pack `{pack}` does not have the `available_tables` attribute.")
|
||||
|
@ -108,6 +109,7 @@ class Constellation:
|
|||
pack = packs[pack_name]
|
||||
pack_cfg = packs_cfg.get(pack_name, {})
|
||||
try:
|
||||
# noinspection PyUnresolvedReferences
|
||||
events = pack["events"].available_events
|
||||
except AttributeError:
|
||||
log.warning(f"Pack `{pack}` does not have the `available_events` attribute.")
|
||||
|
@ -127,6 +129,7 @@ class Constellation:
|
|||
pack = packs[pack_name]
|
||||
pack_cfg = packs_cfg.get(pack_name, {})
|
||||
try:
|
||||
# noinspection PyUnresolvedReferences
|
||||
page_stars = pack["stars"].available_page_stars
|
||||
except AttributeError:
|
||||
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()
|
||||
|
||||
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:
|
||||
log.debug(f"Registering: {SelectedPageStar.path} -> {SelectedPageStar.__qualname__}")
|
||||
try:
|
||||
|
|
|
@ -36,6 +36,7 @@ def run(config_filename, file_format):
|
|||
lines = []
|
||||
|
||||
try:
|
||||
# noinspection PyUnresolvedReferences
|
||||
commands = pack["commands"].available_commands
|
||||
except AttributeError:
|
||||
p(f"Pack `{pack}` does not have the `available_commands` attribute.", err=True)
|
||||
|
|
|
@ -60,6 +60,7 @@ class Server:
|
|||
matching = [client for client in self.identified_clients if client.link_type == link_type]
|
||||
return matching or []
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
async def listener(self, websocket: "websockets.server.WebSocketServerProtocol", path):
|
||||
connected_client = ConnectedClient(websocket)
|
||||
# Wait for identification
|
||||
|
|
|
@ -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]
|
||||
```
|
|
@ -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",
|
||||
]
|
|
@ -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]", "```")
|
|
@ -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()
|
Loading…
Reference in a new issue