d4d4fe63ea0d8f03e59255d754ecf33c08e31a64
[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
19 import warnings
20
21 import gobject
22 import gtk
23 import gtk.glade
24
25 try:
26         import hildon
27 except ImportError:
28         hildon = None
29
30 #try:
31 #       if hasattr(gtk, "Builder"):
32 #               #detected that this is not a legacy system
33 #               raise ImportError 
34 #       #Legacy support
35 #       import gtk.glade
36 #except ImportError:
37 #       gtk.glade = None
38
39 try:
40         import osso
41         try:
42                 import abook
43                 import evolution.ebook as evobook
44         except ImportError:
45                 abook = None
46                 evobook = None
47 except ImportError:
48         osso = None
49
50 try:
51         import doctest
52         import optparse
53 except ImportError:
54         doctest = None
55         optparse = None
56
57 from gcbackend import GCDialer
58
59 import socket
60
61
62 socket.setdefaulttimeout(5)
63
64
65 def makeugly(prettynumber):
66         """
67         function to take a phone number and strip out all non-numeric
68         characters
69
70         >>> makeugly("+012-(345)-678-90")
71         '01234567890'
72         """
73         uglynumber = re.sub('\D', '', prettynumber)
74         return uglynumber
75
76
77 def makepretty(phonenumber):
78         """
79         Function to take a phone number and return the pretty version
80          pretty numbers:
81                 if phonenumber begins with 0:
82                         ...-(...)-...-....
83                 if phonenumber begins with 1: ( for gizmo callback numbers )
84                         1 (...)-...-....
85                 if phonenumber is 13 digits:
86                         (...)-...-....
87                 if phonenumber is 10 digits:
88                         ...-....
89         >>> makepretty("12")
90         '12'
91         >>> makepretty("1234567")
92         '123-4567'
93         >>> makepretty("2345678901")
94         '(234)-567-8901'
95         >>> makepretty("12345678901")    
96         '1 (234)-567-8901'
97         >>> makepretty("01234567890")
98         '+012-(345)-678-90'
99         """
100         if phonenumber is None:
101                 return ""
102
103         if len(phonenumber) < 3:
104                 return phonenumber
105
106         if phonenumber[0] == "0":
107                 prettynumber = ""
108                 prettynumber += "+%s" % phonenumber[0:3]
109                 if 3 < len(phonenumber):
110                         prettynumber += "-(%s)" % phonenumber[3:6]
111                         if 6 < len(phonenumber):
112                                 prettynumber += "-%s" % phonenumber[6:9]
113                                 if 9 < len(phonenumber):
114                                         prettynumber += "-%s" % phonenumber[9:]
115                 return prettynumber
116         elif len(phonenumber) <= 7:
117                 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
118         elif len(phonenumber) > 8 and phonenumber[0] == "1":
119                 prettynumber = "1 (%s)-%s-%s" %(phonenumber[1:4], phonenumber[4:7], phonenumber[7:]) 
120         elif len(phonenumber) > 7:
121                 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
122         return prettynumber
123
124
125 class Dialpad(object):
126
127         __app_name__ = "gc_dialer"
128         __version__ = "0.7.0"
129
130
131         def __init__(self):
132                 self.phonenumber = ""
133                 self.prettynumber = ""
134                 self.areacode = "518"
135                 self.clipboard = gtk.clipboard_get()
136                 self.recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
137                 self.recentviewselection = None
138                 self.callbackNeedsSetup = True
139                 self.recenttime = 0.0
140
141                 for path in [ './gc_dialer.glade',
142                                 '../lib/gc_dialer.glade',
143                                 '/usr/local/lib/gc_dialer.glade' ]:
144                         if os.path.isfile(path):
145                                 #if gtk.glade is None:
146                                 #       self.wTree = gtk.Builder()
147                                 #       self.wTree.add_from_file(path)
148                                 #else:
149                                 self.wTree = gtk.glade.XML(path)
150                                 break
151                 else:
152                         self.ErrPopUp("Cannot find gc_dialer.glade")
153                         gtk.main_quit()
154                         return
155
156                 self.wTree.get_widget("about_title").set_label(self.wTree.get_widget("about_title").get_label()+"\nVersion "+Dialpad.__version__)
157
158                 #Get the buffer associated with the number display
159                 self.numberdisplay = self.wTree.get_widget("numberdisplay")
160                 self.setNumber("")
161                 self.notebook = self.wTree.get_widget("notebook")
162
163                 self.window = self.wTree.get_widget("Dialpad")
164                 if hildon is not None:
165                         self.app = hildon.Program()
166                         self.window.set_title("Keypad")
167                         self.app.add_window(self.window)
168                         self.wTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
169                         self.wTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
170                         self.wTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
171                 else:
172                         warnings.warn("No Hildon", UserWarning, 2)
173
174                 if osso is not None:
175                         self.osso = osso.Context(__name__, Dialpad.__version__, False)
176                         device = osso.DeviceState(self.osso)
177                         device.set_device_state_callback(self.on_device_state_change, 0)
178                         if abook is not None and evobook is not None:
179                                 abook.init_with_name(__name__, self.osso)
180                                 self.ebook = evo.open_addressbook("default")
181                         else:
182                                 warnings.warn("No abook and No evolution address book support", UserWarning, 2)
183                 else:
184                         warnings.warn("No OSSO", UserWarning, 2)
185
186                 if self.window:
187                         self.window.connect("destroy", gtk.main_quit)
188                         self.window.show_all()
189
190                 callbackMapping = {
191                         # Process signals from buttons
192                         "on_digit_clicked"  : self.on_digit_clicked,
193                         "on_dial_clicked"    : self.on_dial_clicked,
194                         "on_loginbutton_clicked" : self.on_loginbutton_clicked,
195                         "on_loginclose_clicked" : self.on_loginclose_clicked,
196                         "on_clearcookies_clicked" : self.on_clearcookies_clicked,
197                 #       "on_callbackentry_changed" : self.on_callbackentry_changed,
198                         "on_notebook_switch_page" : self.on_notebook_switch_page,
199                         "on_recentview_row_activated" : self.on_recentview_row_activated,
200                         "on_back_clicked" : self.Backspace
201                 }
202                 self.wTree.signal_autoconnect(callbackMapping)
203                 self.wTree.get_widget("callbackcombo").get_child().connect("changed", self.on_callbackentry_changed)
204
205                 # Defer initalization of recent view
206                 self.gcd = GCDialer()
207
208                 self.attemptLogin(2)
209                 gobject.idle_add(self.init_grandcentral)
210                 #self.init_grandcentral()
211                 gobject.idle_add(self.init_recentview)
212
213                 #self.reduce_memory()
214
215         def init_grandcentral(self):
216                 """ deferred initalization of the grandcentral info """
217                 
218                 try:
219                         #self.attemptLogin(2)
220                         if self.gcd.isAuthed():
221                                 if self.gcd.getCallbackNumber() is None:
222                                         self.gcd.setSaneCallback()
223                 except:
224                         pass
225                 
226                 self.setAccountNumber()
227                 print "exit init_gc"
228                 return False
229
230         def init_recentview(self):
231                 """ deferred initalization of the recent view treeview """
232
233                 recentview = self.wTree.get_widget("recentview")
234                 recentview.set_model(self.recentmodel)
235                 textrenderer = gtk.CellRendererText()
236
237                 # Add the column to the treeview
238                 column = gtk.TreeViewColumn("Calls", textrenderer, text=1)
239                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
240
241                 recentview.append_column(column)
242
243                 self.recentviewselection = recentview.get_selection()
244                 self.recentviewselection.set_mode(gtk.SELECTION_SINGLE)
245
246                 return False
247
248         def on_recentview_row_activated(self, treeview, path, view_column):
249                 model, itr = self.recentviewselection.get_selected()
250                 if not itr:
251                         return
252
253                 self.setNumber(self.recentmodel.get_value(itr, 0))
254                 self.notebook.set_current_page(0)
255                 self.recentviewselection.unselect_all()
256
257         def on_notebook_switch_page(self, notebook, page, page_num):
258                 if page_num == 1 and (time.time() - self.recenttime) > 300:
259                         gobject.idle_add(self.populate_recentview)
260                 elif page_num ==2 and self.callbackNeedsSetup:
261                         gobject.idle_add(self.setupCallbackCombo)
262                 if hildon:
263                         try:
264                                 self.window.set_title(self.notebook.get_tab_label(self.notebook.get_nth_page(page_num)).get_text())
265                         except:
266                                 self.window.set_title("")
267
268         def populate_recentview(self):
269                 print "Populating"
270                 self.recentmodel.clear()
271                 for item in self.gcd.get_recent():
272                         self.recentmodel.append(item)
273                 self.recenttime = time.time()
274
275                 return False
276
277         def on_clearcookies_clicked(self, data=None):
278                 self.gcd.reset()
279                 self.callbackNeedsSetup = True
280                 self.recenttime = 0.0
281                 self.recentmodel.clear()
282                 self.wTree.get_widget("callbackcombo").get_child().set_text("")
283         
284                 # re-run the inital grandcentral setup
285                 self.attemptLogin(2)
286                 gobject.idle_add(self.init_grandcentral)
287
288         def setupCallbackCombo(self):
289                 combobox = self.wTree.get_widget("callbackcombo")
290                 self.callbacklist = gtk.ListStore(gobject.TYPE_STRING)
291                 combobox.set_model(self.callbacklist)
292                 combobox.set_text_column(0)
293                 for number, description in self.gcd.getCallbackNumbers().iteritems():
294                         self.callbacklist.append([makepretty(number)] )
295
296                 self.wTree.get_widget("callbackcombo").get_child().set_text(makepretty(self.gcd.getCallbackNumber()))
297                 self.callbackNeedsSetup = False
298
299         def on_callbackentry_changed(self, data=None):
300                 text = makeugly(self.wTree.get_widget("callbackcombo").get_child().get_text())
301                 if self.gcd.validate(text) and text != self.gcd.getCallbackNumber():
302                         self.gcd.setCallbackNumber(text)
303                         #self.wTree.get_widget("callbackentry").set_text(self.wTree.get_object("callbackentry").get_text())
304                 #self.reduce_memory()
305
306         def attemptLogin(self, times = 1):
307                 #if self.isHildon:
308                 #       dialog = hildon.LoginDialog(self.window)
309                 #       dialog.set_message("Grandcentral Login")
310                 #else:
311                 dialog = self.wTree.get_widget("login_dialog")
312
313                 while (0 < times) and not self.gcd.isAuthed():
314                         if dialog.run() != gtk.RESPONSE_OK:
315                                 times = 0
316                                 continue
317
318                         #if self.isHildon:
319                         #       username = dialog.get_username()
320                         #       password = dialog.get_password()
321                         #else:
322                         username = self.wTree.get_widget("usernameentry").get_text()
323                         password = self.wTree.get_widget("passwordentry").get_text()
324                         self.wTree.get_widget("passwordentry").set_text("")
325                         print "Attempting login"
326                         self.gcd.login(username, password)
327                         print "hiding dialog"
328                         dialog.hide()
329                         times = times - 1
330
331                 #if self.isHildon:
332                 #       print "destroy dialog"
333                 #       dialog.destroy()
334
335                 return False
336
337         def ErrPopUp(self, msg):
338                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
339
340                 def close(dialog, response, editor):
341                         editor.about_dialog = None
342                         dialog.destroy()
343                 error_dialog.connect("response", close, self)
344                 self.error_dialog = error_dialog
345                 error_dialog.run()
346
347         def on_paste(self, data=None):
348                 contents = self.clipboard.wait_for_text()
349                 phoneNumber = re.sub('\D', '', contents)
350                 self.setNumber(phoneNumber)
351         
352         def on_loginbutton_clicked(self, data=None):
353                 self.wTree.get_widget("login_dialog").response(gtk.RESPONSE_OK)
354
355         def on_loginclose_clicked(self, data=None):
356                 sys.exit(0)
357
358         def on_dial_clicked(self, widget):
359                 self.attemptLogin(3)
360
361                 if not self.gcd.isAuthed() or self.gcd.getCallbackNumber() == "":
362                         self.ErrPopUp("Backend link with grandcentral is not working, please try again")
363                         return
364
365                 #if len(self.phonenumber) == 7:
366                 #       #add default area code
367                 #       self.phonenumber = self.areacode + self.phonenumber
368
369                 try:
370                         callSuccess = self.gcd.dial(self.phonenumber)
371                 except ValueError, e:
372                         self.gcd._msg = e.message
373                         callSuccess = False
374
375                 if not callSuccess:
376                         self.ErrPopUp(self.gcd._msg)
377                 else:
378                         self.setNumber("")
379
380                 self.recentmodel.clear()
381                 self.recenttime = 0.0
382                 #self.reduce_memory()
383         
384         def on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
385                 """
386                 @todo Might be useful to do something when going in offline mode or low memory
387                 @note Hildon specific
388                 """
389                 pass
390
391         def setNumber(self, number):
392                 self.phonenumber = makeugly(number)
393                 self.prettynumber = makepretty(self.phonenumber)
394                 self.numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % ( self.prettynumber ) )
395
396         def setAccountNumber(self):
397                 accountnumber = self.gcd.getAccountNumber()
398                 self.wTree.get_widget("gcnumberlabel").set_label("<span size='23000' weight='bold'>%s</span>" % (accountnumber))
399
400         def Backspace(self, widget):
401                 self.setNumber(self.phonenumber[:-1])
402
403         def on_digit_clicked(self, widget):
404                 self.setNumber(self.phonenumber + widget.get_name()[5])
405
406
407 def run_doctest():
408         failureCount, testCount = doctest.testmod()
409         if not failureCount:
410                 print "Tests Successful"
411                 sys.exit(0)
412         else:
413                 sys.exit(1)
414
415
416 def run_dialpad():
417         #gc.set_threshold(50, 3, 3)
418         gtk.gdk.threads_init()
419         title = 'Dialpad'
420         handle = Dialpad()
421         gtk.main()
422         sys.exit(1)
423
424
425 class DummyOptions(object):
426         def __init__(self):
427                 self.test = False
428
429
430 if __name__ == "__main__":
431         if hildon:
432                 gtk.set_application_name("Dialer")
433
434         try:
435                 parser = optparse.OptionParser()
436                 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
437                 (options, args) = parser.parse_args()
438         except:
439                 args = []
440                 options = DummyOptions()
441
442         if options.test:
443                 run_doctest()
444         else:
445                 run_dialpad()