587 lines
24 KiB
Python
587 lines
24 KiB
Python
#!/usr/bin/env python
|
|
#
|
|
# Electrum - lightweight Bitcoin client
|
|
# Copyright (C) 2012 thomasv@gitorious
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person
|
|
# obtaining a copy of this software and associated documentation files
|
|
# (the "Software"), to deal in the Software without restriction,
|
|
# including without limitation the rights to use, copy, modify, merge,
|
|
# publish, distribute, sublicense, and/or sell copies of the Software,
|
|
# and to permit persons to whom the Software is furnished to do so,
|
|
# subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be
|
|
# included in all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
|
|
from enum import IntEnum
|
|
|
|
from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot
|
|
from PyQt6.QtWidgets import (
|
|
QTreeWidget, QTreeWidgetItem, QMenu, QGridLayout, QComboBox, QLineEdit, QDialog, QVBoxLayout, QHeaderView,
|
|
QCheckBox, QTabWidget, QWidget, QLabel, QPushButton, QHBoxLayout,
|
|
QListWidget, QListWidgetItem,
|
|
)
|
|
from PyQt6.QtGui import QIntValidator
|
|
|
|
from electrum.i18n import _
|
|
from electrum import blockchain
|
|
from electrum.interface import ServerAddr, PREFERRED_NETWORK_PROTOCOL
|
|
from electrum.network import Network, ProxySettings, is_valid_host, is_valid_port
|
|
from electrum.logging import get_logger
|
|
from electrum.util import is_valid_websocket_url
|
|
from electrum.gui import messages
|
|
|
|
from .util import (
|
|
Buttons, CloseButton, HelpButton, read_QIcon, char_width_in_lineedit, PasswordLineEdit, QtEventListener,
|
|
qt_event_listener, Spinner, HelpLabel
|
|
)
|
|
|
|
|
|
_logger = get_logger(__name__)
|
|
|
|
protocol_names = ['TCP', 'SSL']
|
|
protocol_letters = 'ts'
|
|
|
|
|
|
class NetworkDialog(QDialog, QtEventListener):
|
|
def __init__(self, *, network: Network):
|
|
QDialog.__init__(self)
|
|
self.setWindowTitle(_('Network'))
|
|
self.setMinimumSize(500, 500)
|
|
self.tabs = tabs = QTabWidget()
|
|
self._blockchain_tab = ServerWidget(network)
|
|
self._proxy_tab = ProxyWidget(network)
|
|
self._nostr_tab = NostrWidget(network)
|
|
tabs.addTab(self._blockchain_tab, _('Server'))
|
|
tabs.addTab(self._nostr_tab, _('Nostr'))
|
|
tabs.addTab(self._proxy_tab, _('Proxy'))
|
|
vbox = QVBoxLayout(self)
|
|
vbox.addWidget(self.tabs)
|
|
vbox.addLayout(Buttons(CloseButton(self)))
|
|
|
|
def show(self, *, proxy_tab: bool = False):
|
|
super().show()
|
|
self.tabs.setCurrentWidget(self._proxy_tab if proxy_tab else self._blockchain_tab)
|
|
|
|
|
|
class NodesListWidget(QTreeWidget):
|
|
"""List of connected servers."""
|
|
|
|
SERVER_ADDR_ROLE = Qt.ItemDataRole.UserRole + 100
|
|
CHAIN_ID_ROLE = Qt.ItemDataRole.UserRole + 101
|
|
ITEMTYPE_ROLE = Qt.ItemDataRole.UserRole + 102
|
|
|
|
class ItemType(IntEnum):
|
|
CHAIN = 0
|
|
CONNECTED_SERVER = 1
|
|
DISCONNECTED_SERVER = 2
|
|
TOPLEVEL = 3
|
|
|
|
followServer = pyqtSignal([ServerAddr], arguments=['server'])
|
|
followChain = pyqtSignal([str], arguments=['chain_id'])
|
|
setServer = pyqtSignal([str], arguments=['server'])
|
|
|
|
def __init__(self, *, network: Network):
|
|
QTreeWidget.__init__(self)
|
|
self.setHeaderLabels([_('Server'), _('Height')])
|
|
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
self.customContextMenuRequested.connect(self.create_menu)
|
|
self.network = network
|
|
|
|
def create_menu(self, position):
|
|
item = self.currentItem()
|
|
if not item:
|
|
return
|
|
item_type = item.data(0, self.ITEMTYPE_ROLE)
|
|
menu = QMenu()
|
|
if item_type in [self.ItemType.CONNECTED_SERVER, self.ItemType.DISCONNECTED_SERVER]:
|
|
server = item.data(0, self.SERVER_ADDR_ROLE) # type: ServerAddr
|
|
if item_type == self.ItemType.CONNECTED_SERVER:
|
|
def do_follow_server():
|
|
self.followServer.emit(server)
|
|
menu.addAction(read_QIcon("chevron-right.png"), _("Use as server"), do_follow_server)
|
|
elif item_type == self.ItemType.DISCONNECTED_SERVER:
|
|
def do_set_server():
|
|
self.setServer.emit(str(server))
|
|
menu.addAction(read_QIcon("chevron-right.png"), _("Use as server"), do_set_server)
|
|
|
|
def set_bookmark(*, add: bool):
|
|
self.network.set_server_bookmark(server, add=add)
|
|
self.update()
|
|
|
|
if self.network.is_server_bookmarked(server):
|
|
menu.addAction(read_QIcon("bookmark_remove.png"), _("Remove from bookmarks"), lambda: set_bookmark(add=False))
|
|
else:
|
|
menu.addAction(read_QIcon("bookmark_add.png"), _("Bookmark this server"), lambda: set_bookmark(add=True))
|
|
elif item_type == self.ItemType.CHAIN:
|
|
chain_id = item.data(0, self.CHAIN_ID_ROLE)
|
|
|
|
def do_follow_chain():
|
|
self.followChain.emit(chain_id)
|
|
|
|
menu.addAction(_("Follow this branch"), do_follow_chain)
|
|
else:
|
|
return
|
|
menu.exec(self.viewport().mapToGlobal(position))
|
|
|
|
def keyPressEvent(self, event):
|
|
if event.key() in [Qt.Key.Key_F2, Qt.Key.Key_Return, Qt.Key.Key_Enter]:
|
|
self.on_activated(self.currentItem(), self.currentColumn())
|
|
else:
|
|
QTreeWidget.keyPressEvent(self, event)
|
|
|
|
def on_activated(self, item, column):
|
|
# on 'enter' we show the menu
|
|
pt = self.visualItemRect(item).bottomLeft()
|
|
pt.setX(50)
|
|
self.customContextMenuRequested.emit(pt)
|
|
|
|
def update(self):
|
|
self.clear()
|
|
network = self.network
|
|
servers = self.network.get_servers()
|
|
|
|
use_tor = bool(network.is_proxy_tor)
|
|
|
|
# connected servers
|
|
connected_servers_item = QTreeWidgetItem([_("Connected nodes"), ''])
|
|
connected_servers_item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.TOPLEVEL)
|
|
chains = network.get_blockchains()
|
|
n_chains = len(chains)
|
|
for chain_id, interfaces in chains.items():
|
|
b = blockchain.blockchains.get(chain_id)
|
|
if b is None:
|
|
continue
|
|
name = b.get_name()
|
|
if n_chains > 1:
|
|
x = QTreeWidgetItem([name + '@%d'%b.get_max_forkpoint(), '%d'%b.height()])
|
|
x.setData(0, self.ITEMTYPE_ROLE, self.ItemType.CHAIN)
|
|
x.setData(0, self.CHAIN_ID_ROLE, b.get_id())
|
|
else:
|
|
x = connected_servers_item
|
|
for i in interfaces:
|
|
item = QTreeWidgetItem([f"{i.server.to_friendly_name()}", '%d'%i.tip])
|
|
item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.CONNECTED_SERVER)
|
|
item.setData(0, self.SERVER_ADDR_ROLE, i.server)
|
|
item.setToolTip(0, str(i.server))
|
|
if i == network.interface:
|
|
item.setIcon(0, read_QIcon("chevron-right.png"))
|
|
elif network.is_server_bookmarked(i.server):
|
|
item.setIcon(0, read_QIcon("bookmark.png"))
|
|
x.addChild(item)
|
|
if n_chains > 1:
|
|
connected_servers_item.addChild(x)
|
|
|
|
# disconnected servers
|
|
disconnected_servers_item = QTreeWidgetItem([_("Other known servers"), ""])
|
|
disconnected_servers_item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.TOPLEVEL)
|
|
connected_hosts = set([iface.host for ifaces in chains.values() for iface in ifaces])
|
|
protocol = PREFERRED_NETWORK_PROTOCOL
|
|
server_addrs = [
|
|
ServerAddr(_host, port, protocol=protocol)
|
|
for _host, d in servers.items()
|
|
if (port := d.get(protocol))]
|
|
server_addrs.sort(key=lambda x: (-network.is_server_bookmarked(x), str(x)))
|
|
for server in server_addrs:
|
|
if server.host in connected_hosts:
|
|
continue
|
|
if server.host.endswith('.onion') and not use_tor:
|
|
continue
|
|
item = QTreeWidgetItem([server.net_addr_str(), ""])
|
|
item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.DISCONNECTED_SERVER)
|
|
item.setData(0, self.SERVER_ADDR_ROLE, server)
|
|
if network.is_server_bookmarked(server):
|
|
item.setIcon(0, read_QIcon("bookmark.png"))
|
|
disconnected_servers_item.addChild(item)
|
|
|
|
self.addTopLevelItem(connected_servers_item)
|
|
self.addTopLevelItem(disconnected_servers_item)
|
|
|
|
connected_servers_item.setExpanded(True)
|
|
for i in range(connected_servers_item.childCount()):
|
|
connected_servers_item.child(i).setExpanded(True)
|
|
disconnected_servers_item.setExpanded(True)
|
|
|
|
# headers
|
|
h = self.header()
|
|
h.setStretchLastSection(False)
|
|
h.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
|
h.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
|
|
|
|
super().update()
|
|
|
|
|
|
class ProxyWidget(QWidget):
|
|
PROXY_MODES = {
|
|
'socks4': 'SOCKS4',
|
|
'socks5': 'SOCKS5/TOR'
|
|
}
|
|
|
|
torProbeFinished = pyqtSignal([str, int], arguments=['host', 'port'])
|
|
|
|
def __init__(self, network: Network, parent=None):
|
|
super().__init__(parent)
|
|
self.network = network
|
|
self.config = network.config
|
|
|
|
fixed_width_port = 6 * char_width_in_lineedit()
|
|
|
|
# proxy setting.
|
|
self.proxy_cb = QCheckBox(_('Use proxy'))
|
|
self.proxy_mode = QComboBox()
|
|
for k, v in self.PROXY_MODES.items():
|
|
self.proxy_mode.addItem(v, k)
|
|
self.proxy_mode.setCurrentIndex(1)
|
|
self.proxy_host = QLineEdit()
|
|
self.proxy_port = QLineEdit()
|
|
self.proxy_port.setFixedWidth(fixed_width_port)
|
|
self.proxy_port_validator = QIntValidator(1, 65535)
|
|
self.proxy_port.setValidator(self.proxy_port_validator)
|
|
|
|
self.proxy_user = QLineEdit()
|
|
self.proxy_user.setPlaceholderText(_("Proxy username"))
|
|
self.proxy_password = PasswordLineEdit()
|
|
self.proxy_password.setPlaceholderText(_("Proxy password"))
|
|
|
|
grid = QGridLayout(self)
|
|
grid.setSpacing(8)
|
|
|
|
grid.addWidget(self.proxy_cb, 0, 0, 1, 4)
|
|
proxy_helpbutton = HelpButton(
|
|
_('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.'))
|
|
grid.addWidget(proxy_helpbutton, 0, 4, alignment=Qt.AlignmentFlag.AlignRight)
|
|
grid.addWidget(self.proxy_mode, 1, 0, 1, 1)
|
|
grid.addWidget(self.proxy_host, 1, 1, 1, 3)
|
|
grid.addWidget(self.proxy_port, 1, 4, 1, 1)
|
|
grid.addWidget(self.proxy_user, 2, 1, 1, 2)
|
|
grid.addWidget(self.proxy_password, 2, 3, 1, 2)
|
|
|
|
detect_l = QHBoxLayout()
|
|
self.detect_button = QPushButton(_('Detect Tor proxy'))
|
|
self.spinner = Spinner()
|
|
self.spinner.setMargin(5)
|
|
detect_l.addWidget(self.detect_button)
|
|
detect_l.addWidget(self.spinner)
|
|
|
|
grid.addLayout(detect_l, 3, 0, 1, 5, alignment=Qt.AlignmentFlag.AlignLeft)
|
|
|
|
spacer = QVBoxLayout()
|
|
spacer.addStretch(1)
|
|
grid.addLayout(spacer, 4, 0, 1, 5)
|
|
|
|
self.update_from_config()
|
|
self.update()
|
|
|
|
# connect signal handlers after init from config
|
|
self.proxy_cb.stateChanged.connect(self.on_proxy_enable_toggle)
|
|
self.proxy_mode.currentIndexChanged.connect(self.on_proxy_settings_changed)
|
|
self.proxy_host.editingFinished.connect(self.on_proxy_settings_changed)
|
|
self.proxy_port.editingFinished.connect(self.on_proxy_settings_changed)
|
|
self.proxy_user.editingFinished.connect(self.on_proxy_settings_changed)
|
|
self.proxy_password.editingFinished.connect(self.on_proxy_settings_changed)
|
|
self.detect_button.clicked.connect(self.detect_tor)
|
|
|
|
self.torProbeFinished.connect(self.on_tor_probe_finished)
|
|
|
|
def update(self):
|
|
enabled = self.proxy_cb.isChecked() and self.config.cv.NETWORK_PROXY.is_modifiable()
|
|
for item in [
|
|
self.proxy_mode, self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password,
|
|
self.detect_button
|
|
]:
|
|
item.setEnabled(enabled)
|
|
|
|
if not self.proxy_port.hasAcceptableInput() and not is_valid_port(self.proxy_port.text()):
|
|
return
|
|
|
|
if not is_valid_host(self.proxy_host.text()):
|
|
return
|
|
|
|
net_params = self.network.get_parameters()
|
|
proxy = self.get_proxy_settings()
|
|
net_params = net_params._replace(proxy=proxy)
|
|
self.network.run_from_another_thread(self.network.set_parameters(net_params))
|
|
|
|
def update_from_config(self):
|
|
proxy = ProxySettings.from_config(self.config)
|
|
self.proxy_cb.setChecked(proxy.enabled)
|
|
self.proxy_mode.setCurrentText(self.PROXY_MODES.get(proxy.mode))
|
|
self.proxy_host.setText(proxy.host)
|
|
self.proxy_port.setText(proxy.port)
|
|
self.proxy_user.setText(proxy.user)
|
|
self.proxy_password.setText(proxy.password)
|
|
|
|
if not self.config.cv.NETWORK_PROXY.is_modifiable():
|
|
for w in [
|
|
self.proxy_cb, self.proxy_mode, self.proxy_host, self.proxy_port,
|
|
self.proxy_user, self.proxy_password, self.detect_button
|
|
]:
|
|
w.setEnabled(False)
|
|
|
|
def on_proxy_enable_toggle(self):
|
|
# probe if enabled and no pre-existing settings
|
|
# if self.proxy_cb.isChecked() and (not self.proxy_host.text() or not self.proxy_port.text()):
|
|
# self.detect_tor()
|
|
self.update()
|
|
|
|
def on_proxy_settings_changed(self):
|
|
self.update()
|
|
|
|
def get_proxy_settings(self) -> ProxySettings:
|
|
proxy = ProxySettings()
|
|
proxy.enabled = self.proxy_cb.isChecked()
|
|
proxy.mode = self.proxy_mode.currentData()
|
|
proxy.host = self.proxy_host.text()
|
|
proxy.port = self.proxy_port.text()
|
|
proxy.user = self.proxy_user.text()
|
|
proxy.password = self.proxy_password.text()
|
|
return proxy
|
|
|
|
def detect_tor(self):
|
|
self.detect_button.setEnabled(False)
|
|
self.spinner.setVisible(True)
|
|
ProxySettings.probe_tor(self.torProbeFinished.emit) # via signal
|
|
|
|
@pyqtSlot(str, int)
|
|
def on_tor_probe_finished(self, host: str, port: int):
|
|
self.detect_button.setEnabled(True)
|
|
self.spinner.setVisible(False)
|
|
if host:
|
|
self.proxy_mode.setCurrentIndex(1)
|
|
self.proxy_host.setText(host)
|
|
self.proxy_port.setText(str(port))
|
|
self.update()
|
|
|
|
|
|
class ConnectMode(IntEnum):
|
|
AUTOCONNECT = 0
|
|
MANUAL = 1
|
|
ONESERVER = 2
|
|
|
|
class ServerWidget(QWidget, QtEventListener):
|
|
CONNECT_MODES = {
|
|
ConnectMode.AUTOCONNECT: messages.MSG_CONNECTMODE_AUTOCONNECT,
|
|
ConnectMode.MANUAL: messages.MSG_CONNECTMODE_MANUAL,
|
|
ConnectMode.ONESERVER: messages.MSG_CONNECTMODE_ONESERVER,
|
|
}
|
|
|
|
def __init__(self, network: Network, parent=None):
|
|
super().__init__(parent)
|
|
self.network = network
|
|
self.config = network.config
|
|
|
|
self.setLayout(QVBoxLayout())
|
|
|
|
grid = QGridLayout()
|
|
|
|
self.connect_combo = QComboBox()
|
|
for i, v in sorted(self.CONNECT_MODES.items()):
|
|
self.connect_combo.addItem(v, i)
|
|
self.connect_combo.currentIndexChanged.connect(self.on_server_settings_changed)
|
|
grid.addWidget(QLabel(_('Connection mode') + ':'), 0, 0)
|
|
msg = (
|
|
f"""
|
|
{messages.MSG_CONNECTMODE_SERVER_HELP}<br/><br/>
|
|
{messages.MSG_CONNECTMODE_NODES_HELP}
|
|
<ul>
|
|
<li><b>{messages.MSG_CONNECTMODE_AUTOCONNECT}</b>: {messages.MSG_CONNECTMODE_AUTOCONNECT_HELP}</li>
|
|
<li><b>{messages.MSG_CONNECTMODE_MANUAL}</b>: {messages.MSG_CONNECTMODE_MANUAL_HELP}</li>
|
|
<li><b>{messages.MSG_CONNECTMODE_ONESERVER}</b>: {messages.MSG_CONNECTMODE_ONESERVER_HELP}</li>
|
|
</ul>
|
|
"""
|
|
)
|
|
grid.addWidget(HelpButton(msg), 0, 4)
|
|
grid.addWidget(self.connect_combo, 0, 1, 1, 3)
|
|
self.status_label_header = QLabel(_('Status') + ':')
|
|
self.status_label = QLabel('')
|
|
self.status_label_helpbutton = HelpButton(messages.MSG_CONNECTMODE_NODES_HELP)
|
|
grid.addWidget(self.status_label_header, 1, 0)
|
|
grid.addWidget(self.status_label, 1, 1, 1, 3)
|
|
grid.addWidget(self.status_label_helpbutton, 1, 4)
|
|
|
|
self.server_e = QLineEdit()
|
|
self.server_e.editingFinished.connect(self.on_server_settings_changed)
|
|
grid.addWidget(QLabel(_('Server') + ':'), 4, 0)
|
|
grid.addWidget(self.server_e, 4, 1, 1, 3)
|
|
grid.addWidget(HelpButton(messages.MSG_CONNECTMODE_SERVER_HELP), 4, 4)
|
|
|
|
msg = _('This is the height of your local copy of the blockchain.')
|
|
self.height_label_header = QLabel(_('Blockchain') + ':')
|
|
self.height_label = QLabel('')
|
|
self.height_label_helpbutton = HelpButton(msg)
|
|
grid.addWidget(self.height_label_header, 2, 0)
|
|
grid.addWidget(self.height_label, 2, 1)
|
|
grid.addWidget(self.height_label_helpbutton, 2, 4)
|
|
|
|
self.split_label = QLabel('')
|
|
grid.addWidget(self.split_label, 3, 1, 1, 3)
|
|
|
|
self.layout().addLayout(grid)
|
|
|
|
self.nodes_list_widget = NodesListWidget(network=self.network)
|
|
self.nodes_list_widget.followServer.connect(self.follow_server)
|
|
self.nodes_list_widget.followChain.connect(self.follow_branch)
|
|
|
|
def do_set_server(server):
|
|
self.server_e.setText(server)
|
|
self.set_server()
|
|
self.nodes_list_widget.setServer.connect(do_set_server)
|
|
|
|
self.layout().addWidget(self.nodes_list_widget)
|
|
self.nodes_list_widget.update()
|
|
|
|
self.register_callbacks()
|
|
self.destroyed.connect(lambda: self.unregister_callbacks())
|
|
|
|
self.update_from_config()
|
|
self.update()
|
|
|
|
@qt_event_listener
|
|
def on_event_network_updated(self):
|
|
self.nodes_list_widget.update() # NOTE: move event handling to widget itself?
|
|
self.update()
|
|
|
|
def is_auto_connect(self):
|
|
return self.connect_combo.currentIndex() == ConnectMode.AUTOCONNECT
|
|
|
|
def is_one_server(self):
|
|
return self.connect_combo.currentIndex() == ConnectMode.ONESERVER
|
|
|
|
def on_server_settings_changed(self):
|
|
if not self.network._was_started:
|
|
self.update()
|
|
return
|
|
server = self.server_e.text().strip()
|
|
net_params = self.network.get_parameters()
|
|
if server != net_params.server or self.is_auto_connect() != net_params.auto_connect or self.is_one_server() != net_params.oneserver:
|
|
self.set_server()
|
|
|
|
def update(self):
|
|
self.server_e.setEnabled(self.config.cv.NETWORK_SERVER.is_modifiable() and not self.is_auto_connect())
|
|
for item in [
|
|
self.status_label_header, self.status_label, self.status_label_helpbutton,
|
|
self.height_label_header, self.height_label, self.height_label_helpbutton]:
|
|
item.setVisible(self.network._was_started)
|
|
msg = _('Fork detection disabled') if self.is_one_server() else ''
|
|
if self.network._was_started:
|
|
# Network was started, so we don't run in initial setup wizard.
|
|
# behavior in this case is to apply changes immediately.
|
|
# Also, we show block height and potential chain tips
|
|
height_str = _('{} blocks').format(self.network.get_local_height())
|
|
self.height_label.setText(height_str)
|
|
self.status_label.setText(self.network.get_status())
|
|
chains = self.network.get_blockchains()
|
|
if len(chains) > 1:
|
|
chain = self.network.blockchain()
|
|
forkpoint = chain.get_max_forkpoint()
|
|
name = chain.get_name()
|
|
msg = _('Fork detected at block {0}').format(forkpoint) + '\n'
|
|
if self.is_auto_connect():
|
|
msg += _('You are following branch {}').format(name)
|
|
else:
|
|
msg += _('Your server is on branch {0} ({1} blocks)').format(name, chain.get_branch_size())
|
|
self.split_label.setText(msg)
|
|
|
|
def update_from_config(self):
|
|
auto_connect = self.config.NETWORK_AUTO_CONNECT
|
|
one_server = self.config.NETWORK_ONESERVER
|
|
v = ConnectMode.AUTOCONNECT if auto_connect else ConnectMode.ONESERVER if one_server else ConnectMode.MANUAL
|
|
self.connect_combo.setCurrentIndex(v)
|
|
|
|
server = self.config.NETWORK_SERVER
|
|
self.server_e.setText(server)
|
|
|
|
self.server_e.setEnabled(self.config.cv.NETWORK_SERVER.is_modifiable() and not auto_connect)
|
|
self.nodes_list_widget.setEnabled(self.config.cv.NETWORK_SERVER.is_modifiable())
|
|
|
|
def follow_branch(self, chain_id):
|
|
self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id))
|
|
self.update()
|
|
|
|
def follow_server(self, server: ServerAddr):
|
|
self.server_e.setText(str(server))
|
|
self.network.run_from_another_thread(self.network.follow_chain_given_server(server))
|
|
self.update()
|
|
|
|
def set_server(self):
|
|
net_params = self.network.get_parameters()
|
|
try:
|
|
server = ServerAddr.from_str_with_inference(str(self.server_e.text()))
|
|
if not server:
|
|
raise Exception("failed to parse server")
|
|
except Exception:
|
|
return
|
|
net_params = net_params._replace(
|
|
server=server,
|
|
auto_connect=self.is_auto_connect(),
|
|
oneserver=self.is_one_server(),
|
|
)
|
|
self.network.run_from_another_thread(self.network.set_parameters(net_params))
|
|
|
|
|
|
class NostrWidget(QWidget, QtEventListener):
|
|
|
|
def __init__(self, network: Network, parent=None):
|
|
super().__init__(parent)
|
|
self.network = network
|
|
self.config = network.config
|
|
vbox = QVBoxLayout()
|
|
self.setLayout(vbox)
|
|
grid = QGridLayout()
|
|
nostr_relays_label = QLabel(self.config.cv.NOSTR_RELAYS.get_short_desc())
|
|
nostr_helpbutton = HelpButton(self.config.cv.NOSTR_RELAYS.get_long_desc())
|
|
grid.addWidget(nostr_relays_label, 0, 0)
|
|
grid.addWidget(nostr_helpbutton, 0, 1)
|
|
vbox.addLayout(grid)
|
|
|
|
self.relays_list = QListWidget()
|
|
self.relay_edit = QLineEdit()
|
|
self.relay_edit.textChanged.connect(self.on_relay_edited)
|
|
vbox.addWidget(self.relays_list)
|
|
vbox.addStretch()
|
|
self.add_button = QPushButton(_('Add'))
|
|
self.add_button.clicked.connect(self.add_relay)
|
|
self.add_button.setEnabled(False)
|
|
remove_button = QPushButton(_('Remove'))
|
|
remove_button.clicked.connect(self.remove_relay)
|
|
reset_button = QPushButton(_('Reset'))
|
|
reset_button.clicked.connect(self.reset_relays)
|
|
buttons = Buttons(self.relay_edit, self.add_button, remove_button, reset_button)
|
|
vbox.addLayout(buttons)
|
|
self.update_list()
|
|
|
|
def on_relay_edited(self, text):
|
|
self.add_button.setEnabled(is_valid_websocket_url(text))
|
|
|
|
def update_list(self):
|
|
self.relays_list.clear()
|
|
for relay in self.config.get_nostr_relays():
|
|
item = QListWidgetItem(relay)
|
|
self.relays_list.addItem(item)
|
|
|
|
def add_relay(self):
|
|
relay = self.relay_edit.text()
|
|
self.config.add_nostr_relay(relay)
|
|
self.update_list()
|
|
|
|
def remove_relay(self):
|
|
item = self.relays_list.currentItem()
|
|
if item is None:
|
|
return
|
|
self.config.remove_nostr_relay(item.text())
|
|
self.update_list()
|
|
|
|
def reset_relays(self):
|
|
self.config.NOSTR_RELAYS = None
|
|
self.update_list()
|