~certify-web-dev/twisted/certify-trunk

« back to all changes in this revision

Viewing changes to twisted/words/tendril.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2006-01-16 19:56:10 UTC
  • mfrom: (1.1.3 upstream)
  • Revision ID: james.westby@ubuntu.com-20060116195610-ykmxbia4mnnod9o2
Tags: 2.1.0-0ubuntu2
debian/copyright: Include copyright for python 2.3; some 2.3 files
are included in the upstream tarball, but not in the binary packages.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- test-case-name: twisted.words.test.test_tendril -*-
2
 
# $Id: tendril.py,v 1.32 2003/01/08 10:34:29 acapnotic Exp $
3
 
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
4
 
# See LICENSE for details.
5
 
 
6
 
 
7
 
"""Tendril between Words and IRC servers.
8
 
 
9
 
A Tendril, attached to a Words service, signs on as a user to an IRC
10
 
server.  It can then relay traffic for one or more channels/groups
11
 
between the two servers.  Anything it hears on a Words group it will
12
 
repeat as a user in an IRC channel; anyone it hears on IRC will appear
13
 
to be logged in to the Words service and speaking in a group there.
14
 
 
15
 
How to Start a Tendril
16
 
======================
17
 
 
18
 
In manhole::
19
 
 
20
 
  from twisted.internet import reactor as R
21
 
  from twisted.internet.app import theApplication as A
22
 
  from twisted.words import tendril as T
23
 
 
24
 
  w = A.getServiceNamed('twisted.words')
25
 
  f = T.TendrilFactory(w)
26
 
  # Maybe do some customization of f here, i.e.
27
 
  ## f.nickname = 'PartyLink'
28
 
  ## f.groupList = ['this', 'that', 'other']
29
 
  R.connectTCP(irchost, 6667, f)
30
 
 
31
 
Stability: No more stable than L{words<twisted.words.service>}.
32
 
 
33
 
Future plans: Use \"L{Policy<twisted.words.service.Policy>}\" to get
34
 
Perspectives.
35
 
 
36
 
@author: U{Kevin Turner<acapnotic@twistedmatrix.com>}
37
 
"""
38
 
 
39
 
from twisted import copyright
40
 
from twisted.cred import authorizer, error
41
 
from twisted.internet import defer, protocol
42
 
from twisted.persisted import styles
43
 
from twisted.words.protocols import irc
44
 
from twisted.python import log, reflect
45
 
from twisted.words import service
46
 
from twisted.spread.util import LocalAsyncForwarder
47
 
 
48
 
wordsService = service
49
 
del service
50
 
 
51
 
import string
52
 
import traceback
53
 
import types
54
 
 
55
 
True = (1==1)
56
 
False = not True
57
 
 
58
 
_LOGALL = False
59
 
 
60
 
# XXX FIXME -- This will need to be fixed to work asynchronously in order to
61
 
# support multiple-server twisted.words and database access to accounts
62
 
 
63
 
class TendrilFactory(protocol.ReconnectingClientFactory, reflect.Accessor):
64
 
    """I build Tendril clients for a words service.
65
 
 
66
 
    All of a tendril's configurable state is stored here with me.
67
 
    """
68
 
 
69
 
    wordsService = None
70
 
    wordsclient = None
71
 
 
72
 
    networkSuffix = None
73
 
    nickname = None
74
 
    perspectiveName = None
75
 
 
76
 
    protocol = None # will be set to TendrilIRC as soon as it's defined.
77
 
    _groupList = ['tendril_test']
78
 
    _errorGroup = 'TendrilErrors'
79
 
 
80
 
    helptext = (
81
 
        "Hi, I'm a Tendril bridge between here and %(service)s.",
82
 
        "You can send a private message to someone like this:",
83
 
        "/msg %(myNick)s msg theirNick Hi there!",
84
 
        )
85
 
 
86
 
    def __init__(self, service):
87
 
        """Initialize this factory with a words service."""
88
 
        self.reallySet('wordsService', service)
89
 
 
90
 
    def startFactory(self):
91
 
        self.wordsclient = TendrilWords(
92
 
            service=self.wordsService, ircFactory=self,
93
 
            nickname=self.nickname, perspectiveName=self.perspectiveName,
94
 
            networkSuffix=self.networkSuffix, groupList=self.groupList,
95
 
            errorGroup=self.errorGroup)
96
 
 
97
 
    def buildProtocol(self, addr):
98
 
        if self.wordsclient.irc:
99
 
            log.msg("Warning: building a new %s protocol while %s is still active."
100
 
                    % (self.protocol, self.wordsclient.irc))
101
 
 
102
 
        proto = protocol.ClientFactory.buildProtocol(self, addr)
103
 
        self.wordsclient.setIrc(proto)
104
 
 
105
 
        # Ermm.
106
 
        ## self.protocol.__dict__.update(self.getConfiguration())
107
 
        for k in ('nickname', 'helptext'):
108
 
            setattr(proto, k, getattr(self, k))
109
 
 
110
 
        return proto
111
 
 
112
 
    def __getstate__(self):
113
 
        state = self.__dict__.copy()
114
 
        try:
115
 
            del state["wordsclient"]
116
 
        except KeyError:
117
 
            pass
118
 
        return state
119
 
 
120
 
    def set_wordsService(self, service):
121
 
        raise TypeError, "%s.wordsService is a read-only attribute." % (repr(self),)
122
 
 
123
 
    def set_groupList(self, groupList):
124
 
        if self.wordsclient:
125
 
            oldlist = self.wordsclient.groupList
126
 
            if groupList != oldlist:
127
 
                newgroups = filter(lambda g, ol=oldlist: g not in ol,
128
 
                                   groupList)
129
 
                deadgroups = filter(lambda o, gl=groupList: o not in gl,
130
 
                                    oldlist)
131
 
 
132
 
                self.wordsclient.groupList[:] = groupList
133
 
                if self.wordsclient.irc:
134
 
                    for group in newgroups:
135
 
                        self.wordsclient.irc.join(groupToChannelName(group))
136
 
                    for group in deadgroups:
137
 
                        self.wordsclient.irc.part(groupToChannelName(group))
138
 
        self._groupList = groupList
139
 
 
140
 
    def get_groupList(self):
141
 
        if self.wordsclient:
142
 
            return self.wordsclient.groupList
143
 
        else:
144
 
            return self._groupList
145
 
 
146
 
    def set_nickname(self, nick):
147
 
        if self.wordsclient and self.wordsclient.irc:
148
 
            self.wordsclient.irc.setNick(nick)
149
 
        self.reallySet('nickname', nick)
150
 
 
151
 
    def set_errorGroup(self, errorGroup):
152
 
        if self.wordsclient:
153
 
            oldgroup = self.wordsclient.errorGroup
154
 
            if oldgroup != errorGroup:
155
 
                self.wordsclient.joinGroup(errorGroup)
156
 
                self.wordsclient.errorGroup = errorGroup
157
 
                self.wordsclient.leaveGroup(oldgroup)
158
 
        self._errorGroup = errorGroup
159
 
 
160
 
    def get_errorGroup(self):
161
 
        if self.wordsclient:
162
 
            return self.wordsclient.errorGroup
163
 
        else:
164
 
            return self._errorGroup
165
 
 
166
 
    def set_helptext(self, helptext):
167
 
        if isinstance(helptext, types.StringType):
168
 
            helptext = string.split(helptext, '\n')
169
 
        if self.wordsclient and self.wordsclient.irc:
170
 
            self.wordsclient.irc.helptext = helptext
171
 
        self.reallySet('helptext', helptext)
172
 
 
173
 
 
174
 
class ProxiedParticipant(wordsService.WordsClient,
175
 
                         styles.Ephemeral):
176
 
    """I'm the client of a participant who is connected through Tendril.
177
 
    """
178
 
 
179
 
    nickname = None
180
 
    tendril = None
181
 
 
182
 
    def __init__(self, tendril, nickname):
183
 
        self.tendril = tendril
184
 
        self.nickname = nickname
185
 
 
186
 
    def setNick(self, nickname):
187
 
        self.nickname = nickname
188
 
 
189
 
    def receiveDirectMessage(self, sender, message, metadata=None):
190
 
        """Pass this message through tendril to my IRC counterpart.
191
 
        """
192
 
        self.tendril.msgFromWords(self.nickname,
193
 
                                  sender, message, metadata)
194
 
 
195
 
 
196
 
class TendrilIRC(irc.IRCClient, styles.Ephemeral):
197
 
    """I connect to the IRC server and broker traffic.
198
 
    """
199
 
 
200
 
    realname = 'Tendril'
201
 
    versionName = 'Tendril'
202
 
    versionNum = '$Revision: 1.32 $'[11:-2]
203
 
    versionEnv = copyright.longversion
204
 
 
205
 
    helptext = TendrilFactory.helptext
206
 
 
207
 
    words = None
208
 
 
209
 
    def __init__(self):
210
 
        """Create a new Tendril IRC client."""
211
 
        self.dcc_sessions = {}
212
 
 
213
 
    ### Protocol-level methods
214
 
 
215
 
    def connectionLost(self, reason):
216
 
        """When I lose a connection, log out all my IRC participants.
217
 
        """
218
 
        self.log("%s: Connection lost: %s" % (self.transport, reason), 'info')
219
 
        self.words.ircConnectionLost()
220
 
 
221
 
    ### Protocol LineReceiver-level methods
222
 
 
223
 
    def lineReceived(self, line):
224
 
        try:
225
 
            irc.IRCClient.lineReceived(self, line)
226
 
        except:
227
 
            # If you *don't* catch exceptions here, any unhandled exception
228
 
            # raised by anything lineReceived calls (which is most of the
229
 
            # client code) ends up making Connection Lost happen, which
230
 
            # is almost certainly not necessary for us.
231
 
            log.deferr()
232
 
 
233
 
    def sendLine(self, line):
234
 
        """Send a line through my transport, unless my transport isn't up.
235
 
        """
236
 
        if (not self.transport) or (not self.transport.connected):
237
 
            return
238
 
 
239
 
        self.log(line, 'dump')
240
 
        irc.IRCClient.sendLine(self, line)
241
 
 
242
 
    ### Protocol IRCClient server->client methods
243
 
 
244
 
    def irc_JOIN(self, prefix, params):
245
 
        """Join IRC user to the corresponding group.
246
 
        """
247
 
        nick = string.split(prefix,'!')[0]
248
 
        groupName = channelToGroupName(params[0])
249
 
        if nick == self.nickname:
250
 
            self.words.joinGroup(groupName)
251
 
        else:
252
 
            self.words._getParticipant(nick).joinGroup(groupName)
253
 
 
254
 
    def irc_NICK(self, prefix, params):
255
 
        """When an IRC user changes their nickname
256
 
 
257
 
        this does *not* change the name of their perspectivee, just my
258
 
        nickname->perspective and client->nickname mappings.
259
 
        """
260
 
        old_nick = string.split(prefix,'!')[0]
261
 
        new_nick = params[0]
262
 
        if old_nick == self.nickname:
263
 
            self.nickname = new_nick
264
 
        else:
265
 
            self.words.changeParticipantNick(old_nick, new_nick)
266
 
 
267
 
    def irc_PART(self, prefix, params):
268
 
        """Parting IRC members leave the correspoding group.
269
 
        """
270
 
        nick = string.split(prefix,'!')[0]
271
 
        channel = params[0]
272
 
        groupName = channelToGroupName(channel)
273
 
        if nick == self.nickname:
274
 
            self.words.groupMessage(groupName, "I've left %s" % (channel,))
275
 
            self.words.leaveGroup(groupName)
276
 
            self.words.evacuateGroup(groupName)
277
 
            return
278
 
        else:
279
 
            self.words.ircPartParticipant(nick, groupName)
280
 
 
281
 
    def irc_QUIT(self, prefix, params):
282
 
        """When a user quits IRC, log out their participant.
283
 
        """
284
 
        nick = string.split(prefix,'!')[0]
285
 
        if nick == self.nickname:
286
 
            self.words.detach()
287
 
        else:
288
 
            self.words.logoutParticipant(nick)
289
 
 
290
 
    def irc_KICK(self, prefix, params):
291
 
        """Kicked?  Who?  Not me, I hope.
292
 
        """
293
 
        nick = string.split(prefix,'!')[0]
294
 
        channel = params[0]
295
 
        kicked = params[1]
296
 
        group = channelToGroupName(channel)
297
 
        if string.lower(kicked) == string.lower(self.nickname):
298
 
            # Yikes!
299
 
            if self.words.participants.has_key(nick):
300
 
                wordsname = " (%s)" % (self.words._getParticipant(nick).name,)
301
 
            else:
302
 
                wordsname = ''
303
 
            if len(params) > 2:
304
 
                reason = '  "%s"' % (params[2],)
305
 
            else:
306
 
                reason = ''
307
 
 
308
 
            self.words.groupMessage(group, '%s%s kicked me off!%s'
309
 
                              % (prefix, wordsname, reason))
310
 
            self.log("I've been kicked from %s: %s %s"
311
 
                     % (channel, prefix, params), 'NOTICE')
312
 
            self.words.evacuateGroup(group)
313
 
 
314
 
        else:
315
 
            self.words.ircPartParticipant(kicked, group)
316
 
 
317
 
    def irc_INVITE(self, prefix, params):
318
 
        """Accept an invitation, if it's in my groupList.
319
 
        """
320
 
        group = channelToGroupName(params[1])
321
 
        if group in self.groupList:
322
 
            self.log("I'm accepting the invitation to join %s from %s."
323
 
                     % (group, prefix), 'NOTICE')
324
 
            self.words.join(groupToChannelName(group))
325
 
 
326
 
    def irc_TOPIC(self, prefix, params):
327
 
        """Announce the new topic.
328
 
        """
329
 
        # XXX: words groups *do* have topics, but they're currently
330
 
        # not used.  Should we use them?
331
 
        nick = string.split(prefix,'!')[0]
332
 
        channel = params[0]
333
 
        topic = params[1]
334
 
        self.words.groupMessage(channelToGroupName(channel),
335
 
                                "%s has just decreed the topic to be: %s"
336
 
                                % (self.words._getParticipant(nick).name,
337
 
                                   topic))
338
 
 
339
 
    def irc_ERR_BANNEDFROMCHAN(self, prefix, params):
340
 
        """When I can't get on a channel, report it.
341
 
        """
342
 
        self.log("Join failed: %s %s" % (prefix, params), 'NOTICE')
343
 
 
344
 
    irc_ERR_CHANNELISFULL = \
345
 
                          irc_ERR_UNAVAILRESOURCE = \
346
 
                          irc_ERR_INVITEONLYCHAN =\
347
 
                          irc_ERR_NOSUCHCHANNEL = \
348
 
                          irc_ERR_BADCHANNELKEY = irc_ERR_BANNEDFROMCHAN
349
 
 
350
 
    def irc_ERR_NOTREGISTERED(self, prefix, params):
351
 
        self.log("Got ERR_NOTREGISTERED, re-running connectionMade().",
352
 
                 'NOTICE')
353
 
        self.connectionMade()
354
 
 
355
 
 
356
 
    ### Client-To-Client-Protocol methods
357
 
 
358
 
    def ctcpQuery_DCC(self, user, channel, data):
359
 
        """Accept DCC handshakes, for passing on to others.
360
 
        """
361
 
        nick = string.split(user,"!")[0]
362
 
 
363
 
        # We're pretty lenient about what we pass on, but the existance
364
 
        # of at least four parameters (type, arg, host, port) is really
365
 
        # required.
366
 
        if len(string.split(data)) < 4:
367
 
            self.ctcpMakeReply(nick, [('ERRMSG',
368
 
                                       'DCC %s :Malformed DCC request.'
369
 
                                       % (data))])
370
 
            return
371
 
 
372
 
        dcc_text = irc.dccDescribe(data)
373
 
 
374
 
        self.notice(nick, "Got your DCC %s"
375
 
                    % (irc.dccDescribe(data),))
376
 
 
377
 
        pName = self.words._getParticipant(nick).name
378
 
        self.dcc_sessions[pName] = (user, dcc_text, data)
379
 
 
380
 
        self.notice(nick, "If I should pass it on to another user, "
381
 
                    "/msg %s DCC PASSTO theirNick" % (self.nickname,))
382
 
 
383
 
 
384
 
    ### IRCClient client event methods
385
 
 
386
 
    def signedOn(self):
387
 
        """Join my groupList once I've signed on.
388
 
        """
389
 
        self.log("Welcomed by IRC server.", 'info')
390
 
        self.factory.resetDelay()
391
 
        for group in self.words.groupList:
392
 
            self.join(groupToChannelName(group))
393
 
 
394
 
    def privmsg(self, user, channel, message):
395
 
        """Dispatch privmsg as a groupMessage or a command, as appropriate.
396
 
        """
397
 
        nick = string.split(user,'!')[0]
398
 
        if nick == self.nickname:
399
 
            return
400
 
 
401
 
        if string.lower(channel) == string.lower(self.nickname):
402
 
            parts = string.split(message, ' ', 1)
403
 
            cmd = parts[0]
404
 
            if len(parts) > 1:
405
 
                remainder = parts[1]
406
 
            else:
407
 
                remainder = None
408
 
 
409
 
            method = getattr(self, "bot_%s" % cmd, None)
410
 
            if method is not None:
411
 
                method(user, remainder)
412
 
            else:
413
 
                self.botUnknown(user, channel, message)
414
 
        else:
415
 
            # The message isn't to me, so it must be to a group.
416
 
            group = channelToGroupName(channel)
417
 
            self.words.ircParticipantMsg(nick, group, message)
418
 
 
419
 
    def noticed(self, user, channel, message):
420
 
        """Pass channel notices on to the group.
421
 
        """
422
 
        nick = string.split(user,'!')[0]
423
 
        if nick == self.nickname:
424
 
            return
425
 
 
426
 
        if string.lower(channel) == string.lower(self.nickname):
427
 
            # A private notice is most likely an auto-response
428
 
            # from something else, or a message from an IRC service.
429
 
            # Don't treat at as a command.
430
 
            pass
431
 
        else:
432
 
            # The message isn't to me, so it must be to a group.
433
 
            group = channelToGroupName(channel)
434
 
            self.words.ircParticipantMsg(nick, group, message)
435
 
 
436
 
    def action(self, user, channel, message):
437
 
        """Speak about a participant in third-person.
438
 
        """
439
 
        group = channelToGroupName(channel)
440
 
        nick = string.split(user,'!',1)[0]
441
 
        self.words.ircParticipantMsg(nick, group, message, emote=True)
442
 
 
443
 
    ### Bot event methods
444
 
 
445
 
    def bot_msg(self, sender, params):
446
 
        """Pass along a message as a directMessage to a words Participant
447
 
        """
448
 
        (nick, message) = string.split(params, ' ', 1)
449
 
        sender = string.split(sender, '!', 1)[0]
450
 
        try:
451
 
            self.words._getParticipant(sender).directMessage(nick, message)
452
 
        except wordsService.WordsError, e:
453
 
            self.notice(sender, "msg to %s failed: %s" % (nick, e))
454
 
 
455
 
    def bot_help(self, user, params):
456
 
        nick = string.split(user, '!', 1)[0]
457
 
        for l in self.helptext:
458
 
            self.notice(nick, l % {
459
 
                'myNick': self.nickname,
460
 
                'service': self.factory.wordsService,
461
 
                })
462
 
 
463
 
    def botUnknown(self, user, channel, message):
464
 
        parts = string.split(message, ' ', 1)
465
 
        cmd = parts[0]
466
 
        if len(parts) > 1:
467
 
            remainder = parts[1]
468
 
        else:
469
 
            remainder = None
470
 
 
471
 
        if remainder is not None:
472
 
            # Default action is to try anything as a 'msg'
473
 
            # make sure the message is from a user and not a server.
474
 
            if ('!' in user) and ('@' in user):
475
 
                self.bot_msg(user, message)
476
 
        else:
477
 
            # But if the msg would be empty, don't that.
478
 
            # Just act confused.
479
 
            nick = string.split(user, '!', 1)[0]
480
 
            self.notice(nick, "I don't know what to do with '%s'.  "
481
 
                        "`/msg %s help` for help."
482
 
                        % (cmd, self.nickname))
483
 
 
484
 
    def bot_DCC(self, user, params):
485
 
        """Commands for brokering DCC handshakes.
486
 
 
487
 
        DCC -- I'll tell you if I'm holding a DCC request from you.
488
 
 
489
 
        DCC PASSTO nick -- give the DCC request you gave me to this nick.
490
 
 
491
 
        DCC FORGET -- forget any DCC requests you offered to me.
492
 
        """
493
 
        nick = string.split(user,"!")[0]
494
 
        pName = self.words._getParticipant(nick).name
495
 
 
496
 
        if not params:
497
 
            # Do I have a DCC from you?
498
 
            if self.dcc_sessions.has_key(pName):
499
 
                dcc_text = self.dcc_sessions[pName][1]
500
 
                self.notice(nick,
501
 
                            "I have an offer from you for DCC %s"
502
 
                            % (dcc_text,))
503
 
            else:
504
 
                self.notice(nick, "I have no DCC offer from you.")
505
 
            return
506
 
 
507
 
        params = string.split(params)
508
 
 
509
 
        if (params[0] == 'PASSTO') and (len(params) > 1):
510
 
            (cmd, dst) = params[:2]
511
 
            cmd = string.upper(cmd)
512
 
            if self.dcc_sessions.has_key(pName):
513
 
                (origUser, dcc_text, orig_data)=self.dcc_sessions[pName]
514
 
                if dcc_text:
515
 
                    dcc_text = " for " + dcc_text
516
 
                else:
517
 
                    dcc_text = ''
518
 
 
519
 
                ctcpMsg = irc.ctcpStringify([('DCC',orig_data)])
520
 
                try:
521
 
                    self.words._getParticipant(nick).directMessage(dst,
522
 
                                                                   ctcpMsg)
523
 
                except wordsService.WordsError, e:
524
 
                    self.notice(nick, "DCC offer to %s failed: %s"
525
 
                                % (dst, e))
526
 
                else:
527
 
                    self.notice(nick, "DCC offer%s extended to %s."
528
 
                                % (dcc_text, dst))
529
 
                    del self.dcc_sessions[pName]
530
 
 
531
 
            else:
532
 
                self.notice(nick, "I don't have an active DCC"
533
 
                            " handshake from you.")
534
 
 
535
 
        elif params[0] == 'FORGET':
536
 
            if self.dcc_sessions.has_key(pName):
537
 
                del self.dcc_sessions[pName]
538
 
            self.notice(nick, "I have now forgotten any DCC offers"
539
 
                        " from you.")
540
 
        else:
541
 
            self.notice(nick,
542
 
                        "Valid DCC commands are: "
543
 
                        "DCC, DCC PASSTO <nick>, DCC FORGET")
544
 
        return
545
 
 
546
 
 
547
 
    ### Utility
548
 
 
549
 
    def log(self, message, priority=None):
550
 
        """I need to give Twisted a prioritized logging facility one of these days.
551
 
        """
552
 
        if _LOGALL:
553
 
            log.msg(message)
554
 
        elif not (priority in ('dump',)):
555
 
            log.msg(message)
556
 
 
557
 
        if priority in ('info', 'NOTICE', 'ERROR'):
558
 
            self.words.groupMessage(self.words.errorGroup, message)
559
 
 
560
 
TendrilFactory.protocol = TendrilIRC
561
 
 
562
 
class TendrilWords(wordsService.WordsClient):
563
 
    nickname = 'tl'
564
 
    networkSuffix = '@opn'
565
 
    perspectiveName = nickname + networkSuffix
566
 
    participants = None
567
 
    irc = None
568
 
    ircFactory = None
569
 
 
570
 
    def __init__(self, service, ircFactory,
571
 
                 nickname=None, networkSuffix=None, perspectiveName=None,
572
 
                 groupList=None, errorGroup=None):
573
 
        """
574
 
        service -- a twisted.words.service.Service, or at least
575
 
        something with a 'serviceName' attribute and 'createParticipant'
576
 
        and 'getPerspectiveNamed' methods which work like a
577
 
        words..Service.
578
 
 
579
 
        groupList -- a list of strings naming groups on the Words
580
 
        service to join and bridge to their counterparts on the IRC
581
 
        server.
582
 
 
583
 
        nickname -- a string to use as my nickname on the IRC network.
584
 
 
585
 
        networkSuffix -- a string to append to the nickname of the
586
 
        Participants I bring in through IRC, e.g. \"@opn\".
587
 
 
588
 
        perspectiveName -- the name of my perspective with this
589
 
        service.  Defaults to nickname + networkSuffix.
590
 
        """
591
 
        self.service = service
592
 
        self.ircFactory = ircFactory
593
 
        self.participants = {}
594
 
 
595
 
        if nickname:
596
 
            self.nickname = nickname
597
 
        if networkSuffix:
598
 
            self.networkSuffix = networkSuffix
599
 
 
600
 
        if perspectiveName:
601
 
            self.perspectiveName = perspectiveName
602
 
        else:
603
 
            self.perspectiveName = self.nickname + self.networkSuffix
604
 
 
605
 
        if groupList:
606
 
            self.groupList = groupList
607
 
        else:
608
 
            # Copy the class default's list so as to not modify the original.
609
 
            self.groupList = self.groupList[:]
610
 
 
611
 
        if errorGroup:
612
 
            self.errorGroup = errorGroup
613
 
 
614
 
        self.attachToWords()
615
 
 
616
 
    def setIrc(self, ircProtocol):
617
 
        self.irc = ircProtocol
618
 
        self.irc.realname =  'Tendril to %s' % (self.service.serviceName,)
619
 
        self.irc.words = self
620
 
 
621
 
    def setupBot(self, perspective):
622
 
        self.perspective = perspective
623
 
        self.joinGroup(self.errorGroup)
624
 
 
625
 
    def attachToWords(self):
626
 
        """Get my perspective on the Words service; attach as a client.
627
 
        """
628
 
 
629
 
        self.service.addBot(self.perspectiveName, self)
630
 
 
631
 
        # XXX: Decide how much of this code belongs in words..Service.addBot
632
 
#         try:
633
 
#             self.perspective = (
634
 
#                 self.service.getPerspectiveNamed(self.perspectiveName))
635
 
#         except wordsService.UserNonexistantError:
636
 
#             self.perspective = (
637
 
#                 self.service.createParticipant(self.perspectiveName))
638
 
#             if not self.perspective:
639
 
#                 raise RuntimeError, ("service %s won't give me my "
640
 
#                                      "perspective named %s"
641
 
#                                      % (self.service,
642
 
#                                         self.perspectiveName))
643
 
 
644
 
#         if self.perspective.client is self:
645
 
#             log.msg("I seem to be already attached.")
646
 
#             return
647
 
 
648
 
#         try:
649
 
#             self.attach()
650
 
#         except error.Unauthorized:
651
 
#             if self.perspective.client:
652
 
#                 log.msg("%s is attached to my perspective: "
653
 
#                         "kicking it off." % (self.perspective.client,))
654
 
#                 self.perspective.detached(self.perspective.client, None)
655
 
#                 self.attach()
656
 
#             else:
657
 
#                 raise
658
 
 
659
 
 
660
 
 
661
 
    ### WordsClient methods
662
 
    ##      Words.Group --> IRC
663
 
 
664
 
    def memberJoined(self, member, group):
665
 
        """Tell the IRC Channel when someone joins the Words group.
666
 
        """
667
 
        if (group == self.errorGroup) or self.isThisMine(member):
668
 
            return
669
 
        self.irc.say(groupToChannelName(group), "%s joined." % (member,))
670
 
 
671
 
    def memberLeft(self, member, group):
672
 
        """Tell the IRC Channel when someone leaves the Words group.
673
 
        """
674
 
        if (group == self.errorGroup) or self.isThisMine(member):
675
 
            return
676
 
        self.irc.say(groupToChannelName(group), "%s left." % (member,))
677
 
 
678
 
    def receiveGroupMessage(self, sender, group, message, metadata=None):
679
 
        """Pass a message from the Words group on to IRC.
680
 
 
681
 
        Or, if it's in our errorGroup, recognize some debugging commands.
682
 
        """
683
 
        if not (group == self.errorGroup):
684
 
            channel = groupToChannelName(group)
685
 
            if not self.isThisMine(sender):
686
 
                # Test for Special case:
687
 
                # got CTCP, probably through words.ircservice
688
 
                #      (you SUCK!)
689
 
                # ACTION is the only case we'll support here.
690
 
                if message[:8] == irc.X_DELIM + 'ACTION ':
691
 
                    c = irc.ctcpExtract(message)
692
 
                    for tag, data in c['extended']:
693
 
                        if tag == 'ACTION':
694
 
                            self.irc.say(channel, "* %s %s" % (sender, data))
695
 
                        else:
696
 
                            # Not an action.  Repackage the chunk,
697
 
                            msg = "%(X)s%(tag)s %(data)s%(X)s" % {
698
 
                                'X': irc.X_DELIM,
699
 
                                'tag': tag,
700
 
                                'data': data
701
 
                                }
702
 
                            # ctcpQuote it to render it harmless,
703
 
                            msg = irc.ctcpQuote(msg)
704
 
                            # and let it continue on.
705
 
                            c['normal'].append(msg)
706
 
 
707
 
                    for msg in c['normal']:
708
 
                        self.irc.say(channel, "<%s> %s" % (sender, msg))
709
 
                    return
710
 
 
711
 
                elif irc.X_DELIM in message:
712
 
                    message = irc.ctcpQuote(message)
713
 
 
714
 
                if metadata and metadata.has_key('style'):
715
 
                    if metadata['style'] == "emote":
716
 
                        self.irc.say(channel, "* %s %s" % (sender, message))
717
 
                        return
718
 
 
719
 
                self.irc.say(channel, "<%s> %s" % (sender, message))
720
 
        else:
721
 
            # A message in our errorGroup.
722
 
            if message == "participants":
723
 
                s = map(lambda i: str(i[0]), self.participants.values())
724
 
                s = string.join(s, ", ")
725
 
            elif message == "groups":
726
 
                s = map(str, self.perspective.groups)
727
 
                s = string.join(s, ", ")
728
 
            elif message == "transport":
729
 
                s = "%s connected: %s" %\
730
 
                    (self.transport, getattr(self.transport, "connected"))
731
 
            else:
732
 
                s = None
733
 
 
734
 
            if s:
735
 
                self.groupMessage(group, s)
736
 
 
737
 
 
738
 
    ### My methods as a Participant
739
 
    ### (Shortcuts for self.perspective.foo())
740
 
 
741
 
    def joinGroup(self, groupName):
742
 
        return self.perspective.joinGroup(groupName)
743
 
 
744
 
    def leaveGroup(self, groupName):
745
 
        return self.perspective.leaveGroup(groupName)
746
 
 
747
 
    def groupMessage(self, groupName, message):
748
 
        return self.perspective.groupMessage(groupName, message)
749
 
 
750
 
    def directMessage(self, recipientName, message):
751
 
        return self.perspective.directMessage(recipientName, message)
752
 
 
753
 
    ### My methods as a bogus perspective broker
754
 
    ### (Since I grab my perspective directly from the service, it hasn't
755
 
    ###  been issued by a Perspective Broker.)
756
 
 
757
 
    def attach(self):
758
 
        self.perspective.attached(self, None)
759
 
 
760
 
    def detach(self):
761
 
        """Pull everyone off Words, sign off, cut the IRC connection.
762
 
        """
763
 
        if not (self is getattr(self.perspective,'client')):
764
 
            # Not attached.
765
 
            return
766
 
 
767
 
        for g in self.perspective.groups:
768
 
            if g.name != self.errorGroup:
769
 
                self.leaveGroup(g.name)
770
 
        for nick in self.participants.keys()[:]:
771
 
            self.logoutParticipant(nick)
772
 
        self.perspective.detached(self, None)
773
 
        if self.transport and getattr(self.transport, 'connected'):
774
 
            self.ircFactory.doStop()
775
 
            self.transport.loseConnection()
776
 
 
777
 
 
778
 
    ### Participant event methods
779
 
    ##      Words.Participant --> IRC
780
 
 
781
 
    def msgFromWords(self, toNick, sender, message, metadata=None):
782
 
        """Deliver a directMessage as a privmsg over IRC.
783
 
        """
784
 
        if message[0] != irc.X_DELIM:
785
 
            if metadata and metadata.has_key('style'):
786
 
                # Damn.  What am I supposed to do with this?
787
 
                message = "[%s] %s" % (metadata['style'], message)
788
 
 
789
 
            self.irc.msg(toNick, '<%s> %s' % (sender, message))
790
 
        else:
791
 
            # If there is a CTCP delimeter at the beginning of the
792
 
            # message, let's leave it there to accomidate not-so-
793
 
            # tolerant clients.
794
 
            dcc_data = None
795
 
            if message[1:5] == 'DCC ':
796
 
                dcc_query = irc.ctcpExtract(message)['extended'][0]
797
 
                dcc_data = dcc_query[1]
798
 
 
799
 
            if dcc_data:
800
 
                desc = "DCC " + irc.dccDescribe(dcc_data)
801
 
            else:
802
 
                desc = "CTCP request"
803
 
 
804
 
            self.irc.msg(toNick, 'The following %s is from %s'
805
 
                     % (desc, sender))
806
 
            self.irc.msg(toNick, '%s' % (message,))
807
 
 
808
 
 
809
 
    # IRC Participant Management
810
 
 
811
 
    def ircConnectionLost(self):
812
 
        for nick in self.participants.keys()[:]:
813
 
            self.logoutParticipant(nick)
814
 
 
815
 
    def ircPartParticipant(self, nick, groupName):
816
 
        participant = self._getParticipant(nick)
817
 
        try:
818
 
            participant.leaveGroup(groupName)
819
 
        except wordsService.NotInGroupError:
820
 
            pass
821
 
 
822
 
        if not participant.groups:
823
 
            self.logoutParticipant(nick)
824
 
 
825
 
    def ircParticipantMsg(self, nick, groupName, message, emote=False):
826
 
        participant = self._getParticipant(nick)
827
 
        if emote:
828
 
            metadata = {'style': 'emote'}
829
 
        else:
830
 
            metadata = None
831
 
        try:
832
 
            participant.groupMessage(groupName, message, metadata)
833
 
        except wordsService.NotInGroupError:
834
 
            participant.joinGroup(groupName)
835
 
            participant.groupMessage(groupName, message, metadata)
836
 
 
837
 
    def evacuateGroup(self, groupName):
838
 
        """Pull all of my Participants out of this group.
839
 
        """
840
 
        # XXX: This marks another place where we get a little
841
 
        # overly cozy with the words service.
842
 
        group = self.service.getGroup(groupName)
843
 
 
844
 
        allMyMembers = map(lambda m: m[0], self.participants.values())
845
 
        groupMembers = filter(lambda m, a=allMyMembers: m in a,
846
 
                              group.members)
847
 
 
848
 
        for m in groupMembers:
849
 
            m.leaveGroup(groupName)
850
 
 
851
 
    def _getParticipant(self, nick):
852
 
        """Get a Perspective (words.service.Participant) for a IRC user.
853
 
 
854
 
        And if I don't have one around, I'll make one.
855
 
        """
856
 
        if not self.participants.has_key(nick):
857
 
            self._newParticipant(nick)
858
 
 
859
 
        return self.participants[nick][0]
860
 
 
861
 
    def _getClient(self, nick):
862
 
        if not self.participants.has_key(nick):
863
 
            self._newParticipant(nick)
864
 
        return self.participants[nick][1]
865
 
 
866
 
    # TODO: let IRC users authorize themselves and then give them a
867
 
    # *real* perspective (one attached to their identity) instead
868
 
    # of one of my @networkSuffix-Nobody perspectives.
869
 
 
870
 
    def _newParticipant(self, nick):
871
 
        try:
872
 
            p = self.service.getPerspectiveNamed(nick + self.networkSuffix)
873
 
        except wordsService.UserNonexistantError:
874
 
            p = self.service.createParticipant(nick + self.networkSuffix)
875
 
            if not p:
876
 
                raise wordsService.wordsError("Eeek!  Couldn't get OR "
877
 
                                              "make a perspective for "
878
 
                                              "'%s%s'." %
879
 
                                              (nick, self.networkSuffix))
880
 
 
881
 
        c = ProxiedParticipant(self, nick)
882
 
        p.attached(LocalAsyncForwarder(c, wordsService.IWordsClient, 1),
883
 
                   None)
884
 
        # p.attached(c, None)
885
 
 
886
 
        self.participants[nick] = [p, c]
887
 
 
888
 
    def changeParticipantNick(self, old_nick, new_nick):
889
 
        if not self.participants.has_key(old_nick):
890
 
            return
891
 
 
892
 
        (p, c) = self.participants[old_nick]
893
 
        c.setNick(new_nick)
894
 
 
895
 
        self.participants[new_nick] = self.participants[old_nick]
896
 
        del self.participants[old_nick]
897
 
 
898
 
    def logoutParticipant(self, nick):
899
 
        if not self.participants.has_key(nick):
900
 
            return
901
 
 
902
 
        (p, c) = self.participants[nick]
903
 
        p.detached(c, None)
904
 
        c.tendril = None
905
 
        # XXX: This must change if we ever start giving people 'real'
906
 
        #    perspectives!
907
 
        if not p.identityName:
908
 
            self.service.uncachePerspective(p)
909
 
        del self.participants[nick]
910
 
 
911
 
    def isThisMine(self, sender):
912
 
        """Returns true if 'sender' is the name of a perspective I'm providing.
913
 
        """
914
 
        if self.perspectiveName == sender:
915
 
            return "That's ME!"
916
 
 
917
 
        for (p, c) in self.participants.values():
918
 
            if p.name == sender:
919
 
                return 1
920
 
        return 0
921
 
 
922
 
 
923
 
def channelToGroupName(channelName):
924
 
    """Map an IRC channel name to a Words group name.
925
 
 
926
 
    IRC is case-insensitive, words is not.  Arbitrtarily decree that all
927
 
    IRC channels should be lowercase.
928
 
 
929
 
    Warning: This prevents me from relaying text from IRC to
930
 
    a mixed-case Words group.  That is, any words group I'm
931
 
    in should have an all-lowercase name.
932
 
    """
933
 
 
934
 
    # Normalize case and trim leading '#'
935
 
    groupName = string.lower(channelName[1:])
936
 
    return groupName
937
 
 
938
 
def groupToChannelName(groupName):
939
 
    # Don't add a "#" here, because we do so in the outgoing IRC methods.
940
 
    channelName = groupName
941
 
    return channelName