Eric's working directory. Not done yet svn/ericwork@9
authorbrontide <ericew@gmail.com>
Tue, 24 Jun 2008 19:23:55 +0000 (19:23 +0000)
committerbrontide <ericew@gmail.com>
Tue, 24 Jun 2008 19:23:55 +0000 (19:23 +0000)
git-svn-id: file:///svnroot/gc-dialer/ericwork@9 c39d3808-3fe2-4d86-a59f-b7f623ee9f21

browser_emu.py [new file with mode: 0644]
gc_dialer.py [new file with mode: 0755]
gc_dialer_new.glade [new file with mode: 0644]
gcbackend.py [new file with mode: 0644]

diff --git a/browser_emu.py b/browser_emu.py
new file mode 100644 (file)
index 0000000..0bb7905
--- /dev/null
@@ -0,0 +1,326 @@
+"""
+@author:         Laszlo Nagy
+@copyright:   (c) 2005 by Szoftver Messias Bt.
+@licence:       BSD style
+
+Objects of the MozillaEmulator class can emulate a browser that is capable of:
+
+       - cookie management
+       - caching
+       - configurable user agent string
+       - GET and POST
+       - multipart POST (send files)
+       - receive content into file
+       - progress indicator
+
+I have seen many requests on the python mailing list about how to emulate a browser. I'm using this class for years now, without any problems. This is how you can use it:
+
+       1. Use firefox
+       2. Install and open the livehttpheaders plugin
+       3. Use the website manually with firefox
+       4. Check the GET and POST requests in the livehttpheaders capture window
+       5. Create an instance of the above class and send the same GET and POST requests to the server.
+
+Optional steps:
+
+       - For testing, use a MozillaCacher instance - this will cache all pages and make testing quicker
+       - You can change user agent string in the build_opened method
+       - The "encode_multipart_formdata" function can be used alone to create POST data from a list of field values and files
+
+TODO:
+
+- should have a method to save/load cookies
+"""
+
+from __future__ import with_statement
+
+import os
+import md5
+import urllib
+import urllib2
+import mimetypes
+#from gzip import GzipFile
+import cStringIO
+from cPickle import loads, dumps
+import cookielib
+
+
+class MozillaCacher(object):
+       """A dictionary like object, that can cache results on a storage device."""
+
+       def __init__(self,cachedir='.cache'):
+               self.cachedir = cachedir
+               if not os.path.isdir(cachedir):
+                       os.mkdir(cachedir)
+
+       def name2fname(self,name):
+               return os.path.join(self.cachedir,name)
+
+       def __getitem__(self,name):
+               if not isinstance(name,str):
+                       raise TypeError()
+               fname = self.name2fname(name)
+               if os.path.isfile(fname):
+                       return file(fname,'rb').read()
+               else:
+                       raise IndexError()
+
+       def __setitem__(self,name,value):
+               if not isinstance(name,str):
+                       raise TypeError()
+               fname = self.name2fname(name)
+               if os.path.isfile(fname):
+                       os.unlink(fname)
+               with open(fname,'wb+') as f:
+                       f.write(value)
+
+       def __delitem__(self,name):
+               if not isinstance(name,str):
+                       raise TypeError()
+               fname = self.name2fname(name)
+               if os.path.isfile(fname):
+                       os.unlink(fname)
+
+       def __iter__(self):
+               raise NotImplementedError()
+
+       def has_key(self,name):
+               return os.path.isfile(self.name2fname(name))
+
+
+class MozillaEmulator(object):
+
+       def __init__(self,cacher={},trycount=0):
+               """Create a new MozillaEmulator object.
+
+               @param cacher: A dictionary like object, that can cache search results on a storage device.
+                       You can use a simple dictionary here, but it is not recommended.
+                       You can also put None here to disable caching completely.
+               @param trycount: The download() method will retry the operation if it fails. You can specify -1 for infinite retrying.
+                        A value of 0 means no retrying. A value of 1 means one retry. etc."""
+               self.cacher = cacher
+               self.cookies = cookielib.MozillaCookieJar()
+               self.debug = False
+               self.trycount = trycount
+
+       def _hash(self,data):
+               h = md5.new()
+               h.update(data)
+               return h.hexdigest()
+
+       def build_opener(self,url,postdata=None,extraheaders={},forbid_redirect=False):
+               txheaders = {
+                       'Accept':'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png',
+                       'Accept-Language':'en,en-us;q=0.5',
+#                      'Accept-Encoding': 'gzip, deflate',
+                       'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
+#                      'Keep-Alive': '300',
+#                      'Connection': 'keep-alive',
+#                      'Cache-Control': 'max-age=0',
+               }
+               for key,value in extraheaders.iteritems():
+                       txheaders[key] = value
+               req = urllib2.Request(url, postdata, txheaders)
+               self.cookies.add_cookie_header(req)
+               if forbid_redirect:
+                       redirector = HTTPNoRedirector()
+               else:
+                       redirector = urllib2.HTTPRedirectHandler()
+
+               http_handler = urllib2.HTTPHandler(debuglevel=self.debug)
+               https_handler = urllib2.HTTPSHandler(debuglevel=self.debug)
+
+               u = urllib2.build_opener(http_handler,https_handler,urllib2.HTTPCookieProcessor(self.cookies),redirector)
+               u.addheaders = [('User-Agent','Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.8) Gecko/20050511 Firefox/1.0.4')]
+               if not postdata is None:
+                       req.add_data(postdata)
+               return (req,u)
+
+       def download(self,url,postdata=None,extraheaders={},forbid_redirect=False,
+                       trycount=None,fd=None,onprogress=None,only_head=False):
+               """Download an URL with GET or POST methods.
+
+               @param postdata: It can be a string that will be POST-ed to the URL.
+                       When None is given, the method will be GET instead.
+               @param extraheaders: You can add/modify HTTP headers with a dict here.
+               @param forbid_redirect: Set this flag if you do not want to handle
+                       HTTP 301 and 302 redirects.
+               @param trycount: Specify the maximum number of retries here.
+                       0 means no retry on error. Using -1 means infinite retring.
+                       None means the default value (that is self.trycount).
+               @param fd: You can pass a file descriptor here. In this case,
+                       the data will be written into the file. Please note that
+                       when you save the raw data into a file then it won't be cached.
+               @param onprogress: A function that has two parameters:
+                       the size of the resource and the downloaded size. This will be
+                       called for each 1KB chunk. (If the HTTP header does not contain
+                       the content-length field, then the size parameter will be zero!)
+               @param only_head: Create the openerdirector and return it. In other
+                       words, this will not retrieve any content except HTTP headers.
+
+               @return: The raw HTML page data, unless fd was specified. When fd
+                       was given, the return value is undefined.
+               """
+               if trycount is None:
+                       trycount = self.trycount
+               cnt = 0
+               while True:
+                       try:
+                               key = self._hash(url)
+                               if (self.cacher is None) or (not self.cacher.has_key(key)):
+                                       req,u = self.build_opener(url,postdata,extraheaders,forbid_redirect)
+                                       openerdirector = u.open(req)
+                                       if self.debug:
+                                               print req.get_method(),url
+                                               print openerdirector.code,openerdirector.msg
+                                               print openerdirector.headers
+                                       self.cookies.extract_cookies(openerdirector,req)
+                                       if only_head:
+                                               return openerdirector
+                                       if openerdirector.headers.has_key('content-length'):
+                                               length = long(openerdirector.headers['content-length'])
+                                       else:
+                                               length = 0
+                                       dlength = 0
+                                       if fd:
+                                               while True:
+                                                       data = openerdirector.read(1024)
+                                                       dlength += len(data)
+                                                       fd.write(data)
+                                                       if onprogress:
+                                                               onprogress(length,dlength)
+                                                       if not data:
+                                                               break
+                                       else:
+                                               data = ''
+                                               while True:
+                                                       newdata = openerdirector.read(1024)
+                                                       dlength += len(newdata)
+                                                       data += newdata
+                                                       if onprogress:
+                                                               onprogress(length,dlength)
+                                                       if not newdata:
+                                                               break
+                                               #data = openerdirector.read()
+                                               if not (self.cacher is None):
+                                                       self.cacher[key] = data
+                               else:
+                                       data = self.cacher[key]
+                               #try:
+                               #       d2= GzipFile(fileobj=cStringIO.StringIO(data)).read()
+                               #       data = d2
+                               #except IOError:
+                               #       pass
+                               return data
+                       except urllib2.URLError:
+                               cnt += 1
+                               if (trycount > -1) and (trycount < cnt):
+                                       raise
+                               # Retry :-)
+                               if self.debug:
+                                       print "MozillaEmulator: urllib2.URLError, retryting ",cnt
+
+       def post_multipart(self,url,fields, files, forbid_redirect=True):
+               """Post fields and files to an http host as multipart/form-data.
+               fields is a sequence of (name, value) elements for regular form fields.
+               files is a sequence of (name, filename, value) elements for data to be uploaded as files
+               Return the server's response page.
+               """
+               content_type, post_data = encode_multipart_formdata(fields, files)
+               result = self.download(url,post_data, {
+                               'Content-Type': content_type,
+                               'Content-Length': str(len(post_data))
+                       },
+                       forbid_redirect=forbid_redirect
+               )
+               return result
+
+
+class HTTPNoRedirector(urllib2.HTTPRedirectHandler):
+       """This is a custom http redirect handler that FORBIDS redirection."""
+
+       def http_error_302(self, req, fp, code, msg, headers):
+               e = urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp)
+               if e.code in (301,302):
+                       if 'location' in headers:
+                               newurl = headers.getheaders('location')[0]
+                       elif 'uri' in headers:
+                               newurl = headers.getheaders('uri')[0]
+                       e.newurl = newurl
+               raise e
+
+
+def encode_multipart_formdata(fields, files):
+       """
+       fields is a sequence of (name, value) elements for regular form fields.
+       files is a sequence of (name, filename, value) elements for data to be uploaded as files
+       Return (content_type, body) ready for httplib.HTTP instance
+       """
+       BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$'
+       CRLF = '\r\n'
+       L = []
+       for (key, value) in fields:
+               L.append('--' + BOUNDARY)
+               L.append('Content-Disposition: form-data; name="%s"' % key)
+               L.append('')
+               L.append(value)
+       for (key, filename, value) in files:
+               L.append('--' + BOUNDARY)
+               L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename))
+               L.append('Content-Type: %s' % get_content_type(filename))
+               L.append('')
+               L.append(value)
+       L.append('--' + BOUNDARY + '--')
+       L.append('')
+       body = CRLF.join(L)
+       content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
+       return content_type, body
+
+
+def get_content_type(filename):
+       return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
+
+
+def included_sample():
+       # HOW TO USE
+
+       dl = MozillaEmulator()
+       # Make sure that we get cookies from the server before logging in
+       frontpage = dl.download("https://somesite.net/login.php")
+       # Sign in POST
+       post_data = "action=sign_in&username=user1&password=pwd1"
+       page = dl.download("https://somesite.net/sign_in.php",post_data)
+       if "Welcome" in page:
+               # Send a file
+               fdata = file("inventory.txt","rb").read()
+               dl.post_multipart('https://somesimte.net/upload-file.php',
+                                               [('uploadType','Inventory'),('otherfield','othervalue')],
+                                               [('uploadFileName','inventory.txt',fdata)]
+                                       )
+
+
+def gc_test_run(username, password, defaultNumber, destNumber):
+       dl = MozillaEmulator()
+       frontpage = dl.download("https://m.grandcentral.com")
+
+       loginPostData = "username=%s&password=%s" % (username, password)
+       phoneSelection = dl.download("https://www.grandcentral.com/mobile/account/login", loginPostData)
+
+       import re
+       atRegex = re.compile(r"""<input type="hidden" name="a_t" value="(.*)"/>""")
+       atGroup = atRegex.search(phoneSelection)
+       at = atGroup.group(1)
+
+       #phonePostData = "a_t=%s&default_number=%s" % (at, defaultNumber)
+       mainPage = dl.download("https://www.grandcentral.com/mobile/settings/set_forwarding?default_number=%s" % defaultNumber)
+
+       dial_url = "http://www.grandcentral.com/mobile/calls/click_to_call"
+       #dialPostData = "?destno=%s&a_t=%s" % (destNumber, at)
+       dialPage = dl.download("http://www.grandcentral.com/mobile/calls/click_to_call?destno=%s" % destNumber )
+
+       return dl
+
+
+#if __name__ == "__main__":
+       #included_sample()
+       #gc_test_run("username", "password", "8016912439", "7603752984")
diff --git a/gc_dialer.py b/gc_dialer.py
new file mode 100755 (executable)
index 0000000..081941c
--- /dev/null
@@ -0,0 +1,260 @@
+#!/usr/bin/python 
+
+# Grandcentral Dialer
+# Python front-end to a wget script to use grandcentral.com to place outbound VOIP calls.
+# (C) 2008 Mark Bergman
+# bergman@merctech.com
+
+import sys
+import os
+import time
+import re
+import gtk
+import gtk.glade
+import gcbackend
+
+histfile=os.path.expanduser("~")
+histfile=os.path.join(histfile,".gcdialerhist")        # Use the native OS file separator
+liststore = gtk.ListStore(str)
+
+def load_history_list(histfile,liststore):
+       # read the history list, load it into the liststore variable for later
+       # assignment to the combobox menu
+
+       # clear out existing entries
+       dialhist = []
+       liststore.clear()
+       if os.path.isfile(histfile) :
+               histFH = open(histfile,"r")
+               for line in histFH.readlines() :
+                       fields = line.split()   # split the input lines on whitespace
+                       number=fields[0]                #...save only the first field (the phone number)
+                       search4num=re.compile('^' + number + '$')
+                       newnumber=True  # set a flag that the current phone number is not on the history menu
+                       for num in dialhist :
+                               if re.match(search4num,num):
+                                       # the number is already in the drop-down menu list...set the
+                                       # flag and bail out
+                                       newnumber = False
+                                       break
+                       if newnumber == True :
+                               dialhist.append(number) # append the number to the history list
+       
+               histlen=len(dialhist)
+               if histlen > 10 :
+                       dialhist=dialhist[histlen - 10:histlen]         # keep only the last 10 entries
+               dialhist.reverse()      # reverse the list, so that the most recent entry is now first
+       
+               # Now, load the liststore with the entries, for later assignment to the Gtk.combobox menu
+               for entry in dialhist :
+                       entry=makepretty(entry)
+                       liststore.append([entry])
+       # else :
+       #        print "The history file " + histfile + " does not exist"
+
+def makeugly(prettynumber):
+       # function to take a phone number and strip out all non-numeric
+       # characters
+       uglynumber=re.sub('\D','',prettynumber)
+       return uglynumber
+
+def makepretty(phonenumber):
+       # Function to take a phone number and return the pretty version
+       # pretty numbers:
+       #       if phonenumber begins with 0:
+       #               ...-(...)-...-....
+       #       else
+       #               if phonenumber is 13 digits:
+       #                       (...)-...-....
+       #               else if phonenumber is 10 digits:
+       #                       ...-....
+       if len(phonenumber) < 3 :
+               return phonenumber
+
+       if  phonenumber[0] == "0" :
+                       if len(phonenumber) <=3:
+                               prettynumber = "+" + phonenumber[0:3] 
+                       elif len(phonenumber) <=6:
+                               prettynumber = "+" + phonenumber[0:3] + "-(" + phonenumber[3:6] + ")"
+                       elif len(phonenumber) <=9:
+                               prettynumber = "+" + phonenumber[0:3] + "-(" + phonenumber[3:6] + ")-" + phonenumber[6:9]
+                       else:
+                               prettynumber = "+" + phonenumber[0:3] + "-(" + phonenumber[3:6] + ")-" + phonenumber[6:9] + "-" + phonenumber[9:]
+                       return prettynumber
+       elif len(phonenumber) <= 7 :
+                       prettynumber = phonenumber[0:3] + "-" + phonenumber[3:] 
+       elif len(phonenumber) > 7 :
+                       prettynumber = "(" + phonenumber[0:3] + ")-" + phonenumber[3:6] + "-" + phonenumber[6:]
+       return prettynumber
+
+class Dialpad:
+
+       def __init__(self):
+               for file in [ './gc_dialer_new.glade', 
+                               '../lib/gc_dialer_new.glade', 
+                               '/usr/local/lib/gc_dialer_new.glade' ]:
+                       if os.path.isfile(file):
+                               self.gladefile = file
+                               break
+
+               self.phonenumber = ""
+               self.prettynumber = ""
+               self.areacode = "518"
+
+               self.gcd = gcbackend.GCDialer()
+
+               # MODIFY HERE
+               self.gcd.login("username","password")
+               if not self.gcd.isAuthed():
+                       print "oops"
+                       sys.exit(1)
+
+               # AND HERE
+               self.gcd.setCallbackNumber("callback")
+
+               self.wTree = gtk.glade.XML(self.gladefile)
+               self.window = self.wTree.get_widget("Dialpad")
+               #Get the buffer associated with the number display
+               self.numberdisplay = self.wTree.get_widget("numberdisplay")
+               self.dialer_history = self.wTree.get_widget("dialer_history")
+
+               # Load the liststore array with the numbers from the history file
+               #load_history_list(histfile,liststore)
+               # load the dropdown menu with the numbers from the dial history
+               #self.dialer_history.set_model(liststore)
+               #cell = gtk.CellRendererText()
+               #self.dialer_history.pack_start(cell, True)
+               #self.dialer_history.set_active(-1)
+               #self.dialer_history.set_attributes(cell, text=0)
+
+               self.about_dialog = None
+               self.error_dialog = None
+               
+               self.setNumber("")
+
+               #try:
+               if False:
+                       # If we are on Nokia, switch to hildon windows/menu's
+                       
+                       import hildon
+                       self.app = hildon.Program()
+                       self.window = hildon.Window()
+                       self.window.set_title("Dialer")
+                       self.app.add_window(self.window)
+                       self.wTree.get_widget("vbox1").reparent(self.window)
+                       self.wTree.get_widget("Dialpad").destroy()
+
+                       menu = gtk.Menu()
+                       for child in self.wTree.get_widget("MainMenu").get_children():
+                               child.reparent(menu)
+                       self.window.set_menu(menu)
+                       self.wTree.get_widget("MainMenu").destroy()
+
+               #except:
+               #       pass
+
+               if (self.window):
+                       self.window.connect("destroy", gtk.main_quit)
+                       self.window.show_all()
+
+               dic = {
+                       # Routine for processing signal from the combobox (ie., when the
+                       # user selects an entry from the dropdown history
+                       "on_dialer_history_changed" : self.on_dialer_history_changed,
+
+                       # Process signals from buttons
+                       "on_digit_clicked"  : self.on_digit_clicked,
+                       "on_Clear_clicked"   : self.on_Clear_clicked,
+                       "on_dial_clicked"    : self.on_dial_clicked,
+                       "on_back_clicked" : self.Backspace,
+                       "on_About_activate"   : self.on_About_clicked}
+               self.wTree.signal_autoconnect(dic)
+
+       def ErrPopUp(self,msg):
+               error_dialog = gtk.MessageDialog(None,0,gtk.MESSAGE_ERROR,gtk.BUTTONS_CLOSE,msg)
+               def close(dialog, response, editor):
+                       editor.about_dialog = None
+                       dialog.destroy()
+               error_dialog.connect("response", close, self)
+               # error_dialog.connect("delete-event", delete_event, self)
+               self.error_dialog = error_dialog
+               error_dialog.run()
+
+       def on_About_clicked(self, menuitem, data=None):
+               if self.about_dialog: 
+                       self.about_dialog.present()
+                       return
+
+               authors = [ "Mark Bergman <bergman@merctech.com>",
+                               "Eric Warnke <ericew@gmail.com>" ]
+
+               about_dialog = gtk.AboutDialog()
+               about_dialog.set_transient_for(None)
+               about_dialog.set_destroy_with_parent(True)
+               about_dialog.set_name("Grandcentral Dialer")
+               about_dialog.set_version("0.5")
+               about_dialog.set_copyright("Copyright \xc2\xa9 2008 Mark Bergman")
+               about_dialog.set_comments("GUI front-end to initiate outbound call from Grandcentral.com, typically with Grancentral configured to connect the outbound call to a VOIP number accessible via Gizmo on the Internet Tablet.\n\nRequires an existing browser cookie from a previous login session to http://www.grandcentral.com/mobile/messages and the program 'wget'.")
+               about_dialog.set_authors            (authors)
+               about_dialog.set_logo_icon_name     (gtk.STOCK_EDIT)
+
+               # callbacks for destroying the dialog
+               def close(dialog, response, editor):
+                       editor.about_dialog = None
+                       dialog.destroy()
+
+               def delete_event(dialog, event, editor):
+                       editor.about_dialog = None
+                       return True
+
+               about_dialog.connect("response", close, self)
+               about_dialog.connect("delete-event", delete_event, self)
+               self.about_dialog = about_dialog
+               about_dialog.show()
+
+       def on_dial_clicked(self, widget):
+               timestamp=time.asctime(time.localtime())
+
+               if len(self.phonenumber) == 7:
+                       #add default area code
+                       self.phonenumber = self.areacode + self.phonenumber
+                       
+               if self.gcd.dial(self.phonenumber) == True : 
+                       histFH = open(histfile,"a")
+                       histFH.write("%s dialed at %s\n" % ( self.phonenumber, timestamp ) )
+                       histFH.close()
+
+                       # Re-load the updated history of dialed numbers
+                       #load_history_list(histfile,liststore)
+                       #self.dialer_history.set_active(-1)
+                       #self.on_Clear_clicked(widget)
+               else:
+                       self.ErrPopUp(self.gcd._msg)
+
+               self.setNumber("")
+
+       def setNumber(self, number):
+               self.phonenumber = number
+               self.prettynumber = makepretty(number)
+               self.numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % ( self.prettynumber ) )
+
+       def Backspace(self, widget):
+               self.setNumber(self.phonenumber[:-1])
+
+       def on_Clear_clicked(self, widget):
+               self.setNumber("")
+
+       def on_dialer_history_changed(self,widget):
+               # Set the displayed number to the number chosen from the history list
+               history_list = self.dialer_history.get_model()
+               history_index = self.dialer_history.get_active()
+               self.setNumber(makeugly(history_list[history_index][0]))
+
+       def on_digit_clicked(self, widget):
+               self.setNumber(self.phonenumber + re.sub('\D','',widget.get_label()))
+
+if __name__ == "__main__":
+       title = 'Dialpad'
+       handle = Dialpad()
+       gtk.main()
+       sys.exit(1)
diff --git a/gc_dialer_new.glade b/gc_dialer_new.glade
new file mode 100644 (file)
index 0000000..c348954
--- /dev/null
@@ -0,0 +1,520 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
+<!--Generated with glade3 3.4.4 on Tue Jun 24 08:10:43 2008 -->
+<glade-interface>
+  <widget class="GtkWindow" id="Dialpad">
+    <property name="width_request">300</property>
+    <property name="height_request">300</property>
+    <child>
+      <widget class="GtkNotebook" id="notebook">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="tab_pos">GTK_POS_BOTTOM</property>
+        <property name="show_border">False</property>
+        <property name="homogeneous">True</property>
+        <child>
+          <widget class="GtkVBox" id="vbox2">
+            <property name="visible">True</property>
+            <child>
+              <widget class="GtkLabel" id="numberdisplay">
+                <property name="height_request">50</property>
+                <property name="visible">True</property>
+                <property name="label" translatable="yes">&lt;span size="30000" weight="bold"&gt;(518) 555-12121234567&lt;/span&gt;</property>
+                <property name="use_markup">True</property>
+                <property name="justify">GTK_JUSTIFY_CENTER</property>
+              </widget>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">False</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkTable" id="keypadview">
+                <property name="visible">True</property>
+                <property name="n_rows">4</property>
+                <property name="n_columns">3</property>
+                <property name="homogeneous">True</property>
+                <child>
+                  <widget class="GtkButton" id="digit1">
+                    <property name="visible">True</property>
+                    <property name="label" translatable="yes">1</property>
+                    <property name="focus_on_click">False</property>
+                    <property name="response_id">0</property>
+                    <signal name="clicked" handler="on_digit_clicked" object="1"/>
+                    <accelerator key="1" modifiers="" signal="clicked"/>
+                  </widget>
+                </child>
+                <child>
+                  <widget class="GtkButton" id="digit2">
+                    <property name="visible">True</property>
+                    <property name="label" translatable="yes">2 ABC</property>
+                    <property name="response_id">0</property>
+                    <signal name="clicked" handler="on_digit_clicked" object="2"/>
+                    <accelerator key="c" modifiers="" signal="clicked"/>
+                    <accelerator key="b" modifiers="" signal="clicked"/>
+                    <accelerator key="a" modifiers="" signal="clicked"/>
+                    <accelerator key="2" modifiers="" signal="clicked"/>
+                  </widget>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="right_attach">2</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkButton" id="digit3">
+                    <property name="visible">True</property>
+                    <property name="label" translatable="yes">3 DEF</property>
+                    <property name="response_id">0</property>
+                    <signal name="clicked" handler="on_digit_clicked" object="3"/>
+                    <accelerator key="f" modifiers="" signal="clicked"/>
+                    <accelerator key="e" modifiers="" signal="clicked"/>
+                    <accelerator key="d" modifiers="" signal="clicked"/>
+                    <accelerator key="3" modifiers="" signal="clicked"/>
+                  </widget>
+                  <packing>
+                    <property name="left_attach">2</property>
+                    <property name="right_attach">3</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkButton" id="digit4">
+                    <property name="visible">True</property>
+                    <property name="label" translatable="yes">4 GHI</property>
+                    <property name="response_id">0</property>
+                    <signal name="clicked" handler="on_digit_clicked" object="4"/>
+                    <accelerator key="i" modifiers="" signal="clicked"/>
+                    <accelerator key="h" modifiers="" signal="clicked"/>
+                    <accelerator key="g" modifiers="" signal="clicked"/>
+                    <accelerator key="4" modifiers="" signal="clicked"/>
+                  </widget>
+                  <packing>
+                    <property name="top_attach">1</property>
+                    <property name="bottom_attach">2</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkButton" id="digit5">
+                    <property name="visible">True</property>
+                    <property name="label" translatable="yes">5 JKL</property>
+                    <property name="response_id">0</property>
+                    <signal name="clicked" handler="on_digit_clicked" object="5"/>
+                    <accelerator key="l" modifiers="" signal="clicked"/>
+                    <accelerator key="k" modifiers="" signal="clicked"/>
+                    <accelerator key="j" modifiers="" signal="clicked"/>
+                    <accelerator key="5" modifiers="" signal="clicked"/>
+                  </widget>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="right_attach">2</property>
+                    <property name="top_attach">1</property>
+                    <property name="bottom_attach">2</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkButton" id="digit6">
+                    <property name="visible">True</property>
+                    <property name="label" translatable="yes">6 MNO</property>
+                    <property name="response_id">0</property>
+                    <signal name="clicked" handler="on_digit_clicked" object="6"/>
+                    <accelerator key="o" modifiers="" signal="clicked"/>
+                    <accelerator key="n" modifiers="" signal="clicked"/>
+                    <accelerator key="m" modifiers="" signal="clicked"/>
+                    <accelerator key="6" modifiers="" signal="clicked"/>
+                  </widget>
+                  <packing>
+                    <property name="left_attach">2</property>
+                    <property name="right_attach">3</property>
+                    <property name="top_attach">1</property>
+                    <property name="bottom_attach">2</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkButton" id="digit7">
+                    <property name="visible">True</property>
+                    <property name="label" translatable="yes">7 PQRS</property>
+                    <property name="response_id">0</property>
+                    <signal name="clicked" handler="on_digit_clicked" object="7"/>
+                    <accelerator key="s" modifiers="" signal="clicked"/>
+                    <accelerator key="r" modifiers="" signal="clicked"/>
+                    <accelerator key="q" modifiers="" signal="clicked"/>
+                    <accelerator key="p" modifiers="" signal="clicked"/>
+                    <accelerator key="7" modifiers="" signal="clicked"/>
+                  </widget>
+                  <packing>
+                    <property name="top_attach">2</property>
+                    <property name="bottom_attach">3</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkButton" id="digit8">
+                    <property name="visible">True</property>
+                    <property name="label" translatable="yes">8 TUV</property>
+                    <property name="response_id">0</property>
+                    <signal name="clicked" handler="on_digit_clicked" object="8"/>
+                    <accelerator key="v" modifiers="" signal="clicked"/>
+                    <accelerator key="u" modifiers="" signal="clicked"/>
+                    <accelerator key="t" modifiers="" signal="clicked"/>
+                    <accelerator key="8" modifiers="" signal="clicked"/>
+                  </widget>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="right_attach">2</property>
+                    <property name="top_attach">2</property>
+                    <property name="bottom_attach">3</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkButton" id="digit9">
+                    <property name="visible">True</property>
+                    <property name="label" translatable="yes">9 WXYZ</property>
+                    <property name="response_id">0</property>
+                    <signal name="clicked" handler="on_digit_clicked" object="9"/>
+                    <accelerator key="z" modifiers="" signal="clicked"/>
+                    <accelerator key="y" modifiers="" signal="clicked"/>
+                    <accelerator key="x" modifiers="" signal="clicked"/>
+                    <accelerator key="w" modifiers="" signal="clicked"/>
+                    <accelerator key="9" modifiers="" signal="clicked"/>
+                  </widget>
+                  <packing>
+                    <property name="left_attach">2</property>
+                    <property name="right_attach">3</property>
+                    <property name="top_attach">2</property>
+                    <property name="bottom_attach">3</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkButton" id="back">
+                    <property name="visible">True</property>
+                    <property name="label" translatable="yes">Back</property>
+                    <property name="response_id">0</property>
+                    <signal name="clicked" handler="on_back_clicked"/>
+                    <accelerator key="BackSpace" modifiers="" signal="clicked"/>
+                  </widget>
+                  <packing>
+                    <property name="top_attach">3</property>
+                    <property name="bottom_attach">4</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkButton" id="digit0">
+                    <property name="visible">True</property>
+                    <property name="label" translatable="yes">0</property>
+                    <property name="response_id">0</property>
+                    <signal name="clicked" handler="on_digit_clicked" object="0"/>
+                    <accelerator key="0" modifiers="" signal="clicked"/>
+                  </widget>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="right_attach">2</property>
+                    <property name="top_attach">3</property>
+                    <property name="bottom_attach">4</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkButton" id="dial">
+                    <property name="visible">True</property>
+                    <property name="label" translatable="yes">Dial</property>
+                    <property name="response_id">0</property>
+                    <signal name="clicked" handler="on_dial_clicked"/>
+                  </widget>
+                  <packing>
+                    <property name="left_attach">2</property>
+                    <property name="right_attach">3</property>
+                    <property name="top_attach">3</property>
+                    <property name="bottom_attach">4</property>
+                  </packing>
+                </child>
+              </widget>
+              <packing>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </widget>
+          <packing>
+            <property name="tab_expand">True</property>
+            <property name="tab_fill">False</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkLabel" id="keypad">
+            <property name="width_request">60</property>
+            <property name="height_request">60</property>
+            <property name="visible">True</property>
+            <property name="label" translatable="yes">Keypad</property>
+          </widget>
+          <packing>
+            <property name="type">tab</property>
+            <property name="tab_expand">True</property>
+            <property name="tab_fill">False</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkTreeView" id="recentview">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="enable_search">False</property>
+            <property name="fixed_height_mode">True</property>
+            <property name="enable_tree_lines">True</property>
+          </widget>
+          <packing>
+            <property name="position">1</property>
+            <property name="tab_expand">True</property>
+            <property name="tab_fill">False</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkLabel" id="recent">
+            <property name="width_request">60</property>
+            <property name="height_request">40</property>
+            <property name="visible">True</property>
+            <property name="label" translatable="yes">Recent</property>
+          </widget>
+          <packing>
+            <property name="type">tab</property>
+            <property name="position">1</property>
+            <property name="tab_expand">True</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkTable" id="accountview">
+            <property name="visible">True</property>
+            <property name="n_rows">5</property>
+            <property name="n_columns">4</property>
+            <child>
+              <widget class="GtkHBox" id="hbox1">
+                <property name="height_request">25</property>
+                <property name="visible">True</property>
+                <property name="homogeneous">True</property>
+                <child>
+                  <widget class="GtkButton" id="login">
+                    <property name="height_request">25</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="has_default">True</property>
+                    <property name="receives_default">True</property>
+                    <property name="label" translatable="yes">Login</property>
+                    <property name="response_id">0</property>
+                    <signal name="clicked" handler="on_login_clicked"/>
+                  </widget>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="padding">10</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkButton" id="clear">
+                    <property name="height_request">25</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">True</property>
+                    <property name="label" translatable="yes">Forget</property>
+                    <property name="response_id">0</property>
+                    <signal name="clicked" handler="on_clear_clicked"/>
+                  </widget>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="padding">10</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </widget>
+              <packing>
+                <property name="left_attach">2</property>
+                <property name="right_attach">3</property>
+                <property name="top_attach">2</property>
+                <property name="bottom_attach">3</property>
+                <property name="y_options"></property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkComboBoxEntry" id="comboboxentry1">
+                <property name="height_request">25</property>
+                <property name="visible">True</property>
+                <child internal-child="entry">
+                  <widget class="GtkEntry" id="callback">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                  </widget>
+                </child>
+              </widget>
+              <packing>
+                <property name="left_attach">2</property>
+                <property name="right_attach">3</property>
+                <property name="top_attach">3</property>
+                <property name="bottom_attach">4</property>
+                <property name="y_options"></property>
+              </packing>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <widget class="GtkEntry" id="areacode">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="max_length">4</property>
+                <property name="width_chars">3</property>
+              </widget>
+              <packing>
+                <property name="left_attach">2</property>
+                <property name="right_attach">3</property>
+                <property name="top_attach">4</property>
+                <property name="bottom_attach">5</property>
+                <property name="x_options">GTK_FILL</property>
+                <property name="y_options">GTK_FILL</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkLabel" id="label3">
+                <property name="visible">True</property>
+                <property name="xalign">1</property>
+                <property name="xpad">5</property>
+                <property name="label" translatable="yes">Callback Number:</property>
+              </widget>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="right_attach">2</property>
+                <property name="top_attach">3</property>
+                <property name="bottom_attach">4</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkLabel" id="label4">
+                <property name="visible">True</property>
+                <property name="xalign">1</property>
+                <property name="xpad">5</property>
+                <property name="label" translatable="yes">Default Areacode:</property>
+              </widget>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="right_attach">2</property>
+                <property name="top_attach">4</property>
+                <property name="bottom_attach">5</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkEntry" id="password">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+              </widget>
+              <packing>
+                <property name="left_attach">2</property>
+                <property name="right_attach">3</property>
+                <property name="top_attach">1</property>
+                <property name="bottom_attach">2</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkEntry" id="accountname">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+              </widget>
+              <packing>
+                <property name="left_attach">2</property>
+                <property name="right_attach">3</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkLabel" id="label2">
+                <property name="visible">True</property>
+                <property name="xalign">1</property>
+                <property name="xpad">5</property>
+                <property name="label" translatable="yes">Password:</property>
+              </widget>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="right_attach">2</property>
+                <property name="top_attach">1</property>
+                <property name="bottom_attach">2</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkLabel" id="label1">
+                <property name="visible">True</property>
+                <property name="xalign">1</property>
+                <property name="xpad">5</property>
+                <property name="label" translatable="yes">Account Name:</property>
+                <property name="justify">GTK_JUSTIFY_RIGHT</property>
+              </widget>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="right_attach">2</property>
+              </packing>
+            </child>
+          </widget>
+          <packing>
+            <property name="position">2</property>
+            <property name="tab_expand">True</property>
+            <property name="tab_fill">False</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkLabel" id="account">
+            <property name="width_request">60</property>
+            <property name="height_request">60</property>
+            <property name="visible">True</property>
+            <property name="label" translatable="yes">Account</property>
+          </widget>
+          <packing>
+            <property name="type">tab</property>
+            <property name="position">2</property>
+            <property name="tab_fill">False</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkLayout" id="aboutview">
+            <property name="visible">True</property>
+          </widget>
+          <packing>
+            <property name="position">3</property>
+            <property name="tab_expand">True</property>
+            <property name="tab_fill">False</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkLabel" id="about">
+            <property name="width_request">60</property>
+            <property name="height_request">60</property>
+            <property name="visible">True</property>
+            <property name="label" translatable="yes">About</property>
+          </widget>
+          <packing>
+            <property name="type">tab</property>
+            <property name="position">3</property>
+            <property name="tab_fill">False</property>
+          </packing>
+        </child>
+      </widget>
+    </child>
+  </widget>
+</glade-interface>
diff --git a/gcbackend.py b/gcbackend.py
new file mode 100644 (file)
index 0000000..b591785
--- /dev/null
@@ -0,0 +1,109 @@
+#!/usr/bin/python 
+
+# Grandcentral Dialer backend code
+# Eric Warnke <ericew@gmail.com>
+# Copyright 2008 GPLv2
+
+
+import os
+import re
+import browser_emu
+
+class GCDialer:
+       # This class encapsulates all of the knowledge necessary to interace with the grandcentral servers
+       # the functions include login, setting up a callback number, and initalting a callback
+
+       _wgetOKstrRe    = re.compile("This may take a few seconds", re.M)       # string from Grandcentral.com on successful dial 
+       _validateRe     = re.compile("^[0-9]{10,}$")
+       _accessTokenRe  = re.compile(r"""<input type="hidden" name="a_t" [^>]*value="(.*)"/>""")
+       _isLoginPageRe  = re.compile(r"""<form method="post" action="https://www.grandcentral.com/mobile/account/login">""")
+
+       _forwardselectURL       = "http://www.grandcentral.com/mobile/settings/forwarding_select"
+       _loginURL               = "https://www.grandcentral.com/mobile/account/login"
+       _setforwardURL          = "http://www.grandcentral.com/mobile/settings/set_forwarding"
+       _clicktocallURL         = "http://www.grandcentral.com/mobile/calls/click_to_call?a_t=%s&destno=%s"
+
+       def __init__(self, cookieFile = None):
+               # Important items in this function are the setup of the browser emulation and cookie file
+
+               self._msg = ""
+               if cookieFile == None:
+                       cookieFile = os.path.join(os.path.expanduser("~"),".gc_dialer_cookies.txt")
+               self._browser = browser_emu.MozillaEmulator()
+               self._browser.cookies.filename = cookieFile
+               if os.path.isfile(cookieFile):
+                       self._browser.cookies.load()
+               self._lastData = ""
+               self._accessToken = None
+
+       def grabToken(self, data):
+               # Pull the magic cookie from the datastream
+
+               atGroup = GCDialer._accessTokenRe.search(data)
+               try:
+                       self._accessToken = atGroup.group(1)
+               except:
+                       pass
+
+       
+       def isAuthed(self):
+               # Attempts to detect a current session and pull the
+               # auth token ( a_t ) from the page
+
+               self._lastData = self._browser.download(GCDialer._forwardselectURL)
+               if GCDialer._isLoginPageRe.search(self._lastData) is None:
+                       self.grabToken(self._lastData)
+                       return True
+               return False
+
+       def login(self, username, password):
+               # Attempt to login to grandcentral
+
+               if self.isAuthed():
+                       return
+
+               loginPostData = "username=%s&password=%s" % (username, password)
+               self._lastData = self._browser.download(GCDialer._loginURL, loginPostData)
+               self.grabToken(self._lastData)
+
+               self._browser.cookies.save()
+       
+
+       def setCallbackNumber(self, callbacknumber):
+               # set the number that grandcental calls
+               # this should be a proper 10 digit number
+
+               callbackPostData = "?a_t=%s&default_number=%s&from=settings" % ( self._accessToken, callbacknumber )
+               self._lastData = self._browser.download(GCDialer._setforwardURL, callbackPostData)
+               self._browser.cookies.save()
+
+       def validate(self,number):
+               # Can this number be called ( syntax validation only )
+
+               return GCDialer._validateRe.match(number) != None
+
+       def dial(self,number):
+               # This is the main function responsible for initating the callback
+
+               # If the number is not valid throw exception
+               if self.validate(number) == False:
+                       raise ValueError, 'number is not valid'
+
+               # No point if we don't have the magic cookie
+               if not self.isAuthed:
+                       return False
+
+               # Strip leading 1 from 11 digit dialing
+               if len(number) == 11 and number[0] == 1:
+                       number = number[1:] 
+
+               self._lastData = self._browser.download(
+                       GCDialer._clicktocallURL % (self._accessToken, number), 
+                       None, {'Referer' : 'http://www.grandcentral.com/mobile/messages'} )
+
+               if GCDialer._wgetOKstrRe.search(self._lastData) != None:
+                       return True
+               else:
+                       return False
+
+