~isoschiz/endroid/kermit-v2

« back to all changes in this revision

Viewing changes to src/endroid/plugins/hi5.py

  • Committer: Chris Davidson
  • Date: 2013-09-11 13:01:40 UTC
  • mfrom: (62 endroid)
  • mto: This revision was merged to the branch mainline in revision 64.
  • Revision ID: chdavids@cisco.com-20130911130140-efly4faw8soo8uw9
Merge in the hit5 changes

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Endroid - XMPP Bot
 
2
# Copyright 2012, Ensoft Ltd.
 
3
# Created by Jonathan Millican, and mangled beyond recognition by SimonC
 
4
 
 
5
import logging, re, time
 
6
from twisted.internet import defer, error, protocol, reactor
 
7
from endroid.plugins.command import CommandPlugin, command
 
8
 
 
9
HI5_TABLE = 'hi5s'
 
10
 
 
11
class FilterProtocol(protocol.ProcessProtocol):
 
12
    """
 
13
    Send some data to a process via its stdin, close it, and reap the output
 
14
    from its stdout. Twisted arguably ought to provide a utility function for
 
15
    this - there's nothing specific to GPG or high-fiving here.
 
16
    """
 
17
    def __init__(self, in_data, deferred, args):
 
18
        self.in_data = in_data
 
19
        self.deferred = deferred
 
20
        self.args = args
 
21
        self.out_data= ''
 
22
 
 
23
    def connectionMade(self):
 
24
        self.transport.write(self.in_data.encode('utf-8'))
 
25
        self.transport.closeStdin()
 
26
 
 
27
    def outReceived(self, out_data):
 
28
        self.out_data += out_data.decode('utf-8')
 
29
 
 
30
    def outConnectionLost(self):
 
31
        d, self.deferred = self.deferred, None
 
32
        d.callback(self.out_data)
 
33
 
 
34
    def processEnded(self, reason):
 
35
        if isinstance(reason.value, error.ProcessTerminated):
 
36
            logging.error("Problem running filter ({}): {}".
 
37
                format(self.args, reason.value))
 
38
            d, self.deferred = self.deferred, None
 
39
            d.errback(reason.value)
 
40
 
 
41
class Hi5(CommandPlugin):
 
42
    """
 
43
    The Hi5 plugin lets you send anonymous 'High Five!' messages to other
 
44
    users known to Endroid. The idea is that it makes it easy to send a
 
45
    compliment to somebody. The most basic usage is:
 
46
 
 
47
      hi5 user@example.com Nice presentation dude!
 
48
 
 
49
    which, if user@example.com is known to EnDroid and currently logged in,
 
50
    sends both a unicast chat to that user with the anonymous compliment,
 
51
    and also anonymously announces to one or more configured chatrooms that
 
52
    user@example.com got the High Five 'Nice presentation dude!'.
 
53
 
 
54
    Slightly more complicated examples: it's possible to send a compliment
 
55
    to multiple users at once, by using comma-separation, and also omit
 
56
    the domain part of the JID if the user is unambiguous given all the
 
57
    users known to EnDroid. So for example:
 
58
 
 
59
      hi5 bilbo@baggins.org, frodo, sam Good work with the Nazgul guys :-)
 
60
 
 
61
    There is some basic anonymous logging performed by default, that includes
 
62
    only the time/date and recipient JID. However, if you configure a GPG
 
63
    public key, then an asymmetrically encrypted log that also includes the
 
64
    sender and message is done. That provides a last-resort mechanism should
 
65
    someone use the mechanism for poisonous purposes, but requires the private
 
66
    key and passphrase. The 'spelunk_hi5' script can be used for this.
 
67
    """
 
68
 
 
69
    name = "hi5"
 
70
    help = ("Send anonymous 'hi5s' to folks! Your message is sent from EnDroid"
 
71
            " direct to the recipient, as well as being broadcast in any "
 
72
            "configured public chat rooms.")
 
73
    
 
74
    def endroid_init(self):
 
75
        if 'gpg' in self.vars:
 
76
            self.gpg = ('/usr/bin/gpg', '--encrypt', '--armor',
 
77
                        '--keyring', self.vars['gpg'][0],
 
78
                        '--recipient', self.vars['gpg'][1])
 
79
        else:
 
80
            self.gpg = None
 
81
        if not self.database.table_exists(HI5_TABLE):
 
82
            self.database.create_table(HI5_TABLE, ['jids', 'date', 'encrypted'])
 
83
 
 
84
    @command(helphint="{user}[,{user}] {message}")
 
85
    def hi5(self, msg, arg):
 
86
        # Parse the request
 
87
        try:
 
88
            jids, text = self._parse(arg)
 
89
            assert len(text) > 0
 
90
        except Exception:
 
91
            msg.reply("Sorry, couldn't spot a message to send in there. Use "
 
92
                      "something like:\n"
 
93
                      "hi5 frodo@shire.org, sam, bilbo@rivendell Nice job!")
 
94
            return
 
95
 
 
96
        # Sanity checks, and also expand out 'user' to 'user@host' 
 
97
        # if we can do so unambiguously
 
98
        fulljids = []
 
99
        for jid in jids:
 
100
            if '/' in jid:
 
101
                msg.reply("Recipients can't contain '/'s".format(jid))
 
102
                return
 
103
            fulljid = self._get_fulljid(jid)
 
104
            if not fulljid:
 
105
                msg.reply("{0} is not a currently online valid receipient. "
 
106
                          "Sorry.".format(jid))
 
107
                return
 
108
            elif fulljid == msg.sender:
 
109
                msg.reply("You really don't have to resort to complimenting "
 
110
                          "yourself. I already think you're great")
 
111
                return
 
112
            fulljids.append(fulljid)
 
113
 
 
114
        # Do it
 
115
        self._do_hi5(jids, fulljids, text, msg)
 
116
    
 
117
    def _do_hi5(self, jids, fulljids, text, msg):
 
118
        """
 
119
        Actually send the hi5
 
120
        """
 
121
        # jidlist is a nice human-readable representation of the lucky
 
122
        # receipients, like 'Tom, Dick & Harry'
 
123
        if len(jids) > 1:
 
124
            jidlist = ', '.join(jids[:-1]) + ' & ' + jids[-1]
 
125
        else:
 
126
            jidlist = jids[0]
 
127
 
 
128
        msg.reply('A High Five "' + text + '" is being sent to ' + jidlist)
 
129
        self._log_hi5(','.join(fulljids), msg.sender, text)
 
130
 
 
131
        for jid in fulljids:
 
132
            self.messagehandler.send_chat(jid, "You've been sent an anonymous "
 
133
                                               "High Five: " + text)
 
134
        for group in self.vars.get('broadcast', []):
 
135
            groupmsg = '{} {} been sent an anonymous High Five: {}'.format(
 
136
                jidlist, 'have' if len(jids) > 1 else 'has', text)
 
137
            self.messagehandler.send_muc(group, groupmsg)
 
138
    
 
139
    @staticmethod
 
140
    def _parse(msg):
 
141
        """
 
142
        Parse something like ' bilbo@baggins.org, frodo, sam great job! ' into
 
143
        (['bilbo@baggsins.org', 'frodo', 'sam'], 'great job!')
 
144
        """
 
145
        jids = []
 
146
        msg = msg.strip()
 
147
        while True:
 
148
            m = re.match(r'([^ ,]+)( *,? *)(.*)', msg)
 
149
            jid, sep, rest = m.groups(1)
 
150
            jids.append(jid)
 
151
            if sep.strip():
 
152
                msg = rest
 
153
            else:
 
154
                return jids, rest
 
155
 
 
156
    def _get_fulljid(self, jid):
 
157
        """
 
158
        Expand a simple 'user' JID to 'user@host' if we can do so unambiguously
 
159
        """
 
160
        users = self.usermanagement.get_available_users()
 
161
        if jid in users:
 
162
            return jid
 
163
        expansions = []
 
164
        for user in users:
 
165
            if user.startswith(jid + '@'):
 
166
                expansions.append(user)
 
167
        if len(expansions) == 1:
 
168
            return expansions[0]
 
169
 
 
170
    def _log_hi5(self, jidlist, sender, text):
 
171
        """
 
172
        Log the hi5. This either means spawning a GPG process to do an
 
173
        asymmetric encryption of the whole thing, or just writing a basic
 
174
        summary straight away.
 
175
        """
 
176
        now = time.ctime()
 
177
        def db_insert(encrypted):
 
178
            self.database.insert(HI5_TABLE, {'jids':jidlist, 
 
179
                                 'date':now, 'encrypted': encrypted})
 
180
        def db_insert_err(err):
 
181
            logging.error("Error with hi5 database entry: {}".format(err))
 
182
            db_insert(None)
 
183
        if self.gpg:
 
184
            d = defer.Deferred()
 
185
            d.addCallbacks(db_insert, db_insert_err)
 
186
            gpg_log = '{}: {} -> {}: {}'.format(now, sender, jidlist, text) 
 
187
            fp = FilterProtocol(gpg_log, d, self.gpg)
 
188
            reactor.spawnProcess(fp, self.gpg[0], self.gpg, {})
 
189
        else:
 
190
            db_insert(None)
 
191