diff options
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/ops')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/ops/members.py | 50 |
1 files changed, 50 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/ops/members.py b/packages/meshbay-node/src/meshbay_node/ops/members.py index b5da9c0..2562dd6 100644 --- a/packages/meshbay-node/src/meshbay_node/ops/members.py +++ b/packages/meshbay-node/src/meshbay_node/ops/members.py @@ -262,6 +262,52 @@ async def cancel_link_invitation(state: dict, group_id: str, invite_id: str) -> "node": node_cancelled, "hub": hub_cancelled} +async def _after_removal(state: dict, user_id: str, group_ids: list[str]) -> None: + """ + What a removal has to do beyond the roster, whichever door it came through. + + - **A new chat epoch in every group the person could read.** They keep the + key of the epoch they were in; the next one is what they must not get. + - **Every live connection of theirs closed.** The handshake now refuses them + (it consults the roster), so this is what makes the removal immediate + rather than "at their next reconnection". + + It used to live in the MNP handlers only, so a removal from the CLI or the + node page — the doors an operator at their own machine uses — left the + epoch where it was and the person connected. Best effort: a failure here + must not turn a done removal into a refused one, and is logged loudly. + """ + from meshbay_common import MNP_VERSION + from meshbay_common.protocol import MNP + + from meshbay_node.ops.chat import open_chat_epoch + + groups_ctx = state.get("groups_ctx") or {} + for gid in sorted({g for g in group_ids if g}): + try: + result = await open_chat_epoch(state, gid) + except Exception as e: + log.error("Removal of %s: no new chat epoch for group %s (%s) — " + "they still hold the current chat key", user_id[:8], gid[:8], e) + continue + notice = {"type": MNP.CHAT_EPOCH_ACK, "v": MNP_VERSION, "epoch": result["epoch"]} + for session in list((groups_ctx.get(gid) or {}).get("_peers", {}).values()): + try: + session._send(notice) + except Exception: + pass + + webrtc = state.get("webrtc") + if webrtc is not None: + for session in list(getattr(webrtc, "_sessions", {}).values()): + if session._user_id == user_id and ( + not group_ids or session._group_id in group_ids): + try: + await session.close() + except Exception: + pass + + async def revoke_member(state: dict, user_id: str, group_id: str) -> dict: """ Stop serving the group key to someone. @@ -285,6 +331,7 @@ async def revoke_member(state: dict, user_id: str, group_id: str) -> dict: raise OpError("No such member in that group", status=404) log.info("Member revoked: user=%s group=%s member=%s invites_dropped=%d", user_id[:8], group_id[:8], revoked, dropped) + await _after_removal(state, user_id, [group_id] if revoked else []) return {"status": "revoked", "user_id": user_id, "group_id": group_id, "was_member": revoked, "invites_dropped": dropped, # Only what is true: somebody who never redeemed a code never held @@ -297,6 +344,8 @@ async def revoke_member(state: dict, user_id: str, group_id: str) -> dict: async def unpin_member(state: dict, user_id: str) -> dict: """Forget a pinned identity, so the person can pair again with a new key.""" roster = _roster(state) + groups = [m["group_id"] for m in await roster.list_members() + if m.get("user_id") == user_id and m.get("group_id")] if not await roster.unpin(user_id): raise OpError("No such pinned identity", status=404) # Drop the stored keypair bundle too. Left behind, it is served to the next @@ -310,4 +359,5 @@ async def unpin_member(state: dict, user_id: str) -> dict: except Exception: log.warning("unpin: could not drop keypair bundle for %s", user_id[:8]) log.info("Identity unpinned: user=%s", user_id[:8]) + await _after_removal(state, user_id, groups) return {"status": "unpinned", "user_id": user_id} |