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