Merge lp:~slimey/endroid/hi5 into lp:endroid

Proposed by Simon C
Status: Merged
Approved by: Martin Morrison
Approved revision: 69
Merged at revision: 62
Proposed branch: lp:~slimey/endroid/hi5
Merge into: lp:endroid
Diff against target: 299 lines (+254/-0)
4 files modified
bin/spelunk_hi5s (+53/-0)
debian/endroid.install (+1/-0)
etc/endroid.conf (+9/-0)
src/endroid/plugins/hi5.py (+191/-0)
To merge this branch: bzr merge lp:~slimey/endroid/hi5
Reviewer Review Type Date Requested Status
Martin Morrison Approve
Review via email: mp+184862@code.launchpad.net

This proposal supersedes a proposal from 2013-09-08.

Commit message

Move High Five plugin over from Ensoft proprietary repo and upgrade with added awesomeness. Absolutely no overkill here.

Description of the change

Upgrade of High Five plugin:

 - moving from Ensoft-proprietary to open source
 - you can no longer hi5 yourself
 - you can hi5 multiple people with the same compliment
 - you can omit the domain part of the JID if EnDroid can unambiguously resolve this
 - optional asymmetric encryption of messages as a last-resort backstop against idiots

Note that this entails a non-backwards-compatible database schema change for those installations with the previous Ensoft plugin.

Incorporates review comments.

To post a comment you must log in.
Revision history for this message
Martin Morrison (isoschiz) wrote : Posted in a previous version of this proposal

Can anyone say "overkill"? ;-)

- The executable should have a main function, and should use argparse to parse the args so that it doesn't just splurge an exception when miscalled

- reference to spelunk script in docstring uses the .py suffix

- you've used "except Exception as e" but then not used e. It's quite a bit cheaper to not specify the variable to bind to in that case.

- your xrange-based joiner is very unpythonic. Instead, try something like:

if len(jids) > 1:
    jidlist = ' & '.join([', '.join(jids[:-1])] + jids[-1:])
else:
    jidlist = jids[0]

Might even be possible to eliminate the if.

- the have/has code is a prime candidate for the ternary if

review: Needs Fixing
Revision history for this message
Martin Morrison (isoschiz) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'bin/spelunk_hi5s'
2--- bin/spelunk_hi5s 1970-01-01 00:00:00 +0000
3+++ bin/spelunk_hi5s 2013-09-10 19:44:32 +0000
4@@ -0,0 +1,53 @@
5+#!/usr/bin/python
6+#
7+# Decrypt hi5s
8+#
9+# This is all a last-resort mechanism (the whole point of hi5s is that they're
10+# anonymous). The EnDroid database (eg /var/lib/endroid/endroid.db) is the only
11+# required command-line parameter.
12+#
13+# Copyright (C) Ensoft 2013
14+# Created by SimonC
15+
16+import argparse, sqlite3
17+from getpass import getpass
18+from subprocess import Popen, PIPE
19+
20+HI5_DB = 'hi5_hi5s'
21+
22+def set_params():
23+ """
24+ Set all parameters
25+ """
26+ parser = argparse.ArgumentParser(description=
27+ 'Spelunk encrypted high five messages')
28+ parser.add_argument('--keyring', help='gpg secret keyring')
29+ parser.add_argument('db_file', help='path to endroid.db')
30+ args = parser.parse_args()
31+ args.passphrase = getpass('Enter passphrase: ')
32+ return args
33+
34+def main():
35+ args = set_params()
36+ conn = sqlite3.connect(args.db_file)
37+ cur = conn.cursor()
38+ cur.execute('select * from ' + HI5_DB)
39+ for row in cur.fetchall():
40+ gpg_args = ['gpg', '--decrypt', '--armor', '--passphrase-fd', '0',
41+ '--batch', '--quiet']
42+ if args.keyring:
43+ gpg_args.extend(('--secret-keyring', args.keyring))
44+ p = Popen(gpg_args, stdin=PIPE, stdout=PIPE)
45+ p.stdin.write(args.passphrase + '\n')
46+ out, err = p.communicate(row[-1])
47+ p.wait()
48+ if err:
49+ print '!! ERROR:', err
50+ elif p.returncode != 0:
51+ # No encrypted data, so display what we have
52+ print '{}: {}'.format(row[2], row[1])
53+ else:
54+ print out
55+
56+if __name__ == '__main__':
57+ main()
58
59=== modified file 'debian/endroid.install'
60--- debian/endroid.install 2013-08-29 16:00:37 +0000
61+++ debian/endroid.install 2013-09-10 19:44:32 +0000
62@@ -3,6 +3,7 @@
63 etc/init/endroid.conf etc/init/
64 bin/endroid usr/bin/
65 bin/endroid_remote usr/bin/
66+bin/spelunk_hi5s usr/sbin/
67 lib/wokkel-0.7.1-py2.7.egg usr/lib/endroid/dependencies
68 var/endroid.db var/lib/endroid/db
69 doc/EnDroid.png usr/share/endroid/media/
70
71=== modified file 'etc/endroid.conf'
72--- etc/endroid.conf 2013-08-30 17:48:34 +0000
73+++ etc/endroid.conf 2013-09-10 19:44:32 +0000
74@@ -45,6 +45,7 @@
75 endroid.plugins.coolit
76 endroid.plugins.correct
77 endroid.plugins.exec
78+ endroid.plugins.hi5
79 endroid.plugins.remote
80 endroid.plugins.theyfightcrime
81 endroid.plugins.whosonline
82@@ -66,6 +67,7 @@
83 endroid.plugins.compute
84 endroid.plugins.coolit
85 endroid.plugins.exec
86+ endroid.plugins.hi5
87 endroid.plugins.memo
88 endroid.plugins.remote
89 endroid.plugins.theyfightcrime
90@@ -103,6 +105,13 @@
91 # and then put it below - good for 2000 free queries/month.
92 #api_key = 123ABC-DE45FGJIJK
93
94+[ group | room : * : plugin : endroid.plugins.hi5 ]
95+# Configure one or more chatrooms to broadcast anonymous High Fives
96+#broadcast = room@conference.example.com,
97+# Configure GPG to asymmetrically encrypt logs if desired, with
98+# both the keyring containing the public key and the userid to encrypt for
99+#gpg = /home/admin/.gnupg/pubring.gpg, admin@example.com
100+
101 [ group | room : * : plugin : endroid.plugins.exec ]
102 # Configure commands that just spawn a process and send the output.
103 # Format:
104
105=== added file 'src/endroid/plugins/hi5.py'
106--- src/endroid/plugins/hi5.py 1970-01-01 00:00:00 +0000
107+++ src/endroid/plugins/hi5.py 2013-09-10 19:44:32 +0000
108@@ -0,0 +1,191 @@
109+# Endroid - XMPP Bot
110+# Copyright 2012, Ensoft Ltd.
111+# Created by Jonathan Millican, and mangled beyond recognition by SimonC
112+
113+import logging, re, time
114+from twisted.internet import defer, error, protocol, reactor
115+from endroid.plugins.command import CommandPlugin, command
116+
117+HI5_TABLE = 'hi5s'
118+
119+class FilterProtocol(protocol.ProcessProtocol):
120+ """
121+ Send some data to a process via its stdin, close it, and reap the output
122+ from its stdout. Twisted arguably ought to provide a utility function for
123+ this - there's nothing specific to GPG or high-fiving here.
124+ """
125+ def __init__(self, in_data, deferred, args):
126+ self.in_data = in_data
127+ self.deferred = deferred
128+ self.args = args
129+ self.out_data= ''
130+
131+ def connectionMade(self):
132+ self.transport.write(self.in_data.encode('utf-8'))
133+ self.transport.closeStdin()
134+
135+ def outReceived(self, out_data):
136+ self.out_data += out_data.decode('utf-8')
137+
138+ def outConnectionLost(self):
139+ d, self.deferred = self.deferred, None
140+ d.callback(self.out_data)
141+
142+ def processEnded(self, reason):
143+ if isinstance(reason.value, error.ProcessTerminated):
144+ logging.error("Problem running filter ({}): {}".
145+ format(self.args, reason.value))
146+ d, self.deferred = self.deferred, None
147+ d.errback(reason.value)
148+
149+class Hi5(CommandPlugin):
150+ """
151+ The Hi5 plugin lets you send anonymous 'High Five!' messages to other
152+ users known to Endroid. The idea is that it makes it easy to send a
153+ compliment to somebody. The most basic usage is:
154+
155+ hi5 user@example.com Nice presentation dude!
156+
157+ which, if user@example.com is known to EnDroid and currently logged in,
158+ sends both a unicast chat to that user with the anonymous compliment,
159+ and also anonymously announces to one or more configured chatrooms that
160+ user@example.com got the High Five 'Nice presentation dude!'.
161+
162+ Slightly more complicated examples: it's possible to send a compliment
163+ to multiple users at once, by using comma-separation, and also omit
164+ the domain part of the JID if the user is unambiguous given all the
165+ users known to EnDroid. So for example:
166+
167+ hi5 bilbo@baggins.org, frodo, sam Good work with the Nazgul guys :-)
168+
169+ There is some basic anonymous logging performed by default, that includes
170+ only the time/date and recipient JID. However, if you configure a GPG
171+ public key, then an asymmetrically encrypted log that also includes the
172+ sender and message is done. That provides a last-resort mechanism should
173+ someone use the mechanism for poisonous purposes, but requires the private
174+ key and passphrase. The 'spelunk_hi5' script can be used for this.
175+ """
176+
177+ name = "hi5"
178+ help = ("Send anonymous 'hi5s' to folks! Your message is sent from EnDroid"
179+ " direct to the recipient, as well as being broadcast in any "
180+ "configured public chat rooms.")
181+
182+ def endroid_init(self):
183+ if 'gpg' in self.vars:
184+ self.gpg = ('/usr/bin/gpg', '--encrypt', '--armor',
185+ '--keyring', self.vars['gpg'][0],
186+ '--recipient', self.vars['gpg'][1])
187+ else:
188+ self.gpg = None
189+ if not self.database.table_exists(HI5_TABLE):
190+ self.database.create_table(HI5_TABLE, ['jids', 'date', 'encrypted'])
191+
192+ @command(helphint="{user}[,{user}] {message}")
193+ def hi5(self, msg, arg):
194+ # Parse the request
195+ try:
196+ jids, text = self._parse(arg)
197+ assert len(text) > 0
198+ except Exception:
199+ msg.reply("Sorry, couldn't spot a message to send in there. Use "
200+ "something like:\n"
201+ "hi5 frodo@shire.org, sam, bilbo@rivendell Nice job!")
202+ return
203+
204+ # Sanity checks, and also expand out 'user' to 'user@host'
205+ # if we can do so unambiguously
206+ fulljids = []
207+ for jid in jids:
208+ if '/' in jid:
209+ msg.reply("Recipients can't contain '/'s".format(jid))
210+ return
211+ fulljid = self._get_fulljid(jid)
212+ if not fulljid:
213+ msg.reply("{0} is not a currently online valid receipient. "
214+ "Sorry.".format(jid))
215+ return
216+ elif fulljid == msg.sender:
217+ msg.reply("You really don't have to resort to complimenting "
218+ "yourself. I already think you're great")
219+ return
220+ fulljids.append(fulljid)
221+
222+ # Do it
223+ self._do_hi5(jids, fulljids, text, msg)
224+
225+ def _do_hi5(self, jids, fulljids, text, msg):
226+ """
227+ Actually send the hi5
228+ """
229+ # jidlist is a nice human-readable representation of the lucky
230+ # receipients, like 'Tom, Dick & Harry'
231+ if len(jids) > 1:
232+ jidlist = ', '.join(jids[:-1]) + ' & ' + jids[-1]
233+ else:
234+ jidlist = jids[0]
235+
236+ msg.reply('A High Five "' + text + '" is being sent to ' + jidlist)
237+ self._log_hi5(','.join(fulljids), msg.sender, text)
238+
239+ for jid in fulljids:
240+ self.messagehandler.send_chat(jid, "You've been sent an anonymous "
241+ "High Five: " + text)
242+ for group in self.vars.get('broadcast', []):
243+ groupmsg = '{} {} been sent an anonymous High Five: {}'.format(
244+ jidlist, 'have' if len(jids) > 1 else 'has', text)
245+ self.messagehandler.send_muc(group, groupmsg)
246+
247+ @staticmethod
248+ def _parse(msg):
249+ """
250+ Parse something like ' bilbo@baggins.org, frodo, sam great job! ' into
251+ (['bilbo@baggsins.org', 'frodo', 'sam'], 'great job!')
252+ """
253+ jids = []
254+ msg = msg.strip()
255+ while True:
256+ m = re.match(r'([^ ,]+)( *,? *)(.*)', msg)
257+ jid, sep, rest = m.groups(1)
258+ jids.append(jid)
259+ if sep.strip():
260+ msg = rest
261+ else:
262+ return jids, rest
263+
264+ def _get_fulljid(self, jid):
265+ """
266+ Expand a simple 'user' JID to 'user@host' if we can do so unambiguously
267+ """
268+ users = self.usermanagement.get_available_users()
269+ if jid in users:
270+ return jid
271+ expansions = []
272+ for user in users:
273+ if user.startswith(jid + '@'):
274+ expansions.append(user)
275+ if len(expansions) == 1:
276+ return expansions[0]
277+
278+ def _log_hi5(self, jidlist, sender, text):
279+ """
280+ Log the hi5. This either means spawning a GPG process to do an
281+ asymmetric encryption of the whole thing, or just writing a basic
282+ summary straight away.
283+ """
284+ now = time.ctime()
285+ def db_insert(encrypted):
286+ self.database.insert(HI5_TABLE, {'jids':jidlist,
287+ 'date':now, 'encrypted': encrypted})
288+ def db_insert_err(err):
289+ logging.error("Error with hi5 database entry: {}".format(err))
290+ db_insert(None)
291+ if self.gpg:
292+ d = defer.Deferred()
293+ d.addCallbacks(db_insert, db_insert_err)
294+ gpg_log = '{}: {} -> {}: {}'.format(now, sender, jidlist, text)
295+ fp = FilterProtocol(gpg_log, d, self.gpg)
296+ reactor.spawnProcess(fp, self.gpg[0], self.gpg, {})
297+ else:
298+ db_insert(None)
299+

Subscribers

People subscribed via source and target branches