diff options
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node')
6 files changed, 70 insertions, 1 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/chat/store.py b/packages/meshbay-node/src/meshbay_node/chat/store.py index 17cd68e..9e07aff 100644 --- a/packages/meshbay-node/src/meshbay_node/chat/store.py +++ b/packages/meshbay-node/src/meshbay_node/chat/store.py @@ -289,6 +289,12 @@ class ChatStore: await self._db.commit() return cursor.rowcount + async def delete_all(self) -> int: + """The operator's purge. Returns how many rows went.""" + cursor = await self._db.execute("DELETE FROM messages") + await self._db.commit() + return cursor.rowcount + async def message_count(self) -> int: cursor = await self._db.execute("SELECT COUNT(*) FROM messages") row = await cursor.fetchone() diff --git a/packages/meshbay-node/src/meshbay_node/ops/__init__.py b/packages/meshbay-node/src/meshbay_node/ops/__init__.py index bfdfd7b..fc5b7ef 100644 --- a/packages/meshbay-node/src/meshbay_node/ops/__init__.py +++ b/packages/meshbay-node/src/meshbay_node/ops/__init__.py @@ -41,6 +41,7 @@ from meshbay_node.ops.chat import ( ensure_chat_epoch, open_chat_epoch, prune_chat, + purge_chat, ) from meshbay_node.ops.core import ( OpError, @@ -140,6 +141,7 @@ __all__ = [ "pair_operator", "plug_root", "prune_chat", + "purge_chat", "prune_index_cache", "read_denylist", "read_roster", diff --git a/packages/meshbay-node/src/meshbay_node/ops/chat.py b/packages/meshbay-node/src/meshbay_node/ops/chat.py index 539284b..9fc7b94 100644 --- a/packages/meshbay-node/src/meshbay_node/ops/chat.py +++ b/packages/meshbay-node/src/meshbay_node/ops/chat.py @@ -252,3 +252,22 @@ async def prune_chat(state: dict, group_id: str, max_age_days: int) -> dict: group_id[:8], removed) return {"group_id": group_id, "removed": removed, "max_age_days": max_age_days} + + +async def purge_chat(state: dict, group_id: str) -> dict: + """ + Delete every stored message of the group. Epoch keys are kept, as in + `prune_chat`, and attachments too: they are files in a shared folder. + + The replay index goes with the rows, which is harmless: the node takes a + sealed message only from the device that signed it, so the one party able + to send a purged message again is its author. + """ + ctx = _group_ctx(state, group_id) + store = ctx.get("chat_store") + if store is None: + raise OpError("This group has no chat store", status=404) + removed = await store.delete_all() + log.info("Chat purged for group %s: %d message(s) removed", + group_id[:8], removed) + return {"group_id": group_id, "removed": removed} diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc/admin.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc/admin.py index 48734ab..7b3b1c9 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc/admin.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/admin.py @@ -15,6 +15,7 @@ from meshbay_common.adminop import ( OP_CHAT_DIRECTORY, OP_CHAT_EPOCH, OP_CHAT_LINK_PREVIEW, + OP_CHAT_PURGE, OP_DIR_DELETE, OP_FILE_DELETE, OP_INVITE_CANCEL, @@ -68,6 +69,7 @@ _ADMIN_EXECUTORS = { OP_CHAT_LINK_PREVIEW: "_admin_exec_chat_link_preview", OP_SEARCH_LISTED: "_admin_exec_search_listed", OP_CHAT_EPOCH: "_admin_exec_chat_epoch", + OP_CHAT_PURGE: "_admin_exec_chat_purge", OP_ROOT_EJECT: "_admin_exec_root_eject", OP_ROOT_PLUG: "_admin_exec_root_plug", } diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc/chat.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc/chat.py index dc43ae2..f200884 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc/chat.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/chat.py @@ -7,7 +7,12 @@ import time import blake3 from meshbay_common import MNP_VERSION -from meshbay_common.adminop import OP_CHAT_DIRECTORY, OP_CHAT_EPOCH, OP_CHAT_LINK_PREVIEW +from meshbay_common.adminop import ( + OP_CHAT_DIRECTORY, + OP_CHAT_EPOCH, + OP_CHAT_LINK_PREVIEW, + OP_CHAT_PURGE, +) from meshbay_common.chatbox import NONCE_LEN as CHAT_NONCE_LEN from meshbay_common.chatbox import SIG_LEN as CHAT_SIG_LEN from meshbay_common.groupbox import PURPOSE_CHAT_KEYS, seal @@ -227,6 +232,40 @@ class ChatMixin: self._broadcast_to_group({"type": MNP.CHAT_EPOCH_ACK, "v": MNP_VERSION, "epoch": result["epoch"]}) + def _do_chat_purge(self, msg: dict) -> None: + """ + Delete the group's stored chat. Operator only, and signed. The subject is + the group id, so a signature for one group's purge purges no other. + + Always this connection's group, never one named in the message: the ack + is broadcast to this group, and an operator purges the chat they see. + """ + group_id = self._group_id + if not group_id: + self._send({"type": "error", "detail": "No group on this connection"}) + return + if not self._has_admin_authority(): + self._send({"type": "error", "detail": "No authorized key for this"}) + return + self._issue_admin_challenge(OP_CHAT_PURGE, group_id, group_id=group_id) + + async def _admin_exec_chat_purge( + self, pending: dict, transcript: bytes, sig: bytes, + ) -> None: + if not await self._verify_admin_sig(transcript, sig): + self._send({"type": "error", "detail": "Signature verification failed"}) + self._audit("admin_auth_failed", f"chat_purge:{pending['subject'][:8]}") + return + try: + result = await self._run_op(ops.purge_chat, pending["subject"]) + except ops.OpError as e: + self._send({"type": "error", "detail": e.message}) + return + self._audit("chat_purge", str(result["removed"])) + # Every open chat panel empties, not only the operator's. + self._broadcast_to_group({"type": MNP.CHAT_PURGE_ACK, + "v": MNP_VERSION, "removed": result["removed"]}) + async def _do_chat_keys_req(self, msg: dict) -> None: """ Hand this member every chat epoch key the group has, sealed. diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc/dispatch.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc/dispatch.py index a8b5767..5d4ed94 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc/dispatch.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/dispatch.py @@ -56,6 +56,7 @@ _HANDLERS = { MNP.CHAT_LINK_PREVIEW: ("_do_chat_link_preview", INLINE), MNP.SEARCH_LISTED: ("_do_search_listed", INLINE), MNP.CHAT_EPOCH: ("_do_chat_epoch", INLINE), + MNP.CHAT_PURGE: ("_do_chat_purge", INLINE), MNP.CHAT_KEYS_REQ: ("_do_chat_keys_req", SPAWNED), MNP.GROUP_ROSTER_REQ: ("_do_group_roster_req", SPAWNED), MNP.MEDIA_META_REQ: ("_do_media_meta_request", SPAWNED), |