- support for timeout/sticky,
[enigma2-plugins.git] / growlee / src / plugin.py
1 from Plugins.Plugin import PluginDescriptor
2
3 from Tools import Notifications
4 from netgrowl import GrowlRegistrationPacket, GrowlNotificationPacket, \
5                 GROWL_UDP_PORT, md5_constructor
6 from twisted.internet.protocol import DatagramProtocol
7 from twisted.internet import reactor
8 from twisted.web.client import getPage
9 from struct import unpack
10 from socket import gaierror
11 from urllib import urlencode
12
13 from Screens.Setup import SetupSummary
14 from Screens.Screen import Screen
15 from Screens.MessageBox import MessageBox
16 from Components.ActionMap import ActionMap
17 from Components.config import config, getConfigListEntry, ConfigSubsection, \
18                 ConfigText, ConfigPassword, ConfigYesNo, ConfigSelection, ConfigSet
19 from Components.ConfigList import ConfigListScreen
20 from Components.Sources.StaticText import StaticText
21
22 config.plugins.growlee = ConfigSubsection()
23 config.plugins.growlee.enable_incoming = ConfigYesNo(default=False)
24 config.plugins.growlee.enable_outgoing = ConfigYesNo(default=False)
25 config.plugins.growlee.address = ConfigText(fixed_size=False)
26 config.plugins.growlee.password = ConfigPassword()
27 config.plugins.growlee.prowl_api_key = ConfigText(fixed_size=False)
28 config.plugins.growlee.protocol = ConfigSelection(default="growl", choices = [("growl", "Growl"), ("snarl", "Snarl"), ("prowl", "Prowl")])
29 config.plugins.growlee.blacklist = ConfigSet(choices = [])
30
31 NOTIFICATIONID = 'GrowleeReceivedNotification'
32
33 class GrowleeConfiguration(Screen, ConfigListScreen):
34         def __init__(self, session):
35                 Screen.__init__(self, session)
36                 self.skinName = [ "GrowleeConfiguration", "Setup" ]
37
38                 # Buttons
39                 self["key_red"] = StaticText(_("Cancel"))
40                 self["key_green"] = StaticText(_("OK"))
41
42                 # Summary
43                 self.setup_title = "Growlee Configuration"
44                 self.onChangedEntry = []
45
46                 # Define Actions
47                 self["actions"] = ActionMap(["SetupActions"],
48                         {
49                                 "cancel": self.keyCancel,
50                                 "save": self.keySave,
51                         }
52                 )
53
54                 config.plugins.growlee.protocol.addNotifier(self.setupList, initial_call=False)
55                 ConfigListScreen.__init__(
56                         self,
57                         [],
58                         session=session,
59                         on_change=self.changed
60                 )
61
62                 # Trigger change
63                 self.setupList()
64                 self.changed()
65
66         def changed(self):
67                 for x in self.onChangedEntry:
68                         x()
69
70         def setupList(self, *args):
71                 l = [
72                         getConfigListEntry(_("Type"), config.plugins.growlee.protocol),
73                         getConfigListEntry(_("Send Notifications?"), config.plugins.growlee.enable_outgoing),
74                 ]
75
76                 if config.plugins.growlee.protocol.value == "prowl":
77                         l.append(getConfigListEntry(_("API Key"), config.plugins.growlee.prowl_api_key))
78                 else:
79                         l.extend((
80                                 getConfigListEntry(_("Receive Notifications?"), config.plugins.growlee.enable_incoming),
81                                 getConfigListEntry(_("Address"), config.plugins.growlee.address),
82                                 getConfigListEntry(_("Password"), config.plugins.growlee.password),
83                         ))
84
85                 self["config"].list = l
86
87         def getCurrentEntry(self):
88                 cur = self["config"].getCurrent()
89                 return cur and cur[0]
90
91         def getCurrentValue(self):
92                 cur = self["config"].getCurrent()
93                 return cur and str(cur[1].getText())
94
95         def createSummary(self):
96                 return SetupSummary
97
98         def keySave(self):
99                 if self["config"].isChanged():
100                         global port
101                         if port:
102                                 def maybeConnect(*args, **kwargs):
103                                         if config.plugins.growlee.enable_incoming.value or config.plugins.growlee.enable_outgoing.value:
104                                                 doConnect()
105
106                                 d = port.stopListening()
107                                 if d is not None:
108                                         d.addCallback(maybeConnect).addErrback(emergencyDisable)
109                                 else:
110                                         maybeConnect()
111                         elif config.plugins.growlee.enable_incoming.value or config.plugins.growlee.enable_outgoing.value:
112                                 doConnect()
113
114                 self.saveAll()
115                 self.close()
116
117         def close(self):
118                 config.plugins.growlee.protocol.notifiers.remove(self.setupList)
119                 Screen.close(self)
120
121 def configuration(session, **kwargs):
122         session.open(GrowleeConfiguration)
123
124 def doConnect():
125         global port
126         if config.plugins.growlee.protocol.value == "snarl":
127                 port = reactor.listenTCP(GROWL_UDP_PORT, growlProtocolOneWrapper)
128         else:
129                 port = reactor.listenUDP(GROWL_UDP_PORT, growlProtocolOneWrapper)
130
131 def emergencyDisable(*args, **kwargs):
132         global port
133         if port:
134                 port.stopListening()
135                 port = None
136
137         if gotNotification in Notifications.notificationAdded:
138                 Notifications.notificationAdded.remove(gotNotification)
139         Notifications.AddPopup(
140                 _("Network error.\nDisabling Growlee!"),
141                 MessageBox.TYPE_ERROR,
142                 10
143         )
144
145 class GrowlProtocolOneWrapper(DatagramProtocol):
146         def startProtocol(self):
147                 proto = config.plugins.growlee.protocol.value
148                 if config.plugins.growlee.enable_outgoing.value and not proto == "prowl":
149                         addr = (config.plugins.growlee.address.value, GROWL_UDP_PORT)
150                         if proto == "growl":
151                                 p = GrowlRegistrationPacket(password=config.plugins.growlee.password.value)
152                                 p.addNotification()
153                                 payload = p.payload()
154                         else: #proto == "snarl":
155                                 payload = "type=SNP#?version=1.0#?action=register#?app=growlee\r\n"
156                         try:
157                                 self.transport.write(payload, addr)
158                         except gaierror:
159                                 emergencyDisable()
160
161         def doStop(self):
162                 if config.plugins.growlee.enable_outgoing.value and config.plugins.growlee.protocol.value == "snarl":
163                         addr = (config.plugins.growlee.address.value, GROWL_UDP_PORT)
164                         payload = "type=SNP#?version=1.0#?action=unregister#?app=growlee\r\n"
165                         try:
166                                 self.transport.write(payload, addr)
167                         except gaierror:
168                                 pass
169                 DaragramProtocol.doStop(self)
170
171         def sendNotification(self, *args, **kwargs):
172                 if not self.transport or not config.plugins.growlee.enable_outgoing.value:
173                         return
174
175                 proto = config.plugins.growlee.protocol.value
176                 if proto == "prowl":
177                         headers = {'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'}
178                         data = {
179                                 'apikey': config.plugins.growlee.prowl_api_key.value,
180                                 'application': "growlee",
181                                 'event': kwargs.get('title', 'No title.'),
182                                 'description': kwargs.get('description', 'No message.'),
183                                 'priority': kwargs.get('priority', 0),
184                         }
185
186                         getPage('https://prowl.weks.net/publicapi/add/', method = 'POST', headers = headers, postdata = urlencode(data)).addErrback(emergencyDisable)
187                 else:
188                         addr = (config.plugins.growlee.address.value, GROWL_UDP_PORT)
189                         if proto == "growl":
190                                 # map timeout -> sticky
191                                 if "timeout" in kwargs:
192                                         if kwargs["timeout"] == -1:
193                                                 kwargs["sticky"] = True
194                                         del kwargs["timeout"]
195
196                                 p = GrowlNotificationPacket(*args, application="growlee", **kwargs)
197                                 payload = p.payload()
198                         else: #proto == "snarl":
199                                 title = kwargs.get('title', 'No title.')
200                                 text = kwargs.get('description', 'No message.')
201                                 timeout = kwargs.get('timeout', -1)
202
203                                 # NOTE: timeout = 0 means sticky, so add one second to map -1 to 0 and make 0 non-sticky
204                                 if timeout < 2:
205                                         timeout += 1
206
207                                 payload = "type=SNP#?version=1.0#?action=notification#?app=growlee#?class=growleeClass#?title=%s#?text=%s#?timeout=%d\r\n" % (title, text, timeout)
208                         try:
209                                 self.transport.write(payload, addr)
210                         except gaierror:
211                                 emergencyDisable()
212
213         def datagramReceived(self, data, addr):
214                 proto = config.plugins.growlee.protocol.value
215                 if proto == "prowl" or not config.plugins.growlee.enable_incoming.value:
216                         return
217
218                 Len = len(data)
219                 if proto == "growl":
220                         if Len < 16:
221                                 return
222
223                         digest = data[-16:]
224                         password = config.plugins.growlee.password.value
225                         checksum = md5_constructor()
226                         checksum.update(data[:-16])
227                         if password:
228                                 checksum.update(password)
229                         if digest != checksum.digest():
230                                 return
231
232                         # notify packet
233                         if data[1] == '\x01':
234                                 nlen, tlen, dlen, alen = unpack("!HHHH",str(data[4:12]))
235                                 notification, title, description = unpack("%ds%ds%ds" % (nlen, tlen, dlen), data[12:Len-alen-16])
236
237                                 Notifications.AddNotificationWithID(
238                                         NOTIFICATIONID,
239                                         MessageBox,
240                                         text = title + '\n' + description,
241                                         type = MessageBox.TYPE_INFO,
242                                         timeout = 5,
243                                         close_on_any_key = True,
244                                 )
245
246                         # TODO: do we want to handle register packets? :-)
247                 else: #proto == "snarl":
248                         if Len < 23 or not data[:23] == "type=SNP#?version=1.0#?":
249                                 return
250
251                         items = data[23:].split('#?')
252
253                         title = ''
254                         description = ''
255                         timeout = 5
256                         for item in items:
257                                 key, value = item.split('=')
258                                 if key == "action":
259                                         if value != "notification":
260                                                 # NOTE: we pretend to handle&accept pretty much everything one throws at us
261                                                 addr = (config.plugins.growlee.address.value, GROWL_UDP_PORT)
262                                                 payload = "SNP/1.0/0/OK\r\n"
263                                                 try:
264                                                         self.transport.write(payload, addr)
265                                                 except gaierror:
266                                                         emergencyDisable()
267                                                 return
268                                 elif key == "title":
269                                         title = value
270                                 elif key == "text":
271                                         description = value
272                                 elif key == "timeout":
273                                         timeout = int(value)
274
275                         Notifications.AddNotificationWithID(
276                                 NOTIFICATIONID,
277                                 MessageBox,
278                                 text = title + '\n' + description,
279                                 type = MessageBox.TYPE_INFO,
280                                 timeout = timeout,
281                                 close_on_any_key = True,
282                         )
283
284                         # return ok
285                         addr = (config.plugins.growlee.address.value, GROWL_UDP_PORT)
286                         payload = "SNP/1.0/0/OK\r\n"
287                         try:
288                                 self.transport.write(payload, addr)
289                         except gaierror:
290                                 emergencyDisable()
291
292
293 growlProtocolOneWrapper = GrowlProtocolOneWrapper()
294 port = None
295
296 def gotNotification():
297         notifications = Notifications.notifications
298         if notifications:
299                 _, screen, args, kwargs, id = notifications[-1]
300                 if screen is MessageBox and id != NOTIFICATIONID and id not in config.plugins.growlee.blacklist.value:
301
302                         type = kwargs.get("type", 0)
303                         timeout = kwargs.get("timeout", -1)
304
305                         if "text" in kwargs:
306                                 description = kwargs["text"]
307                         else:
308                                 description = args[0]
309                         description = description.decode('utf-8')
310
311                         # NOTE: priority is in [-2; 2] but type is [0; 3] so map it
312                         # XXX: maybe priority==type-2 would be more appropriate
313                         growlProtocolOneWrapper.sendNotification(title="Dreambox", description=description, password=config.plugins.growlee.password.value, priority=type-1, timeout=timeout)
314
315 def autostart(**kwargs):
316         if config.plugins.growlee.enable_incoming.value or config.plugins.growlee.enable_outgoing.value:
317                 doConnect()
318
319         # NOTE: we need to be the first one to be notified since other listeners
320         # may remove the notifications from the list for good
321         Notifications.notificationAdded.insert(0, gotNotification)
322
323 def Plugins(**kwargs):
324         return [
325                 PluginDescriptor(
326                         where=PluginDescriptor.WHERE_SESSIONSTART,
327                         fnc=autostart,
328                 ),
329                 PluginDescriptor(
330                         name="Growlee",
331                         description=_("Configure Growlee"), 
332                         where=PluginDescriptor.WHERE_PLUGINMENU,
333                         fnc=configuration,
334                 ),
335         ]
336