diff --git a/README.md b/README.md index 35d67866..9487cba4 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/docs_source/conf.py b/docs_source/conf.py index 60596c2e..da982811 100644 --- a/docs_source/conf.py +++ b/docs_source/conf.py @@ -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__) diff --git a/pyproject.toml b/pyproject.toml index 72e41907..96d0227f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/royalnet/__main__.py b/royalnet/__main__.py index b252d89a..47b9b383 100644 --- a/royalnet/__main__.py +++ b/royalnet/__main__.py @@ -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") diff --git a/royalnet/alchemy/alchemy.py b/royalnet/alchemy/alchemy.py index 30cd708f..35908ebc 100644 --- a/royalnet/alchemy/alchemy.py +++ b/royalnet/alchemy/alchemy.py @@ -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: diff --git a/royalnet/backpack/commands/royalnetaliases.py b/royalnet/backpack/commands/royalnetaliases.py index 510934ce..1ee5bb04 100644 --- a/royalnet/backpack/commands/royalnetaliases.py +++ b/royalnet/backpack/commands/royalnetaliases.py @@ -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) diff --git a/royalnet/backpack/commands/royalnetroles.py b/royalnet/backpack/commands/royalnetroles.py index b44d60b5..21490497 100644 --- a/royalnet/backpack/commands/royalnetroles.py +++ b/royalnet/backpack/commands/royalnetroles.py @@ -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) diff --git a/royalnet/backpack/stars/api_user_passwd.py b/royalnet/backpack/stars/api_user_passwd.py index 88a63c56..2ee40032 100644 --- a/royalnet/backpack/stars/api_user_passwd.py +++ b/royalnet/backpack/stars/api_user_passwd.py @@ -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: diff --git a/royalnet/backpack/stars/docs.py b/royalnet/backpack/stars/docs.py index 1f0e7816..f7a2892b 100644 --- a/royalnet/backpack/stars/docs.py +++ b/royalnet/backpack/stars/docs.py @@ -45,7 +45,8 @@ class DocsStar(PageStar): Royalnet Docs - + diff --git a/royalnet/backpack/tables/matrix.py b/royalnet/backpack/tables/matrix.py deleted file mode 100644 index f7223ce2..00000000 --- a/royalnet/backpack/tables/matrix.py +++ /dev/null @@ -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"" - - def __str__(self): - return f"[c]matrix:{self.matrix_id}[/c]" diff --git a/royalnet/backpack/tables/tokens.py b/royalnet/backpack/tables/tokens.py index fa74a879..4af5ffb6 100644 --- a/royalnet/backpack/tables/tokens.py +++ b/royalnet/backpack/tables/tokens.py @@ -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): diff --git a/royalnet/backpack/tables/users.py b/royalnet/backpack/tables/users.py index fb9b736e..a2b02393 100644 --- a/royalnet/backpack/tables/users.py +++ b/royalnet/backpack/tables/users.py @@ -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: diff --git a/royalnet/constellation/constellation.py b/royalnet/constellation/constellation.py index 30741000..9b87e4e6 100644 --- a/royalnet/constellation/constellation.py +++ b/royalnet/constellation/constellation.py @@ -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: diff --git a/royalnet/generate.py b/royalnet/generate.py index 7ade61ac..e2534302 100644 --- a/royalnet/generate.py +++ b/royalnet/generate.py @@ -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) diff --git a/royalnet/herald/server.py b/royalnet/herald/server.py index aeee9067..ed530427 100644 --- a/royalnet/herald/server.py +++ b/royalnet/herald/server.py @@ -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 diff --git a/royalnet/serf/matrix/README.md b/royalnet/serf/matrix/README.md deleted file mode 100644 index 1543c9e0..00000000 --- a/royalnet/serf/matrix/README.md +++ /dev/null @@ -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] -``` diff --git a/royalnet/serf/matrix/__init__.py b/royalnet/serf/matrix/__init__.py deleted file mode 100644 index 023f7862..00000000 --- a/royalnet/serf/matrix/__init__.py +++ /dev/null @@ -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", -] diff --git a/royalnet/serf/matrix/escape.py b/royalnet/serf/matrix/escape.py deleted file mode 100644 index 738b3e7c..00000000 --- a/royalnet/serf/matrix/escape.py +++ /dev/null @@ -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]", "```") diff --git a/royalnet/serf/matrix/matrixserf.py b/royalnet/serf/matrix/matrixserf.py deleted file mode 100644 index e5494184..00000000 --- a/royalnet/serf/matrix/matrixserf.py +++ /dev/null @@ -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 `_ 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()