diff --git a/res/ui/generateKeyDialog.ui b/res/ui/generateKeyDialog.ui new file mode 100644 index 0000000000000000000000000000000000000000..5d5baf263b7b37365e524d2de9c5ee795109f658 --- /dev/null +++ b/res/ui/generateKeyDialog.ui @@ -0,0 +1,189 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>GenerateKeyDialog</class> + <widget class="QDialog" name="GenerateKeyDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>269</width> + <height>176</height> + </rect> + </property> + <property name="windowTitle"> + <string>Generate a key</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Key parameters</string> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Your name</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="edit_name"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="placeholderText"> + <string>John Doo</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <item> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Passphrase</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="edit_password"> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Type it again</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="edit_password_bis"> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QLabel" name="label_errors"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="button_box"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>button_box</sender> + <signal>accepted()</signal> + <receiver>GenerateKeyDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>button_box</sender> + <signal>rejected()</signal> + <receiver>GenerateKeyDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>edit_name</sender> + <signal>textEdited(QString)</signal> + <receiver>GenerateKeyDialog</receiver> + <slot>check()</slot> + <hints> + <hint type="sourcelabel"> + <x>165</x> + <y>42</y> + </hint> + <hint type="destinationlabel"> + <x>134</x> + <y>87</y> + </hint> + </hints> + </connection> + <connection> + <sender>edit_password</sender> + <signal>textEdited(QString)</signal> + <receiver>GenerateKeyDialog</receiver> + <slot>check()</slot> + <hints> + <hint type="sourcelabel"> + <x>167</x> + <y>73</y> + </hint> + <hint type="destinationlabel"> + <x>134</x> + <y>87</y> + </hint> + </hints> + </connection> + <connection> + <sender>edit_password_bis</sender> + <signal>textEdited(QString)</signal> + <receiver>GenerateKeyDialog</receiver> + <slot>check()</slot> + <hints> + <hint type="sourcelabel"> + <x>170</x> + <y>104</y> + </hint> + <hint type="destinationlabel"> + <x>134</x> + <y>87</y> + </hint> + </hints> + </connection> + </connections> + <slots> + <slot>check()</slot> + </slots> +</ui> diff --git a/res/ui/mainwindow.ui b/res/ui/mainwindow.ui index dd455633e42dfcebc9b1dd3322ce9cb5b6b9709d..01f0a8ca8ae00aaf5a8ca38236abdc1dba2e309d 100644 --- a/res/ui/mainwindow.ui +++ b/res/ui/mainwindow.ui @@ -130,7 +130,9 @@ <addaction name="menu_change_account"/> <addaction name="action_add_account"/> <addaction name="separator"/> - <addaction name="action_save"/> + <addaction name="action_export"/> + <addaction name="action_import"/> + <addaction name="separator"/> <addaction name="action_quit"/> </widget> <widget class="QMenu" name="menu_contacts"> @@ -233,6 +235,16 @@ <string>Configure</string> </property> </action> + <action name="action_import"> + <property name="text"> + <string>Import</string> + </property> + </action> + <action name="action_export"> + <property name="text"> + <string>Export</string> + </property> + </action> </widget> <resources/> <connections> @@ -252,22 +264,6 @@ </hint> </hints> </connection> - <connection> - <sender>action_save</sender> - <signal>triggered()</signal> - <receiver>MainWindow</receiver> - <slot>save()</slot> - <hints> - <hint type="sourcelabel"> - <x>-1</x> - <y>-1</y> - </hint> - <hint type="destinationlabel"> - <x>225</x> - <y>199</y> - </hint> - </hints> - </connection> <connection> <sender>action_quit</sender> <signal>triggered()</signal> @@ -348,14 +344,47 @@ </hint> </hints> </connection> + <connection> + <sender>action_import</sender> + <signal>triggered()</signal> + <receiver>MainWindow</receiver> + <slot>import_account()</slot> + <hints> + <hint type="sourcelabel"> + <x>-1</x> + <y>-1</y> + </hint> + <hint type="destinationlabel"> + <x>248</x> + <y>218</y> + </hint> + </hints> + </connection> + <connection> + <sender>action_export</sender> + <signal>triggered()</signal> + <receiver>MainWindow</receiver> + <slot>export_account()</slot> + <hints> + <hint type="sourcelabel"> + <x>-1</x> + <y>-1</y> + </hint> + <hint type="destinationlabel"> + <x>248</x> + <y>218</y> + </hint> + </hints> + </connection> </connections> <slots> <slot>open_add_account_dialog()</slot> - <slot>save()</slot> + <slot>import_account()</slot> <slot>open_transfer_money_dialog()</slot> <slot>open_add_contact_dialog()</slot> <slot>open_configure_account_dialog()</slot> <slot>refresh_main_window()</slot> <slot>refresh_wallet_content(QModelIndex)</slot> + <slot>export_account()</slot> </slots> </ui> diff --git a/src/cutecoin/core/__init__.py b/src/cutecoin/core/__init__.py index 05fc098b10c6f04575e91bbe3c7c7b8729a6a2ad..7be79c4fd556990173769a9518b9dd92816a888d 100644 --- a/src/cutecoin/core/__init__.py +++ b/src/cutecoin/core/__init__.py @@ -7,11 +7,14 @@ Created on 1 févr. 2014 import os import logging import json +import tarfile import gnupg from cutecoin.core import config -from cutecoin.tools.exceptions import KeyAlreadyUsed +from cutecoin.tools.exceptions import NameAlreadyExists, BadAccountFile from cutecoin.models.account import Account +from cutecoin.models.account.communities import Communities +from cutecoin.models.account.wallets import Wallets class Core(object): @@ -36,13 +39,22 @@ class Core(object): if name == a.name: return a - def add_account(self, account): + def create_account(self, name): for a in self.accounts: - if a.keyid == account.keyid: - raise KeyAlreadyUsed(account, account.keyid, a) - + if a.name == name: + raise NameAlreadyExists(name) + + account_path = os.path.join(config.parameters['home'], name) + if not os.path.exists(account_path): + logging.info("Creating account directory") + os.makedirs(account_path) + account = Account.create(name, + Communities.create(), + Wallets.create(), + config.parameters) self.accounts.append(account) self.current_account = account + return account def del_account(self, account): self.accounts.remove(account) @@ -61,19 +73,55 @@ class Core(object): data = json.load(json_data) json_data.close() - for accountData in data['localAccounts']: - self.accounts.append(Account.load(accountData)) + for account_name in data['local_accounts']: + account_path = os.path.join(config.parameters['home'], + account_name, 'properties') + json_data = open(account_path, 'r') + data = json.load(json_data) + self.accounts.append(Account.load(data)) - def save(self): + def save(self, account): with open(config.parameters['data'], 'w') as outfile: json.dump(self.jsonify(), outfile, indent=4, sort_keys=True) + account_path = os.path.join(config.parameters['home'], + account.name, 'properties') + with open(account_path, 'w') as outfile: + json.dump(account.jsonify(), outfile, indent=4, sort_keys=True) + + def import_account(self, file, name): + with tarfile.open(file, "r") as tar: + path = os.path.join(config.parameters['home'], + name) + for obj in ["properties", "keyring", "secretkeyring"]: + try: + tar.getmember(obj) + except KeyError: + raise BadAccountFile(file) + return + tar.extractall(path) + + account_path = os.path.join(config.parameters['home'], + name, 'properties') + json_data = open(account_path, 'r') + data = json.load(json_data) + account = Account.load(data) + self.accounts.append(account) + self.save(account) + + def export_account(self, file, account): + with tarfile.open(file, "w") as tar: + for file in ["properties", "keyring", "secretkeyring"]: + path = os.path.join(config.parameters['home'], + account.name, file) + tar.add(path, file) + def jsonify_accounts(self): data = [] for account in self.accounts: - data.append(account.jsonify()) + data.append(account.name) return data def jsonify(self): - data = {'localAccounts': self.jsonify_accounts()} + data = {'local_accounts': self.jsonify_accounts()} return data diff --git a/src/cutecoin/gui/mainWindow.py b/src/cutecoin/gui/mainWindow.py index 92202f5b329eeafa3d193272ec9940ab8f4f17d7..c34f4cee9afb39ae1fbef831223167b1eaca8334 100644 --- a/src/cutecoin/gui/mainWindow.py +++ b/src/cutecoin/gui/mainWindow.py @@ -4,12 +4,13 @@ Created on 1 févr. 2014 @author: inso ''' from cutecoin.gen_resources.mainwindow_uic import Ui_MainWindow -from PyQt5.QtWidgets import QMainWindow, QAction +from PyQt5.QtWidgets import QMainWindow, QAction, QFileDialog from PyQt5.QtCore import QSignalMapper, QModelIndex from cutecoin.gui.processConfigureAccount import ProcessConfigureAccount from cutecoin.gui.transferMoneyDialog import TransferMoneyDialog from cutecoin.gui.communityTabWidget import CommunityTabWidget from cutecoin.gui.addContactDialog import AddContactDialog +from cutecoin.gui.importAccountDialog import ImportAccountDialog from cutecoin.models.account.wallets.listModel import WalletsListModel from cutecoin.models.coin.listModel import CoinsListModel from cutecoin.models.transaction.sentListModel import SentListModel @@ -39,9 +40,6 @@ class MainWindow(QMainWindow, Ui_MainWindow): dialog.accepted.connect(self.refresh) dialog.exec_() - def save(self): - self.core.save() - def action_change_account(self, account_name): self.core.current_account = self.core.get_account(account_name) logging.info('Changing account to ' + self.core.current_account.name) @@ -122,6 +120,24 @@ class MainWindow(QMainWindow, Ui_MainWindow): def refresh_wallet_content(self, index): if index.isValid(): current_wallet = self.core.current_account.wallets[index.row()] - self.list_wallet_content.setModel(CoinsListModel(current_wallet, current_wallet.coins)) + self.list_wallet_content.setModel(CoinsListModel(current_wallet, + current_wallet.coins)) else: self.list_wallet_content.setModel(CoinsListModel(None, [])) + + def import_account(self): + dialog = ImportAccountDialog(self.core, self) + dialog.accepted.connect(self.refresh) + dialog.exec_() + + def export_account(self): + selected_file = QFileDialog.getSaveFileName(self, + "Export an account", + "", + "All account files (*.acc)") + path = "" + if selected_file[0][-4:] == ".acc": + path = selected_file[0] + else: + path = selected_file[0] + ".acc" + self.core.export_account(path, self.core.current_account) diff --git a/src/cutecoin/gui/processConfigureAccount.py b/src/cutecoin/gui/processConfigureAccount.py index c7171714a27a57de6dccc9119f005a1bc070e56e..343f502405a7cb5550bdbebb81466bb9b3f98774 100644 --- a/src/cutecoin/gui/processConfigureAccount.py +++ b/src/cutecoin/gui/processConfigureAccount.py @@ -7,17 +7,11 @@ from cutecoin.gen_resources.accountConfigurationDialog_uic import Ui_AccountConf from cutecoin.gui.generateKeyDialog import GenerateKeyDialog from cutecoin.gui.processConfigureCommunity import ProcessConfigureCommunity from cutecoin.models.account.communities.listModel import CommunitiesListModel -from cutecoin.tools.exceptions import KeyAlreadyUsed -from cutecoin.models.account import Account -from cutecoin.models.account.communities import Communities -from cutecoin.models.account.wallets import Wallets +from cutecoin.tools.exceptions import KeyAlreadyUsed, Error from cutecoin.models.node import Node -from cutecoin.core import config from PyQt5.QtWidgets import QDialog, QErrorMessage, QFileDialog, QMessageBox -import gnupg - class Step(): def __init__(self, config_dialog, previous_step=None, next_step=None): @@ -38,11 +32,8 @@ class StepPageInit(Step): def process_next(self): if self.config_dialog.account is None: - self.config_dialog.account = Account.create( - self.config_dialog.edit_account_name.text(), - Communities.create(), - Wallets.create(), - config.parameters) + name = self.config_dialog.edit_account_name.text() + self.config_dialog.account = self.config_dialog.core.create_account(name) else: name = self.config_dialog.edit_account_name.text() self.config_dialog.account.name = name @@ -193,11 +184,15 @@ class ProcessConfigureAccount(QDialog, Ui_AccountConfigurationDialog): def next(self): if self.step.next_step is not None: if self.step.is_valid(): - self.step.process_next() - self.step = self.step.next_step - next_index = self.stacked_pages.currentIndex() + 1 - self.stacked_pages.setCurrentIndex(next_index) - self.step.display_page() + try: + self.step.process_next() + self.step = self.step.next_step + next_index = self.stacked_pages.currentIndex() + 1 + self.stacked_pages.setCurrentIndex(next_index) + self.step.display_page() + except Error as e: + QErrorMessage(self).showMessage(e.message) + else: self.accept() @@ -215,5 +210,6 @@ class ProcessConfigureAccount(QDialog, Ui_AccountConfigurationDialog): self.core.add_account(self.account) except KeyAlreadyUsed as e: QErrorMessage(self).showMessage(e.message) + self.core.save(self.account) self.accepted.emit() self.close() diff --git a/src/cutecoin/models/account/__init__.py b/src/cutecoin/models/account/__init__.py index 635645d450f4f67e1443a90edf88ff009d3b3d0b..b9dd695c92e3855214541972867de8920755fbcc 100644 --- a/src/cutecoin/models/account/__init__.py +++ b/src/cutecoin/models/account/__init__.py @@ -8,6 +8,7 @@ import ucoin import gnupg import logging import json +import os from cutecoin.models.account.wallets import Wallets from cutecoin.models.account.communities import Communities from cutecoin.models.community import Community @@ -44,8 +45,8 @@ class Account(object): ''' Constructor ''' - keyring = confpath['home'] + name + "_keyring" - secret_keyring = confpath['home'] + name + "_secretkeyring" + keyring = os.path.join(confpath['home'], name, "keyring") + secret_keyring = os.path.join(confpath['home'], name, "secretkeyring") account = cls('', name, communities, wallets, [], keyring, secret_keyring) return account diff --git a/src/cutecoin/tools/exceptions.py b/src/cutecoin/tools/exceptions.py index c40723ccf8a2067fa3cc1251a00591bf22ea2b94..2b3468f5c8f641b9632524b7e8cfbe9749bf4049 100644 --- a/src/cutecoin/tools/exceptions.py +++ b/src/cutecoin/tools/exceptions.py @@ -83,3 +83,39 @@ class KeyAlreadyUsed(Error): keyid + " is already used by " + found_account.name) + + +class NameAlreadyExists(Error): + + ''' + Exception raised trying to add an account using + a key already used for another account. + ''' + + def __init__(self, account): + ''' + Constructor + ''' + super( + KeyAlreadyUsed, + self) .__init__( + "Cannot add account " + + account.name + + " the name already exists") + + +class BadAccountFile(Error): + + ''' + Exception raised trying to add an account using + a key already used for another account. + ''' + + def __init__(self, path): + ''' + Constructor + ''' + super( + BadAccountFile, + self) .__init__( + "File " + path + " is not an account file")