diff --git a/src/sakia/data/entities/identity.py b/src/sakia/data/entities/identity.py index 08b68dc1b5dfe570e487b9099f45258ba5a75876..e0bf250f249587d8d5f2805ed308940a61ddb16a 100644 --- a/src/sakia/data/entities/identity.py +++ b/src/sakia/data/entities/identity.py @@ -28,5 +28,4 @@ class Identity: :return: the document :rtype: duniterpy.documents.Identity """ - return IdentityDoc(3, self.currency, self.pubkey, - self.uid, self.blockstamp, self.signature) + return IdentityDoc(3, self.currency, self.pubkey, self.uid, self.blockstamp, self.signature) diff --git a/src/sakia/gui/dialogs/connection_cfg/controller.py b/src/sakia/gui/dialogs/connection_cfg/controller.py index fc6cb55487d55d213e7fea6ad588e14bba5b9c8d..4b6ab37177679047323d613d77da29dcc00ca824 100644 --- a/src/sakia/gui/dialogs/connection_cfg/controller.py +++ b/src/sakia/gui/dialogs/connection_cfg/controller.py @@ -158,8 +158,7 @@ class ConnectionConfigController(QObject): if mode == ConnectionConfigController.REGISTER: self.view.display_info(self.tr("Broadcasting identity...")) self.view.stream_log("Broadcasting identity...") - password = await self.password_asker.async_exec() - result, connection_identity = await self.model.publish_selfcert(password) + result, connection_identity = await self.model.publish_selfcert() if result[0]: await self.view.show_success(self.model.notification()) else: @@ -191,6 +190,9 @@ class ConnectionConfigController(QObject): self.view.button_next.disconnect() asyncio.ensure_future(self.process()) return + finally: + if self.model.node_connector: + await self.model.node_connector.session.close() self.accept() def check_key(self): diff --git a/src/sakia/gui/dialogs/connection_cfg/model.py b/src/sakia/gui/dialogs/connection_cfg/model.py index 709897eeb4a263ba01f060e0babef358b7bc1d5f..82bf2bd9db094a78e21df373810f4c56a9e8c13a 100644 --- a/src/sakia/gui/dialogs/connection_cfg/model.py +++ b/src/sakia/gui/dialogs/connection_cfg/model.py @@ -30,14 +30,9 @@ class ConnectionConfigModel(QObject): self.identities_processor = identities_processor async def create_connection(self, server, port, secured): - session = aiohttp.ClientSession() - try: - self.node_connector = await NodeConnector.from_address(None, secured, server, port, session) - self.connection = Connection(self.node_connector.node.currency, "", "") - self.node_connector.node.state = Node.ONLINE - except: - session.close() - raise + self.node_connector = await NodeConnector.from_address(None, secured, server, port) + self.connection = Connection(self.node_connector.node.currency, "", "") + self.node_connector.node.state = Node.ONLINE def notification(self): return self.app.parameters.notifications @@ -50,11 +45,13 @@ class ConnectionConfigModel(QObject): self.connection.N = scrypt_params.N self.connection.r = scrypt_params.r self.connection.p = scrypt_params.p + self.connection.password = password self.connection.pubkey = SigningKey(self.connection.salt, password, scrypt_params).pubkey def insert_or_update_connection(self): ConnectionsProcessor(self.app.db.connections_repo).commit_connection(self.connection) NodesProcessor(self.app.db.nodes_repo).commit_node(self.node_connector.node) + self.node_connector.session.close() def insert_or_update_identity(self, identity): self.identities_processor.insert_or_update_identity(identity) @@ -106,11 +103,11 @@ class ConnectionConfigModel(QObject): transactions_processor = TransactionsProcessor.instanciate(self.app) await transactions_processor.initialize_transactions(identity, log_stream) - async def publish_selfcert(self, password): + async def publish_selfcert(self): """" Publish the self certification of the connection identity """ - return await self.app.documents_service.broadcast_identity(self.connection, password) + return await self.app.documents_service.broadcast_identity(self.connection, self.connection.password) async def check_registered(self): """ @@ -120,9 +117,6 @@ class ConnectionConfigModel(QObject): identity = Identity(self.connection.currency, self.connection.pubkey, self.connection.uid) found_identity = Identity(self.connection.currency, self.connection.pubkey, self.connection.uid) - def _parse_uid_certifiers(data): - return identity.uid == data['uid'], identity.uid, data['uid'] - def _parse_uid_lookup(data): timestamp = BlockUID.empty() found_uid = "" @@ -134,11 +128,9 @@ class ConnectionConfigModel(QObject): timestamp = uid_data["meta"]["timestamp"] found_uid = uid_data["uid"] found_identity.timestamp = timestamp # We save the timestamp in the found identity + found_identity.signature = uid_data["self"] return identity.uid == found_uid, identity.uid, found_uid - def _parse_pubkey_certifiers(data): - return identity.pubkey == data['pubkey'], identity.pubkey, data['pubkey'] - def _parse_pubkey_lookup(data): timestamp = BlockUID.empty() found_uid = "" @@ -150,6 +142,7 @@ class ConnectionConfigModel(QObject): timestamp = BlockUID.from_str(uid_data["meta"]["timestamp"]) found_uid = uid_data["uid"] found_identity.timestamp = timestamp # We save the timestamp in the found identity + found_identity.signature = uid_data["self"] if found_uid == identity.uid: found_result = result['pubkey'], found_uid if found_result[1] == identity.uid: @@ -157,27 +150,23 @@ class ConnectionConfigModel(QObject): else: return False, identity.pubkey, None - async def execute_requests(parsers, search): + async def execute_requests(parser, search): tries = 0 - request = bma.wot.certifiers_of nonlocal registered for endpoint in [e for e in self.node_connector.node.endpoints if isinstance(e, BMAEndpoint)]: if not registered[0] and not registered[2]: try: - data = await self.node_connector.safe_request(endpoint, request, req_args={'search': search}) + data = await self.node_connector.safe_request(endpoint, bma.wot.lookup, + req_args={'search': search}) if data: - registered = parsers[request](data) + registered = parser(data) tries += 1 except errors.DuniterError as e: if e.ucode in (errors.NO_MEMBER_MATCHING_PUB_OR_UID, e.ucode == errors.NO_MATCHING_IDENTITY): - if request == bma.wot.certifiers_of: - request = bma.wot.lookup - tries = 0 - else: tries += 1 else: - tries += 1 + raise else: break @@ -187,20 +176,12 @@ class ConnectionConfigModel(QObject): registered = (False, identity.uid, None) # We execute search based on pubkey # And look for account UID - uid_parsers = { - bma.wot.certifiers_of: _parse_uid_certifiers, - bma.wot.lookup: _parse_uid_lookup - } - await execute_requests(uid_parsers, identity.pubkey) + await execute_requests(_parse_uid_lookup, identity.pubkey) # If the uid wasn't found when looking for the pubkey # We look for the uid and check for the pubkey if not registered[0] and not registered[2]: - pubkey_parsers = { - bma.wot.certifiers_of: _parse_pubkey_certifiers, - bma.wot.lookup: _parse_pubkey_lookup - } - await execute_requests(pubkey_parsers, identity.uid) + await execute_requests(_parse_pubkey_lookup, identity.uid) return registered, found_identity diff --git a/src/sakia/gui/main_window/toolbar/controller.py b/src/sakia/gui/main_window/toolbar/controller.py index c0dbd339ff68f0ca335c1f4c6ee9fe1a381d13ea..6ec8802c354fe7cd5b718cb231555d69fe4b7869 100644 --- a/src/sakia/gui/main_window/toolbar/controller.py +++ b/src/sakia/gui/main_window/toolbar/controller.py @@ -1,5 +1,3 @@ -import logging - from PyQt5.QtCore import Qt, QObject from PyQt5.QtWidgets import QDialog, QMessageBox @@ -9,6 +7,7 @@ from sakia.gui.dialogs.certification.controller import CertificationController from sakia.gui.dialogs.transfer.controller import TransferController from sakia.gui.widgets import toast from sakia.gui.widgets.dialogs import QAsyncMessageBox, QAsyncFileDialog, dialog_async_exec +from sakia.gui.password_asker import PasswordAskerDialog from .model import ToolbarModel from .view import ToolbarView @@ -29,7 +28,6 @@ class ToolbarController(QObject): self.model = model self.view.button_certification.clicked.connect(self.open_certification_dialog) self.view.button_send_money.clicked.connect(self.open_transfer_money_dialog) - self.view.action_gen_revokation.triggered.connect(self.action_save_revokation) self.view.action_publish_uid.triggered.connect(self.publish_uid) self.view.button_membership.clicked.connect(self.send_membership_demand) self.view.action_add_connection.triggered.connect(self.open_add_connection_dialog) @@ -53,31 +51,6 @@ class ToolbarController(QObject): self.view.button_send_money.setEnabled(enabled) self.view.button_membership.setEnabled(enabled) - @asyncify - async def action_save_revokation(self, checked=False): - password = await self.password_asker.async_exec() - if self.password_asker.result() == QDialog.Rejected: - return - - raw_document = await self.account.generate_revokation(self.community, password) - # Testable way of using a QFileDialog - selected_files = await QAsyncFileDialog.get_save_filename(self, self.tr("Save a revokation document"), - "", self.tr("All text files (*.txt)")) - if selected_files: - path = selected_files[0] - if not path.endswith('.txt'): - path = "{0}.txt".format(path) - with open(path, 'w') as save_file: - save_file.write(raw_document) - - dialog = QMessageBox(QMessageBox.Information, self.tr("Revokation file"), - self.tr("""<div>Your revokation document has been saved.</div> -<div><b>Please keep it in a safe place.</b></div> -The publication of this document will remove your identity from the network.</p>"""), QMessageBox.Ok, - self) - dialog.setTextFormat(Qt.RichText) - await dialog_async_exec(dialog) - @asyncify async def send_membership_demand(self, checked=False): password = await self.password_asker.async_exec() diff --git a/src/sakia/gui/main_window/toolbar/view.py b/src/sakia/gui/main_window/toolbar/view.py index 9bfe620b7b38eca62d0871359699fb91e3caf73b..3ab97e60650df9028820fe926718275ebed0f11a 100644 --- a/src/sakia/gui/main_window/toolbar/view.py +++ b/src/sakia/gui/main_window/toolbar/view.py @@ -23,11 +23,6 @@ class ToolbarView(QFrame, Ui_SakiaToolbar): tool_menu.addAction(self.action_publish_uid) tool_menu.addAction(self.action_revoke_uid) - menu_advanced = QMenu(self.tr("Advanced"), self.toolbutton_menu) - self.action_gen_revokation = QAction(self.tr("Save revokation document"), menu_advanced) - menu_advanced.addAction(self.action_gen_revokation) - tool_menu.addMenu(menu_advanced) - menu_options = QMenu(self.tr("Options"), self.toolbutton_menu) self.action_add_connection = QAction(self.tr("Add a connection"), menu_options) menu_options.addAction(self.action_add_connection) diff --git a/src/sakia/gui/navigation/controller.py b/src/sakia/gui/navigation/controller.py index 36ff87b6010f43872fbf76fb6c705eab415a606d..d40cac944b4e2b3a1c85670cb61bbcf3b1824950 100644 --- a/src/sakia/gui/navigation/controller.py +++ b/src/sakia/gui/navigation/controller.py @@ -1,5 +1,6 @@ from .model import NavigationModel from .view import NavigationView +from sakia.models.generic_tree import GenericTreeModel from .txhistory.controller import TxHistoryController from .homescreen.controller import HomeScreenController from .network.controller import NetworkController @@ -7,7 +8,12 @@ from .identities.controller import IdentitiesController from .informations.controller import InformationsController from .graphs.wot.controller import WotController from sakia.data.entities import Connection -from PyQt5.QtCore import pyqtSignal, QObject +from PyQt5.QtCore import pyqtSignal, QObject, Qt +from PyQt5.QtWidgets import QMenu, QAction, QMessageBox, QDialog +from PyQt5.QtGui import QCursor +from sakia.decorators import asyncify +from sakia.gui.password_asker import PasswordAskerDialog +from sakia.gui.widgets.dialogs import QAsyncFileDialog, dialog_async_exec class NavigationController(QObject): @@ -36,6 +42,8 @@ class NavigationController(QObject): 'Wot': WotController } self.view.current_view_changed.connect(self.handle_view_change) + self.view.setContextMenuPolicy(Qt.CustomContextMenu) + self.view.customContextMenuRequested.connect(self.tree_context_menu) @classmethod def create(cls, parent, app): @@ -89,3 +97,41 @@ class NavigationController(QObject): raw_node = self.model.add_connection(connection) self.view.add_connection(raw_node) self.parse_node(raw_node) + + def tree_context_menu(self, point): + mapped = self.view.tree_view.mapFromParent(point) + index = self.view.tree_view.indexAt(mapped) + raw_data = self.view.tree_view.model().data(index, GenericTreeModel.ROLE_RAW_DATA) + if raw_data and raw_data["component"] == "Informations": + menu = QMenu(self.view) + action_gen_revokation = QAction(self.tr("Save revokation document"), menu) + menu.addAction(action_gen_revokation) + action_gen_revokation.triggered.connect(lambda c: + self.action_save_revokation(raw_data['misc']['connection'])) + + # Show the context menu. + menu.popup(QCursor.pos()) + + def action_save_revokation(self, connection): + password = PasswordAskerDialog(connection).exec() + if not password: + return + + raw_document = self.model.generate_revokation(connection, password) + # Testable way of using a QFileDialog + selected_files = QAsyncFileDialog.get_save_filename(self, self.tr("Save a revokation document"), + "", self.tr("All text files (*.txt)")) + if selected_files: + path = selected_files[0] + if not path.endswith('.txt'): + path = "{0}.txt".format(path) + with open(path, 'w') as save_file: + save_file.write(raw_document) + + dialog = QMessageBox(QMessageBox.Information, self.tr("Revokation file"), + self.tr("""<div>Your revokation document has been saved.</div> +<div><b>Please keep it in a safe place.</b></div> +The publication of this document will remove your identity from the network.</p>"""), QMessageBox.Ok, + self) + dialog.setTextFormat(Qt.RichText) + dialog.exec() diff --git a/src/sakia/gui/navigation/model.py b/src/sakia/gui/navigation/model.py index f0f4aa253e957116b545583af587399a6b05937d..b6424d1215fd6dea06f6c944e231adc8c8255f2c 100644 --- a/src/sakia/gui/navigation/model.py +++ b/src/sakia/gui/navigation/model.py @@ -38,7 +38,7 @@ class NavigationModel(QObject): def create_node(self, connection): return { - 'title': connection.currency, + 'title': connection.title(), 'component': "Informations", 'dependencies': { 'blockchain_service': self.app.blockchain_services[connection.currency], @@ -124,3 +124,6 @@ class NavigationModel(QObject): return self._current_data['misc'].get('connection', None) else: return None + + def generate_revokation(self, connection, password): + return self.app.documents_service.generate_revokation(connection, password) \ No newline at end of file diff --git a/src/sakia/gui/navigation/navigation.ui b/src/sakia/gui/navigation/navigation.ui index 266315e5655d3fb9fd7c34bf21de42db92fa99ce..74af0ce9b1d4976fa1a730e53d8c580594a83953 100644 --- a/src/sakia/gui/navigation/navigation.ui +++ b/src/sakia/gui/navigation/navigation.ui @@ -28,6 +28,12 @@ <verstretch>0</verstretch> </sizepolicy> </property> + <property name="minimumSize"> + <size> + <width>150</width> + <height>0</height> + </size> + </property> <property name="editTriggers"> <set>QAbstractItemView::NoEditTriggers</set> </property> diff --git a/src/sakia/services/documents.py b/src/sakia/services/documents.py index 33c91a6c9be2f1ad7d18915c3f4e712977a6436a..428860d732f4217fd12abfea77c096227c384a67 100644 --- a/src/sakia/services/documents.py +++ b/src/sakia/services/documents.py @@ -139,7 +139,7 @@ class DocumentsService: certification = Certification(6, connection.currency, connection.pubkey, identity.pubkey, blockUID, None) - key = SigningKey(connection.salt, connection.password, connection.scrypt_params) + key = SigningKey(connection.salt, password, connection.scrypt_params) certification.sign(identity.document(), [key]) signed_cert = certification.signed_raw(identity.document()) self._logger.debug("Certification : {0}".format(signed_cert)) @@ -190,19 +190,18 @@ class DocumentsService: await r.release() return result - async def generate_revokation(self, currency, identity, salt, password): + def generate_revokation(self, connection, password): """ Generate account revokation document for given community - :param str currency: The currency of the identity - :param sakia.data.entities.IdentityDoc identity: The certified identity - :param str salt: The account SigningKey salt + :param sakia.data.entities.Connection connection: The connection of the identity :param str password: The account SigningKey password """ - document = Revocation(PROTOCOL_VERSION, currency, identity.pubkey, "") - self_cert = identity.document() + document = Revocation(2, connection.currency, connection.pubkey, "") + identity = self._identities_processor.get_written(connection.currency, connection.pubkey) + self_cert = identity[0].document() - key = SigningKey(salt, password) + key = SigningKey(connection.salt, password, connection.scrypt_params) document.sign(self_cert, [key]) return document.signed_raw(self_cert)