* Trying to make some areas clearer to myself through comments
[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 import time
14
15 from browser_emu import MozillaEmulator
16
17
18 class GCDialer(object):
19         """
20         This class encapsulates all of the knowledge necessary to interace with the grandcentral servers
21         the functions include login, setting up a callback number, and initalting a callback
22         """
23
24         _gcDialingStrRe = re.compile("This may take a few seconds", re.M)       # string from Grandcentral.com on successful dial
25         _validateRe     = re.compile("^[0-9]{10,}$")
26         _accessTokenRe  = re.compile(r"""<input type="hidden" name="a_t" [^>]*value="(.*)"/>""")
27         _isLoginPageRe  = re.compile(r"""<form method="post" action="https://www.grandcentral.com/mobile/account/login">""")
28         _callbackRe     = re.compile(r"""name="default_number" value="(\d+)" />\s+(.*)\s$""", re.M)
29         _accountNumRe   = re.compile(r"""<img src="/images/mobile/inbox_logo.gif" alt="GrandCentral" />\s*(.{14})\s*&nbsp""", re.M)
30         _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)
31
32         _forwardselectURL       = "http://www.grandcentral.com/mobile/settings/forwarding_select"
33         _loginURL               = "https://www.grandcentral.com/mobile/account/login"
34         _setforwardURL          = "http://www.grandcentral.com/mobile/settings/set_forwarding?from=settings"
35         _clicktocallURL         = "http://www.grandcentral.com/mobile/calls/click_to_call?a_t=%s&destno=%s"
36         _inboxallURL            = "http://www.grandcentral.com/mobile/messages/inbox?types=all"
37
38         def __init__(self, cookieFile = None):
39                 # Important items in this function are the setup of the browser emulation and cookie file
40                 self._msg = ""
41
42                 self._browser = MozillaEmulator(None, 0)
43                 if cookieFile is None:
44                         cookieFile = os.path.join(os.path.expanduser("~"), ".gc_dialer_cookies.txt")
45                 self._browser.cookies.filename = cookieFile
46                 if os.path.isfile(cookieFile):
47                         self._browser.cookies.load()
48
49                 self._accessToken = None
50                 self._accountNum = None
51                 self._callbackNumbers = {}
52                 self._lastAuthed = 0.0
53
54         def isAuthed(self, force = False):
55                 """
56                 Attempts to detect a current session and pull the auth token ( a_t ) from the page.
57                 @note Once logged in try not to reauth more than once a minute.
58                 @returns If authenticated
59                 """
60
61                 if time.time() - self._lastAuthed < 60 and not force:
62                         return True
63
64                 try:    
65                         forwardSelectionPage = self._browser.download(GCDialer._forwardselectURL)
66                         self._browser.cookies.save()
67                         if GCDialer._isLoginPageRe.search(forwardSelectionPage) is None:
68                                 self._grabToken(forwardSelectionPage)
69                                 self._lastAuthed = time.time()
70                                 return True
71                 except:
72                         pass
73                 return False
74
75         def login(self, username, password):
76                 """
77                 Attempt to login to grandcentral
78                 @returns Whether login was successful or not
79                 """
80                 try:
81                         if self.isAuthed():
82                                 return True
83                         loginPostData = urllib.urlencode( {'username' : username , 'password' : password } )
84                         loginSuccessOrFailurePage = self._browser.download(GCDialer._loginURL, loginPostData)
85                         return self.isAuthed()
86                 except:
87                         pass
88                 return False
89
90         def dial(self, number):
91                 """
92                 This is the main function responsible for initating the callback
93                 """
94                 self._msg = ""
95
96                 # If the number is not valid throw exception
97                 if self.validate(number) is False:
98                         raise ValueError('number is not valid')
99
100                 # No point if we don't have the magic cookie
101                 if not self.isAuthed():
102                         self._msg = "Not authenticated"
103                         return False
104
105                 # Strip leading 1 from 11 digit dialing
106                 if len(number) == 11 and number[0] == 1:
107                         number = number[1:]
108
109                 try:
110                         callSuccessPage = self._browser.download(
111                                 GCDialer._clicktocallURL % (self._accessToken, number),
112                                 None, {'Referer' : 'http://www.grandcentral.com/mobile/messages'} )
113
114                         if GCDialer._gcDialingStrRe.search(callSuccessPage) is not None:
115                                 return True
116                         else:
117                                 self._msg = "Grand Central returned an error"
118                                 return False
119                 except:
120                         pass
121         
122                 self._msg = "Unknown Error"
123                 return False
124
125         def clear_caches(self):
126                 """
127                 @todo Fill this in
128                 """
129                 pass
130
131         def reset(self):
132                 self._lastAuthed = 0.0
133                 self._browser.cookies.clear()
134                 self._browser.cookies.save()
135
136         def getAccountNumber(self):
137                 """
138                 @returns The grand central phone number
139                 """
140                 return self._accountNum
141
142         def validate(self, number):
143                 """
144                 @returns If This number be called ( syntax validation only )
145                 """
146                 return GCDialer._validateRe.match(number) is not None
147
148         def setSaneCallback(self):
149                 """
150                 Try to set a sane default callback number on these preferences
151                 1) 1747 numbers ( Gizmo )
152                 2) anything with gizmo in the name
153                 3) anything with computer in the name
154                 4) the first value
155                 """
156                 print "setSaneCallback"
157                 numbers = self.getCallbackNumbers()
158
159                 for number, description in numbers.iteritems():
160                         if not re.compile(r"""1747""").match(number) is None:
161                                 self.setCallbackNumber(number)
162                                 return
163
164                 for number, description in numbers.iteritems():
165                         if not re.compile(r"""gizmo""", re.I).search(description) is None:
166                                 self.setCallbackNumber(number)
167                                 return
168
169                 for number, description in numbers.iteritems():
170                         if not re.compile(r"""computer""", re.I).search(description) is None:
171                                 self.setCallbackNumber(number)
172                                 return
173
174                 for number, description in numbers.iteritems():
175                         self.setCallbackNumber(number)
176                         return
177
178         def getCallbackNumbers(self):
179                 """
180                 @returns a dictionary mapping call back numbers to descriptions
181                 @note These results are cached for 30 minutes.
182                 """
183                 print "getCallbackNumbers"
184                 if time.time() - self._lastAuthed < 1800 or self.isAuthed():
185                         return self._callbackNumbers
186
187                 return {}
188
189         def setCallbackNumber(self, callbacknumber):
190                 """
191                 Set the number that grandcental calls
192                 @param callbacknumber should be a proper 10 digit number
193                 """
194                 print "setCallbackNumber %s" % (callbacknumber)
195                 try:
196                         callbackPostData = urllib.urlencode({'a_t' : self._accessToken, 'default_number' : callbacknumber })
197                         callbackSetPage = self._browser.download(GCDialer._setforwardURL, callbackPostData)
198                         self._browser.cookies.save()
199                 except:
200                         pass
201
202         def getCallbackNumber(self):
203                 """
204                 @returns Current callback number or None
205                 """
206                 for c in self._browser.cookies:
207                         if c.name == "pda_forwarding_number":
208                                 return c.value
209                 return None
210
211         def get_recent(self):
212                 """
213                 @returns Iterable of (Phone Number, Description)
214                 """
215                 try:
216                         recentCallsPage = self._browser.download(GCDialer._inboxallURL)
217                         for match in self._inboxRe.finditer(recentCallsPage):
218                                 yield (match.group(4), "%s on %s from/to %s - %s" % (match.group(1).capitalize(), match.group(2), match.group(3), match.group(4)))
219                 except:
220                         pass
221
222         def _grabToken(self, data):
223                 "Pull the magic cookie from the datastream"
224                 atGroup = GCDialer._accessTokenRe.search(data)
225                 try:
226                         self._accessToken = atGroup.group(1)
227                 except:
228                         pass
229
230                 anGroup = GCDialer._accountNumRe.search(data)
231                 try:
232                         self._accountNum = anGroup.group(1)
233                 except:
234                         pass
235
236                 self._callbackNumbers = {}
237                 try:
238                         for match in GCDialer._callbackRe.finditer(data):
239                                 self._callbackNumbers[match.group(1)] = match.group(2)
240                 except:
241                         pass