Update label sync client for full encryption/decryption both on client and website
[electrum-nvc.git] / plugins / labels.py
1 from electrum.util import print_error
2 from electrum_gui.i18n import _
3 import httplib, urllib
4 import hashlib
5 import json
6 from urlparse import urlparse, parse_qs
7 try:
8     import PyQt4
9 except:
10     sys.exit("Error: Could not import PyQt4 on Linux systems, you may try 'sudo apt-get install python-qt4'")
11
12 from PyQt4.QtGui import *
13 from PyQt4.QtCore import *
14 import PyQt4.QtCore as QtCore
15 import PyQt4.QtGui as QtGui
16 import aes
17 import base64
18 from electrum_gui import bmp, pyqrnative, BasePlugin
19 from electrum_gui.i18n import _
20 from electrum_gui.gui_classic import HelpButton
21
22 class Plugin(BasePlugin):
23     def version(self):
24         return "0.2"
25
26     def encode(self, message):
27         encrypted = aes.encryptData(self.encode_password, unicode(message))
28         encoded_message = base64.b64encode(encrypted)
29
30         return encoded_message
31
32     def decode(self, message):
33         decoded_message = aes.decryptData(self.encode_password, base64.b64decode(unicode(message)) )
34
35         return decoded_message
36
37     def __init__(self, gui):
38         self.target_host = 'labelectrum.herokuapp.com'
39         BasePlugin.__init__(self, gui, 'labels', _('Label Sync'),_('This plugin can sync your labels accross multiple Electrum installs by using a remote database to save your data. Labels are not encrypted, \
40 transactions and addresses are however. This code might increase the load of your wallet with a few micoseconds as it will sync labels on each startup.\n\n\
41 To get started visit http://labelectrum.herokuapp.com/ to sign up for an account.'))
42
43         self.wallet = gui.wallet
44         self.gui = gui
45         self.config = gui.config
46         self.labels = self.wallet.labels
47         self.transactions = self.wallet.transactions
48         self.encode_password = hashlib.sha1(self.config.get("master_public_key")).digest().encode('hex')[:32]
49
50         self.wallet_id = hashlib.sha256(str(self.config.get("master_public_key"))).digest().encode('hex')
51
52         addresses = [] 
53         for k, account in self.wallet.accounts.items():
54             for address in account[0]:
55                 addresses.append(address)
56
57         self.addresses = addresses
58
59     def auth_token(self):
60         return self.config.get("plugin_label_api_key")
61
62     def init_gui(self):
63         if self.is_enabled() and self.auth_token():
64             # If there is an auth token we can try to actually start syncing
65             self.full_pull()
66
67     def is_available(self):
68         return True
69
70     def requires_settings(self):
71         return True
72
73     def set_label(self, item,label, changed):
74         if not changed:
75             return 
76
77         bundle = {"label": {"external_id": self.encode(item), "text": self.encode(label)}}
78         params = json.dumps(bundle)
79         connection = httplib.HTTPConnection(self.target_host)
80         connection.request("POST", ("/api/wallets/%s/labels.json?auth_token=%s" % (self.wallet_id, self.auth_token())), params, {'Content-Type': 'application/json'})
81
82         response = connection.getresponse()
83         if response.reason == httplib.responses[httplib.NOT_FOUND]:
84             return
85         response = json.loads(response.read())
86
87     def settings_dialog(self):
88         def check_for_api_key(api_key):
89             if api_key and len(api_key) > 12:
90               self.config.set_key("plugin_label_api_key", str(self.auth_token_edit.text()))
91               self.upload.setEnabled(True)
92               self.download.setEnabled(True)
93               self.accept.setEnabled(True)
94             else:
95               self.upload.setEnabled(False)
96               self.download.setEnabled(False)
97               self.accept.setEnabled(False)
98
99         d = QDialog(self.gui)
100         layout = QGridLayout(d)
101         layout.addWidget(QLabel("API Key: "),0,0)
102
103         self.auth_token_edit = QLineEdit(self.auth_token())
104         self.auth_token_edit.textChanged.connect(check_for_api_key)
105
106         layout.addWidget(QLabel("Label sync options: "),2,0)
107         layout.addWidget(self.auth_token_edit, 0,1,1,2)
108
109         decrypt_key_text =  QLineEdit(self.encode_password)
110         decrypt_key_text.setReadOnly(True)
111         layout.addWidget(decrypt_key_text, 1,1)
112         layout.addWidget(QLabel("Decryption key: "),1,0)
113         layout.addWidget(HelpButton("This key can be used on the LabElectrum website to decrypt your data in case you want to review it online."),1,2)
114
115         self.upload = QPushButton("Force upload")
116         self.upload.clicked.connect(self.full_push)
117         layout.addWidget(self.upload, 2,1)
118
119         self.download = QPushButton("Force download")
120         self.download.clicked.connect(lambda: self.full_pull(True))
121         layout.addWidget(self.download, 2,2)
122
123         c = QPushButton(_("Cancel"))
124         c.clicked.connect(d.reject)
125
126         self.accept = QPushButton(_("Done"))
127         self.accept.clicked.connect(d.accept)
128
129         layout.addWidget(c,3,1)
130         layout.addWidget(self.accept,3,2)
131
132         check_for_api_key(self.auth_token())
133
134         if d.exec_():
135           return True
136         else:
137           return False
138
139     def toggle(self):
140         enabled = not self.is_enabled()
141         self.set_enabled(enabled)
142         self.init_gui()
143
144         if not self.auth_token() and enabled: # First run, throw plugin settings in your face
145             if self.settings_dialog():
146               self.set_enabled(True)
147               return True
148             else:
149               self.set_enabled(False)
150               return False
151         return enabled
152
153     def full_push(self):
154         if self.do_full_push():
155             QMessageBox.information(None, _("Labels uploaded"), _("Your labels have been uploaded."))
156
157     def full_pull(self, force = False):
158         if self.do_full_pull(force) and force:
159             QMessageBox.information(None, _("Labels synchronized"), _("Your labels have been synchronized."))
160             self.gui.update_history_tab()
161             self.gui.update_completions()
162             self.gui.update_receive_tab()
163             self.gui.update_contacts_tab()
164
165     def do_full_push(self):
166         bundle = {"labels": {}}
167         for key, value in self.labels.iteritems():
168             encoded = self.encode(key)
169             bundle["labels"][encoded] = self.encode(value)
170
171         params = json.dumps(bundle)
172         connection = httplib.HTTPConnection(self.target_host)
173         connection.request("POST", ("/api/wallets/%s/labels/batch.json?auth_token=%s" % (self.wallet_id, self.auth_token())), params, {'Content-Type': 'application/json'})
174
175         response = connection.getresponse()
176         if response.reason == httplib.responses[httplib.NOT_FOUND]:
177             return
178         try:
179             response = json.loads(response.read())
180         except ValueError as e:
181             return False
182
183         if "error" in response:
184             QMessageBox.warning(None, _("Error"),_("Could not sync labels: %s" % response["error"]))
185             return False
186
187         return True
188
189     def do_full_pull(self, force = False):
190         connection = httplib.HTTPConnection(self.target_host)
191         connection.request("GET", ("/api/wallets/%s/labels.json?auth_token=%s" % (self.wallet_id, self.auth_token())),"", {'Content-Type': 'application/json'})
192         response = connection.getresponse()
193         if response.reason == httplib.responses[httplib.NOT_FOUND]:
194             return
195         try:
196             response = json.loads(response.read())
197         except ValueError as e:
198             return False
199
200         if "error" in response:
201             QMessageBox.warning(None, _("Error"),_("Could not sync labels: %s" % response["error"]))
202             return False
203
204         for label in response:
205              decoded_key = self.decode(label["external_id"]) 
206              decoded_label = self.decode(label["text"]) 
207              if force or not self.labels.get(decoded_key):
208                  self.labels[decoded_key] = decoded_label 
209         return True