swchp2pcom 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
swchp2pcom/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Swarmchestrate P2P Communication Library"""
2
+
3
+ from .swchpeer import SwchPeer
4
+
5
+ __all__ = [
6
+ 'SwchPeer',
7
+ ]
swchp2pcom/factory.py ADDED
@@ -0,0 +1,261 @@
1
+ import logging
2
+ import json
3
+ import uuid
4
+ import time
5
+ from typing import Optional, Dict, Any, List
6
+
7
+ from twisted.internet.protocol import Factory
8
+ from twisted.internet.task import LoopingCall
9
+
10
+ from swchp2pcom.node import P2PNode
11
+ from swchp2pcom.peers import Peers
12
+ from swchp2pcom.message_types import SystemMessageType
13
+
14
+ class P2PFactory(Factory):
15
+ def __init__(self, peer_id: str, metadata: dict, public_ip: Optional[str] = None, public_port: Optional[str] = None):
16
+ """
17
+ Initialize the P2P factory with peer information and metadata.
18
+ :param peer_id: Unique identifier for this peer
19
+ :param metadata: Dictionary containing peer metadata (e.g., universe, peer_type, etc.)
20
+ :param public_ip: Public IP address of this peer
21
+ :param public_port: Public port of this peer
22
+ """
23
+ self.peers = Peers()
24
+
25
+ self.seen_messages = {} # Dictionary to store message_id -> timestamp
26
+ self.message_ttl = 20 # Time to live for messages in seconds
27
+ self.id = peer_id # Unique ID for this node
28
+
29
+ self.public_ip = public_ip
30
+ self.public_port = public_port
31
+ self.metadata = metadata
32
+
33
+ self.peers.add_peer(self.id)
34
+ self.peers.set_public_info(self.id, public_ip, public_port)
35
+ self.peers.set_peer_metadata(self.id, metadata)
36
+
37
+ self.logger = logging.getLogger(__name__) # Initialize logger
38
+
39
+ self.user_defined_msg_handlers=dict()
40
+
41
+ self.event_listeners = {
42
+ 'entered': [],
43
+ 'left': [],
44
+ 'peer:connected': [],
45
+ 'peer:disconnected': [],
46
+ 'peer:discovered': [],
47
+ 'peer:undiscovered': [],
48
+ 'peer:all_disconnected': [],
49
+ 'message': []
50
+ }
51
+
52
+ self._connection_count = 0 # Private connection counter
53
+ self._is_shutting_down = False # Track intentional shutdown
54
+
55
+ # Start cleanup task for old messages
56
+ self.cleanup_task = LoopingCall(self._cleanup_old_messages)
57
+ self.cleanup_task.start(5) # Run cleanup every 5 seconds
58
+
59
+ def _cleanup_old_messages(self):
60
+ """Remove messages older than message_ttl seconds"""
61
+ current_time = time.time()
62
+ expired_messages = [
63
+ msg_id for msg_id, timestamp in self.seen_messages.items()
64
+ if current_time - timestamp > self.message_ttl
65
+ ]
66
+
67
+ for msg_id in expired_messages:
68
+ del self.seen_messages[msg_id]
69
+
70
+ if expired_messages:
71
+ self.logger.debug(f"Cleaned up {len(expired_messages)} expired messages")
72
+
73
+ def _is_message_seen(self, message_id: str) -> bool:
74
+ """Check if a message has been seen before"""
75
+ return message_id in self.seen_messages
76
+
77
+ def _mark_message_seen(self, message_id: str):
78
+ """Mark a message as seen with current timestamp"""
79
+ self.seen_messages[message_id] = time.time()
80
+
81
+ def buildProtocol(self, addr):
82
+ """Create a new P2PNode protocol instance"""
83
+ node = P2PNode(self, self.peers) # Pass the factory instance to P2PNode
84
+ return node
85
+
86
+ def _increment_connection_count(self):
87
+ """Private method to increment connection count"""
88
+ self._connection_count += 1
89
+
90
+ def _decrement_connection_count(self):
91
+ """Private method to decrement connection count"""
92
+ self._connection_count -= 1
93
+
94
+ def disconnect_from_peer(self, peer_id: str) -> None:
95
+ """Disconnect from a specific peer by closing their transport connection."""
96
+ peer_info = self.peers.get_peer_info(peer_id)
97
+ if not peer_info:
98
+ self.logger.warning(f"Cannot disconnect: peer {peer_id} not found")
99
+ raise ValueError(f"No such peer {peer_id} in registry.")
100
+ # Close both local and remote connections if they exist
101
+ for connection_type in ['local', 'remote']:
102
+ if connection_type in peer_info and 'transport' in peer_info[connection_type]:
103
+ transport = peer_info[connection_type]['transport']
104
+ if transport:
105
+ transport.loseConnection()
106
+
107
+ def get_connection_count(self) -> int:
108
+ """Private method to get current connection count"""
109
+ return self._connection_count
110
+
111
+ def send_message(self, message: dict, peer_transport: Optional[Any] = None) -> None:
112
+ """Send a message to all connected peers or a specific peer."""
113
+ if "message_id" in message:
114
+ self._mark_message_seen(message["message_id"])
115
+ else:
116
+ self.logger.debug("Message without message_id generating id...")
117
+ message_id = str(uuid.uuid4())
118
+ message["message_id"] = message_id
119
+ self._mark_message_seen(message_id)
120
+
121
+ # Ensure the message has required fields
122
+ if "message_type" not in message:
123
+ self.logger.error("Message must have a 'message_type' field.")
124
+ return
125
+ if "peer_id" not in message:
126
+ self.logger.debug("Message must have a 'peer_id' field. Setting to factory ID.")
127
+ message["peer_id"] = self.id
128
+
129
+ if message.get("peer_id") != self.id:
130
+ self.logger.debug(f"Forwarding message: {message}")
131
+ else:
132
+ self.logger.debug(f"Sending message {message}")
133
+
134
+ serialized_message = json.dumps(message) + "\n"
135
+ data = serialized_message.encode("utf-8")
136
+
137
+ if peer_transport:
138
+ peer_transport.write(data)
139
+ else:
140
+ for transport in self.peers.get_peer_transports():
141
+ transport.write(data)
142
+
143
+ def send_to_peer(self, peer_id: str, message: dict) -> None:
144
+ """Send a message to a specific peer identified by peer_id.
145
+ If the peer is not connected, the message will be sent to all connected peers.
146
+ :param peer_id: Identifier for the peer.
147
+ :param message: The message to send, which must include 'message_type' and 'payload'.
148
+ """
149
+ # Ensure target_id is set
150
+ message["target_id"] = peer_id
151
+
152
+ peer_info = self.peers.get_peer_info(peer_id)
153
+ if not peer_info:
154
+ self.logger.error(f"No such peer {peer_id} in registry.")
155
+ raise ValueError(f"No such peer {peer_id} in registry.")
156
+
157
+ # Find an active transport (local or remote) for that peer
158
+ transport = None
159
+ for loc in ("remote", "local"):
160
+ info = peer_info.get(loc)
161
+ if info and "transport" in info:
162
+ transport = info["transport"]
163
+ break
164
+ if not transport:
165
+ self.logger.debug(f"Peer {peer_id} is not currently connected. Sending to all connected peers.")
166
+ self.send_message(message)
167
+ else:
168
+ self.logger.debug(f"Sending message to peer {peer_id}")
169
+ self.send_message(message, transport)
170
+
171
+ def broadcast_remove_peer(self, peer_id: str):
172
+ """Broadcast a message to all peers to remove a disconnected peer."""
173
+ message_id = str(uuid.uuid4())
174
+ message = {
175
+ "message_type": SystemMessageType.BROADCAST_REMOVE_PEER.value,
176
+ "message_id": message_id,
177
+ "peer_id": self.id,
178
+ "remove_peer_id": peer_id,
179
+ }
180
+ self.send_message(message)
181
+
182
+ def send_intentional_disconnect(self, peer_id: str):
183
+ """Send a message to indicate an intentional disconnect."""
184
+ message_id = str(uuid.uuid4())
185
+ message = {
186
+ "message_type": SystemMessageType.SEND_INTENTIONAL_DISCONNECT.value,
187
+ "message_id": message_id,
188
+ "peer_id": self.id,
189
+ }
190
+ self.send_to_peer(peer_id, message)
191
+
192
+ def add_event_listener(self, event_name, listener):
193
+ """Register an event listener for a specific event"""
194
+ if event_name in self.event_listeners:
195
+ self.event_listeners[event_name].append(listener)
196
+ else:
197
+ self.event_listeners[event_name] = [listener]
198
+
199
+ def remove_event_listener(self, event_name, listener):
200
+ """Remove an event listener for a specific event"""
201
+ if event_name in self.event_listeners:
202
+ self.event_listeners[event_name].remove(listener)
203
+
204
+ def set_shutting_down(self, shutting_down: bool):
205
+ """Set the shutdown state to distinguish intentional vs unintentional disconnections"""
206
+ self._is_shutting_down = shutting_down
207
+
208
+ def on_entered_event(self):
209
+ """Trigger the entered event"""
210
+ self.logger.info(f"Successfully entered the network.")
211
+ for listener in self.event_listeners.get('entered', []):
212
+ listener()
213
+
214
+ def on_left_event(self):
215
+ """Trigger the left event"""
216
+ self.logger.info(f"Left the network.")
217
+ for listener in self.event_listeners.get('left', []):
218
+ listener()
219
+
220
+ def on_peer_connected_event(self, peer_id: str):
221
+ """Trigger the peer:connected event"""
222
+ self._increment_connection_count()
223
+ self.logger.info(f"Connection established with {peer_id}. Connection count: {self._connection_count}")
224
+ for listener in self.event_listeners.get('peer:connected', []):
225
+ listener(peer_id)
226
+
227
+ def on_peer_disconnected_event(self, peer_id: str):
228
+ """Trigger the peer:disconnected event"""
229
+ self._decrement_connection_count()
230
+ self.logger.info(f"Connection lost with {peer_id}. Connection count: {self._connection_count}")
231
+
232
+ # Check if this was the last connection and it wasn't intentional
233
+ if self._connection_count == 0:
234
+ self.logger.info("Disconnected from all peers.")
235
+ for listener in self.event_listeners.get('peer:all_disconnected', []):
236
+ listener()
237
+
238
+ for listener in self.event_listeners.get('peer:disconnected', []):
239
+ listener(peer_id)
240
+
241
+ def on_peer_discovered_event(self, peer_id: str):
242
+ """Trigger the peer:discovered event"""
243
+ self.logger.info(f"Peer discovered: {peer_id}")
244
+ for listener in self.event_listeners.get('peer:discovered', []):
245
+ listener(peer_id)
246
+
247
+ def on_peer_undiscovered_event(self, peer_id: str):
248
+ """Trigger the peer:undiscovered event"""
249
+ self.logger.info(f"Peer undiscovered: {peer_id}")
250
+ for listener in self.event_listeners.get('peer:undiscovered', []):
251
+ listener(peer_id)
252
+
253
+ def emit_message(self, peer_id: str, message: dict):
254
+ """Trigger the message event with formatted payload"""
255
+ event_data = {
256
+ 'peer_id': peer_id,
257
+ 'message_type': message.get('message_type'),
258
+ 'payload': message.get('payload')
259
+ }
260
+ for listener in self.event_listeners.get('message', []):
261
+ listener(event_data)
@@ -0,0 +1,65 @@
1
+ """
2
+ Message type constants and enums for the SwchPeer P2P communication library.
3
+
4
+ This module defines standardized message types used for system-level communication
5
+ between peers in the network, ensuring consistency and reducing the risk of typos
6
+ in message type strings throughout the codebase.
7
+ """
8
+
9
+ from enum import Enum, unique
10
+
11
+
12
+ @unique
13
+ class SystemMessageType(Enum):
14
+ """
15
+ Enumeration of system-level message types used for P2P network management.
16
+
17
+ These message types are reserved for internal network operations such as
18
+ peer discovery, connection management, and network state synchronization.
19
+ User-defined message types should not use these reserved values.
20
+ """
21
+
22
+ # System message types
23
+ SEND_WELCOME_INFO = "system_send_welcome_info"
24
+ BROADCAST_PEER_LIST_UPDATE = "system_broadcast_peer_list_update"
25
+ BROADCAST_REMOVE_PEER = "system_broadcast_remove_peer"
26
+ SEND_INTENTIONAL_DISCONNECT = "system_send_intentional_disconnect"
27
+
28
+ def __str__(self):
29
+ """Return the string value of the message type."""
30
+ return self.value
31
+
32
+ @classmethod
33
+ def is_system_message(cls, message_type: str) -> bool:
34
+ """
35
+ Check if a message type is a reserved system message.
36
+
37
+ Args:
38
+ message_type: The message type string to check
39
+
40
+ Returns:
41
+ True if the message type is a system message, False otherwise
42
+ """
43
+ return message_type in [msg_type.value for msg_type in cls]
44
+
45
+ @classmethod
46
+ def get_all_system_types(cls) -> list[str]:
47
+ """
48
+ Get a list of all system message type strings.
49
+
50
+ Returns:
51
+ List of all system message type values
52
+ """
53
+ return [msg_type.value for msg_type in cls]
54
+
55
+ def is_system_message(message_type: str) -> bool:
56
+ """
57
+ Convenience function to check if a message type is a system message.
58
+
59
+ Args:
60
+ message_type: The message type string to check
61
+
62
+ Returns:
63
+ True if the message type is a system message, False otherwise
64
+ """
65
+ return SystemMessageType.is_system_message(message_type)
swchp2pcom/node.py ADDED
@@ -0,0 +1,287 @@
1
+ from twisted.internet.protocol import Protocol
2
+ from twisted.internet.task import LoopingCall
3
+ import uuid
4
+ import json
5
+ import logging
6
+ from typing import Optional, Dict, Any, List
7
+
8
+ from swchp2pcom.peers import Peers
9
+ from swchp2pcom.message_types import SystemMessageType
10
+
11
+ class P2PNode(Protocol):
12
+ def __init__(self, factory, peers: Peers, is_initiator: bool = False, is_entering: bool = False):
13
+ self.factory = factory
14
+ self.peers = peers
15
+
16
+ self.buffer = "" # Buffer to hold partial messages
17
+ self.is_initiator = is_initiator # Track if this node initiated the connection
18
+ self.is_entering = is_entering # Track if this node is created for entering the network
19
+ self.remote_id: Optional[str] = None # ID of the remote peer
20
+ self.logger = logging.getLogger(__name__) # Initialize logger
21
+
22
+ def connectionMade(self):
23
+ """Handle new connection."""
24
+ peer = self.transport.getPeer()
25
+ host = self.transport.getHost()
26
+
27
+ peer_address = f"{peer.host}:{peer.port}"
28
+ host_address = f"{host.host}:{host.port}"
29
+
30
+ self.logger.debug(f"Connected to peer at {peer_address} from {host_address}")
31
+ if self.is_initiator:
32
+ self.logger.info(f"Made connection to a peer...")
33
+ else:
34
+ self.logger.info(f"Peer made connection to us...")
35
+
36
+ self.send_welcome_info()
37
+
38
+ def dataReceived(self, data: bytes):
39
+ """Handle incoming data."""
40
+ try:
41
+ decoded_data = data.decode("utf-8")
42
+ except UnicodeDecodeError as e:
43
+ self.logger.error(f"Error decoding data: {e}")
44
+ return
45
+
46
+ self.buffer += decoded_data
47
+ lines = self.buffer.split("\n")
48
+ self.buffer = lines.pop() # Save incomplete data
49
+
50
+ for line in lines:
51
+ if not line.strip():
52
+ continue # Skip empty lines
53
+ try:
54
+ parsed_message = json.loads(line)
55
+ self.process_message(parsed_message)
56
+ except json.JSONDecodeError as e:
57
+ self.logger.error(f"Error decoding message: {e}")
58
+
59
+ def process_message(self, message: Dict[str, Any]):
60
+ """Handle and forward broadcast messages."""
61
+ message_id = message.get("message_id")
62
+ target_id = message.get("target_id")
63
+
64
+ if not message_id:
65
+ self.logger.warning("Received message without message_id")
66
+ return
67
+
68
+ if self.factory._is_message_seen(message_id):
69
+ return # Deduplicate messages
70
+
71
+ self.logger.debug(f"Received message: {message}")
72
+ self.factory._mark_message_seen(message_id)
73
+
74
+ message_type = message.get("message_type")
75
+
76
+ match message_type:
77
+ case SystemMessageType.BROADCAST_PEER_LIST_UPDATE.value:
78
+ self.handle_peer_list_update(message)
79
+ case SystemMessageType.SEND_WELCOME_INFO.value:
80
+ self.handle_welcome_info(message)
81
+ case SystemMessageType.BROADCAST_REMOVE_PEER.value:
82
+ self.handle_remove_peer(message)
83
+ case SystemMessageType.SEND_INTENTIONAL_DISCONNECT.value:
84
+ self.handle_intentional_disconnect(message)
85
+ case _:
86
+ # Forward message if we're not the target or not broadcast
87
+ if target_id not in ("*", self.factory.id):
88
+ self.factory.send_to_peer(target_id, message)
89
+ return
90
+
91
+ # Forward message if it is a broadcast
92
+ if target_id == "*":
93
+ self.factory.send_message(message)
94
+
95
+ self.logger.info(f"Recieved user-defined message: {message}")
96
+
97
+ # Handle message since it must be a broadcast or targeted user-defined message
98
+ if message_type in self.factory.user_defined_msg_handlers:
99
+ func = self.factory.user_defined_msg_handlers[message_type]
100
+ func(message.get("peer_id",""), message.get("payload",""))
101
+ else:
102
+ self.logger.warning(f"Unknown message type received: {message_type}")
103
+
104
+
105
+ # Emit message event
106
+ self.factory.emit_message(message.get("peer_id", ""), message)
107
+
108
+ def send_welcome_info(self):
109
+ """Send peer info to the connected peer."""
110
+ message_id = str(uuid.uuid4())
111
+ peer_public_info_list = self.factory.peers.get_known_peers_public_info()
112
+ message = {
113
+ "message_type": SystemMessageType.SEND_WELCOME_INFO.value,
114
+ "message_id": message_id,
115
+ "peer_id": self.factory.id,
116
+ "peer_public_info": self.peers.get_peer_info(self.factory.id)["public"],
117
+ "peer_metadata": self.peers.get_peer_metadata(self.factory.id),
118
+ "peers": peer_public_info_list
119
+ }
120
+ self.factory.send_message(message, peer_transport=self.transport)
121
+
122
+ def handle_welcome_info(self, message: Dict[str, Any]):
123
+ """Update peer info upon receiving process_peer_info message."""
124
+ remote_peer_id = message.get("peer_id")
125
+
126
+ if not remote_peer_id:
127
+ self.logger.error(f"Received {message.get("message_type")} without id")
128
+ return
129
+
130
+ # Store the remote peer ID
131
+ self.remote_id = remote_peer_id
132
+
133
+ is_new_peer = not self.peers.get_peer_info(remote_peer_id)
134
+
135
+ if is_new_peer:
136
+ # Add new peer to the peer list
137
+ self.peers.add_peer(remote_peer_id)
138
+ self.peers.set_public_info(remote_peer_id, message["peer_public_info"]["host"], message["peer_public_info"]["port"])
139
+ self.peers.set_peer_metadata(remote_peer_id, message.get("peer_metadata", {}))
140
+
141
+ # Set the transport information for the peer
142
+ peer = self.transport.getPeer()
143
+ if self.is_initiator:
144
+ self.peers.set_local_info(remote_peer_id, peer.host, peer.port, self.transport)
145
+ else:
146
+ self.peers.set_remote_info(remote_peer_id, peer.host, peer.port, self.transport)
147
+
148
+ # Raise peer discovered and connected events
149
+ if is_new_peer:
150
+ self.factory.on_peer_discovered_event(remote_peer_id)
151
+ self.factory.on_peer_connected_event(remote_peer_id)
152
+
153
+ # Process the peers list from the welcome message
154
+ for peer_id, public_info, metadata in message.get("peers", []):
155
+ if not self.peers.get_peer_info(peer_id):
156
+ # Add new peer to the peer list
157
+ self.peers.add_peer(peer_id)
158
+ self.peers.set_public_info(peer_id, public_info["host"], public_info["port"])
159
+ self.peers.set_peer_metadata(peer_id, metadata)
160
+
161
+ # Raise peer discovered event
162
+ self.factory.on_peer_discovered_event(peer_id)
163
+
164
+ # Let others know about the new peer in the network
165
+ if is_new_peer and len(self.peers.get_all_peers_items()) > 2:
166
+ # If this is a new peer, broadcast the updated peer list
167
+ self.broadcast_peer_list_update()
168
+
169
+ if (not self.is_entering) and is_new_peer:
170
+ # Log the public peer list
171
+ self.log_public_peer_list(message=f"Peer {remote_peer_id} entered. Updated peer list")
172
+
173
+ if self.is_entering:
174
+ self.log_public_peer_list(message=f"Recieved peers after entering. Current peers")
175
+ self.factory.on_entered_event()
176
+ self.is_entering = False # Reset entering state
177
+
178
+ def broadcast_peer_list_update(self):
179
+ """Broadcast the known peer list to all connected peers."""
180
+ message_id = str(uuid.uuid4())
181
+ peer_public_info_list = self.factory.peers.get_known_peers_public_info()
182
+ message = {
183
+ "message_type": SystemMessageType.BROADCAST_PEER_LIST_UPDATE.value,
184
+ "message_id": message_id,
185
+ "peer_id": self.factory.id,
186
+ "peers": peer_public_info_list
187
+ }
188
+ self.factory.send_message(message)
189
+
190
+ def handle_peer_list_update(self, message: Dict[str, Any]):
191
+ """Update the known peer list."""
192
+ peers = message.get("peers", [])
193
+ any_changed = False
194
+
195
+ for peer_id, public_info, metadata in peers:
196
+ old = self.peers.get_peer_info(peer_id)
197
+
198
+ if not old:
199
+ # Add new peer to the peer list
200
+ self.peers.add_peer(peer_id)
201
+ self.peers.set_public_info(peer_id, public_info["host"], public_info["port"])
202
+ self.peers.set_peer_metadata(peer_id, metadata)
203
+ any_changed = True
204
+
205
+ self.factory.on_peer_discovered_event(peer_id)
206
+ if old and (old.get("public") != public_info):
207
+ self.logger.info(f"New public info for peer: {peer_id}, public_info: {public_info}")
208
+ self.peers.set_public_info(peer_id, public_info["host"], public_info["port"])
209
+ any_changed = True
210
+
211
+ if old and (old.get("metadata") != metadata):
212
+ self.logger.info(f"New metadata for peer: {peer_id}, metadata: {metadata}")
213
+ self.peers.set_peer_metadata(peer_id, metadata)
214
+ any_changed = True
215
+
216
+ if any_changed:
217
+ self.log_public_peer_list()
218
+
219
+ # Forward the update peer list message to all peers
220
+ self.factory.send_message(message)
221
+
222
+ def handle_remove_peer(self, message: Dict[str, Any]):
223
+ """Remove a peer from all_peers."""
224
+ peer_id = message.get("remove_peer_id")
225
+ if peer_id:
226
+ # Raise peer undiscovered event
227
+ self.factory.on_peer_undiscovered_event(peer_id)
228
+
229
+ if self.peers.remove_peer_info(peer_id):
230
+ self.log_public_peer_list(message=f"Peer {peer_id} left. Peer list updated")
231
+ else:
232
+ self.logger.debug(f"Peer {peer_id} not found in peer list.")
233
+
234
+ # Propagate the removal to other peers
235
+ self.factory.send_message(message)
236
+
237
+ def handle_intentional_disconnect(self, message: Dict[str, Any]):
238
+ """Handle intentional disconnect from a peer."""
239
+ self.peers.set_is_intentional_disconnect(message.get("peer_id"),True)
240
+
241
+ def connectionLost(self, reason):
242
+ """Handle lost connection."""
243
+ self.logger.debug(f"Connection lost: {reason.getErrorMessage()}")
244
+
245
+ if not self.remote_id:
246
+ self.logger.debug("Remote ID is not set, cannot handle disconnection properly.")
247
+ return
248
+
249
+ # If we doesnt have information about the peer (Could be because we already removed him, or the peer was never added)
250
+ if not self.peers.get_peer_info(self.remote_id):
251
+ self.factory.on_peer_disconnected_event(self.remote_id)
252
+ return
253
+
254
+ # If it wasnt intentional
255
+ if not self.peers.get_is_intentional_disconnect(self.remote_id):
256
+ self.logger.debug("Unintentional disconnect detected, removing peer info.")
257
+ self.factory.on_peer_undiscovered_event(self.remote_id)
258
+
259
+ self.peers.remove_peer_info(self.remote_id)
260
+ self.factory.on_peer_disconnected_event(self.remote_id)
261
+
262
+ # Broadcast the removal of the peer
263
+ self.factory.broadcast_remove_peer(self.remote_id)
264
+
265
+ # log the public peer list
266
+ self.log_public_peer_list(message=f"Peer {self.remote_id} left. Peer list updated")
267
+ # If it was intentional, and he doesnt needs to be removed from the network
268
+ else:
269
+ if self.peers.get_peer_info(self.remote_id):
270
+ if self.is_initiator:
271
+ self.peers.remove_peer_info(self.remote_id,"local")
272
+ else:
273
+ self.peers.remove_peer_info(self.remote_id,"remote")
274
+
275
+ # Reset the intentional disconnect flag
276
+ self.peers.set_is_intentional_disconnect(self.remote_id, False)
277
+ # Raise peer disconnected event
278
+ self.factory.on_peer_disconnected_event(self.remote_id)
279
+
280
+ def log_public_peer_list(self, message: str = "Peer list updated"):
281
+ self.logger.info(
282
+ f"\n{'-'*13}\n{message}:\n" +
283
+ "\n".join(f"id: {pid}, host: {info['public']['host']}, port: {info['public']['port']}"
284
+ for pid, info in self.peers.get_all_peers_items() if info["public"]) +
285
+ f"\n{'-'*13}"
286
+ )
287
+