aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/ops/members.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/ops/members.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/ops/members.py50
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}