-
Notifications
You must be signed in to change notification settings - Fork 1
feat: Repeater admin view with login, CLI, and password storage #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,17 +11,23 @@ | |
| SendResultDict, | ||
| ) | ||
|
|
||
| TXT_TYPE_CLI_DATA = 1 | ||
|
|
||
| async def send_text(*, node: MeshNodeProtocol, peer_name: str, message: str) -> dict: | ||
| """Send a direct text message to a peer via PacketBuilder.""" | ||
| from pymc_core.protocol.packet_builder import PacketBuilder | ||
|
|
||
| def _resolve_contact(node: MeshNodeProtocol, peer_name: str) -> object: | ||
| contacts = node.contacts | ||
| contact = None | ||
| if contacts is not None: | ||
| contact = contacts.get_by_name(peer_name) | ||
| if contact is None: | ||
| raise RuntimeError(f"Contact '{peer_name}' not found") | ||
| if contact is not None: | ||
| return contact | ||
| raise RuntimeError(f"Contact '{peer_name}' not found") | ||
|
|
||
|
|
||
| async def send_text(*, node: MeshNodeProtocol, peer_name: str, message: str) -> dict: | ||
| """Send a direct text message to a peer via PacketBuilder.""" | ||
| from pymc_core.protocol.packet_builder import PacketBuilder | ||
|
|
||
| contact = _resolve_contact(node, peer_name) | ||
|
|
||
| pkt, ack_crc = PacketBuilder.create_text_message( | ||
| contact=contact, | ||
|
|
@@ -61,12 +67,7 @@ async def request_telemetry( | |
| """Request telemetry data from a remote peer via PacketBuilder.""" | ||
| from pymc_core.protocol.packet_builder import PacketBuilder | ||
|
|
||
| contacts = node.contacts | ||
| contact = None | ||
| if contacts is not None: | ||
| contact = contacts.get_by_name(contact_name) | ||
| if contact is None: | ||
| raise RuntimeError(f"Contact '{contact_name}' not found") | ||
| contact = _resolve_contact(node, contact_name) | ||
|
|
||
| pkt, _ts = PacketBuilder.create_telem_request( | ||
| contact=contact, | ||
|
|
@@ -125,6 +126,124 @@ def _on_response(success: bool, text: str, parsed: dict) -> None: | |
| } | ||
|
|
||
|
|
||
| async def send_login( | ||
| *, | ||
| node: MeshNodeProtocol, | ||
| peer_name: str, | ||
| password: str, | ||
| timeout: float = 10.0, | ||
| ) -> dict: | ||
| """Send a login request to a repeater and wait for the response.""" | ||
| from pymc_core.protocol.packet_builder import PacketBuilder | ||
|
|
||
| contact = _resolve_contact(node, peer_name) | ||
|
|
||
| login_handler = node.dispatcher.login_response_handler | ||
| dest_hash = bytes.fromhex(contact.public_key)[0] | ||
| login_handler.store_login_password(dest_hash, password) | ||
|
|
||
| login_event = asyncio.Event() | ||
| login_result: dict = {"success": False, "data": {}} | ||
|
|
||
| def _on_login(success: bool, data: dict) -> None: | ||
| login_result["success"] = success | ||
| login_result["data"] = data | ||
| login_event.set() | ||
|
|
||
| login_handler.set_login_callback(_on_login) | ||
| try: | ||
| pkt = PacketBuilder.create_login_packet( | ||
| contact=contact, | ||
| local_identity=node.identity, | ||
| password=password, | ||
| ) | ||
| await node.dispatcher.send_packet(pkt, wait_for_ack=False) | ||
| await asyncio.wait_for(login_event.wait(), timeout=timeout) | ||
| except asyncio.TimeoutError: | ||
| return {"success": False, "reason": "Login response timeout"} | ||
| finally: | ||
| login_handler.set_login_callback(None) | ||
| login_handler.clear_login_password(dest_hash) | ||
|
|
||
| data = login_result["data"] | ||
| return { | ||
| "success": login_result["success"], | ||
| "repeater": peer_name, | ||
| "is_admin": data.get("is_admin", False), | ||
| "keep_alive_interval": data.get("keep_alive_interval", 0), | ||
| "acl_permissions": data.get("reserved", data.get("permissions", 0)), | ||
| "firmware_ver_level": data.get("firmware_ver_level"), | ||
| "reason": "Login successful" if login_result["success"] else "Login failed", | ||
| } | ||
|
|
||
|
|
||
| async def send_logout(*, node: MeshNodeProtocol, peer_name: str) -> dict: | ||
| """Send a logout/disconnect to a repeater.""" | ||
| from pymc_core.protocol.packet_builder import PacketBuilder | ||
|
|
||
| contact = _resolve_contact(node, peer_name) | ||
|
|
||
| pkt, _crc = PacketBuilder.create_logout_packet( | ||
| contact=contact, | ||
| local_identity=node.identity, | ||
| ) | ||
| await node.dispatcher.send_packet(pkt, wait_for_ack=False) | ||
| return {"success": True, "repeater": peer_name} | ||
|
|
||
|
|
||
| async def send_repeater_command( | ||
| *, | ||
| node: MeshNodeProtocol, | ||
| peer_name: str, | ||
| command: str, | ||
| timeout: float = 15.0, | ||
| ) -> dict: | ||
| """Send a CLI command to a repeater and wait for the response.""" | ||
| from pymc_core.protocol.packet_builder import PacketBuilder | ||
|
|
||
| contact = _resolve_contact(node, peer_name) | ||
|
|
||
| text_handler = node.dispatcher.text_message_handler | ||
| response_event = asyncio.Event() | ||
| response_data: dict = {"text": None} | ||
|
|
||
| expected_key = contact.public_key | ||
|
|
||
| def _on_response(message_text: str, sender_contact: object) -> None: | ||
| sender_key = getattr(sender_contact, "public_key", None) | ||
| if sender_key is not None and sender_key != expected_key: | ||
| return | ||
| response_data["text"] = message_text | ||
| response_event.set() | ||
|
|
||
| text_handler.set_command_response_callback(_on_response) | ||
| try: | ||
| pkt, _crc = PacketBuilder.create_text_message( | ||
| contact=contact, | ||
| local_identity=node.identity, | ||
| message=command, | ||
| txt_type=TXT_TYPE_CLI_DATA, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with πΒ / π. |
||
| ) | ||
| await node.dispatcher.send_packet(pkt, wait_for_ack=False) | ||
| await asyncio.wait_for(response_event.wait(), timeout=timeout) | ||
| return { | ||
| "success": True, | ||
| "repeater": peer_name, | ||
| "command": command, | ||
| "response_text": response_data["text"], | ||
| } | ||
| except asyncio.TimeoutError: | ||
| return { | ||
| "success": False, | ||
| "repeater": peer_name, | ||
| "command": command, | ||
| "response_text": None, | ||
| "reason": "Command response timeout", | ||
| } | ||
| finally: | ||
| text_handler.set_command_response_callback(None) | ||
|
|
||
|
|
||
| async def send_advert( | ||
| *, | ||
| node: MeshNodeProtocol, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
send_repeater_command()installs a global command-response callback and resolves the wait on the first callback invocation, but_on_responseignores the providedsender_contact. If another repeater emits a command response while this request is in flight, the wrong payload can satisfy the wait and be attributed topeer_name, producing cross-repeater response mixups.Useful? React with πΒ / π.