Skip to content
Snippets Groups Projects
Commit db196c74 authored by inso's avatar inso
Browse files

New revocation component

parent 7e8f5726
No related branches found
No related tags found
No related merge requests found
duniterpy>=0.20.dev0
git+https://github.com/Insoleet/quamash.git@master
asynctest
networkx
attr
\ No newline at end of file
networkx
\ No newline at end of file
from sakia.gui.component.controller import ComponentController
from .view import RevocationView
from .model import RevocationModel
from duniterpy.documents import MalformedDocumentError
from sakia.tools.decorators import asyncify
import asyncio
class RevocationController(ComponentController):
"""
The revocation view
"""
def __init__(self, parent, view, model):
"""
Constructor of the revocation component
:param sakia.gui.revocation.view.revocationView: the view
:param sakia.gui.revocation.model.revocationModel model: the model
"""
super().__init__(parent, view, model)
self.handle_next_step(init=True)
self.view.button_next.clicked.connect(lambda checked: self.handle_next_step(False))
self._steps = (
{
'page': self.ui.page_load_file,
'init': self.init_dialog,
'next': self.revocation_selected
},
{
'page': self.ui.page_destination,
'init': self.init_publication_page,
'next': self.publish
}
)
self._current_step = 0
@classmethod
def create(cls, parent, app, **kwargs):
"""
Instanciate a revocation component
:param sakia.gui.component.controller.ComponentController parent:
:param sakia.core.Application app:
:return: a new revocation controller
:rtype: revocationController
"""
account = kwargs['account']
view = RevocationView(parent.view)
model = RevocationModel(None, app, account)
revocation = cls(parent, view, model)
model.setParent(revocation)
return revocation
@classmethod
def open_dialog(cls, parent, app, account):
"""
Certify and identity
:param sakia.gui.component.controller.ComponentController parent: the parent
:param sakia.core.Application app: the application
:param sakia.core.Account account: the account certifying the identity
:return:
"""
dialog = cls.create(parent, app, account=account)
dialog.refresh()
return dialog.exec()
@property
def view(self) -> RevocationView:
return self._view
@property
def model(self) -> RevocationModel:
return self._model
def handle_next_step(self, init=False):
if self._current_step < len(self._steps) - 1:
if not init:
self.view.button_next.clicked.disconnect(self._steps[self._current_step]['next'])
self._current_step += 1
self._steps[self._current_step]['init']()
self.view.stackedWidget.setCurrentWidget(self._steps[self._current_step]['page'])
self.view.button_next.clicked.connect(self._steps[self._current_step]['next'])
def load_from_file(self):
selected_file = self.view.select_revocation_file()
try:
self.model.load_revocation(selected_file)
except FileNotFoundError:
pass
except MalformedDocumentError:
self.view.malformed_file_error()
self.button_next.setEnabled(False)
def revocation_selected(self):
pass
def init_publication_page(self):
communities_names = self.model.communities_names()
self.view.set_communities_names(communities_names)
def publish(self):
self.view.button_next.setEnabled(False)
if self.view.ask_for_confirmation():
self.accept()
else:
self.view.button_next.setEnabled(True)
@asyncify
async def accept(self):
if self.ui.radio_community.isChecked():
index = self.view.combo_community.currentIndex()
result, error = await self.model.send_to_community(index)
else:
server = self.ui.edit_address.text()
port = self.ui.spinbox_port.value()
result, error = await self.model.send_to_node(server, port)
if result:
self.view.accept()
else:
await self.view.revocation_broadcast_error(error)
def async_exec(self):
future = asyncio.Future()
self.widget.finished.connect(lambda r: future.set_result(r))
self.widget.open()
self.refresh()
return future
def exec(self):
self.widget.exec()
from sakia.gui.component.model import ComponentModel
from duniterpy.documents.certification import Revocation
from duniterpy.api import bma, errors
from sakia.core.net import Node
import aiohttp
class RevocationModel(ComponentModel):
"""
The model of HomeScreen component
"""
def __init__(self, parent, app, account):
super().__init__(parent)
self.app = app
self.account = account
self.revocation_document = None
self.revoked_selfcert = None
def load_revocation(self, path):
"""
Load a revocation document from a file
:param str path:
"""
with open(path, 'r') as file:
file_content = file.read()
self.revocation_document = Revocation.from_signed_raw(file_content)
self.revoked_selfcert = Revocation.extract_self_cert(file_content)
def communities_names(self):
return [c.name for c in self.account.communities]
async def send_to_community(self, index):
community = self.account.communities[index]
responses = await community.bma_access.broadcast(bma.wot.Revoke, {},
{
'revocation': self.revocation_document.signed_raw(
self.revoked_selfcert)
})
result = False, ""
for r in responses:
if r.status == 200:
result = True, (await r.json())
elif not result[0]:
result = False, (await r.text())
else:
await r.release()
return result
async def send_to_node(self, server, port):
session = aiohttp.ClientSession()
try:
node = await Node.from_address(None, server, port, session=session)
conn_handler = node.endpoint.conn_handler()
await bma.wot.Revoke(conn_handler).post(session,
revocation=self.revocation_document.signed_raw(
self.revoked_selfcert))
except (ValueError, errors.DuniterError,
aiohttp.errors.ClientError, aiohttp.errors.DisconnectedError,
aiohttp.errors.TimeoutError) as e:
return False, str(e)
finally:
session.close()
return True, ""
......@@ -166,6 +166,13 @@ QGroupBox::title {
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_target">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
......
from PyQt5.QtWidgets import QDialog, QFileDialog, QMessageBox
from .revocation_uic import Ui_RevocationDialog
from enum import Enum
from sakia.gui.widgets.dialogs import QAsyncMessageBox
from sakia.tools.decorators import asyncify
class RevocationView(QDialog, Ui_RevocationDialog):
"""
Home screen view
"""
class PublicationMode(Enum):
ADDRESS = 0
COMMUNITY = 1
def __init__(self, parent):
"""
Constructor
"""
super().__init__(parent)
self.setupUi(self)
self.button_next.setEnabled(False)
self.button_load.clicked.connect(self.load_from_file)
self.radio_address.toggled.connect(lambda c: self.publication_mode_changed(RevocationView.PublicationMode.ADDRESS))
self.radio_community.toggled.connect(lambda c: self.publication_mode_changed(RevocationView.PublicationMode.COMMUNITY))
self.edit_address.textChanged.connect(self.refresh)
self.spinbox_port.valueChanged.connect(self.refresh)
self.combo_community.currentIndexChanged.connect(self.refresh)
def publication_mode_changed(self, radio):
self.edit_address.setEnabled(radio == RevocationView.PublicationMode.ADDRESS)
self.spinbox_port.setEnabled(radio == RevocationView.PublicationMode.ADDRESS)
self.combo_community.setEnabled(radio == RevocationView.PublicationMode.COMMUNITY)
self.refresh_revocation_label()
def refresh_target(self):
if self.radio_community.isChecked():
target = self.tr(
"All nodes of community {name}".format(name=self.combo_community.currentText()))
elif self.radio_address.isChecked():
target = self.tr("Address {address}:{port}".format(address=self.edit_address.text(),
port=self.spinbox_port.value()))
else:
target = ""
self.label_target.setText("""
<h4>Publication address</h4>
<div>{target}</div>
""".format(target=target))
def select_revocation_file(self):
"""
Get a revocation file using a file dialog
:rtype: str
"""
selected_files = QFileDialog.getOpenFileName(self.widget,
self.tr("Load a revocation file"),
"",
self.tr("All text files (*.txt)"))
selected_file = selected_files[0]
return selected_file
def malformed_file_error(self):
QMessageBox.critical(self, self.tr("Error loading document"),
self.tr("Loaded document is not a revocation document"),
buttons=QMessageBox.Ok)
async def revocation_broadcast_error(self, error):
await QAsyncMessageBox.critical(self, self.tr("Error broadcasting document"),
error)
def show_revoked_selfcert(self, selfcert):
text = self.tr("""
<div>Identity revoked : {uid} (public key : {pubkey}...)</div>
<div>Identity signed on block : {timestamp}</div>
""".format(uid=selfcert.uid,
pubkey=selfcert.pubkey[:12],
timestamp=selfcert.timestamp))
self.label_revocation_content.setText(text)
def set_communities_names(self, names):
self.combo_community.clear()
for name in names:
self.combo_community.addItem(name)
self.radio_community.setChecked(True)
def ask_for_confirmation(self):
answer = QMessageBox.warning(self.widget, self.tr("Revocation"),
self.tr("""<h4>The publication of this document will remove your identity from the network.</h4>
<li>
<li> <b>This identity won't be able to join the targeted community anymore.</b> </li>
<li> <b>This identity won't be able to generate Universal Dividends anymore.</b> </li>
<li> <b>This identity won't be able to certify individuals anymore.</b> </li>
</li>
Please think twice before publishing this document.
"""), QMessageBox.Ok | QMessageBox.Cancel)
return answer == QMessageBox.Ok
@asyncify
async def accept(self):
await QAsyncMessageBox.information(self.widget, self.tr("Revocation broadcast"),
self.tr("The document was successfully broadcasted."))
super().accept()
import asyncio
import aiohttp
import logging
from duniterpy.api import errors
from duniterpy.api import bma
from duniterpy.documents import MalformedDocumentError
from duniterpy.documents.certification import Revocation
from sakia.core.net import Node
from PyQt5.QtWidgets import QDialog, QFileDialog, QMessageBox
from PyQt5.QtCore import QObject
from .widgets.dialogs import QAsyncMessageBox
from ..tools.decorators import asyncify
from ..presentation.revocation_uic import Ui_RevocationDialog
class RevocationDialog(QObject):
"""
A dialog to revoke an identity
"""
def __init__(self, app, account, widget, ui):
"""
Constructor if a certification dialog
:param sakia.core.Application app:
:param sakia.core.Account account:
:param PyQt5.QtWidgets widget: the widget of the dialog
:param sakia.gen_resources.revocation_uic.Ui_RevocationDialog ui: the view of the certification dialog
:return:
"""
super().__init__()
self.widget = widget
self.ui = ui
self.ui.setupUi(self.widget)
self.app = app
self.account = account
self.revocation_document = None
self.revoked_selfcert = None
self._steps = (
{
'page': self.ui.page_load_file,
'init': self.init_dialog,
'next': self.revocation_selected
},
{
'page': self.ui.page_destination,
'init': self.init_publication_page,
'next': self.publish
}
)
self._current_step = 0
self.handle_next_step(init=True)
self.ui.button_next.clicked.connect(lambda checked: self.handle_next_step(False))
def handle_next_step(self, init=False):
if self._current_step < len(self._steps) - 1:
if not init:
self.ui.button_next.clicked.disconnect(self._steps[self._current_step]['next'])
self._current_step += 1
self._steps[self._current_step]['init']()
self.ui.stackedWidget.setCurrentWidget(self._steps[self._current_step]['page'])
self.ui.button_next.clicked.connect(self._steps[self._current_step]['next'])
def init_dialog(self):
self.ui.button_next.setEnabled(False)
self.ui.button_load.clicked.connect(self.load_from_file)
self.ui.radio_address.toggled.connect(lambda c: self.publication_mode_changed("address"))
self.ui.radio_community.toggled.connect(lambda c: self.publication_mode_changed("community"))
self.ui.edit_address.textChanged.connect(self.refresh)
self.ui.spinbox_port.valueChanged.connect(self.refresh)
self.ui.combo_community.currentIndexChanged.connect(self.refresh)
def publication_mode_changed(self, radio):
self.ui.edit_address.setEnabled(radio == "address")
self.ui.spinbox_port.setEnabled(radio == "address")
self.ui.combo_community.setEnabled(radio == "community")
self.refresh()
def load_from_file(self):
selected_files = QFileDialog.getOpenFileName(self.widget,
self.tr("Load a revocation file"),
"",
self.tr("All text files (*.txt)"))
selected_file = selected_files[0]
try:
with open(selected_file, 'r') as file:
file_content = file.read()
self.revocation_document = Revocation.from_signed_raw(file_content)
self.revoked_selfcert = Revocation.extract_self_cert(file_content)
self.refresh()
self.ui.button_next.setEnabled(True)
except FileNotFoundError:
pass
except MalformedDocumentError:
QMessageBox.critical(self.widget, self.tr("Error loading document"),
self.tr("Loaded document is not a revocation document"),
QMessageBox.Ok)
self.ui.button_next.setEnabled(False)
def revocation_selected(self):
pass
def init_publication_page(self):
self.ui.combo_community.clear()
if self.account:
for community in self.account.communities:
self.ui.combo_community.addItem(community.currency)
self.ui.radio_community.setChecked(True)
else:
self.ui.radio_address.setChecked(True)
self.ui.radio_community.setEnabled(False)
def publish(self):
self.ui.button_next.setEnabled(False)
answer = QMessageBox.warning(self.widget, self.tr("Revocation"),
self.tr("""<h4>The publication of this document will remove your identity from the network.</h4>
<li>
<li> <b>This identity won't be able to join the targeted community anymore.</b> </li>
<li> <b>This identity won't be able to generate Universal Dividends anymore.</b> </li>
<li> <b>This identity won't be able to certify individuals anymore.</b> </li>
</li>
Please think twice before publishing this document.
"""), QMessageBox.Ok | QMessageBox.Cancel)
if answer == QMessageBox.Ok:
self.accept()
else:
self.ui.button_next.setEnabled(True)
@asyncify
async def accept(self):
try:
session = aiohttp.ClientSession()
if self.ui.radio_community.isChecked():
community = self.account.communities[self.ui.combo_community.currentIndex()]
await community.bma_access.broadcast(bma.wot.Revoke, {},
{
'revocation': self.revocation_document.signed_raw(self.revoked_selfcert)
})
elif self.ui.radio_address.isChecked():
server = self.ui.edit_address.text()
port = self.ui.spinbox_port.value()
node = await Node.from_address(None, server, port, session=session)
conn_handler = node.endpoint.conn_handler()
await bma.wot.Revoke(conn_handler).post(session,
revocation=self.revocation_document.signed_raw(self.revoked_selfcert))
except (MalformedDocumentError, ValueError, errors.DuniterError,
aiohttp.errors.ClientError, aiohttp.errors.DisconnectedError,
aiohttp.errors.TimeoutError) as e:
await QAsyncMessageBox.critical(self.widget, self.tr("Error broadcasting document"),
str(e))
else:
await QAsyncMessageBox.information(self.widget, self.tr("Revocation broadcast"),
self.tr("The document was successfully broadcasted."))
self.widget.accept()
finally:
session.close()
@classmethod
def open_dialog(cls, app, account):
"""
Certify and identity
:param sakia.core.Application app: the application
:param sakia.core.Account account: the account certifying the identity
:return:
"""
dialog = cls(app, account, QDialog(), Ui_RevocationDialog())
dialog.refresh()
return dialog.exec()
def refresh(self):
if self.revoked_selfcert:
text = self.tr("""
<div>Identity revoked : {uid} (public key : {pubkey}...)</div>
<div>Identity signed on block : {timestamp}</div>
""".format(uid=self.revoked_selfcert.uid,
pubkey=self.revoked_selfcert.pubkey[:12],
timestamp=self.revoked_selfcert.timestamp))
self.ui.label_revocation_content.setText(text)
if self.ui.radio_community.isChecked():
target = self.tr("All nodes of community {name}".format(name=self.ui.combo_community.currentText()))
elif self.ui.radio_address.isChecked():
target = self.tr("Address {address}:{port}".format(address=self.ui.edit_address.text(),
port=self.ui.spinbox_port.value()))
else:
target = ""
self.ui.label_revocation_info.setText("""
<h4>Revocation document</h4>
<div>{text}</div>
<h4>Publication address</h4>
<div>{target}</div>
""".format(text=text,
target=target))
else:
self.ui.label_revocation_content.setText("")
def async_exec(self):
future = asyncio.Future()
self.widget.finished.connect(lambda r: future.set_result(r))
self.widget.open()
self.refresh()
return future
def exec(self):
self.widget.exec()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment