--- /dev/null
+"""
+@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")
--- /dev/null
+#!/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)
--- /dev/null
+<?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"><span size="30000" weight="bold">(518) 555-12121234567</span></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>
--- /dev/null
+#!/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
+
+