- reverse lookup also for CH, IT and AT
[enigma2-plugins.git] / fritzcall / src / plugin.py
1 # -*- coding: utf-8 -*-
2 from Screens.Screen import Screen
3 from Screens.MessageBox import MessageBox
4 from Screens import Standby
5
6 from Components.ActionMap import ActionMap
7 from Components.Label import Label
8 from Components.Button import Button
9 from Components.config import config, ConfigSubsection, ConfigSelection, ConfigIP, ConfigEnableDisable, getConfigListEntry, ConfigText, ConfigInteger
10 from Components.ConfigList import ConfigListScreen
11 from Components.ScrollLabel import ScrollLabel
12
13 from Plugins.Plugin import PluginDescriptor
14 from Tools import Notifications
15
16 from twisted.internet import reactor
17 from twisted.internet.protocol import ReconnectingClientFactory
18 from twisted.protocols.basic import LineReceiver
19 from twisted.web.client import getPage
20
21 from os import path as os_path
22 from urllib import urlencode 
23 import re
24
25 import gettext
26 from Tools.Directories import resolveFilename, SCOPE_PLUGINS
27 try:
28                 _ = gettext.translation('FritzCall', resolveFilename(SCOPE_PLUGINS, "Extensions/FritzCall/locale"), [config.osd.language.getText()]).gettext
29 except IOError:
30                 pass
31
32
33 my_global_session = None
34
35 config.plugins.FritzCall = ConfigSubsection()
36 config.plugins.FritzCall.enable = ConfigEnableDisable(default = False)
37 config.plugins.FritzCall.hostname = ConfigIP(default = [192, 168, 178, 1])
38 config.plugins.FritzCall.afterStandby = ConfigSelection(choices = [("none", _("show nothing")), ("inList", _("show as list")), ("each", _("show each call"))])
39 config.plugins.FritzCall.filter = ConfigEnableDisable(default = False)
40 config.plugins.FritzCall.filtermsn = ConfigText(default = "", fixed_size = False)
41 config.plugins.FritzCall.showOutgoing = ConfigEnableDisable(default = False)
42 config.plugins.FritzCall.timeout = ConfigInteger(default = 15, limits = (0,60))
43 config.plugins.FritzCall.lookup = ConfigEnableDisable(default = False)
44 config.plugins.FritzCall.internal = ConfigEnableDisable(default = False)
45 config.plugins.FritzCall.fritzphonebook = ConfigEnableDisable(default = False)
46 config.plugins.FritzCall.phonebook = ConfigEnableDisable(default = False)
47 config.plugins.FritzCall.addcallers = ConfigEnableDisable(default = False)
48 config.plugins.FritzCall.phonebookLocation = ConfigSelection(choices = [("/etc/enigma2/PhoneBook.txt", _("Flash")), ("/media/usb/PhoneBook.txt", _("USB Stick")), ("/media/cf/PhoneBook.txt", _("CF Drive")), ("/media/hdd/PhoneBook.txt", _("Harddisk"))])
49 config.plugins.FritzCall.password = ConfigText(default = "", fixed_size = False)
50 config.plugins.FritzCall.showType = ConfigEnableDisable(default = True)
51 config.plugins.FritzCall.showShortcut = ConfigEnableDisable(default = False)
52 config.plugins.FritzCall.showVanity = ConfigEnableDisable(default = False)
53 config.plugins.FritzCall.prefix = ConfigText(default = "", fixed_size = False)
54 config.plugins.FritzCall.country = ConfigSelection(choices = [("DE", _("Germany")), ("CH", _("Switzerland")), ("IT", _("Italy")), ("AT", _("Austria"))])
55
56 def html2utf8(in_html):
57         try:
58                 import htmlentitydefs
59                 htmlentitynumbermask = re.compile('(&#(\d{1,5}?);)')
60                 htmlentitynamemask = re.compile('(&(\D{1,5}?);)')
61                 entities = htmlentitynamemask.finditer(in_html)
62                 entitydict = {}
63                 for x in entities:
64                         entitydict[x.group(1)] = x.group(2)
65                 for key, name in entitydict.items():
66                         try:
67                                 entitydict[key] = htmlentitydefs.name2codepoint[name]
68                         except KeyError:
69                                 pass
70                 entities = htmlentitynumbermask.finditer(in_html)
71                 for x in entities:
72                         entitydict[x.group(1)] = x.group(2)
73                 for key, codepoint in entitydict.items():
74                         try:
75                                 in_html = in_html.replace(key, (unichr(int(codepoint)).encode('utf8')))
76                         except ValueError:
77                                 pass
78         except ImportError:
79                 return in_html.replace("&", "&").replace("ß", "").replace("ä", "").replace("ö", "").replace("ü", "").replace("Ä", "").replace("Ö", "").replace("Ü", "")
80         return in_html
81
82 class FritzCallFBF:
83         def __init__(self):
84                 print "[FritzCallFBF] __init__"
85                 self.missedCallback = None
86                 self.loginCallback = None
87
88         def notify(self, text):
89                 Notifications.AddNotification(MessageBox, text, type=MessageBox.TYPE_ERROR, timeout=config.plugins.FritzCall.timeout.value)
90
91         def errorLogin(self, error):
92                 text = _("FRITZ!Box Login failed! - Error: %s") %error
93                 self.notify(text)
94
95         def _gotPageLogin(self, html):
96 #               print "[FritzCallPhonebook] _gotPageLogin"
97                 # workaround: exceptions in gotPage-callback were ignored
98                 try:
99                         print "[FritzCallFBF] _gotPageLogin: verify login"
100                         found = re.match('.*<p class="errorMessage">FEHLER:&nbsp;Das angegebene Kennwort', html, re.S)
101                         if found:
102                                 text = _("FRITZ!Box Login failed! - Wrong Password!")
103                                 self.notify(text)
104                         else:
105                                 self.loginCallback()
106                         loginCallback = None
107                 except:
108                         import traceback, sys
109                         traceback.print_exc(file=sys.stdout)
110                         #raise e
111
112         def login(self):
113                 print "[FritzCallFBF] Login"
114                 if config.plugins.FritzCall.password.value != "":
115                         host = "%d.%d.%d.%d" %tuple(config.plugins.FritzCall.hostname.value)
116                         uri =  "/cgi-bin/webcm"
117                         parms = "login:command/password=%s" %(config.plugins.FritzCall.password.value)
118                         url = "http://%s%s" %(host, uri)
119                         getPage(url, method="POST", headers = {'Content-Type': "application/x-www-form-urlencoded",'Content-Length': str(len(parms))}, postdata=parms).addCallback(self._gotPageLogin).addErrback(self.errorLogin)
120                 else:
121                         self.loginCallback()
122                         self.loginCallback = None
123
124         def errorLoad(self, error):
125                 text = _("Could not load phonebook from FRITZ!Box - Error: %s") %error
126                 self.notify(text)
127
128         def _gotPageLoad(self, html):
129                 print "[FritzCallFBF] _gotPageLoad"
130                 # workaround: exceptions in gotPage-callback were ignored
131                 try:
132                         self.parseFritzBoxPhonebook(html)
133                 except:
134                         import traceback, sys
135                         traceback.print_exc(file=sys.stdout)
136                         #raise e
137
138         def loadFritzBoxPhonebook(self):
139                 print "[FritzCallFBF] loadFritzBoxPhonebook"
140                 if config.plugins.FritzCall.fritzphonebook.value:
141                         print "[FritzCallFBF] loadFritzBoxPhonebook: logging in"
142                         self.loginCallback = self._loadFritzBoxPhonebook
143                         self.login()
144
145         def _loadFritzBoxPhonebook(self):
146                         host = "%d.%d.%d.%d" %tuple(config.plugins.FritzCall.hostname.value)
147                         uri = "/cgi-bin/webcm"# % tuple(config.plugins.FritzCall.hostname.value)
148                         parms = urlencode({'getpage':'../html/de/menus/menu2.html', 'var:lang':'de','var:pagename':'fonbuch','var:menu':'fon'})
149                         url = "http://%s%s?%s" %(host, uri, parms)
150
151                         getPage(url).addCallback(self._gotPageLoad).addErrback(self.errorLoad)
152
153         def parseFritzBoxPhonebook(self, html):
154                 print "[FritzCallFBF] parseFritzBoxPhonebook"
155
156                 table = html2utf8(html)
157                 if re.search('TrFonName', table):
158                         #===============================================================================
159                         #                                New Style: 7170 / 7270 (FW 54.04.58, 54.04.63-11941) 
160                         #       We expect one line with TrFonName followed by several lines with
161                         #       TrFonNr(Type,Number,Shortcut,Vanity), which all belong to the name in TrFonName.
162                         #===============================================================================
163                         # entrymask = re.compile('(TrFonName\("[^"]+", "[^"]+", "[^"]+"\);</SCRIPT>\s+[<SCRIPT type=text/javascript>TrFonNr\("[^"]+", "[^"]+", "[^"]+", "[^"]+"\);</SCRIPT>\s+]+)<SCRIPT type=text/javascript>document.write(TrFon1());</SCRIPT>', re.DOTALL)
164                         # entrymask = re.compile('(TrFonName\("[^"]+", "[^"]+", "[^"]+"\);.*?[.*?TrFonNr\("[^"]+", "[^"]+", "[^"]+", "[^"]+"\);.*?]+).*?document.write(TrFon1());', re.DOTALL)
165                         entrymask = re.compile('(TrFonName\("[^"]+", "[^"]+", "[^"]*"\);.*?)TrFon1\(\)', re.S)
166                         entries = entrymask.finditer(html)
167                         for entry in entries:
168                                 # print entry.group(1)
169                                 found = re.match('TrFonName\("[^"]*", "([^"]+)", "[^"]*"\);', entry.group(1))
170                                 if found:
171                                         name = found.group(1)
172                                 else:
173                                         continue
174                                 detailmask = re.compile('TrFonNr\("([^"]*)", "([^"]*)", "([^"]*)", "([^"]*)"\);', re.S)
175                                 details = detailmask.finditer(entry.group(1))
176                                 for found in details:
177                                         thisname = name
178
179                                         type = found.group(1)
180                                         if config.plugins.FritzCall.showType.value:
181                                                 if type == "mobile":
182                                                         thisname = thisname + " (" +_("mobile") + ")"
183                                                 elif type == "home":
184                                                         thisname = thisname + " (" +_("home") + ")"
185                                                 elif type == "work":
186                                                         thisname = thisname + " (" +_("work") + ")"
187
188                                         if config.plugins.FritzCall.showShortcut.value and found.group(3):
189                                                 thisname = thisname + ", " + _("Shortcut") + ": " + found.group(3)
190                                         if config.plugins.FritzCall.showVanity.value and found.group(4):
191                                                 thisname = thisname + ", " + _("Vanity") + ": " + found.group(4)
192
193                                         thisnumber = found.group(2).strip()
194                                         thisname = thisname.strip()
195                                         if thisnumber:
196                                                 print "[FritzCallFBF] Adding '''%s''' with '''%s''' from FRITZ!Box Phonebook!" %(thisname, thisnumber)
197                                                 phonebook.phonebook[thisnumber] = thisname
198                                         else:
199                                                 print "[FritzCallFBF] ignoring empty number for %s" %thisname
200                                         continue
201
202                 elif re.search('TrFon', table):
203                         #===============================================================================
204                         #                               Old Style: 7050 (FW 14.04.33)
205                         #       We expect one line with TrFon(No,Name,Number,Shortcut,Vanity)
206                         #===============================================================================                                
207                         entrymask = re.compile('TrFon\("[^"]*", "([^"]*)", "([^"]*)", "([^"]*)", "([^"]*)"\)', re.S)
208                         entries = entrymask.finditer(html)
209                         for found in entries:
210                                 name = found.group(1).strip()
211                                 thisnumber = found.group(2).strip()
212                                 if config.plugins.FritzCall.showShortcut.value and found.group(3):
213                                         name = name + ", " + _("Shortcut") + ": " + found.group(3)
214                                 if config.plugins.FritzCall.showVanity.value and found.group(4):
215                                         name = name + ", " +_("Vanity") +": " + found.group(4)
216                                 if thisnumber:
217                                         print "[FritzCallFBF] Adding '''%s''' with '''%s''' from FRITZ!Box Phonebook!" %(name, thisnumber)
218                                         phonebook.phonebook[thisnumber] = name
219                                 else:
220                                         print "[FritzCallFBF] ignoring empty number for %s" %name
221                                 continue
222                 else:
223                         self.notify(_("Could not parse FRITZ!Box Phonebook entry"))
224
225         def errorCalls(self, error):
226                 text = _("Could not load missed calls from FRITZ!Box - Error: %s") %error
227                 self.notify(text)
228
229         def _gotPageCalls(self, html):
230                 def _resolveNumber(number):
231                         if number.isdigit():
232                                 if config.plugins.FritzCall.internal.value and len(number) > 3 and number[0]=="0": number = number[1:]
233                                 name = phonebook.search(number)
234                                 if name:
235                                         found = re.match('(.*?)\n.*', name)
236                                         if found:
237                                                 name = found.group(1)
238                                         number = name
239                         elif number == "":
240                                 number = _("UNKNOWN")
241                         # if len(number) > 20: number = number[:20]
242                         return number
243
244                 # check for error: wrong password or password not set... TODO
245                 found = re.search('Melden Sie sich mit dem Kennwort der FRITZ!Box an', html)
246                 if found:
247                         text = _("You need to set the password of the FRITZ!Box\nin the configuration dialog to display missed calls\n\nIt could be a communication issue, just try again.")
248                         # self.session.open(MessageBox, text, MessageBox.TYPE_ERROR, timeout=config.plugins.FritzCall.timeout.value)
249                         self.notify(text)
250                         return
251
252                 # print "[FritzCallFBF] _gotPageCalls:\n" + html
253                 lines = html.splitlines()
254                 text = ""
255                 for line in lines:
256                         # print line
257                         found = re.match(".*2;([^;]*);;([^;]*);;([^;]*)", line)
258                         if found:
259                                 date = found.group(1)
260                                 caller = _resolveNumber(found.group(2))
261                                 callee = _resolveNumber(found.group(3))
262                                 while (len(caller) + len(callee)) > 40:
263                                         if len(caller) > len(callee):
264                                                 caller = caller[:-1]
265                                         else:
266                                                 callee = callee[:-1]
267                                 found = re.match("(\d\d.\d\d.)\d\d( \d\d:\d\d)", date)
268                                 if found: date = found.group(1) + found.group(2)
269                                 text = text + "\n" + date + " " + caller + " -> " + callee
270
271                 # print "[FritzCallFBF] _gotPageCalls result:\n" + text
272
273                 if self.missedCallback is not None:
274                         # print "[FritzCallFBF] _gotPageCalls call callback with\n" + text
275                         self.missedCallback(text = text)
276                         self.missedCallback = None
277
278         def getMissedCalls(self, callback):
279                 #
280                 # call sequence must be:
281                 # - login
282                 # - getPage -> _gotPageLogin
283                 # - loginCallback (_getMissedCalls)
284                 # - getPage -> _getMissedCalls1
285                 print "[FritzCallFBF] getMissedCalls"
286                 self.missedCallback = callback
287                 self.loginCallback = self._getMissedCalls
288                 self.login()
289
290         def _getMissedCalls(self):
291                 #
292                 # we need this to fill Anrufliste.csv
293                 # http://repeater1/cgi-bin/webcm?getpage=../html/de/menus/menu2.html&var:lang=de&var:menu=fon&var:pagename=foncalls
294                 #
295                 print "[FritzCallFBF] _getMissedCalls"
296                 host = "%d.%d.%d.%d" %tuple(config.plugins.FritzCall.hostname.value)
297                 parms = urlencode({'getpage':'../html/de/menus/menu2.html', 'var:lang':'de','var:pagename':'foncalls','var:menu':'fon'})
298                 url = "http://%s/cgi-bin/webcm?%s" %(host, parms)
299                 getPage(url).addCallback(self._getMissedCalls1).addErrback(self.errorCalls)
300
301         def _getMissedCalls1(self, html):
302                 #
303                 # finally we should have successfully lgged in and filled the csv
304                 #
305                 print "[FritzCallFBF] _getMissedCalls1"
306                 host = "%d.%d.%d.%d" %tuple(config.plugins.FritzCall.hostname.value)
307                 parms = urlencode({'getpage':'../html/de/FRITZ!Box_Anrufliste.csv'})
308                 url = "http://%s/cgi-bin/webcm?%s" %(host, parms)
309                 getPage(url).addCallback(self._gotPageCalls).addErrback(self.errorCalls)
310
311 fritzbox = FritzCallFBF()
312
313 class FritzDisplayMissedCalls(Screen):
314
315         skin = """
316                 <screen name="FritzDisplayMissedCalls" position="100,90" size="550,420" title="Missed calls" >
317                         <widget name="statusbar" position="0,0" size="550,22" font="Regular;22" />
318                         <widget name="list" position="0,22" size="550,398" font="Regular;22" />
319                 </screen>"""
320
321         def __init__(self, session, text = ""):
322                 self.skin = FritzDisplayMissedCalls.skin
323                 Screen.__init__(self, session)
324
325                 self["setupActions"] = ActionMap(["OkCancelActions", "DirectionActions"],
326                 {
327                         "down": self.pageDown,
328                         "up": self.pageUp,
329                         "right": self.pageDown,
330                         "left": self.pageUp,
331                         "cancel": self.ok,
332                         "save": self.ok,
333                         "ok": self.ok,}, -2)
334                 
335                 if text == "":
336                         self["statusbar"] = Label(_("Getting missed calls from FRITZ!Box..."))
337                         self["list"] = ScrollLabel("")
338                         fritzbox.getMissedCalls(self.gotMissedCalls)
339                 else:
340                         self["statusbar"] = Label(_("Missed calls during Standby"))
341                         self["list"] = ScrollLabel(text)
342
343         def ok(self):
344                 self.close()
345
346         def pageDown(self):
347                 self["list"].pageDown()
348
349         def pageUp(self):
350                 self["list"].pageUp()
351
352         def gotMissedCalls(self, text):
353                 # print "[FritzDisplayMissedCalls] gotMissedCalls:\n" + text
354                 self["statusbar"].setText(_("Missed calls"))
355                 self["list"].setText(text)
356
357
358 class FritzCallPhonebook:
359         def __init__(self):
360                 self.phonebook = {}
361                 self.reload()
362
363         def notify(self, text):
364                 Notifications.AddNotification(MessageBox, text, type=MessageBox.TYPE_ERROR, timeout=config.plugins.FritzCall.timeout.value)
365
366         def create(self):
367                 try:
368                         f = open(config.plugins.FritzCall.phonebookLocation.value, 'w')
369                         f.write("01234567890#Name, Street, Location (Keep the Spaces!!!)\n");
370                         f.close()
371                         return True
372                 except IOError:
373                         return False
374
375         def reload(self):
376                 print "[FritzCallPhonebook] reload"
377                 self.phonebook.clear()
378
379                 if not config.plugins.FritzCall.enable.value:
380                         return
381
382                 exists = False
383                 
384                 if config.plugins.FritzCall.phonebook.value:
385                         if not os_path.exists(config.plugins.FritzCall.phonebookLocation.value):
386                                 if(self.create()):
387                                         exists = True
388                         else:
389                                 exists = True
390         
391                         if exists:
392                                 for line in open(config.plugins.FritzCall.phonebookLocation.value):
393                                         try:
394                                                 number, name = line.split("#")
395                                                 if not self.phonebook.has_key(number):
396                                                         self.phonebook[number] = name
397                                         except ValueError:
398                                                 print "[FritzCallPhonebook] Could not parse internal Phonebook Entry %s" %line
399
400                 if config.plugins.FritzCall.fritzphonebook.value:
401                         fritzbox.loadFritzBoxPhonebook()
402
403         def search(self, number):
404                 print "[FritzCallPhonebook] Searching for %s" %number
405                 name = None
406                 if config.plugins.FritzCall.phonebook.value or config.plugins.FritzCall.fritzphonebook.value:
407                         if self.phonebook.has_key(number):
408                                 name = self.phonebook[number].replace(", ", "\n").strip()
409                 return name
410
411         def add(self, number, name):
412                 print "[FritzCallPhonebook] add"
413 #===============================================================================
414 #               It could happen, that two reverseLookups are running in parallel,
415 #               so check first, whether we have already added the number to the phonebook.
416 #===============================================================================
417                 if phonebook.search(number) is None and number <> 0 and config.plugins.FritzCall.phonebook.value and config.plugins.FritzCall.addcallers.value:
418                         try:
419                                 f = open(config.plugins.FritzCall.phonebookLocation.value, 'a')
420                                 name = name.strip() + "\n"
421                                 string = "%s#%s" %(number, name)
422                                 self.phonebook[number] = name;
423                                 f.write(string)
424                                 f.close()
425                                 print "[FritzCallPhonebook] added %s with %sto Phonebook.txt" %(number, name)
426                                 return True
427
428                         except IOError:
429                                 return False
430
431 phonebook = FritzCallPhonebook()
432
433 class FritzCallSetup(ConfigListScreen, Screen):
434         skin = """
435                 <screen position="100,90" size="550,420" title="FritzCall Setup" >
436                 <widget name="config" position="20,10" size="510,300" scrollbarMode="showOnDemand" />
437                 <widget name="consideration" position="20,320" font="Regular;20" halign="center" size="510,50" />
438                 <ePixmap position="5,375" zPosition="4" size="140,40" pixmap="skin_default/buttons/red.png" transparent="1" alphatest="on" />
439                 <ePixmap position="145,375" zPosition="4" size="140,40" pixmap="skin_default/buttons/green.png" transparent="1" alphatest="on" />
440                 <ePixmap position="285,375" zPosition="4" size="140,40" pixmap="skin_default/buttons/yellow.png" transparent="1" alphatest="on" />
441                 <widget name="key_red" position="5,375" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
442                 <widget name="key_green" position="145,375" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
443                 <widget name="key_yellow" position="285,375" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
444                 </screen>"""
445
446         def __init__(self, session, args = None):
447
448                 Screen.__init__(self, session)
449                 self.session = session
450
451                 self["consideration"] = Label(_("You need to enable the monitoring on your FRITZ!Box by dialing #96*5*!"))
452                 self.list = []
453
454                 # Initialize Buttons
455                 self["key_red"] = Button(_("Cancel"))
456                 self["key_green"] = Button(_("OK"))
457                 self["key_yellow"] = Button(_("Missed Calls"))
458
459                 self["setupActions"] = ActionMap(["SetupActions", "ColorActions"],
460                 {
461                         "cancel": self.cancel,
462                         "red": self.cancel,     # not strictly needed, better for clarity
463                         "save": self.save,
464                         "green": self.save,     # not strictly needed, better for clarity
465                         "yellow": self.displayMissedCalls,
466                         "ok": self.save,
467                 }, -2)
468
469                 ConfigListScreen.__init__(self, self.list)
470                 self.createSetup()
471
472
473         def keyLeft(self):
474                 ConfigListScreen.keyLeft(self)
475                 self.createSetup()
476
477         def keyRight(self):
478                 ConfigListScreen.keyRight(self)
479                 self.createSetup()
480
481         def createSetup(self):
482                 self.list = [ ]
483                 self.list.append(getConfigListEntry(_("Call monitoring"), config.plugins.FritzCall.enable))
484                 if config.plugins.FritzCall.enable.value:
485                         self.list.append(getConfigListEntry(_("FRITZ!Box FON IP address"), config.plugins.FritzCall.hostname))
486
487                         self.list.append(getConfigListEntry(_("Show after Standby"), config.plugins.FritzCall.afterStandby))
488
489                         self.list.append(getConfigListEntry(_("Show Calls for specific MSN"), config.plugins.FritzCall.filter))
490                         if config.plugins.FritzCall.filter.value:
491                                 self.list.append(getConfigListEntry(_("MSN to show (separated by ,)"), config.plugins.FritzCall.filtermsn))
492
493                         self.list.append(getConfigListEntry(_("Show Outgoing Calls"), config.plugins.FritzCall.showOutgoing))
494                         self.list.append(getConfigListEntry(_("Timeout for Call Notifications (seconds)"), config.plugins.FritzCall.timeout))
495                         self.list.append(getConfigListEntry(_("Reverse Lookup Caller ID (DE,CH,IT,AT only)"), config.plugins.FritzCall.lookup))
496                         if config.plugins.FritzCall.lookup.value:
497                                 self.list.append(getConfigListEntry(_("Country"), config.plugins.FritzCall.country))
498
499                         self.list.append(getConfigListEntry(_("Password Accessing FRITZ!Box"), config.plugins.FritzCall.password))
500                         self.list.append(getConfigListEntry(_("Read PhoneBook from FRITZ!Box"), config.plugins.FritzCall.fritzphonebook))
501                         if config.plugins.FritzCall.fritzphonebook.value:
502                                 self.list.append(getConfigListEntry(_("Append type of number"), config.plugins.FritzCall.showType))
503                                 self.list.append(getConfigListEntry(_("Append shortcut number"), config.plugins.FritzCall.showShortcut))
504                                 self.list.append(getConfigListEntry(_("Append vanity name"), config.plugins.FritzCall.showVanity))
505
506                         self.list.append(getConfigListEntry(_("Use internal PhoneBook"), config.plugins.FritzCall.phonebook))
507                         if config.plugins.FritzCall.phonebook.value:
508                                 self.list.append(getConfigListEntry(_("PhoneBook Location"), config.plugins.FritzCall.phonebookLocation))
509                                 self.list.append(getConfigListEntry(_("Automatically add new Caller to PhoneBook"), config.plugins.FritzCall.addcallers))
510
511                         self.list.append(getConfigListEntry(_("Strip Leading 0"), config.plugins.FritzCall.internal))
512                         self.list.append(getConfigListEntry(_("Prefix for Outgoing Calls"), config.plugins.FritzCall.prefix))
513
514                 self["config"].list = self.list
515                 self["config"].l.setList(self.list)
516
517         def save(self):
518 #               print "[FritzCallSetup] save"
519                 for x in self["config"].list:
520                         x[1].save()
521                 if fritz_call is not None:
522                         fritz_call.connect()
523
524                         if config.plugins.FritzCall.phonebook.value:
525                                 if not os_path.exists(config.plugins.FritzCall.phonebookLocation.value):
526                                         if not phonebook.create():
527                                                 Notifications.AddNotification(MessageBox, _("Can't create PhoneBook.txt"), type=MessageBox.TYPE_INFO, timeout=config.plugins.FritzCall.timeout.value)
528                                 else:
529                                         print "[FritzCallSetup] called phonebook.reload()"
530                                         phonebook.reload()
531
532                 self.close()
533
534         def cancel(self):
535 #               print "[FritzCallSetup] cancel"
536                 for x in self["config"].list:
537                         x[1].cancel()
538                 self.close()
539
540         def displayMissedCalls(self):
541                 self.session.open(FritzDisplayMissedCalls)
542
543
544 standbyMode = False
545
546 class FritzCallList:
547         def __init__(self):
548                 self.callList = [ ]
549         
550         def add(self, event, date, number, caller, phone):
551                 print "[FritzCallList] add"
552                 if len(self.callList) > 10:
553                         if self.callList[0] != "Start":
554                                 self.callList[0] = "Start"
555                         del self.callList[1]
556
557                 self.callList.append((event, number, date, caller, phone))
558         
559         def display(self):
560                 print "[FritzCallList] display"
561                 global standbyMode
562                 global my_global_session
563                 standbyMode = False
564                 # Standby.inStandby.onClose.remove(self.display) object does not exist anymore...
565                 # build screen from call list
566                 text = "\n"
567                 if self.callList[0] == "Start":
568                         text = text + _("Last 10 calls:\n")
569                         del self.callList[0]
570
571                 for call in self.callList:
572                         (event, number, date, caller, phone) = call
573                         if event == "RING":
574                                 direction = "->"
575                         else:
576                                 direction = "<-"
577                         found = re.match(".*(\d\d.\d\d.)\d\d( \d\d:\d\d)", date)
578                         if found: date = found.group(1) + found.group(2)
579                         found = re.match(".*\((.*)\)", phone)
580                         if found: phone = found.group(1)
581                         # if len(phone) > 20: phone = phone[:20]
582
583                         if caller == _("UNKNOWN") and number != "":
584                                 caller = number
585                         else:
586                                 found = re.match("(.*)\n.*", caller)
587                                 if found: caller = found.group(1)
588                         # if len(caller) > 20: caller = caller[:20]
589                         while (len(caller) + len(phone)) > 40:
590                                 if len(caller) > len(phone):
591                                         caller = caller[:-1]
592                                 else:
593                                         phone = phone[:-1]
594
595                         text = text + "%s %s %s %s\n" %(date, caller, direction, phone)
596
597                 print "[FritzCallList] display: '%s %s %s %s'" %(date, caller, direction, phone)
598                 # display screen
599                 Notifications.AddNotification(MessageBox, text, type=MessageBox.TYPE_INFO)
600                 # my_global_session.open(FritzDisplayMissedCalls, text) # TODO please HELP: from where can I get a session?
601                 self.callList = [ ]
602                 self.text = ""
603
604 callList = FritzCallList()
605
606 def notifyCall(event, date, number, caller, phone):
607         if Standby.inStandby is None or config.plugins.FritzCall.afterStandby.value == "each":
608                 if event == "RING":
609                         text = _("Incoming Call on %s from\n---------------------------------------------\n%s\n%s\n---------------------------------------------\nto: %s") % (date, number, caller, phone)
610                 else:
611                         text = _("Outgoing Call on %s to\n---------------------------------------------\n%s\n%s\n---------------------------------------------\nfrom: %s") % (date, number, caller, phone)
612                 print "[FritzCall] notifyCall:\n%s" %text
613                 Notifications.AddNotification(MessageBox, text, type=MessageBox.TYPE_INFO, timeout=config.plugins.FritzCall.timeout.value)
614         elif config.plugins.FritzCall.afterStandby.value == "inList":
615                 #
616                 # if not yet done, register function to show call list
617                 global standbyMode
618                 if not standbyMode :
619                         standbyMode = True
620                         Standby.inStandby.onHide.append(callList.display)
621                 # add text/timeout to call list
622                 callList.add(event, date, number, caller, phone)
623                 print "[FritzCall] notifyCall: added to callList"
624         else: # this is the "None" case
625                 print "[FritzCall] notifyCall: standby and no show"
626
627
628 #===============================================================================
629 #               We need a separate class for each invocation of reverseLookup to retain
630 #               the necessary data for the notification
631 #===============================================================================
632
633 class FritzReverseLookupAndNotifier:
634         def __init__(self, event, number, caller, phone, date):
635                 self.event = event
636                 self.number = number
637                 self.caller = caller
638                 self.phone = phone
639                 self.date = date
640
641                 countries = {
642                                         "0049": ("http://www.dasoertliche.de/?form_name=search_inv&ph=%s", self.gotPageDasOertliche, self.gotErrorDasOertliche),
643                                         "0041": ("http://tel.search.ch/result.html?name=&m...&tel=%s", self.gotPageTelSearchCH, self.gotErrorLast),
644                                         "0039": ("http://www.paginebianche.it/execute.cgi?btt=1&ts=106&cb=8&mr=10&rk=&om=&qs=%s", self.gotPagePaginebiancheIT, self.gotErrorLast),
645                                         "0043": ("http://www.telefonabc.at/result.aspx?telpre=%s&telnr=%s&exact=1", self.gotTelefonabcAT, self.gotErrorLast)
646                                         }
647
648                 print "[FritzReverseLookupAndNotifier] reverse Lookup for %s!" %self.number
649
650                 if config.plugins.FritzCall.country.value == "DE":
651                         countrycode = "0049"
652                 elif config.plugins.FritzCall.country.value == "CH":
653                         countrycode = "0041"
654                 elif config.plugins.FritzCall.country.value == "IT":
655                         countrycode = "0039"
656                 elif config.plugins.FritzCall.country.value == "AT":
657                         countrycode = "0043"
658                 else:
659                         print "[FritzReverseLookupAndNotifier] reverse Lookup: unknown country?!?!"
660                         countrycode = "0049"
661
662                 if self.number[:2] == "00":
663                         countrycode = self.number[:4]
664
665                 if countries.has_key(countrycode):
666                         (url, callBack, errBack) = countries[countrycode]
667                         if countrycode != "0043":
668                                 url = url %self.number.replace(countrycode,"0")
669                         else:      # for Austria we must separate the number
670                                 number = self.number.replace(countrycode,"0")
671                                 if number[:2] == "01":  # Wien
672                                         print "[FritzReverseLookupAndNotifier] AT: Wien"
673                                         url = url % ("01", number[2:])
674                                 elif number[1:4] in ["316", "463", "512", "650", "662", "660", "664", "676", "678", "680", "681", "688", "699", "720", "732"]:
675                                         print "[FritzReverseLookupAndNotifier] AT: short prefix"
676                                         url = url % (number[:4], number[4:])
677                                 else:
678                                         print "[FritzReverseLookupAndNotifier] AT: others"
679                                         url = url % (number[:5], number[5:])
680                                 
681                         getPage(url, method="GET").addCallback(callBack).addErrback(errBack)
682                 else:
683                         print "[FritzReverseLookupAndNotifier] call from country, which is not handled"
684
685
686         def notifyAndReset(self, timeout=config.plugins.FritzCall.timeout.value):
687                 notifyCall(self.event, self.date, self.number, self.caller, self.phone)
688                 # kill that object...
689
690         def gotErrorLast(self, error):
691                 self.caller = _("UNKNOWN")
692                 self.notifyAndReset()
693
694         def gotErrorDasOertliche(self, error):                   # so we try Klicktel
695                 url = "http://www.klicktel.de/telefonbuch/backwardssearch.html?newSearch=1&boxtype=backwards&vollstaendig=%s" %self.number
696                 getPage(url, method="GET").addCallback(self.gotPageKlicktel).addErrback(self.gotErrorLast)
697
698         def gotPageDasOertliche(self, html):
699                 print "[FritzReverseLookupAndNotifier] gotPageDasOertliche"
700                 try:
701                         found = re.match('.*<td.*?class="cel-data border.*?>(.*?)</td>', html, re.S)
702                         if found:
703                                 td = found.group(1)                                     # group(1) is the content of (.*?) in our pattern
704                                 td = td.decode("ISO-8859-1").encode("UTF-8")
705                                 found = re.match('.*<div.*</div>.*<div>.*<a.*class="entry">([^<]*)</a>.*</div>(.*)', td, re.S)
706                                 if found:
707                                         name = found.group(1)
708                                         td = found.group(2)
709                                         text = re.sub("<.*?>", "", td)          # remove tags and their content
710                                         text = text.split("\n")
711                                         #===============================================================================
712                                         # 
713                                         #       The logic here is as follows:
714                                         #       
715                                         #       We are looking for a line
716                                         #       containing 5 digits (PLZ) followed by a space followed by a word at the
717                                         #       end of the line. If found, we assume, that that is the address. The right
718                                         #       one at time of writing was 10.
719                                         #       
720                                         #===============================================================================
721
722                                         addrLine = 10   # as of 08.06.08 that was the correct one
723                                         for i in range(0,len(text)-1):   # look for a line containing the address, i.e. "PLZ Name" at the end of the line
724                                                 if re.search('\d\d\d\d\d \S+$', text[i].replace("&nbsp;", " ").strip()):
725                                                         addrLine = i
726                                                         break
727                                         address = text[addrLine].replace("&nbsp;", " ").replace(", ", "\n").strip();
728                                         print "[FritzReverseLookupAndNotifier] Reverse lookup succeeded with DasOertliche:\nName: %s\n\nAddress: %s" %(name, address)
729
730                                         self.caller = "%s\n%s" %(name, address)
731
732                                         if self.event == "RING":
733                                                 phonebook.add(self.number, self.caller.replace("\n", ", "))
734
735                                         self.notifyAndReset()
736                                         return True
737
738                 except:
739                         import traceback, sys
740                         traceback.print_exc(file=sys.stdout)
741                         #raise e
742
743                 url = "http://www.klicktel.de/telefonbuch/backwardssearch.html?newSearch=1&boxtype=backwards&vollstaendig=%s" %self.number
744                 getPage(url, method="GET").addCallback(self.gotPageKlicktel).addErrback(self.gotErrorKlicktel)
745                 
746         def gotPageKlicktel(self, html):
747                 print "[FritzReverseLookupAndNotifier] gotPageKlicktel"
748                 try:
749                         html = html.decode("ISO-8859-1").encode("UTF-8")
750                         html = html.replace("<br />", ", ")
751                         found = re.match('.*<a class="head" href=".*" title=""><span class="title">(.*)</span></a>.*<span class="location">([\S ,]+)</span>', html, re.S)
752                         if found:
753                                 name = found.group(1)
754                                 address = found.group(2)
755                                 print "[FritzProtocol] Reverse lookup succeeded with Klicktel:\nName: %s\n\nAddress: %s" %(name, address)
756
757                                 self.caller = "%s\n%s" %(name, address)
758
759                                 if self.event == "RING":
760                                         phonebook.add(self.number, self.caller.replace("\n", ", "))
761                         
762                                 self.notifyAndReset()
763                                 return True
764
765                 except:
766                         import traceback, sys
767                         traceback.print_exc(file=sys.stdout)
768                         #raise e
769                 self.caller = _("UNKNOWN")
770                 self.notifyAndReset()
771
772
773         def gotPageTelSearchCH(self, html):
774                 print "[FritzReverseLookupAndNotifier] gotPageTelSearchCH"
775                 try:
776                         html = html.decode("ISO-8859-1").encode("UTF-8")
777                         html = html.replace("<br />", ", ")
778                         found = re.match('.*<table class="record">.*<a href="[^"]+">([\S ,]+)</a>.*<div class="raddr">([\S ,]+)</div>.*</table>', html, re.S)
779                         if found:
780                                 name = found.group(1).replace(",","")
781                                 address = found.group(2)
782                                 print "[FritzProtocol] Reverse lookup succeeded:\nName: %s\n\nAddress: %s" %(name, address)
783
784                                 self.caller = "%s, %s" %(name, address)
785
786                                 if self.number != 0 and config.plugins.FritzCall.addcallers.value and self.event == "RING":
787                                         phonebook.add(self.number, self.caller)
788                                         
789                                 self.notifyAndReset()
790                                 return True
791
792                 except:
793                         import traceback, sys
794                         traceback.print_exc(file=sys.stdout)
795                         #raise e
796                 self.caller = _("UNKNOWN")
797                 self.notifyAndReset()
798
799
800         def gotPagePaginebiancheIT(self, html):
801                 print "[FritzReverseLookupAndNotifier] gotPagePaginebiancheIT"
802                 try:
803                         html = html.decode("ISO-8859-1").encode("UTF-8")
804                         found = re.match('.*<div class="client-identifying-pg(.*class="org">.*class="postal-code">.*</span>.*class="locality">.*</span>.*class="region">.*</span>.*class="street-address">.*)</span></p></address>', html, re.S)
805                         if found:
806                                 html = found.group(1)
807                                 found = re.match('.*class="org">([^<]*).*class="postal-code">([^<]*).*</span>.*class="locality">([^<]*).*</span>.*class="region">([^<]*).*</span>.*class="street-address">([^<]*).*', html, re.S)
808                                 if found:
809                                         name = found.group(1)
810                                         postalcode = found.group(2)
811                                         locality = found.group(3)
812                                         region = found.group(4)
813                                         streetaddress = found.group(5).replace(",","")
814                                         address =  streetaddress+ ", " + postalcode + " " + locality + " " + region
815                                         print "[FritzProtocol] Reverse lookup succeeded:\nName: %s\n\nAddress: %s" %(name, address)
816
817                                         self.caller = "%s, %s" %(name, address)
818
819                                         if self.number != 0 and config.plugins.FritzCall.addcallers.value and self.event == "RING":
820                                                 phonebook.add(self.number, self.caller)
821
822                                         self.notifyAndReset()
823                                         return True
824
825                 except:
826                         import traceback, sys
827                         traceback.print_exc(file=sys.stdout)
828                         #raise e
829                 self.caller = _("UNKNOWN")
830                 self.notifyAndReset()
831
832
833         def gotTelefonabcAT(self, html):
834                 print "[FritzReverseLookupAndNotifier] gotTelefonabcAT"
835                 try:
836                         html = html.decode("ISO-8859-1").encode("UTF-8")
837                         html = html.replace("<b>","").replace("</b>","")
838                         found = re.match('.*(<td class="name">.*.*<td colspan="2" class="address small">.*</td>)', html, re.S)
839                         if found:
840                                 html = found.group(1)
841                                 found = re.match('.*<td class="name">\r\n([^<]*)</td>.*<td colspan="2" class="address small">\r\n([^<]*)</td>', html, re.S)
842                                 if found:
843                                         name = found.group(1)
844                                         address =  found.group(2)
845                                         myprint("[FritzProtocol] Reverse lookup succeeded:\nName: %s\n\nAddress: %s" %(name, address))
846
847                                         self.caller = "%s, %s" %(name, address)
848
849                                         if self.number != 0 and config.plugins.FritzCall.addcallers.value and self.event == "RING":
850                                                 phonebook.add(self.number, self.caller)
851
852                                         self.notifyAndReset()
853                                         return True
854
855                 except:
856                         import traceback, sys
857                         traceback.print_exc(file=sys.stdout)
858                         #raise e
859                 self.caller = _("UNKNOWN")
860                 self.notifyAndReset()
861
862
863
864 class FritzProtocol(LineReceiver):
865         def __init__(self):
866                 print "[FritzProtocol] __init__"
867                 self.resetValues()
868
869         def resetValues(self):
870                 print "[FritzProtocol] resetValues"
871                 self.number = '0'
872                 self.caller = None
873                 self.phone = None
874                 self.date = '0'
875
876         def notifyAndReset(self, timeout=config.plugins.FritzCall.timeout.value):
877                 notifyCall(self.event, self.date, self.number, self.caller, self.phone)
878                 self.resetValues()
879
880         def lineReceived(self, line):
881                 print "[FritzProtocol] lineReceived: %s" %line
882 #15.07.06 00:38:54;CALL;1;4;<from/extern>;<to/our msn>;
883 #15.07.06 00:38:58;DISCONNECT;1;0;
884 #15.07.06 00:39:22;RING;0;<from/extern>;<to/our msn>;
885 #15.07.06 00:39:27;DISCONNECT;0;0;
886                 a = []
887                 a = line.split(';')
888                 (self.date, self.event) = a[0:2]
889
890                 if self.event == "RING" or (self.event == "CALL" and config.plugins.FritzCall.showOutgoing.value):
891                         phone = a[4]
892                          
893                         if self.event == "RING":
894                                 number = a[3] 
895                         else:
896                                 number = a[5]
897                                 
898                         print "[FritzProtocol] lineReceived phone: '''%s''' number: '''%s'''" % (phone, number)
899
900                         filtermsns = config.plugins.FritzCall.filtermsn.value.split(",")
901                         for i in range(len(filtermsns)):
902                                 filtermsns[i] = filtermsns[i].strip()
903                         if not (config.plugins.FritzCall.filter.value and phone not in filtermsns):
904                                 print "[FritzProtocol] lineReceived no filter hit"
905                                 phonename = phonebook.search(phone)                # do we have a name for the number of our side?
906                                 if phonename is not None:
907                                         self.phone = "%s (%s)" %(phone, phonename)
908                                 else:
909                                         self.phone = phone
910
911                                 if config.plugins.FritzCall.internal.value and len(number) > 3 and number[0]=="0":
912                                         self.number = number[1:]
913                                 else:
914                                         self.number = number
915
916                                 if self.event == "CALL" and self.number[0] != '0':                                        # should only happen for outgoing
917                                         self.number = config.plugins.FritzCall.prefix.value + self.number
918
919                                 if self.number is not "":
920                                         print "[FritzProtocol] lineReceived phonebook.search: %s" %self.number
921                                         self.caller = phonebook.search(self.number)
922                                         print "[FritzProtocol] lineReceived phonebook.search reault: %s" %self.caller
923                                         if (self.caller is None) and config.plugins.FritzCall.lookup.value:
924                                                 FritzReverseLookupAndNotifier(self.event, self.number, self.caller, self.phone, self.date)
925                                                 return                                                  # reverselookup is supposed to handle the message itself 
926
927                                 if self.caller is None:
928                                         self.caller = _("UNKNOWN")
929
930                                 self.notifyAndReset()
931
932
933 class FritzClientFactory(ReconnectingClientFactory):
934         initialDelay = 20
935         maxDelay = 500
936
937         def __init__(self):
938                 self.hangup_ok = False
939
940         def startedConnecting(self, connector):
941                 Notifications.AddNotification(MessageBox, _("Connecting to FRITZ!Box..."), type=MessageBox.TYPE_INFO, timeout=2)
942
943         def buildProtocol(self, addr):
944                 Notifications.AddNotification(MessageBox, _("Connected to FRITZ!Box!"), type=MessageBox.TYPE_INFO, timeout=4)
945                 self.resetDelay()
946                 return FritzProtocol()
947
948         def clientConnectionLost(self, connector, reason):
949                 if not self.hangup_ok:
950                         Notifications.AddNotification(MessageBox, _("Connection to FRITZ!Box! lost\n (%s)\nretrying...") % reason.getErrorMessage(), type=MessageBox.TYPE_INFO, timeout=config.plugins.FritzCall.timeout.value)
951                 ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
952
953         def clientConnectionFailed(self, connector, reason):
954                 Notifications.AddNotification(MessageBox, _("Connecting to FRITZ!Box failed\n (%s)\nretrying...") % reason.getErrorMessage(), type=MessageBox.TYPE_INFO, timeout=config.plugins.FritzCall.timeout.value)
955                 ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)
956
957 class FritzCall:
958         def __init__(self):
959                 self.dialog = None
960                 self.d = None
961                 self.connect()
962
963         def connect(self):
964                 self.abort()
965                 if config.plugins.FritzCall.enable.value:
966                         f = FritzClientFactory()
967                         self.d = (f, reactor.connectTCP("%d.%d.%d.%d" % tuple(config.plugins.FritzCall.hostname.value), 1012, f))
968
969         def shutdown(self):
970                 self.abort()
971
972         def abort(self):
973                 if self.d is not None:
974                         self.d[0].hangup_ok = True
975                         self.d[0].stopTrying()
976                         self.d[1].disconnect()
977                         self.d = None
978
979 def displayMissedCalls(session, servicelist):
980         session.open(FritzDisplayMissedCalls)
981
982 def main(session):
983         session.open(FritzCallSetup)
984
985 fritz_call = None
986
987 def autostart(reason, **kwargs):
988         global fritz_call
989
990         # ouch, this is a hack
991         if kwargs.has_key("session"):
992                 global my_global_session
993                 my_global_session = kwargs["session"]
994                 return
995
996         print "[FRITZ!Call] - Autostart"
997         if reason == 0:
998                 fritz_call = FritzCall()
999         elif reason == 1:
1000                 fritz_call.shutdown()
1001                 fritz_call = None
1002
1003 def Plugins(**kwargs):
1004         what = _("Display FRITZ!box-Fon calls on screen")
1005         what_missed = _("Missed calls")
1006         if os_path.exists("plugin.png"):
1007                 return [ PluginDescriptor(name="FritzCall", description=what, where = PluginDescriptor.WHERE_PLUGINMENU, icon = "plugin.png", fnc=main),
1008                         PluginDescriptor(name=what_missed, description=what_missed, where = PluginDescriptor.WHERE_EXTENSIONSMENU, fnc=displayMissedCalls),
1009                         PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART, PluginDescriptor.WHERE_AUTOSTART], fnc = autostart) ]
1010         else:
1011                 return [ PluginDescriptor(name="FritzCall", description=what, where = PluginDescriptor.WHERE_PLUGINMENU, fnc=main),
1012                         PluginDescriptor(name=what_missed, description=what_missed, where = PluginDescriptor.WHERE_EXTENSIONSMENU, fnc=displayMissedCalls),
1013                         PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART, PluginDescriptor.WHERE_AUTOSTART], fnc = autostart) ]