dddb9e74aa9cf25cf130ad2ae05b30d88cd4f02b
[gc-dialer] / gc_dialer / gcbackend.py
1 #!/usr/bin/python
2
3 """
4 Grandcentral Dialer backend code
5 Eric Warnke <ericew@gmail.com>
6 Copyright 2008 GPLv2
7 """
8
9
10 import os
11 import re
12 import urllib
13
14 from browser_emu import MozillaEmulator
15
16
17 class GCDialer(object):
18         """
19         This class encapsulates all of the knowledge necessary to interace with the grandcentral servers
20         the functions include login, setting up a callback number, and initalting a callback
21         """
22
23         _gcDialingStrRe = re.compile("This may take a few seconds", re.M)       # string from Grandcentral.com on successful dial
24         _validateRe     = re.compile("^[0-9]{10,}$")
25         _accessTokenRe  = re.compile(r"""<input type="hidden" name="a_t" [^>]*value="(.*)"/>""")
26         _isLoginPageRe  = re.compile(r"""<form method="post" action="https://www.grandcentral.com/mobile/account/login">""")
27         _callbackRe     = re.compile(r"""name="default_number" value="(\d+)" />\s+(.*)\s$""", re.M)
28         _accountNumRe   = re.compile(r"""<img src="/images/mobile/inbox_logo.gif" alt="GrandCentral" />\s*(.{14})\s*&nbsp""", re.M)
29         _inboxRe        = re.compile(r"""<td>.*?(voicemail|received|missed|call return).*?</td>\s+<td>\s+<font size="2">\s+(.*?)\s+&nbsp;\|&nbsp;\s+<a href="/mobile/contacts/.*?">(.*?)\s?</a>\s+<br/>\s+(.*?)\s?<a href=""", re.S)
30
31         _forwardselectURL       = "http://www.grandcentral.com/mobile/settings/forwarding_select"
32         _loginURL               = "https://www.grandcentral.com/mobile/account/login"
33         _setforwardURL          = "http://www.grandcentral.com/mobile/settings/set_forwarding?from=settings"
34         _clicktocallURL         = "http://www.grandcentral.com/mobile/calls/click_to_call?a_t=%s&destno=%s"
35         _inboxallURL            = "http://www.grandcentral.com/mobile/messages/inbox?types=all"
36
37         def __init__(self, cookieFile = None):
38                 # Important items in this function are the setup of the browser emulation and cookie file
39                 self._msg = ""
40                 if cookieFile is None:
41                         cookieFile = os.path.join(os.path.expanduser("~"), ".gc_dialer_cookies.txt")
42                 self._browser = MozillaEmulator(None, 0)
43                 self._browser.cookies.filename = cookieFile
44                 if os.path.isfile(cookieFile):
45                         self._browser.cookies.load()
46                 #else:
47                 #       self._browser.cookies.save()
48                 self._lastData = ""
49                 self._accessToken = None
50                 self._accountNum = None
51
52         def grabToken(self, data):
53                 "Pull the magic cookie from the datastream"
54                 atGroup = GCDialer._accessTokenRe.search(data)
55                 try:
56                         self._accessToken = atGroup.group(1)
57                 except:
58                         pass
59
60                 anGroup = GCDialer._accountNumRe.search(data)
61                 try:
62                         self._accountNum = anGroup.group(1)
63                 except:
64                         pass
65
66         def getAccountNumber(self):
67                 return self._accountNum
68
69         def isAuthed(self):
70                 """
71                 Attempts to detect a current session and pull the
72                 auth token ( a_t ) from the page
73                 """
74                 self._lastData = self._browser.download(GCDialer._forwardselectURL)
75                 self._browser.cookies.save()
76                 if GCDialer._isLoginPageRe.search(self._lastData) is None:
77                         self.grabToken(self._lastData)
78                         return True
79                 return False
80
81         def login(self, username, password):
82                 """
83                 Attempt to login to grandcentral
84                 """
85                 if self.isAuthed():
86                         return
87
88                 loginPostData = urllib.urlencode( {'username' : username , 'password' : password } )
89                 self._lastData = self._browser.download(GCDialer._loginURL, loginPostData)
90                 return self.isAuthed()
91
92         def setSaneCallback(self):
93                 """
94                 Try to set a sane default callback number on these preferences
95                 1) 1747 numbers ( Gizmo )
96                 2) anything with gizmo in the name
97                 3) anything with computer in the name
98                 4) the first value
99                 """
100                 numbers = self.getCallbackNumbers()
101
102                 for number, description in numbers.iteritems():
103                         if not re.compile(r"""1747""").match(number) is None:
104                                 self.setCallbackNumber(number)
105                                 return
106
107                 for number, description in numbers.iteritems():
108                         if not re.compile(r"""gizmo""", re.I).search(description) is None:
109                                 self.setCallbackNumber(number)
110                                 return
111
112                 for number, description in numbers.iteritems():
113                         if not re.compile(r"""computer""", re.I).search(description) is None:
114                                 self.setCallbackNumber(number)
115                                 return
116
117                 for number, description in numbers.iteritems():
118                         self.setCallbackNumber(number)
119                         return
120
121         def getCallbackNumbers(self):
122                 """
123                 @returns a dictionary mapping call back numbers to descriptions
124                 """
125                 retval = {}
126
127                 self._lastData = self._browser.download(GCDialer._forwardselectURL)
128                 for match in GCDialer._callbackRe.finditer(self._lastData):
129                         retval[match.group(1)] = match.group(2)
130
131                 return retval
132
133         def setCallbackNumber(self, callbacknumber):
134                 """
135                 set the number that grandcental calls
136                 this should be a proper 10 digit number
137                 """
138                 callbackPostData = urllib.urlencode({'a_t' : self._accessToken, 'default_number' : callbacknumber })
139                 self._lastData = self._browser.download(GCDialer._setforwardURL, callbackPostData)
140                 self._browser.cookies.save()
141
142         def getCallbackNumber(self):
143                 for c in self._browser.cookies:
144                         if c.name == "pda_forwarding_number":
145                                 return c.value
146                 return None
147
148         def reset(self):
149                 self._browser.cookies.clear()
150                 self._browser.cookies.save()
151
152         def validate(self, number):
153                 """
154                 Can this number be called ( syntax validation only )
155                 """
156                 return GCDialer._validateRe.match(number) is not None
157
158         def dial(self, number):
159                 """
160                 This is the main function responsible for initating the callback
161                 """
162                 # If the number is not valid throw exception
163                 if self.validate(number) is False:
164                         raise ValueError('number is not valid')
165
166                 # No point if we don't have the magic cookie
167                 if not self.isAuthed():
168                         return False
169
170                 # Strip leading 1 from 11 digit dialing
171                 if len(number) == 11 and number[0] == 1:
172                         number = number[1:]
173
174                 self._lastData = self._browser.download(
175                         GCDialer._clicktocallURL % (self._accessToken, number),
176                         None, {'Referer' : 'http://www.grandcentral.com/mobile/messages'}
177                 )
178
179                 if GCDialer._gcDialingStrRe.search(self._lastData) is not None:
180                         return True
181                 else:
182                         return False
183
184         def get_recent(self):
185                 self._lastData = self._browser.download(GCDialer._inboxallURL)
186                 for match in self._inboxRe.finditer(self._lastData):
187                         #print "type: %s date: %s person: %s number: %s" % (match.group(1), match.group(2), match.group(3), match.group(4))
188                         yield (match.group(4), "%s on %s from/to %s - %s" % (match.group(1).capitalize(), match.group(2), match.group(3), match.group(4)))