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 +7 -0
- swchp2pcom/factory.py +261 -0
- swchp2pcom/message_types.py +65 -0
- swchp2pcom/node.py +287 -0
- swchp2pcom/peers.py +287 -0
- swchp2pcom/swchpeer.py +869 -0
- swchp2pcom-0.3.0.dist-info/METADATA +383 -0
- swchp2pcom-0.3.0.dist-info/RECORD +9 -0
- swchp2pcom-0.3.0.dist-info/WHEEL +4 -0
swchp2pcom/__init__.py
ADDED
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
|
+
|