diff --git a/jmdaemon/jmdaemon/onionmc.py b/jmdaemon/jmdaemon/onionmc.py index 30a0cdc..8e6f63f 100644 --- a/jmdaemon/jmdaemon/onionmc.py +++ b/jmdaemon/jmdaemon/onionmc.py @@ -3,6 +3,7 @@ from jmdaemon.protocol import COMMAND_PREFIX, JM_VERSION from jmbase import get_log, JM_APP_NAME, JMHiddenService, stop_reactor import json import copy +import random from typing import Callable, Union, Tuple, List from twisted.internet import reactor, task, protocol from twisted.protocols import basic @@ -116,6 +117,9 @@ class OnionCustomMessageDecodingError(Exception): class InvalidLocationStringError(Exception): pass +class OnionDirectoryPeerNotFound(Exception): + pass + class OnionCustomMessage(object): """ Encapsulates the messages passed over the wire to and from other onion peers @@ -586,6 +590,14 @@ class OnionDirectoryPeer(OnionPeer): except OnionPeerConnectionError: reactor.callLater(self.delay, self.try_to_connect) + def register_connection(self) -> None: + self.messagechannel.update_directory_map(self, connected=True) + super().register_connection() + + def register_disconnection(self) -> None: + self.messagechannel.update_directory_map(self, connected=False) + super().register_disconnection() + class OnionMessageChannel(MessageChannel): """ Sends messages to other nodes of the same type over Tor @@ -687,6 +699,14 @@ class OnionMessageChannel(MessageChannel): # the rpc connection calls are not using twisted) self.wait_for_directories_loop = None + # this dict plays the same role as `active_channels` in `MessageChannelCollection`. + # it has structure {nick: set(),..} where set() has elements that are dicts: + # {OnionPeer: bool}. + # Entries get updated with changing connection status of directories, + # allowing us to decide where to send each message we want to send when we have no + # direct connection. + self.active_directories = {} + def info_callback(self, msg: str) -> None: log.info(msg) @@ -787,11 +807,10 @@ class OnionMessageChannel(MessageChannel): log.debug("Privmsg peer: {} but don't have peerid; " "sending via directory.".format(nick)) try: - # TODO change this to redundant or switching - peer_sendable = self.get_connected_directory_peers()[0] - except Exception as e: + peer_sendable = self.get_directory_for_nick(nick) + except OnionDirectoryPeerNotFound: log.warn("Failed to send privmsg because no " - "directory peer is connected. Error: {}".format(repr(e))) + "directory peer is connected.") return self._send(peer_sendable, encoded_privmsg) @@ -965,6 +984,28 @@ class OnionMessageChannel(MessageChannel): except Exception as e: log.debug("Invalid Joinmarket message: {}, error was: {}".format( msgval, repr(e))) + # add the nick to the directories map, whether pubmsg or privmsg, but + # only if it passed the above syntax Exception catch: + if peer.directory and not self.self_as_peer.directory: + if from_nick not in self.active_directories: + self.active_directories[from_nick] = {} + self.active_directories[from_nick][peer] = True + + def update_directory_map(self, p: OnionDirectoryPeer, connected: bool) -> None: + nicks = [] + for nick in self.active_directories: + if p in self.active_directories[nick]: + nicks.append(nick) + for nick in nicks: + self.active_directories[nick][p] = connected + + def get_directory_for_nick(self, nick: str) -> OnionDirectoryPeer: + if nick not in self.active_directories: + raise OnionDirectoryPeerNotFound + adn = self.active_directories[nick] + if len(adn) == 0: + raise OnionDirectoryPeerNotFound + return random.choice([x for x in list(adn) if adn[x] is True]) def forward_pubmsg_to_peers(self, msg: str, from_nick: str) -> None: """ Used by directory nodes currently. Takes a received