e14ca1098a0c77e711cf1dc6b7eccd4dea631e7b
[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 makeugly(prettynumber):
61         """
62         function to take a phone number and strip out all non-numeric
63         characters
64
65         >>> makeugly("+012-(345)-678-90")
66         '01234567890'
67         """
68         uglynumber = re.sub('\D', '', prettynumber)
69         return uglynumber
70
71
72 def makepretty(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         >>> makepretty("12")
85         '12'
86         >>> makepretty("1234567")
87         '123-4567'
88         >>> makepretty("2345678901")
89         '(234)-567-8901'
90         >>> makepretty("12345678901")    
91         '1 (234)-567-8901'
92         >>> makepretty("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                 self.clipboard = gtk.clipboard_get()
137                 self.recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
138                 self.recentviewselection = None
139                 self.callbackNeedsSetup = True
140                 self.recenttime = 0.0
141
142                 for path in Dialpad._glade_files:
143                         if os.path.isfile(path):
144                                 self.wTree = gtk.glade.XML(path)
145                                 break
146                 else:
147                         self.ErrPopUp("Cannot find gc_dialer.glade")
148                         gtk.main_quit()
149                         return
150
151                 self.wTree.get_widget("about_title").set_label(self.wTree.get_widget("about_title").get_label()+"\nVersion "+Dialpad.__version__)
152
153                 #Get the buffer associated with the number display
154                 self.numberdisplay = self.wTree.get_widget("numberdisplay")
155                 self.setNumber("")
156                 self.notebook = self.wTree.get_widget("notebook")
157
158                 self.window = self.wTree.get_widget("Dialpad")
159
160                 global hildon
161                 self.app = None
162                 if hildon is not None and isinstance(self.window, gtk.Window):
163                         warnings.warn("Hildon installed but glade file not updated to work with hildon", UserWarning, 2)
164                         hildon = None
165                 elif hildon is not None:
166                         self.app = hildon.Program()
167                         self.window.set_title("Keypad")
168                         self.app.add_window(self.window)
169                         self.wTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
170                         self.wTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
171                         self.wTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
172                 else:
173                         warnings.warn("No Hildon", UserWarning, 2)
174
175                 self.osso = None
176                 self.ebook = None
177                 if osso is not None:
178                         self.osso = osso.Context(Dialpad.__app_name__, Dialpad.__version__, False)
179                         device = osso.DeviceState(self.osso)
180                         device.set_device_state_callback(self._on_device_state_change, 0)
181                         if abook is not None and evobook is not None:
182                                 abook.init_with_name(Dialpad.__app_name__, self.osso)
183                                 self.ebook = evo.open_addressbook("default")
184                         else:
185                                 warnings.warn("No abook and No evolution address book support", UserWarning, 2)
186                 else:
187                         warnings.warn("No OSSO", UserWarning, 2)
188
189                 self.connection = None
190                 if conic is not None:
191                         self.connection = conic.Connection()
192                         self.connection.connect("connection-event", self._on_connection_change, Dialpad.__app_magic__)
193                         self.connection.request_connection(conic.CONNECT_FLAG_NONE)
194                 else:
195                         warnings.warn("No Internet Connectivity API ", UserWarning, 2)
196
197                 if self.window:
198                         self.window.connect("destroy", gtk.main_quit)
199                         self.window.show_all()
200
201                 callbackMapping = {
202                         # Process signals from buttons
203                         "on_digit_clicked"  : self._on_digit_clicked,
204                         "on_dial_clicked"    : self._on_dial_clicked,
205                         "on_loginbutton_clicked" : self._on_loginbutton_clicked,
206                         "on_loginclose_clicked" : self._on_loginclose_clicked,
207                         "on_clearcookies_clicked" : self._on_clearcookies_clicked,
208                         "on_notebook_switch_page" : self._on_notebook_switch_page,
209                         "on_recentview_row_activated" : self._on_recentview_row_activated,
210                         "on_back_clicked" : self._on_backspace
211                 }
212                 self.wTree.signal_autoconnect(callbackMapping)
213                 self.wTree.get_widget("callbackcombo").get_child().connect("changed", self._on_callbackentry_changed)
214
215                 # Defer initalization of recent view
216                 self.gcd = GCDialer()
217
218                 self.attemptLogin(2)
219                 gobject.idle_add(self._init_grandcentral)
220                 gobject.idle_add(self._init_recent_view)
221
222         def _init_grandcentral(self):
223                 """ deferred initalization of the grandcentral info """
224                 
225                 try:
226                         if self.gcd.isAuthed():
227                                 if self.gcd.getCallbackNumber() is None:
228                                         self.gcd.setSaneCallback()
229                 except:
230                         pass
231                 
232                 self.setAccountNumber()
233                 print "exit init_gc"
234                 return False
235
236         def _init_recent_view(self):
237                 """ deferred initalization of the recent view treeview """
238
239                 recentview = self.wTree.get_widget("recentview")
240                 recentview.set_model(self.recentmodel)
241                 textrenderer = gtk.CellRendererText()
242
243                 # Add the column to the treeview
244                 column = gtk.TreeViewColumn("Calls", textrenderer, text=1)
245                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
246
247                 recentview.append_column(column)
248
249                 self.recentviewselection = recentview.get_selection()
250                 self.recentviewselection.set_mode(gtk.SELECTION_SINGLE)
251
252                 return False
253
254         def _setupCallbackCombo(self):
255                 combobox = self.wTree.get_widget("callbackcombo")
256                 self.callbacklist = gtk.ListStore(gobject.TYPE_STRING)
257                 combobox.set_model(self.callbacklist)
258                 combobox.set_text_column(0)
259                 for number, description in self.gcd.getCallbackNumbers().iteritems():
260                         self.callbacklist.append([makepretty(number)] )
261
262                 self.wTree.get_widget("callbackcombo").get_child().set_text(makepretty(self.gcd.getCallbackNumber()))
263                 self.callbackNeedsSetup = False
264
265         def populate_recentview(self):
266                 print "Populating"
267                 self.recentmodel.clear()
268                 for item in self.gcd.get_recent():
269                         self.recentmodel.append(item)
270                 self.recenttime = time.time()
271
272                 return False
273
274         def attemptLogin(self, times = 1):
275                 dialog = self.wTree.get_widget("login_dialog")
276
277                 while (0 < times) and not self.gcd.isAuthed():
278                         if dialog.run() != gtk.RESPONSE_OK:
279                                 times = 0
280                                 continue
281
282                         username = self.wTree.get_widget("usernameentry").get_text()
283                         password = self.wTree.get_widget("passwordentry").get_text()
284                         self.wTree.get_widget("passwordentry").set_text("")
285                         print "Attempting login"
286                         self.gcd.login(username, password)
287                         print "hiding dialog"
288                         dialog.hide()
289                         times = times - 1
290
291                 return False
292
293         def ErrPopUp(self, msg):
294                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
295
296                 def close(dialog, response, editor):
297                         editor.about_dialog = None
298                         dialog.destroy()
299                 error_dialog.connect("response", close, self)
300                 error_dialog.run()
301
302         def setNumber(self, number):
303                 self.phonenumber = makeugly(number)
304                 self.prettynumber = makepretty(self.phonenumber)
305                 self.numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % ( self.prettynumber ) )
306
307         def setAccountNumber(self):
308                 accountnumber = self.gcd.getAccountNumber()
309                 self.wTree.get_widget("gcnumberlabel").set_label("<span size='23000' weight='bold'>%s</span>" % (accountnumber))
310
311         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
312                 """
313                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
314                 For system_inactivity, we have no background tasks to pause
315
316                 @note Hildon specific
317                 """
318                 if memory_low:
319                         self.gcd.clear_caches()
320                         re.purge()
321                         gc.collect()
322
323         def _on_connection_change(self, connection, event, magicIdentifier):
324                 """
325                 @note Hildon specific
326                 """
327                 status = event.get_status()
328                 error = event.get_error()
329                 iap_id = event.get_iap_id()
330                 bearer = event.get_bearer_type()
331
332                 if status == conic.STATUS_CONNECTED:
333                         self.window.set_sensitive(True)
334                 elif status == conic.STATUS_DISCONNECTED:
335                         self.window.set_sensitive(False)
336
337         def _on_loginbutton_clicked(self, data=None):
338                 self.wTree.get_widget("login_dialog").response(gtk.RESPONSE_OK)
339
340         def _on_loginclose_clicked(self, data=None):
341                 sys.exit(0)
342
343         def _on_clearcookies_clicked(self, data=None):
344                 self.gcd.reset()
345                 self.callbackNeedsSetup = True
346                 self.recenttime = 0.0
347                 self.recentmodel.clear()
348                 self.wTree.get_widget("callbackcombo").get_child().set_text("")
349         
350                 # re-run the inital grandcentral setup
351                 self.attemptLogin(2)
352                 gobject.idle_add(self._init_grandcentral)
353
354         def _on_callbackentry_changed(self, data=None):
355                 text = makeugly(self.wTree.get_widget("callbackcombo").get_child().get_text())
356                 if self.gcd.validate(text) and text != self.gcd.getCallbackNumber():
357                         self.gcd.setCallbackNumber(text)
358
359         def _on_recentview_row_activated(self, treeview, path, view_column):
360                 model, itr = self.recentviewselection.get_selected()
361                 if not itr:
362                         return
363
364                 self.setNumber(self.recentmodel.get_value(itr, 0))
365                 self.notebook.set_current_page(0)
366                 self.recentviewselection.unselect_all()
367
368         def _on_notebook_switch_page(self, notebook, page, page_num):
369                 if page_num == 1 and (time.time() - self.recenttime) > 300:
370                         gobject.idle_add(self.populate_recentview)
371                 elif page_num ==2 and self.callbackNeedsSetup:
372                         gobject.idle_add(self._setupCallbackCombo)
373                 if hildon:
374                         try:
375                                 self.window.set_title(self.notebook.get_tab_label(self.notebook.get_nth_page(page_num)).get_text())
376                         except:
377                                 self.window.set_title("")
378
379         def _on_dial_clicked(self, widget):
380                 self.attemptLogin(3)
381
382                 if not self.gcd.isAuthed() or self.gcd.getCallbackNumber() == "":
383                         self.ErrPopUp("Backend link with grandcentral is not working, please try again")
384                         return
385
386                 try:
387                         callSuccess = self.gcd.dial(self.phonenumber)
388                 except ValueError, e:
389                         self.gcd._msg = e.message
390                         callSuccess = False
391
392                 if not callSuccess:
393                         self.ErrPopUp(self.gcd._msg)
394                 else:
395                         self.setNumber("")
396
397                 self.recentmodel.clear()
398                 self.recenttime = 0.0
399         
400         def _on_paste(self, data=None):
401                 contents = self.clipboard.wait_for_text()
402                 phoneNumber = re.sub('\D', '', contents)
403                 self.setNumber(phoneNumber)
404         
405         def _on_digit_clicked(self, widget):
406                 self.setNumber(self.phonenumber + widget.get_name()[5])
407
408         def _on_backspace(self, widget):
409                 self.setNumber(self.phonenumber[:-1])
410
411
412 def run_doctest():
413         failureCount, testCount = doctest.testmod()
414         if not failureCount:
415                 print "Tests Successful"
416                 sys.exit(0)
417         else:
418                 sys.exit(1)
419
420
421 def run_dialpad():
422         gtk.gdk.threads_init()
423         title = 'Dialpad'
424         handle = Dialpad()
425         gtk.main()
426         sys.exit(0)
427
428
429 class DummyOptions(object):
430         def __init__(self):
431                 self.test = False
432
433
434 if __name__ == "__main__":
435         if hildon:
436                 gtk.set_application_name("Dialer")
437
438         try:
439                 parser = optparse.OptionParser()
440                 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
441                 (options, args) = parser.parse_args()
442         except:
443                 args = []
444                 options = DummyOptions()
445
446         if options.test:
447                 run_doctest()
448         else:
449                 run_dialpad()