43cf9b1b383d52abd92d12800f3f5f5cdde89f12
[gc-dialer] / gc_dialer / gc_dialer.py
1 #!/usr/bin/python2.5
2
3
4 """
5 Grandcentral Dialer
6 Python front-end to a wget script to use grandcentral.com to place outbound VOIP calls.
7 (C) 2008 Mark Bergman
8 bergman@merctech.com
9 """
10
11
12 import sys
13 import gc
14 import os
15 import threading
16 import time
17 import re
18 import warnings
19
20 import gobject
21 import gtk
22 import gtk.glade
23
24 try:
25         import hildon
26 except ImportError:
27         hildon = None
28
29 try:
30         import osso
31         try:
32                 import abook
33                 import evolution.ebook as evobook
34         except ImportError:
35                 abook = None
36                 evobook = None
37 except ImportError:
38         osso = None
39
40 try:
41         import conic
42 except ImportError:
43         conic = None
44
45 try:
46         import doctest
47         import optparse
48 except ImportError:
49         doctest = None
50         optparse = None
51
52 from gcbackend import GCDialer
53
54 import socket
55
56
57 socket.setdefaulttimeout(5)
58
59
60 def make_ugly(prettynumber):
61         """
62         function to take a phone number and strip out all non-numeric
63         characters
64
65         >>> make_ugly("+012-(345)-678-90")
66         '01234567890'
67         """
68         uglynumber = re.sub('\D', '', prettynumber)
69         return uglynumber
70
71
72 def make_pretty(phonenumber):
73         """
74         Function to take a phone number and return the pretty version
75         pretty numbers:
76                 if phonenumber begins with 0:
77                         ...-(...)-...-....
78                 if phonenumber begins with 1: ( for gizmo callback numbers )
79                         1 (...)-...-....
80                 if phonenumber is 13 digits:
81                         (...)-...-....
82                 if phonenumber is 10 digits:
83                         ...-....
84         >>> make_pretty("12")
85         '12'
86         >>> make_pretty("1234567")
87         '123-4567'
88         >>> make_pretty("2345678901")
89         '(234)-567-8901'
90         >>> make_pretty("12345678901")
91         '1 (234)-567-8901'
92         >>> make_pretty("01234567890")
93         '+012-(345)-678-90'
94         """
95         if phonenumber is None:
96                 return ""
97
98         if len(phonenumber) < 3:
99                 return phonenumber
100
101         if phonenumber[0] == "0":
102                 prettynumber = ""
103                 prettynumber += "+%s" % phonenumber[0:3]
104                 if 3 < len(phonenumber):
105                         prettynumber += "-(%s)" % phonenumber[3:6]
106                         if 6 < len(phonenumber):
107                                 prettynumber += "-%s" % phonenumber[6:9]
108                                 if 9 < len(phonenumber):
109                                         prettynumber += "-%s" % phonenumber[9:]
110                 return prettynumber
111         elif len(phonenumber) <= 7:
112                 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
113         elif len(phonenumber) > 8 and phonenumber[0] == "1":
114                 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
115         elif len(phonenumber) > 7:
116                 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
117         return prettynumber
118
119
120 class Dialpad(object):
121
122         __app_name__ = "gc_dialer"
123         __version__ = "0.7.0"
124         __app_magic__ = 0xdeadbeef
125
126         _glade_files = [
127                 './gc_dialer.glade',
128                 '../lib/gc_dialer.glade',
129                 '/usr/local/lib/gc_dialer.glade',
130         ]
131
132         def __init__(self):
133                 self._phonenumber = ""
134                 self._prettynumber = ""
135                 self._areacode = "518"
136
137                 self._clipboard = gtk.clipboard_get()
138
139                 self._deviceIsOnline = True
140                 self._callbackNeedsSetup = True
141                 self._recenttime = 0.0
142                 self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
143                 self._recentviewselection = None
144
145                 for path in Dialpad._glade_files:
146                         if os.path.isfile(path):
147                                 self._widgetTree = gtk.glade.XML(path)
148                                 break
149                 else:
150                         self.display_error_message("Cannot find gc_dialer.glade")
151                         gtk.main_quit()
152                         return
153
154                 self._widgetTree.get_widget("about_title").set_label(self._widgetTree.get_widget("about_title").get_label()+"\nVersion "+Dialpad.__version__)
155
156                 #Get the buffer associated with the number display
157                 self._numberdisplay = self._widgetTree.get_widget("numberdisplay")
158                 self.set_number("")
159                 self._notebook = self._widgetTree.get_widget("notebook")
160
161                 self._window = self._widgetTree.get_widget("Dialpad")
162
163                 global hildon
164                 self._app = None
165                 if hildon is not None and isinstance(self._window, gtk.Window):
166                         warnings.warn("Hildon installed but glade file not updated to work with hildon", UserWarning, 2)
167                         hildon = None
168                 elif hildon is not None:
169                         self._app = hildon.Program()
170                         self._window.set_title("Keypad")
171                         self._app.add_window(self._window)
172                         self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
173                         self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
174                         self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
175                 else:
176                         warnings.warn("No Hildon", UserWarning, 2)
177
178                 self._osso = None
179                 self._ebook = None
180                 if osso is not None:
181                         self._osso = osso.Context(Dialpad.__app_name__, Dialpad.__version__, False)
182                         device = osso.DeviceState(self._osso)
183                         device.set_device_state_callback(self._on_device_state_change, 0)
184                         if abook is not None and evobook is not None:
185                                 abook.init_with_name(Dialpad.__app_name__, self._osso)
186                                 self._ebook = evo.open_addressbook("default")
187                         else:
188                                 warnings.warn("No abook and No evolution address book support", UserWarning, 2)
189                 else:
190                         warnings.warn("No OSSO", UserWarning, 2)
191
192                 self._connection = None
193                 if conic is not None:
194                         self._connection = conic.Connection()
195                         self._connection.connect("connection-event", self._on_connection_change, Dialpad.__app_magic__)
196                         self._connection.request_connection(conic.CONNECT_FLAG_NONE)
197                 else:
198                         warnings.warn("No Internet Connectivity API ", UserWarning, 2)
199
200                 callbackMapping = {
201                         # Process signals from buttons
202                         "on_loginbutton_clicked": self._on_loginbutton_clicked,
203                         "on_loginclose_clicked": self._on_loginclose_clicked,
204
205                         "on_dialpad_quit": (lambda data: gtk.main_quit()),
206                         "on_paste": self._on_paste,
207                         "on_clear_number": self._on_clear_number,
208
209                         "on_clearcookies_clicked": self._on_clearcookies_clicked,
210                         "on_notebook_switch_page": self._on_notebook_switch_page,
211                         "on_recentview_row_activated": self._on_recentview_row_activated,
212
213                         "on_digit_clicked": self._on_digit_clicked,
214                         "on_back_clicked": self._on_backspace,
215                         "on_dial_clicked": self._on_dial_clicked,
216                 }
217                 self._widgetTree.signal_autoconnect(callbackMapping)
218
219                 if self._window:
220                         self._window.connect("destroy", gtk.main_quit)
221                         self._window.show_all()
222
223                 self._gcBackend = GCDialer()
224
225                 self.attempt_login(2)
226                 gobject.idle_add(self._init_grandcentral)
227                 # Defer initalization of recent view
228                 gobject.idle_add(self._init_recent_view)
229
230         def _init_grandcentral(self):
231                 """ Deferred initalization of the grandcentral info """
232
233                 if self._gcBackend.is_authed():
234                         if self._gcBackend.get_callback_number() is None:
235                                 self._gcBackend.set_sane_callback()
236                         self.set_account_number()
237
238                 return False
239
240         def _init_recent_view(self):
241                 """ Deferred initalization of the recent view treeview """
242
243                 recentview = self._widgetTree.get_widget("recentview")
244                 recentview.set_model(self._recentmodel)
245                 textrenderer = gtk.CellRendererText()
246
247                 # Add the column to the treeview
248                 column = gtk.TreeViewColumn("Calls", textrenderer, text=1)
249                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
250
251                 recentview.append_column(column)
252
253                 self._recentviewselection = recentview.get_selection()
254                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
255
256                 return False
257
258         def _setup_callback_combo(self):
259                 combobox = self._widgetTree.get_widget("callbackcombo")
260                 self.callbacklist = gtk.ListStore(gobject.TYPE_STRING)
261                 combobox.set_model(self.callbacklist)
262                 combobox.set_text_column(0)
263                 for number, description in self._gcBackend.get_callback_numbers().iteritems():
264                         self.callbacklist.append([make_pretty(number)] )
265
266                 self._widgetTree.get_widget("callbackcombo").get_child().set_text(make_pretty(self._gcBackend.get_callback_number()))
267                 self._callbackNeedsSetup = False
268
269         def populate_recentview(self):
270                 self._recentmodel.clear()
271                 for personsName, phoneNumber, date, action in self._gcBackend.get_recent():
272                         item = (phoneNumber, "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber))
273                         self._recentmodel.append(item)
274                 self._recenttime = time.time()
275
276                 return False
277
278         def attempt_login(self, times = 1):
279                 assert 0 < times, "That was pointless having 0 or less login attempts"
280                 dialog = self._widgetTree.get_widget("login_dialog")
281
282                 while (0 < times) and not self._gcBackend.is_authed():
283                         dialog.run()
284
285                         username = self._widgetTree.get_widget("usernameentry").get_text()
286                         password = self._widgetTree.get_widget("passwordentry").get_text()
287                         self._widgetTree.get_widget("passwordentry").set_text("")
288
289                         loggedIn = self._gcBackend.login(username, password)
290                         dialog.hide()
291                         if loggedIn:
292                                 return True
293                         times -= 1
294
295                 return False
296
297         def display_error_message(self, msg):
298                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
299
300                 def close(dialog, response, editor):
301                         editor.about_dialog = None
302                         dialog.destroy()
303                 error_dialog.connect("response", close, self)
304                 error_dialog.run()
305
306         def set_number(self, number):
307                 self._phonenumber = make_ugly(number)
308                 self._prettynumber = make_pretty(self._phonenumber)
309                 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % ( self._prettynumber ) )
310
311         def set_account_number(self):
312                 accountnumber = self._gcBackend.get_account_number()
313                 self._widgetTree.get_widget("gcnumberlabel").set_label("<span size='23000' weight='bold'>%s</span>" % (accountnumber))
314
315         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
316                 """
317                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
318                 For system_inactivity, we have no background tasks to pause
319
320                 @note Hildon specific
321                 """
322                 if memory_low:
323                         self._gcBackend.clear_caches()
324                         re.purge()
325                         gc.collect()
326
327         def _on_connection_change(self, connection, event, magicIdentifier):
328                 """
329                 @note Hildon specific
330                 """
331                 status = event.get_status()
332                 error = event.get_error()
333                 iap_id = event.get_iap_id()
334                 bearer = event.get_bearer_type()
335
336                 if status == conic.STATUS_CONNECTED:
337                         self._window.set_sensitive(True)
338                         self._deviceIsOnline = True
339                 elif status == conic.STATUS_DISCONNECTED:
340                         self._window.set_sensitive(False)
341                         self._deviceIsOnline = False
342
343         def _on_loginbutton_clicked(self, data=None):
344                 self._widgetTree.get_widget("login_dialog").response(gtk.RESPONSE_OK)
345
346         def _on_loginclose_clicked(self, data=None):
347                 sys.exit(0)
348
349         def _on_clearcookies_clicked(self, data=None):
350                 self._gcBackend.reset()
351                 self._callbackNeedsSetup = True
352                 self._recenttime = 0.0
353                 self._recentmodel.clear()
354                 self._widgetTree.get_widget("callbackcombo").get_child().set_text("")
355
356                 # re-run the inital grandcentral setup
357                 self.attempt_login(2)
358                 gobject.idle_add(self._init_grandcentral)
359
360         def _on_callbackentry_changed(self, data=None):
361                 """
362                 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
363                 """
364                 text = make_ugly(self._widgetTree.get_widget("callbackcombo").get_child().get_text())
365                 if self._gcBackend.is_valid_syntax(text) and text != self._gcBackend.get_callback_number():
366                         self._gcBackend.set_callback_number(text)
367
368         def _on_recentview_row_activated(self, treeview, path, view_column):
369                 model, itr = self._recentviewselection.get_selected()
370                 if not itr:
371                         return
372
373                 self.set_number(self._recentmodel.get_value(itr, 0))
374                 self._notebook.set_current_page(0)
375                 self._recentviewselection.unselect_all()
376
377         def _on_notebook_switch_page(self, notebook, page, page_num):
378                 if page_num == 1 and (time.time() - self._recenttime) > 300:
379                         gobject.idle_add(self.populate_recentview)
380                 elif page_num ==2 and self._callbackNeedsSetup:
381                         gobject.idle_add(self._setup_callback_combo)
382
383                 if hildon:
384                         self._window.set_title(self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text())
385
386         def _on_dial_clicked(self, widget):
387                 """
388                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
389                 """
390                 loggedIn = self.attempt_login(2)
391                 if not loggedIn or not self._gcBackend.is_authed() or self._gcBackend.get_callback_number() == "":
392                         self.display_error_message("Backend link with grandcentral is not working, please try again")
393                         return
394
395                 try:
396                         callSuccess = self._gcBackend.dial(self._phonenumber)
397                 except ValueError, e:
398                         self._gcBackend._msg = e.message
399                         callSuccess = False
400
401                 if not callSuccess:
402                         self.display_error_message(self._gcBackend._msg)
403                 else:
404                         self.set_number("")
405
406                 self._recentmodel.clear()
407                 self._recenttime = 0.0
408
409         def _on_paste(self, data=None):
410                 contents = self._clipboard.wait_for_text()
411                 phoneNumber = re.sub('\D', '', contents)
412                 self.set_number(phoneNumber)
413
414         def _on_clear_number(self, data=None):
415                 self.set_number("")
416
417         def _on_digit_clicked(self, widget):
418                 self.set_number(self._phonenumber + widget.get_name()[5])
419
420         def _on_backspace(self, widget):
421                 self.set_number(self._phonenumber[:-1])
422
423
424 def run_doctest():
425         failureCount, testCount = doctest.testmod()
426         if not failureCount:
427                 print "Tests Successful"
428                 sys.exit(0)
429         else:
430                 sys.exit(1)
431
432
433 def run_dialpad():
434         gtk.gdk.threads_init()
435         title = 'Dialpad'
436         handle = Dialpad()
437         gtk.main()
438         sys.exit(0)
439
440
441 class DummyOptions(object):
442
443         def __init__(self):
444                 self.test = False
445
446
447 if __name__ == "__main__":
448         if hildon is not None:
449                 gtk.set_application_name("Dialer")
450
451         if optparse is not None:
452                 parser = optparse.OptionParser()
453                 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
454                 (options, args) = parser.parse_args()
455         else:
456                 args = []
457                 options = DummyOptions()
458
459         if options.test:
460                 run_doctest()
461         else:
462                 run_dialpad()