diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8c8a44e..50d0c15 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,7 +10,7 @@ default: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY build frontend: - image: node:24-alpine + image: node:22-alpine stage: build frontend before_script: [] variables: diff --git a/CHANGELOG.md b/CHANGELOG.md index d9de2b7..3a04252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,3 @@ -# v0.5.2 (2025-05-05) - -* Improved tombstone handling to ensure that the tombstone sender has - permissions to invite users to the target room. -* Fixed autojoin and online flags not being applied if set during client - creation (thanks to [@bnsh] in [#258]). -* Fixed plugin web apps not being cleared properly when unloading plugins. - -[@bnsh]: https://github.com/bnsh -[#258]: https://github.com/maubot/maubot/pull/258 - # v0.5.1 (2025-01-03) * Updated Docker image to Alpine 3.21. diff --git a/Dockerfile b/Dockerfile index 204a78a..2c6bad4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ -FROM node:24 AS frontend-builder +FROM node:22 AS frontend-builder COPY ./maubot/management/frontend /frontend RUN cd /frontend && yarn --prod && yarn build -FROM alpine:3.22 +FROM alpine:3.21 RUN apk add --no-cache \ python3 py3-pip py3-setuptools py3-wheel \ @@ -11,8 +11,6 @@ RUN apk add --no-cache \ su-exec \ yq \ py3-aiohttp \ - py3-aiodns \ - py3-brotli \ py3-attrs \ py3-bcrypt \ py3-cffi \ diff --git a/Dockerfile.ci b/Dockerfile.ci index fb76f50..9712a16 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -1,4 +1,4 @@ -FROM alpine:3.22 +FROM alpine:3.21 RUN apk add --no-cache \ python3 py3-pip py3-setuptools py3-wheel \ diff --git a/maubot/__meta__.py b/maubot/__meta__.py index 7225152..dd9b22c 100644 --- a/maubot/__meta__.py +++ b/maubot/__meta__.py @@ -1 +1 @@ -__version__ = "0.5.2" +__version__ = "0.5.1" diff --git a/maubot/client.py b/maubot/client.py index 972f0de..bdb76fc 100644 --- a/maubot/client.py +++ b/maubot/client.py @@ -350,41 +350,11 @@ class Client(DBClient): } async def _handle_tombstone(self, evt: StateEvent) -> None: - if evt.state_key != "": - return if not evt.content.replacement_room: self.log.info(f"{evt.room_id} tombstoned with no replacement, ignoring") return - is_joined = await self.client.state_store.is_joined( - evt.content.replacement_room, - self.client.mxid, - ) - if is_joined: - self.log.debug( - f"Ignoring tombstone from {evt.room_id} to {evt.content.replacement_room} " - f"sent by {evt.sender}: already joined to replacement room" - ) - return - self.log.debug( - f"Following tombstone from {evt.room_id} to {evt.content.replacement_room} " - f"sent by {evt.sender}" - ) _, server = self.client.parse_user_id(evt.sender) - room_id = await self.client.join_room(evt.content.replacement_room, servers=[server]) - power_levels = await self.client.get_state_event(room_id, EventType.ROOM_POWER_LEVELS) - create_event = await self.client.get_state_event( - room_id, EventType.ROOM_CREATE, format="event" - ) - if power_levels.get_user_level(evt.sender, create_event) < power_levels.invite: - self.log.warning( - f"{evt.room_id} was tombstoned into {room_id} by {evt.sender}," - " but the sender doesn't have invite power levels, leaving..." - ) - await self.client.leave_room( - room_id, - f"Followed tombstone from {evt.room_id} by {evt.sender}," - " but sender doesn't have sufficient power level for invites", - ) + await self.client.join_room(evt.content.replacement_room, servers=[server]) async def _handle_invite(self, evt: StrippedStateEvent) -> None: if evt.state_key == self.id and evt.content.membership == Membership.INVITE: @@ -504,14 +474,8 @@ class Client(DBClient): self.start_sync() async def _update_remote_profile(self) -> None: - try: - profile = await self.client.get_profile(self.id) - self.remote_displayname, self.remote_avatar_url = ( - profile.displayname, - profile.avatar_url, - ) - except Exception: - self.log.warning("Failed to update own profile from server", exc_info=True) + profile = await self.client.get_profile(self.id) + self.remote_displayname, self.remote_avatar_url = profile.displayname, profile.avatar_url async def delete(self) -> None: try: diff --git a/maubot/example-config.yaml b/maubot/example-config.yaml index 7991461..f21ac1e 100644 --- a/maubot/example-config.yaml +++ b/maubot/example-config.yaml @@ -54,8 +54,7 @@ server: port: 29316 # Public base URL where the server is visible. public_url: https://example.com - # The base path for the UI. Note that this does not change the API path. - # Add a path prefix to public_url if you want everything on a subpath. + # The base path for the UI. ui_base_path: /_matrix/maubot # The base path for plugin endpoints. The instance ID will be appended directly. plugin_base_path: /_matrix/maubot/plugin/ @@ -79,9 +78,8 @@ homeservers: # When this is empty, `mbc auth --register` won't work, but `mbc auth` (login) will. secret: null -# List of administrator users. Each key is a username and the value is the password. -# Plaintext passwords will be bcrypted on startup. Set empty password to prevent normal login. -# Root is a special user that can't have a password and will always exist. +# List of administrator users. Plaintext passwords will be bcrypted on startup. Set empty password +# to prevent normal login. Root is a special user that can't have a password and will always exist. admins: root: "" diff --git a/maubot/management/api/client.py b/maubot/management/api/client.py index d2ad35d..2a8964c 100644 --- a/maubot/management/api/client.py +++ b/maubot/management/api/client.py @@ -78,8 +78,8 @@ async def _create_client(user_id: UserID | None, data: dict) -> web.Response: ) client.enabled = data.get("enabled", True) client.sync = data.get("sync", True) - await client.update_autojoin(data.get("autojoin", True), save=False) - await client.update_online(data.get("online", True), save=False) + client.autojoin = data.get("autojoin", True) + client.online = data.get("online", True) client.displayname = data.get("displayname", "disable") client.avatar_url = data.get("avatar_url", "disable") await client.update() diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js index d1a51b8..b173e4c 100644 --- a/maubot/management/frontend/src/api.js +++ b/maubot/management/frontend/src/api.js @@ -218,7 +218,6 @@ export function getAvatarURL({ id, avatar_url }) { return null } avatar_url = avatar_url.substring("mxc://".length) - // Note: the maubot backend will replace the query param with an authorization header return `${BASE_PATH}/proxy/${id}/_matrix/client/v1/media/download/${avatar_url}?access_token=${ localStorage.accessToken}` } diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js index e9d7541..c60129c 100644 --- a/maubot/management/frontend/src/pages/dashboard/Client.js +++ b/maubot/management/frontend/src/pages/dashboard/Client.js @@ -70,9 +70,9 @@ class Client extends BaseMainView { get initialState() { return { id: "", - displayname: "disable", + displayname: "", homeserver: "", - avatar_url: "disable", + avatar_url: "", access_token: "", device_id: "", fingerprint: null, diff --git a/maubot/matrix.py b/maubot/matrix.py index 8a811af..87c7b70 100644 --- a/maubot/matrix.py +++ b/maubot/matrix.py @@ -15,7 +15,7 @@ # along with this program. If not, see . from __future__ import annotations -from typing import Any, Awaitable +from typing import Awaitable from html import escape import asyncio @@ -63,7 +63,7 @@ async def parse_formatted( else: return message, escape(message) text = (await MaubotHTMLParser().parse(html)).text - if len(text) > 100 and len(text) + len(html) > 40000: + if len(text) + len(html) > 60000: text = text[:100] + "[long message cut off]" return text, html @@ -88,7 +88,6 @@ class MaubotMessageEvent(MessageEvent): reply: bool | str = False, in_thread: bool | None = None, edits: EventID | MessageEvent | None = None, - extra_content: dict[str, Any] | None = None, ) -> EventID: """ Respond to the message. @@ -108,7 +107,6 @@ class MaubotMessageEvent(MessageEvent): the root if necessary. edits: An event ID or MessageEvent to edit. If set, the reply and in_thread parameters are ignored, as edits can't change the reply or thread status. - extra_content: Extra content to add to the event. Returns: The ID of the response event. @@ -145,9 +143,6 @@ class MaubotMessageEvent(MessageEvent): ) else: content.set_reply(self) - if extra_content: - for k, v in extra_content.items(): - content[k] = v return await self.client.send_message_event(self.room_id, event_type, content) def reply( @@ -157,7 +152,6 @@ class MaubotMessageEvent(MessageEvent): markdown: bool = True, allow_html: bool = False, in_thread: bool | None = None, - extra_content: dict[str, Any] | None = None, ) -> Awaitable[EventID]: """ Reply to the message. The parameters are the same as :meth:`respond`, @@ -175,7 +169,6 @@ class MaubotMessageEvent(MessageEvent): thread. If set to ``False``, the response will never be in a thread. If set to ``True``, the response will always be in a thread, creating one with this event as the root if necessary. - extra_content: Extra content to add to the event. Returns: The ID of the response event. @@ -187,7 +180,6 @@ class MaubotMessageEvent(MessageEvent): reply=True, in_thread=in_thread, allow_html=allow_html, - extra_content=extra_content, ) def mark_read(self) -> Awaitable[None]: @@ -264,18 +256,14 @@ class MaubotMatrixClient(MatrixClient): markdown: str, *, allow_html: bool = False, - render_markdown: bool = True, msgtype: MessageType = MessageType.TEXT, edits: EventID | MessageEvent | None = None, relates_to: RelatesTo | None = None, - extra_content: dict[str, Any] = None, **kwargs, ) -> EventID: content = TextMessageEventContent(msgtype=msgtype, format=Format.HTML) content.body, content.formatted_body = await parse_formatted( - markdown, - allow_html=allow_html, - render_markdown=render_markdown, + markdown, allow_html=allow_html ) if relates_to: if edits: @@ -283,9 +271,6 @@ class MaubotMatrixClient(MatrixClient): content.relates_to = relates_to elif edits: content.set_edit(edits) - if extra_content: - for k, v in extra_content.items(): - content[k] = v return await self.send_message(room_id, content, **kwargs) def dispatch_event(self, event: Event, source: SyncStream) -> list[asyncio.Task]: diff --git a/maubot/plugin_server.py b/maubot/plugin_server.py index e5c246c..9dd2df4 100644 --- a/maubot/plugin_server.py +++ b/maubot/plugin_server.py @@ -40,8 +40,6 @@ class PluginWebApp(web.UrlDispatcher): self._resources = [] self._named_resources = {} self._middleware = [] - self._resource_index = {} - self._matched_sub_app_resources = [] async def handle(self, request: web.Request) -> web.StreamResponse: match_info = await self.resolve(request) diff --git a/requirements.txt b/requirements.txt index 169d053..e1df001 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -mautrix>=0.20.9rc3,<0.21 +mautrix>=0.20.7,<0.21 aiohttp>=3,<4 yarl>=1,<2 asyncpg>=0.20,<1