* add possiblity to show only calls for a specific MSN
[enigma2-plugins.git] / fritzcall / src / plugin.py
1 # -*- coding: utf-8 -*-
2 from enigma import eTimer
3
4 from Screens.Screen import Screen
5 from Screens.MessageBox import MessageBox
6
7 from Components.ActionMap import ActionMap
8 from Components.Label import Label
9 from Components.config import config, ConfigSubsection, ConfigSelection, ConfigIP, ConfigEnableDisable, getConfigListEntry, ConfigText, ConfigInteger
10 from Components.ConfigList import ConfigList, ConfigListScreen
11
12 from Plugins.Plugin import PluginDescriptor
13 from Tools import Notifications
14
15 from twisted.internet import reactor
16 from twisted.internet.protocol import ReconnectingClientFactory
17 from twisted.protocols.basic import LineReceiver
18 from twisted.web.client import getPage
19 from twisted.web2.client.http import HTTPClientProtocol, ClientRequest
20
21 from os import path as os_path
22 from urllib import urlencode
23 import re
24
25
26 my_global_session = None
27
28 config.plugins.FritzCall = ConfigSubsection()
29 config.plugins.FritzCall.enable = ConfigEnableDisable(default = False)
30 config.plugins.FritzCall.hostname = ConfigIP(default = [192, 168, 178, 1])
31 config.plugins.FritzCall.filter = ConfigEnableDisable(default = False)
32 config.plugins.FritzCall.filtermsn = ConfigText(default = "", fixed_size = False)
33 config.plugins.FritzCall.showOutgoing = ConfigEnableDisable(default = False)
34 config.plugins.FritzCall.timeout = ConfigInteger(default = 15, limits = (0,60))
35 config.plugins.FritzCall.lookup = ConfigEnableDisable(default = False)
36 config.plugins.FritzCall.internal = ConfigEnableDisable(default = False)
37 config.plugins.FritzCall.fritzphonebook = ConfigEnableDisable(default = False)
38 config.plugins.FritzCall.phonebook = ConfigEnableDisable(default = False)
39 config.plugins.FritzCall.addcallers = ConfigEnableDisable(default = False)
40 config.plugins.FritzCall.phonebookLocation = ConfigSelection(choices = [("/media/usb/PhoneBook.txt", _("USB Stick")), ("/media/cf/PhoneBook.txt", _("CF Drive")), ("/media/hdd/PhoneBook.txt", _("Harddisk"))])
41 config.plugins.FritzCall.password = ConfigText(default = "", fixed_size = False)
42 config.plugins.FritzCall.prefix = ConfigText(default = "", fixed_size = False)
43                 
44 class FritzCallPhonebook:
45         def __init__(self):
46                 self.phonebook = {}
47                 self.reload()
48                 
49         def notify(self, text):
50                 Notifications.AddNotification(MessageBox, text, type=MessageBox.TYPE_ERROR, timeout=config.plugins.FritzCall.timeout.value)
51
52         def create(self):
53                 try:
54                         f = open(config.plugins.FritzCall.phonebookLocation.value, 'w')
55                         f.write("01234567890#Name, Street, Location (Keep the Spaces!!!)\n");
56                         f.close()
57                         return True
58                 except:
59                         return False
60         
61         def error(self, error):
62                 if self.event == "LOGIN":
63                         text = _("Fritz!Box Login failed! - Error: %s") %error
64                         self.notify(text)
65                 elif self.event == "LOAD":
66                         text = _("Could not load phonebook from Fritz!Box - Error: %s") %error
67                         self.notify(text)
68
69         def loadFritzBoxPhonebook(self):
70                 print "[FritzCallPhonebook] loadFritzBoxPhonebook"
71                 self.event = "LOAD"
72                 
73                 host = "%d.%d.%d.%d" %tuple(config.plugins.FritzCall.hostname.value)
74                 uri = "/cgi-bin/webcm"# % tuple(config.plugins.FritzCall.hostname.value)
75                 parms = urlencode({'getpage':'../html/de/menus/menu2.html', 'var:lang':'de','var:pagename':'fonbuch','var:menu':'fon'})
76                         
77                 url = "http://%s%s?%s" %(host, uri, parms)
78                 
79                 getPage(url).addCallback(self._gotPage).addErrback(self.error)
80                 
81         def parseFritzBoxPhonebook(self, html):         
82                 found = re.match('.*<table id="tList".*?</tr>\n(.*?)</table>', html, re.S)
83                 
84                 if found:                                                                                       
85                         table = found.group(1)                                                  
86                         text = re.sub("<.*?>", "", table)                               
87                         text = text.split('\n')
88                          
89                         for line in text:                       
90                                 if line.strip() != "":
91                                         try:
92                                                 line = line.replace("\"", "")
93                                                 line = line.split(", ")
94                                                 name = line[1]
95                                                 number = line[2]
96                                                 name = name.replace("&szlig;", "?").replace("&auml;", "?").replace("&ouml;", "?").replace("&uuml;", "?").replace("&Auml;", "?").replace("&Ouml;", "?").replace("&Uuml;", "?")
97                                                 print "[FritzCallPhonebook] Adding '''%s''' with '''%s''' from Fritz!Box Phonebook!" %(name, number)
98                                                 self.phonebook[number.strip()] = name.strip()
99                                                 
100                                         except IOError():
101                                                 print "[FritzCallPhonebook] Could not parse Fritz!Box Phonebook entry"
102         
103         def _gotPage(self, html):
104 #               print "[FritzCallPhonebook] _gotPage"
105                 # workaround: exceptions in gotPage-callback were ignored
106                 try:
107                         if self.event == "LOGIN":
108                                 self.verifyLogin(html)
109                         if self.event == "LOAD":
110                                 self.parseFritzBoxPhonebook(html)
111                 except:
112                         import traceback, sys
113                         traceback.print_exc(file=sys.stdout)
114                         #raise e
115         
116         def login(self):
117                 print "[FritzCallPhonebook] Login"
118                 self.event = "LOGIN"
119                         
120                 host = "%d.%d.%d.%d" %tuple(config.plugins.FritzCall.hostname.value)
121                 uri =  "/cgi-bin/webcm"
122                 parms = "login:command/password=%s" %(config.plugins.FritzCall.password.value)          
123                 url = "http://%s%s" %(host, uri)                
124
125                 getPage(url, method="POST", headers = {'Content-Type': "application/x-www-form-urlencoded",'Content-Length': str(len(parms))}, postdata=parms).addCallback(self._gotPage).addErrback(self.error)
126                 
127         def verifyLogin(self, html):
128 #               print "[FritzCallPhonebook] verifyLogin - html: %s" %html
129                 self.event = "LOAD"
130                 found = re.match('.*<p class="errorMessage">FEHLER:&nbsp;Das angegebene Kennwort', html, re.S)
131                 if not found:
132                         self.loadFritzBoxPhonebook()
133                 else:
134                         text = _("Fritz!Box Login failed! - Wrong Password!")
135                         self.notify(text)
136
137         def reload(self):
138 #               print "[FritzCallPhonebook] reload"
139                 self.phonebook.clear()
140                 exists = False
141                 if not os_path.exists(config.plugins.FritzCall.phonebookLocation.value):
142                         if(self.create()):
143                                 exists = True
144                 else:
145                         exists = True
146                                 
147                 if exists:
148                         for line in open(config.plugins.FritzCall.phonebookLocation.value):
149                                 try:
150                                         number, name = line.split("#")
151                                         if not self.phonebook.has_key(number):  
152                                                 self.phonebook[number] = name 
153                                 except IOError():
154                                         print "[FritzCallPhonebook] Could not parse internal Phonebook Entry %s" %line 
155
156                 if config.plugins.FritzCall.fritzphonebook.value:
157                         if config.plugins.FritzCall.password.value != "":
158                                 self.login()
159                         else:
160                                 self.loadFritzBoxPhonebook()
161
162         def search(self, number):
163 #               print "[FritzCallPhonebook] Searching for %s" %number
164                 name = None
165                 if config.plugins.FritzCall.phonebook.value:
166                         if self.phonebook.has_key(number):
167                                 name = self.phonebook[number].replace(", ", "\n")
168                 return name
169
170         def add(self, number, name):
171 #               print "[FritzCallPhonebook] add"
172                 if config.plugins.FritzCall.phonebook.value and config.plugins.FritzCall.addcallers.value:                      
173                         try:
174                                 f = open(config.plugins.FritzCall.phonebookLocation.value, 'a')
175                                 name = name.strip() + "\n"
176                                 string = "%s#%s" %(number, name)                
177                                 self.phonebook[number] = name;  
178                                 f.write(string)                                 
179                                 f.close()
180                                 return True
181         
182                         except IOError():
183                                 return False
184
185 phonebook = FritzCallPhonebook()
186                 
187 class FritzCallSetup(ConfigListScreen, Screen):
188         skin = """
189                 <screen position="100,90" size="550,420" title="FritzCall Setup" >
190                 <widget name="config" position="20,10" size="510,300" scrollbarMode="showOnDemand" />
191                 <widget name="consideration" position="20,320" font="Regular;20" halign="center" size="510,50" />
192                 </screen>"""
193
194         def __init__(self, session, args = None):
195                 
196                 Screen.__init__(self, session)
197                 
198                 self["consideration"] = Label(_("You need to enable the monitoring on your Fritz!Box by dialing #96*5*!"))
199                 self.list = []
200                 
201                 self["setupActions"] = ActionMap(["SetupActions"], 
202                 {
203                         "save": self.save, 
204                         "cancel": self.cancel, 
205                         "ok": self.save, 
206                 }, -2)
207
208                 ConfigListScreen.__init__(self, self.list)
209                 self.createSetup()
210
211                 
212         def keyLeft(self):
213                 ConfigListScreen.keyLeft(self)
214                 self.createSetup()
215
216         def keyRight(self):
217                 ConfigListScreen.keyRight(self)
218                 self.createSetup()
219
220         def createSetup(self):
221                 self.list = [ ]
222                 self.list.append(getConfigListEntry(_("Call monitoring"), config.plugins.FritzCall.enable))
223                 if config.plugins.FritzCall.enable.value:
224                         self.list.append(getConfigListEntry(_("Fritz!Box FON IP address"), config.plugins.FritzCall.hostname))
225                         
226                         self.list.append(getConfigListEntry(_("Show Calls for specific MSN"), config.plugins.FritzCall.filter))
227                         if config.plugins.FritzCall.filter.value:
228                                 self.list.append(getConfigListEntry(_("MSN to show"), config.plugins.FritzCall.filtermsn))
229                                 
230                         self.list.append(getConfigListEntry(_("Show Outgoing Calls"), config.plugins.FritzCall.showOutgoing))
231                         self.list.append(getConfigListEntry(_("Timeout for Call Notifications (seconds)"), config.plugins.FritzCall.timeout))
232                         self.list.append(getConfigListEntry(_("Reverse Lookup Caller ID (DE only)"), config.plugins.FritzCall.lookup))
233                 
234                         self.list.append(getConfigListEntry(_("Read PhoneBook from Fritz!Box"), config.plugins.FritzCall.fritzphonebook))
235                         if config.plugins.FritzCall.fritzphonebook.value:
236                                 self.list.append(getConfigListEntry(_("Password Accessing Fritz!Box"), config.plugins.FritzCall.password))
237                         
238                         self.list.append(getConfigListEntry(_("Use internal PhoneBook"), config.plugins.FritzCall.phonebook))
239                         if config.plugins.FritzCall.phonebook.value:
240                                 self.list.append(getConfigListEntry(_("PhoneBook Location"), config.plugins.FritzCall.phonebookLocation))
241                                 self.list.append(getConfigListEntry(_("Automatically add new Caller to PhoneBook"), config.plugins.FritzCall.addcallers))
242                         
243                         self.list.append(getConfigListEntry(_("Strip Leading 0"), config.plugins.FritzCall.internal))
244                         self.list.append(getConfigListEntry(_("Prefix for Outgoing Calls"), config.plugins.FritzCall.prefix))
245                 
246                 self["config"].list = self.list
247                 self["config"].l.setList(self.list)
248
249         def save(self):
250 #               print "[FritzCallSetup] save"
251                 for x in self["config"].list:
252                         x[1].save()
253                 if fritz_call is not None:
254                         fritz_call.connect()
255
256                         if config.plugins.FritzCall.phonebook.value:
257                                 if not os_path.exists(config.plugins.FritzCall.phonebookLocation.value):
258                                         if not phonebook.create():
259                                                 Notifications.AddNotification(MessageBox, _("Can't create PhoneBook.txt"), type=MessageBox.TYPE_INFO, timeout=config.plugins.FritzCall.timeout.value)                                   
260                                 else:
261                                         print "[FritzCallSetup] called phonebook.reload()"
262                                         phonebook.reload()
263                 
264                 self.close()
265
266         def cancel(self):
267 #               print "[FritzCallSetup] cancel"
268                 for x in self["config"].list:
269                         x[1].cancel()
270                 self.close()    
271
272 class FritzProtocol(LineReceiver):
273         def __init__(self):
274 #               print "[FritzProtocol] __init__"
275                 self.resetValues()
276         
277         def resetValues(self):
278 #               print "[FritzProtocol] resetValues"
279                 self.number = '0'
280                 self.caller = None
281                 self.phone = None
282                 self.date = '0'
283         
284         def notify(self, text, timeout=config.plugins.FritzCall.timeout.value):
285                 Notifications.AddNotification(MessageBox, text, type=MessageBox.TYPE_INFO, timeout=timeout)
286         
287         def handleIncoming(self):
288 #               print "[FritzProtocol] handle Incoming!"
289                 
290                 text = _("Incoming Call ")
291                 if self.caller is not None:
292                         text += _("on %s from\n---------------------------------------------\n%s\n%s\n---------------------------------------------\nto: %s") % (self.date, self.number, self.caller, self.phone)
293                 else:
294                         text += _("on %s from\n---------------------------------------------\n%s (UNKNOWN)\n---------------------------------------------\nto: %s") % (self.date, self.number, self.phone)
295                 
296                 self.notify(text)
297                 self.resetValues()
298
299         def handleOutgoing(self):
300 #               print "[FritzProtocol] handle Outgoing!"
301                 text = _("Outgoing Call ")
302                 if(self.caller is not None):    
303                         text += _("on %s to\n---------------------------------------------\n%s\n%s\n---------------------------------------------\nfrom: %s") % (self.date, self.number, self.caller, self.phone)
304                 else:
305                         text += _("on %s to\n---------------------------------------------\n%s (UNKNOWN)\n\n---------------------------------------------\nfrom: %s") % (self.date, self.number, self.phone)#
306
307                 self.notify(text)
308                 self.resetValues()
309
310         def handleEvent(self):
311 #               print "[FritzProtocol] handleEvent!"
312                 if self.event == "RING":
313                         self.handleIncoming()
314                 elif self.event == "CALL":
315                         self.handleOutgoing()
316                 
317         def handleEventOnError(self, error):
318 #               print "[FritzProtocol] handleEventOnError - Error :%s" %error
319                 self.handleEvent()
320                 
321         def _gotPage(self, data):
322 #               print "[FritzProtocol] _gotPage"
323                 try:
324                         self.gotPage(data)
325                 except:
326                         import traceback, sys
327                         traceback.print_exc(file=sys.stdout)
328                         #raise e
329                         self.handleEvent()
330         
331         def gotPage(self, html):
332 #               print "[FritzProtocol] gotPage"
333                 f = open("/tmp/reverseLookup.html", "w")
334                 f.write(html)
335                 f.close()
336                 found = re.match('.*<td.*?class="cel-data border.*?>(.*?)</td>', html, re.S)
337                 if found:                                                                       
338                         td = found.group(1)                                     # group(1) is the content of (.*?) in our pattern
339                         text = re.sub("<.*?>", "", td)          # remove tags and their content
340                         text = text.split("\n")
341
342                         #wee need to strip the values as there a lots of whitespaces
343                         name = text[2].strip()
344                         address = text[8].replace("&nbsp;", " ").replace(", ", "\n").strip();
345 #                       print "[FritzProtocol] Reverse lookup succeeded:\nName: %s\n\nAddress: %s" %(name, address)
346                         
347                         self.caller = "%s\n%s" %(name, address)
348                         
349                         #Autoadd to PhoneBook.txt if enabled
350                         if config.plugins.FritzCall.addcallers.value and self.event == "RING":
351                                 phonebook.add(self.number, self.caller.replace("\n", ", "))
352 #               else:
353 #                       print "[FritzProtocol] Reverse lookup without result!"  
354
355                 self.handleEvent()
356                 
357         def reverseLookup(self):
358 #               print "[FritzProtocol] reverse Lookup!"
359                 url = "http://www.dasoertliche.de/?form_name=search_inv&ph=%s" %self.number
360                 getPage(url,method="GET").addCallback(self._gotPage).addErrback(self.handleEventOnError)
361
362         def lineReceived(self, line):
363 #               print "[FritzProtocol] lineReceived"
364 #15.07.06 00:38:54;CALL;1;4;<provider>;<callee>;
365 #15.07.06 00:38:58;DISCONNECT;1;0;
366 #15.07.06 00:39:22;RING;0;<caller>;<outgoing msn>;
367 #15.07.06 00:39:27;DISCONNECT;0;0;
368
369                 a = line.split(';')
370                 (self.date, self.event) = a[0:2]
371                 
372                 #incoming Call
373                 if self.event == "RING":
374                         phone = a[4]
375                         
376                         if not config.plugins.FritzCall.filter.value or config.plugins.FritzCall.filtermsn.value == phone:      
377                                 phonename = phonebook.search(phone)
378                                 if phonename is not None:
379                                         self.phone = "%s (%s)" %(phone, phonename)
380                                 else:
381                                         self.phone = phone
382                                 
383                                 if config.plugins.FritzCall.internal.value and a[3][0]=="0" and len(a[3]) > 3:
384                                         self.number = a[3][1:]
385                                 else:
386                                         self.number = a[3]
387                                 
388                                 self.caller = phonebook.search(self.number)
389                                 if self.caller is None:
390                                         if config.plugins.FritzCall.lookup.value:
391                                                 self.reverseLookup()
392                                         else:
393                                                 self.handleEvent()
394                                 else:
395                                         self.handleEvent()
396                 
397                 #Outgoing Call
398                 elif config.plugins.FritzCall.showOutgoing.value and self.event == "CALL":
399                         self.phone = a[4]
400
401                         if not config.plugins.FritzCall.filter.value or config.plugins.FritzCall.filtermsn.value == self.phone:
402
403                                 if config.plugins.FritzCall.internal.value and a[5][0]=="0" and len(a[3]) > 3:
404                                         self.number = a[5][1:]
405                                 else:
406                                         self.number = a[5]
407                                         
408                                 self.caller = phonebook.search(self.number)
409
410                                 if self.number[0] != '0':
411                                         self.number = config.plugins.FritzCall.prefix.value + self.number
412                                 
413                                 if self.caller is None:
414                                         if config.plugins.FritzCall.lookup.value:
415                                                 self.reverseLookup()
416                                 else:
417                                         self.handleEvent()
418                                 
419                                                                 
420 class FritzClientFactory(ReconnectingClientFactory):
421         initialDelay = 20
422         maxDelay = 500
423         
424         def __init__(self):
425                 self.hangup_ok = False
426
427         def startedConnecting(self, connector):
428                 Notifications.AddNotification(MessageBox, _("Connecting to Fritz!Box..."), type=MessageBox.TYPE_INFO, timeout=2)
429         
430         def buildProtocol(self, addr):
431                 Notifications.AddNotification(MessageBox, _("Connected to Fritz!Box!"), type=MessageBox.TYPE_INFO, timeout=4)
432                 self.resetDelay()
433                 return FritzProtocol()
434         
435         def clientConnectionLost(self, connector, reason):
436                 if not self.hangup_ok:
437                         Notifications.AddNotification(MessageBox, _("Connection to Fritz!Box! lost\n (%s)\nretrying...") % reason.getErrorMessage(), type=MessageBox.TYPE_INFO, timeout=config.plugins.FritzCall.timeout.value)
438                 ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
439         
440         def clientConnectionFailed(self, connector, reason):
441                 Notifications.AddNotification(MessageBox, _("Connecting to Fritz!Box failed\n (%s)\nretrying...") % reason.getErrorMessage(), type=MessageBox.TYPE_INFO, timeout=config.plugins.FritzCall.timeout.value)
442                 ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)
443
444 class FritzCall:
445         def __init__(self):
446                 self.dialog = None
447                 self.d = None
448                 self.connect()
449         
450         def connect(self):      
451                 self.abort()
452                 if config.plugins.FritzCall.enable.value:
453                         f = FritzClientFactory()
454                         self.d = (f, reactor.connectTCP("%d.%d.%d.%d" % tuple(config.plugins.FritzCall.hostname.value), 1012, f))
455
456         def shutdown(self):
457                 self.abort()
458
459         def abort(self):
460                 if self.d is not None:
461                         self.d[0].hangup_ok = True 
462                         self.d[0].stopTrying()
463                         self.d[1].disconnect()
464                         self.d = None
465
466 def main(session):
467         session.open(FritzCallSetup)
468
469 fritz_call = None
470
471 def autostart(reason, **kwargs):
472         global fritz_call
473         
474         # ouch, this is a hack  
475         if kwargs.has_key("session"):
476                 global my_global_session
477                 my_global_session = kwargs["session"]
478                 return
479         
480         print "[Fritz!Call] - Autostart"
481         if reason == 0:
482                 fritz_call = FritzCall()
483         elif reason == 1:
484                 fritz_call.shutdown()
485                 fritz_call = None
486
487 def Plugins(**kwargs):
488         return [ PluginDescriptor(name="FritzCall", description="Display Fritzbox-Fon calls on screen", where = PluginDescriptor.WHERE_PLUGINMENU, fnc=main), 
489                 PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART, PluginDescriptor.WHERE_AUTOSTART], fnc = autostart) ]