~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): Moshe Zadka
  • Date: 2002-03-08 07:14:16 UTC
  • Revision ID: james.westby@ubuntu.com-20020308071416-oxvuw76tpcpi5v1q
Tags: upstream-0.15.5
ImportĀ upstreamĀ versionĀ 0.15.5

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
 
 
2
# Twisted, the Framework of Your Internet
 
3
# Copyright (C) 2001 Matthew W. Lefkowitz
 
4
#
 
5
# This library is free software; you can redistribute it and/or
 
6
# modify it under the terms of version 2.1 of the GNU Lesser General Public
 
7
# License as published by the Free Software Foundation.
 
8
#
 
9
# This library is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 
12
# Lesser General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU Lesser General Public
 
15
# License along with this library; if not, write to the Free Software
 
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
17
 
 
18
from twisted.persisted import styles
 
19
from twisted.protocols import irc
 
20
from twisted.python import log
 
21
from twisted.words import service
 
22
from twisted import copyright
 
23
from twisted.cred import authorizer
 
24
 
 
25
wordsService = service
 
26
del service
 
27
 
 
28
import string
 
29
import sys
 
30
import traceback
 
31
 
 
32
_LOGALL = 0
 
33
 
 
34
# XXX FIXME -- This will need to be fixed to work asynchronously in order to
 
35
# support multiple-server twisted.words and database access to accounts
 
36
 
 
37
class ProxiedParticipant(wordsService.WordsClientInterface,
 
38
                         styles.Ephemeral):
 
39
    """I'm the client of a participant who is connected through Tendril.
 
40
    """
 
41
 
 
42
    nickname = None
 
43
    tendril = None
 
44
 
 
45
    def __init__(self, tendril, nickname):
 
46
        self.tendril = tendril
 
47
        self.nickname = nickname
 
48
 
 
49
    def setNick(self, nickname):
 
50
        self.nickname = nickname
 
51
 
 
52
    def receiveDirectMessage(self, sender, message, metadata=None):
 
53
        """Pass this message through tendril to my IRC counterpart.
 
54
        """
 
55
        self.tendril.msgFromWords(self.nickname,
 
56
                                  sender, message, metadata)
 
57
 
 
58
 
 
59
class TendrilClient(irc.IRCClient, wordsService.WordsClientInterface):
 
60
    """I connect to the IRC server and broker traffic.
 
61
    """
 
62
 
 
63
    networkSuffix = '@opn'
 
64
    nickname = 'tl'
 
65
    groupList = ['tendril_test']
 
66
 
 
67
    participants = None
 
68
 
 
69
    errorGroup = 'TendrilErrors'
 
70
    perspectiveName = nickname + networkSuffix
 
71
 
 
72
    realname = 'Tendril'
 
73
    versionName = 'Tendril'
 
74
    versionNum = '$Revision: 1.14 $'[11:-2]
 
75
    versionEnv = copyright.longversion
 
76
 
 
77
    helptext = (
 
78
        "Hi, I'm a Tendril bridge between here and %(service)s.",
 
79
        "You can send a private message to someone like this:",
 
80
        "/msg %(myNick)s msg theirNick Hi there!",
 
81
        )
 
82
 
 
83
    def __init__(self, service, groupList=None,
 
84
                 nickname=None, networkSuffix=None, perspectiveName=None):
 
85
        """Create a new Tendril client.
 
86
 
 
87
        service -- a twisted.words.service.Service, or at least
 
88
        something with a 'serviceName' attribute and 'createParticipant'
 
89
        and 'getPerspectiveNamed' methods which work like a
 
90
        words..Service.
 
91
 
 
92
        groupList -- a list of strings naming groups on the Words
 
93
        service to join and bridge to their counterparts on the IRC
 
94
        server.
 
95
 
 
96
        nickname -- a string to use as my nickname on the IRC network.
 
97
 
 
98
        networkSuffix -- a string to append to the nickname of the
 
99
        Participants I bring in through IRC, e.g. \"@opn\".
 
100
 
 
101
        perspectiveName -- the name of my perspective with this
 
102
        service.  Defaults to nickname + networkSuffix.
 
103
 
 
104
        To connect me to an IRC server, pass me as the 'protocol' when
 
105
        constructing a tcp.Client.
 
106
        """
 
107
        self.participants = {}
 
108
        self.dcc_sessions = {}
 
109
 
 
110
        if nickname:
 
111
            self.nickname = nickname
 
112
        if networkSuffix:
 
113
            self.networkSuffix = networkSuffix
 
114
 
 
115
        if not perspectiveName:
 
116
            perspectiveName = self.nickname + self.networkSuffix
 
117
 
 
118
        self.perspectiveName = perspectiveName
 
119
 
 
120
        if groupList:
 
121
            self.groupList = list(groupList)
 
122
        else:
 
123
            self.groupList = list(TendrilClient.groupList)
 
124
 
 
125
        self.service = service
 
126
        self.realname = 'Tendril to %s' % (service.serviceName,)
 
127
 
 
128
        self.attachToWords()
 
129
 
 
130
        self.joinGroup(self.errorGroup)
 
131
 
 
132
 
 
133
    def attachToWords(self):
 
134
        """Get my perspective on the Words service; attach as a client.
 
135
        """
 
136
 
 
137
        try:
 
138
            self.perspective = (
 
139
                self.service.getPerspectiveNamed(self.perspectiveName))
 
140
        except wordsService.UserNonexistantError:
 
141
            self.perspective = (
 
142
                self.service.createParticipant(self.perspectiveName))
 
143
            if not self.perspective:
 
144
                raise RuntimeError, ("service %s won't give me my "
 
145
                                     "perspective named %s"
 
146
                                     % (self.service,
 
147
                                        self.perspectiveName))
 
148
 
 
149
        if self.perspective.client is self:
 
150
            log.msg("I seem to be already attached.")
 
151
            return
 
152
 
 
153
        try:
 
154
            self.attach()
 
155
        except authorizer.Unauthorized:
 
156
            if self.perspective.client:
 
157
                log.msg("%s is attached to my perspective: "
 
158
                        "kicking it off." % (self.perspective.client,))
 
159
                self.perspective.detached(self.perspective.client, None)
 
160
                self.attach()
 
161
            else:
 
162
                raise
 
163
 
 
164
    def __getstate__(self):
 
165
        dct = self.__dict__.copy()
 
166
        # Don't save my imaginary friends.
 
167
        dct["participants"] = {}
 
168
        return dct
 
169
 
 
170
 
 
171
    ### Protocol-level methods
 
172
 
 
173
    def connectionLost(self):
 
174
        """When I lose a connection, log out all my IRC participants.
 
175
        """
 
176
        self.log("%s: Connection lost." % (self.transport,), 'info')
 
177
        for nick in self.participants.keys()[:]:
 
178
            self.logoutParticipant(nick)
 
179
 
 
180
    ### Protocol LineReceiver-level methods
 
181
 
 
182
    def lineReceived(self, line):
 
183
        self.log(line, 'dump')
 
184
 
 
185
        try:
 
186
            irc.IRCClient.lineReceived(self, line)
 
187
        except:
 
188
            log.deferr()
 
189
 
 
190
 
 
191
    def sendLine(self, line):
 
192
        """Send a line through my transport, unless my transport isn't up.
 
193
        """
 
194
        if (not self.transport) or (not self.transport.connected):
 
195
            return
 
196
 
 
197
        self.log(line, 'dump')
 
198
        irc.IRCClient.sendLine(self, line)
 
199
 
 
200
 
 
201
    ### Protocol IRCClient server->client methods
 
202
 
 
203
    def irc_JOIN(self, prefix, params):
 
204
        """Join IRC user to the corresponding group.
 
205
        """
 
206
        nick = string.split(prefix,'!')[0]
 
207
        groupName = channelToGroupName(params[0])
 
208
        if nick == self.nickname:
 
209
            self.joinGroup(groupName)
 
210
        else:
 
211
            self._getParticipant(nick).joinGroup(groupName)
 
212
 
 
213
    def irc_NICK(self, prefix, params):
 
214
        """When an IRC user changes their nickname
 
215
 
 
216
        this does *not* change the name of their perspectivee, just my
 
217
        nickname->perspective and client->nickname mappings.
 
218
        """
 
219
        old_nick = string.split(prefix,'!')[0]
 
220
        new_nick = params[0]
 
221
        if old_nick == self.nickname:
 
222
            self.nickname = new_nick
 
223
        else:
 
224
            self.changeParticipantNick(old_nick, new_nick)
 
225
 
 
226
    def irc_PART(self, prefix, params):
 
227
        """Parting IRC members leave the correspoding group.
 
228
        """
 
229
        nick = string.split(prefix,'!')[0]
 
230
        channel = params[0]
 
231
        groupName = channelToGroupName(channel)
 
232
        if nick == self.nickname:
 
233
            self.groupMessage(groupName, "I've left %s" % (channel,))
 
234
            self.leaveGroup(groupName)
 
235
            self.evacuateGroup(groupName)
 
236
            return
 
237
 
 
238
        participant = self._getParticipant(nick)
 
239
        try:
 
240
            participant.leaveGroup(groupName)
 
241
        except wordsService.NotInGroupError:
 
242
            pass
 
243
 
 
244
        if not participant.groups:
 
245
            self.logoutParticipant(nick)
 
246
 
 
247
    def irc_QUIT(self, prefix, params):
 
248
        """When a user quits IRC, log out their participant.
 
249
        """
 
250
        nick = string.split(prefix,'!')[0]
 
251
        if nick == self.nickname:
 
252
            self.detach()
 
253
        else:
 
254
            self.logoutParticipant(nick)
 
255
 
 
256
    def irc_KICK(self, prefix, params):
 
257
        """Kicked?  Who?  Not me, I hope.
 
258
        """
 
259
        nick = string.split(prefix,'!')[0]
 
260
        channel = params[0]
 
261
        kicked = params[1]
 
262
        group = channelToGroupName(channel)
 
263
        if string.lower(kicked) == string.lower(self.nickname):
 
264
            # Yikes!
 
265
            if self.participants.has_key(nick):
 
266
                wordsname = " (%s)" % (self._getParticipant(nick).name,)
 
267
            else:
 
268
                wordsname = ''
 
269
            if len(params) > 2:
 
270
                reason = '  "%s"' % (params[2],)
 
271
            else:
 
272
                reason = ''
 
273
 
 
274
            self.groupMessage(group, '%s%s kicked me off!%s'
 
275
                              % (prefix, wordsname, reason))
 
276
            self.log("I've been kicked from %s: %s %s"
 
277
                     % (channel, prefix, params), 'NOTICE')
 
278
            self.evacuateGroup(group)
 
279
 
 
280
        else:
 
281
            try:
 
282
                self._getParticipant(kicked).leaveGroup(group)
 
283
            except wordsService.NotInGroupError:
 
284
                pass
 
285
 
 
286
    def irc_INVITE(self, prefix, params):
 
287
        """Accept an invitation, if it's in my groupList.
 
288
        """
 
289
        group = channelToGroupName(params[1])
 
290
        if group in self.groupList:
 
291
            self.log("I'm accepting the invitation to join %s from %s."
 
292
                     % (group, prefix), 'NOTICE')
 
293
            self.join(groupToChannelName(group))
 
294
 
 
295
    def irc_TOPIC(self, prefix, params):
 
296
        """Announce the new topic.
 
297
        """
 
298
        # XXX: words groups *do* have topics, but they're currently
 
299
        # not used.  Should we use them?
 
300
        nick = string.split(prefix,'!')[0]
 
301
        channel = params[0]
 
302
        topic = params[1]
 
303
        self.groupMessage(channelToGroupName(channel),
 
304
                          "%s has just decreed the topic to be: %s"
 
305
                          % (self._getParticipant(nick).name,
 
306
                             topic))
 
307
 
 
308
    def irc_ERR_BANNEDFROMCHAN(self, prefix, params):
 
309
        """When I can't get on a channel, report it.
 
310
        """
 
311
        self.log("Join failed: %s %s" % (prefix, params), 'NOTICE')
 
312
 
 
313
    irc_ERR_CHANNELISFULL = \
 
314
                          irc_ERR_UNAVAILRESOURCE = \
 
315
                          irc_ERR_INVITEONLYCHAN =\
 
316
                          irc_ERR_NOSUCHCHANNEL = \
 
317
                          irc_ERR_BADCHANNELKEY = irc_ERR_BANNEDFROMCHAN
 
318
 
 
319
    def irc_ERR_NOTREGISTERED(self, prefix, params):
 
320
        self.log("Got ERR_NOTREGISTERED, re-running connectionMade().",
 
321
                 'NOTICE')
 
322
        self.connectionMade()
 
323
 
 
324
 
 
325
    ### Client-To-Client-Protocol methods
 
326
 
 
327
    def ctcpQuery_DCC(self, user, channel, data):
 
328
        """Accept DCC handshakes, for passing on to others.
 
329
        """
 
330
        nick = string.split(user,"!")[0]
 
331
 
 
332
        # We're pretty lenient about what we pass on, but the existance
 
333
        # of at least four parameters (type, arg, host, port) is really
 
334
        # required.
 
335
        if len(string.split(data)) < 4:
 
336
            self.ctcpMakeReply(nick, [('ERRMSG',
 
337
                                       'DCC %s :Malformed DCC request.'
 
338
                                       % (data))])
 
339
            return
 
340
 
 
341
        dcc_text = irc.dccDescribe(data)
 
342
 
 
343
        self.notice(nick, "Got your DCC %s"
 
344
                    % (irc.dccDescribe(data),))
 
345
 
 
346
        pName = self._getParticipant(nick).name
 
347
        self.dcc_sessions[pName] = (user, dcc_text, data)
 
348
 
 
349
        self.notice(nick, "If I should pass it on to another user, "
 
350
                    "/msg %s DCC PASSTO theirNick" % (self.nickname,))
 
351
 
 
352
 
 
353
    ### IRCClient client event methods
 
354
 
 
355
    def signedOn(self):
 
356
        """Join my groupList once I've signed on.
 
357
        """
 
358
        self.log("Welcomed by IRC server.", 'info')
 
359
        for group in self.groupList:
 
360
            self.join(groupToChannelName(group))
 
361
 
 
362
    def privmsg(self, user, channel, message):
 
363
        """Dispatch privmsg as a groupMessage or a command, as appropriate.
 
364
        """
 
365
        nick = string.split(user,'!')[0]
 
366
        if nick == self.nickname:
 
367
            return
 
368
 
 
369
        if string.lower(channel) == string.lower(self.nickname):
 
370
            parts = string.split(message, ' ', 1)
 
371
            cmd = parts[0]
 
372
            if len(parts) > 1:
 
373
                remainder = parts[1]
 
374
            else:
 
375
                remainder = None
 
376
 
 
377
            method = getattr(self, "bot_%s" % cmd, None)
 
378
            if method is not None:
 
379
                method(user, remainder)
 
380
            else:
 
381
                self.botUnknown(user, channel, message)
 
382
        else:
 
383
            # The message isn't to me, so it must be to a group.
 
384
            group = channelToGroupName(channel)
 
385
            try:
 
386
                self._getParticipant(nick).groupMessage(group, message)
 
387
            except wordsService.NotInGroupError:
 
388
                self._getParticipant(nick).joinGroup(group)
 
389
                self._getParticipant(nick).groupMessage(group, message)
 
390
 
 
391
    def noticed(self, user, channel, message):
 
392
        """Pass channel notices on to the group.
 
393
        """
 
394
        nick = string.split(user,'!')[0]
 
395
        if nick == self.nickname:
 
396
            return
 
397
 
 
398
        if string.lower(channel) == string.lower(self.nickname):
 
399
            # A private notice is most likely an auto-response
 
400
            # from something else, or a message from an IRC service.
 
401
            # Don't treat at as a command.
 
402
            pass
 
403
        else:
 
404
            # The message isn't to me, so it must be to a group.
 
405
            group = channelToGroupName(channel)
 
406
            try:
 
407
                self._getParticipant(nick).groupMessage(group, message)
 
408
            except wordsService.NotInGroupError:
 
409
                self._getParticipant(nick).joinGroup(group)
 
410
                self._getParticipant(nick).groupMessage(group, message)
 
411
 
 
412
    def action(self, user, channel, message):
 
413
        """Speak about a participant in third-person.
 
414
        """
 
415
        group = channelToGroupName(channel)
 
416
        nick = string.split(user,'!',1)[0]
 
417
        try:
 
418
            self._getParticipant(nick).groupMessage(group, message,
 
419
                                                    {'style': 'emote'})
 
420
        except wordsService.NotInGroupError:
 
421
            self._getParticipant(nick).joinGroup(group)
 
422
            self._getParticipant(nick).groupMessage(group, message,
 
423
                                                    {'style': 'emote'})
 
424
 
 
425
    ### Bot event methods
 
426
 
 
427
    def bot_msg(self, sender, params):
 
428
        """Pass along a message as a directMessage to a words Participant
 
429
        """
 
430
        (nick, message) = string.split(params, ' ', 1)
 
431
        sender = string.split(sender, '!', 1)[0]
 
432
        try:
 
433
            self._getParticipant(sender).directMessage(nick, message)
 
434
        except wordsService.WordsError, e:
 
435
            self.notice(sender, "msg to %s failed: %s" % (nick, e))
 
436
 
 
437
    def bot_help(self, user, params):
 
438
        nick = string.split(user, '!', 1)[0]
 
439
        for l in self.helptext:
 
440
            self.notice(nick, l % {
 
441
                'myNick': self.nickname,
 
442
                'service': self.service.serviceName,
 
443
                })
 
444
 
 
445
    def botUnknown(self, user, channel, message):
 
446
        parts = string.split(message, ' ', 1)
 
447
        cmd = parts[0]
 
448
        if len(parts) > 1:
 
449
            remainder = parts[1]
 
450
        else:
 
451
            remainder = None
 
452
 
 
453
        if remainder is not None:
 
454
            # Default action is to try anything as a 'msg'
 
455
            # make sure the message is from a user and not a server.
 
456
            if ('!' in user) and ('@' in user):
 
457
                self.bot_msg(user, message)
 
458
        else:
 
459
            # But if the msg would be empty, don't that.
 
460
            # Just act confused.
 
461
            nick = string.split(user, '!', 1)[0]
 
462
            self.notice(nick, "I don't know what to do with '%s'.  "
 
463
                        "`/msg %s help` for help."
 
464
                        % (cmd, self.nickname))
 
465
 
 
466
    def bot_DCC(self, user, params):
 
467
        """Commands for brokering DCC handshakes.
 
468
 
 
469
        DCC -- I'll tell you if I'm holding a DCC request from you.
 
470
 
 
471
        DCC PASSTO nick -- give the DCC request you gave me to this nick.
 
472
 
 
473
        DCC FORGET -- forget any DCC requests you offered to me.
 
474
        """
 
475
        nick = string.split(user,"!")[0]
 
476
        pName = self._getParticipant(nick).name
 
477
 
 
478
        if not params:
 
479
            # Do I have a DCC from you?
 
480
            if self.dcc_sessions.has_key(pName):
 
481
                dcc_text = self.dcc_sessions[pName][1]
 
482
                self.notice(nick,
 
483
                            "I have an offer from you for DCC %s"
 
484
                            % (dcc_text,))
 
485
            else:
 
486
                self.notice(nick, "I have no DCC offer from you.")
 
487
            return
 
488
 
 
489
        params = string.split(params)
 
490
 
 
491
        if (params[0] == 'PASSTO') and (len(params) > 1):
 
492
            (cmd, dst) = params[:2]
 
493
            cmd = string.upper(cmd)
 
494
            if self.dcc_sessions.has_key(pName):
 
495
                (origUser, dcc_text, orig_data)=self.dcc_sessions[pName]
 
496
                if dcc_text:
 
497
                    dcc_text = " for " + dcc_text
 
498
                else:
 
499
                    dcc_text = ''
 
500
 
 
501
                ctcpMsg = irc.ctcpStringify([('DCC',orig_data)])
 
502
                try:
 
503
                    self._getParticipant(nick).directMessage(dst,
 
504
                                                            ctcpMsg)
 
505
                except wordsService.WordsError, e:
 
506
                    self.notice(nick, "DCC offer to %s failed: %s"
 
507
                                % (dst, e))
 
508
                else:
 
509
                    self.notice(nick, "DCC offer%s extended to %s."
 
510
                                % (dcc_text, dst))
 
511
                    del self.dcc_sessions[pName]
 
512
 
 
513
            else:
 
514
                self.notice(nick, "I don't have an active DCC"
 
515
                            " handshake from you.")
 
516
 
 
517
        elif params[0] == 'FORGET':
 
518
            if self.dcc_sessions.has_key(pName):
 
519
                del self.dcc_sessions[pName]
 
520
            self.notice(nick, "I have now forgotten any DCC offers"
 
521
                        " from you.")
 
522
        else:
 
523
            self.notice(nick,
 
524
                        "Valid DCC commands are: "
 
525
                        "DCC, DCC PASSTO <nick>, DCC FORGET")
 
526
        return
 
527
 
 
528
    ### WordsClient methods
 
529
    ##      Words.Group --> IRC
 
530
 
 
531
    def memberJoined(self, member, group):
 
532
        """Tell the IRC Channel when someone joins the Words group.
 
533
        """
 
534
        if (group == self.errorGroup) or self.isThisMine(member):
 
535
            return
 
536
        self.say(groupToChannelName(group), "%s joined." % (member,))
 
537
 
 
538
    def memberLeft(self, member, group):
 
539
        """Tell the IRC Channel when someone leaves the Words group.
 
540
        """
 
541
        if (group == self.errorGroup) or self.isThisMine(member):
 
542
            return
 
543
        self.say(groupToChannelName(group), "%s left." % (member,))
 
544
 
 
545
    def receiveGroupMessage(self, sender, group, message, metadata=None):
 
546
        """Pass a message from the Words group on to IRC.
 
547
 
 
548
        Or, if it's in our errorGroup, recognize some debugging commands.
 
549
        """
 
550
        if not (group == self.errorGroup):
 
551
            channel = groupToChannelName(group)
 
552
            if not self.isThisMine(sender):
 
553
                # Test for Special case:
 
554
                # got CTCP, probably through words.ircservice
 
555
                #      (you SUCK!)
 
556
                # ACTION is the only case we'll support here.
 
557
                if message[:8] == irc.X_DELIM + 'ACTION ':
 
558
                    c = irc.ctcpExtract(message)
 
559
                    for tag, data in c['extended']:
 
560
                        if tag == 'ACTION':
 
561
                            self.say(channel, "* %s %s" % (sender, data))
 
562
                        else:
 
563
                            # Not an action.  Repackage the chunk,
 
564
                            msg = "%(X)s%(tag)s %(data)s%(X)s" % {
 
565
                                'X': irc.X_DELIM,
 
566
                                'tag': tag,
 
567
                                'data': data
 
568
                                }
 
569
                            # ctcpQuote it to render it harmless,
 
570
                            msg = irc.ctcpQuote(msg)
 
571
                            # and let it continue on.
 
572
                            c['normal'].append(msg)
 
573
 
 
574
                    for msg in c['normal']:
 
575
                        self.say(channel, "<%s> %s" % (sender, msg))
 
576
                    return
 
577
 
 
578
                elif irc.X_DELIM in message:
 
579
                    message = irc.ctcpQuote(message)
 
580
 
 
581
                if metadata and metadata.has_key('style'):
 
582
                    if metadata['style'] == "emote":
 
583
                        self.say(channel, "* %s %s" % (sender, message))
 
584
                        return
 
585
 
 
586
                self.say(channel, "<%s> %s" % (sender, message))
 
587
        else:
 
588
            # A message in our errorGroup.
 
589
            if message == "participants":
 
590
                s = map(lambda i: str(i[0]), self.participants.values())
 
591
                s = string.join(s, ", ")
 
592
            elif message == "groups":
 
593
                s = map(str, self.perspective.groups)
 
594
                s = string.join(s, ", ")
 
595
            elif message == "transport":
 
596
                s = "%s connected: %s" %\
 
597
                    (self.transport, getattr(self.transport, "connected"))
 
598
            else:
 
599
                s = None
 
600
 
 
601
            if s:
 
602
                self.groupMessage(group, s)
 
603
 
 
604
 
 
605
    ### My methods as a Participant
 
606
    ### (Shortcuts for self.perspective.foo())
 
607
 
 
608
    def joinGroup(self, groupName):
 
609
        return self.perspective.joinGroup(groupName)
 
610
 
 
611
    def leaveGroup(self, groupName):
 
612
        return self.perspective.leaveGroup(groupName)
 
613
 
 
614
    def groupMessage(self, groupName, message):
 
615
        return self.perspective.groupMessage(groupName, message)
 
616
 
 
617
    def directMessage(self, recipientName, message):
 
618
        return self.perspective.directMessage(recipientName, message)
 
619
 
 
620
    ### My methods as a bogus perspective broker
 
621
    ### (Since I grab my perspective directly from the service, it hasn't
 
622
    ###  been issued by a Perspective Broker.)
 
623
 
 
624
    def attach(self):
 
625
        self.perspective.attached(self, None)
 
626
 
 
627
    def detach(self):
 
628
        """Pull everyone off Words, sign off, cut the IRC connection.
 
629
        """
 
630
        if not (self is getattr(self.perspective,'client')):
 
631
            # Not attached.
 
632
            return
 
633
 
 
634
        for g in self.perspective.groups:
 
635
            self.leaveGroup(g.name)
 
636
        for nick in self.participants.keys()[:]:
 
637
            self.logoutParticipant(nick)
 
638
        self.perspective.detached(self, None)
 
639
        if self.transport and getattr(self.transport, 'connected'):
 
640
            self.transport.loseConnection()
 
641
 
 
642
    ### Participant event methods
 
643
    ##      Words.Participant --> IRC
 
644
 
 
645
    def msgFromWords(self, toNick, sender, message, metadata=None):
 
646
        """Deliver a directMessage as a privmsg over IRC.
 
647
        """
 
648
        if message[0] != irc.X_DELIM:
 
649
            if metadata and metadata.has_key('style'):
 
650
                # Damn.  What am I supposed to do with this?
 
651
                message = "[%s] %s" % (metadata['style'], message)
 
652
 
 
653
            self.msg(toNick, '<%s> %s' % (sender, message))
 
654
        else:
 
655
            # If there is a CTCP delimeter at the beginning of the
 
656
            # message, let's leave it there to accomidate not-so-
 
657
            # tolerant clients.
 
658
            dcc_data = None
 
659
            if message[1:5] == 'DCC ':
 
660
                dcc_query = irc.ctcpExtract(message)['extended'][0]
 
661
                dcc_data = dcc_query[1]
 
662
 
 
663
            if dcc_data:
 
664
                desc = "DCC " + irc.dccDescribe(dcc_data)
 
665
            else:
 
666
                desc = "CTCP request"
 
667
 
 
668
            self.msg(toNick, 'The following %s is from %s'
 
669
                     % (desc, sender))
 
670
            self.msg(toNick, '%s' % (message,))
 
671
 
 
672
 
 
673
    # IRC Participant Management
 
674
 
 
675
    def evacuateGroup(self, groupName):
 
676
        """Pull all of my Participants out of this group.
 
677
        """
 
678
        # XXX: This marks another place where we get a little
 
679
        # overly cozy with the words service.
 
680
        group = self.service.getGroup(groupName)
 
681
 
 
682
        allMyMembers = map(lambda m: m[0], self.participants.values())
 
683
        groupMembers = filter(lambda m, a=allMyMembers: m in a,
 
684
                              group.members)
 
685
 
 
686
        for m in groupMembers:
 
687
            m.leaveGroup(groupName)
 
688
 
 
689
    def _getParticipant(self, nick):
 
690
        """Get a Perspective (words.service.Participant) for a IRC user.
 
691
 
 
692
        And if I don't have one around, I'll make one.
 
693
        """
 
694
        if not self.participants.has_key(nick):
 
695
            self._newParticipant(nick)
 
696
 
 
697
        return self.participants[nick][0]
 
698
 
 
699
    def _getClient(self, nick):
 
700
        if not self.participants.has_key(nick):
 
701
            self._newParticipant(nick)
 
702
        return self.participants[nick][1]
 
703
 
 
704
    # TODO: let IRC users authorize themselves and then give them a
 
705
    # *real* perspective (one attached to their identity) instead
 
706
    # of one of my @networkSuffix-Nobody perspectives.
 
707
 
 
708
    def _newParticipant(self, nick):
 
709
        try:
 
710
            p = self.service.getPerspectiveNamed(nick +
 
711
                                                 self.networkSuffix)
 
712
        except wordsService.UserNonexistantError:
 
713
            p = self.service.createParticipant(nick + self.networkSuffix)
 
714
            if not p:
 
715
                raise wordsService.wordsError("Eeek!  Couldn't get OR "
 
716
                                              "make a perspective for "
 
717
                                              "'%s%s'." %
 
718
                                              (nick, self.networkSuffix))
 
719
 
 
720
        c = ProxiedParticipant(self, nick)
 
721
        p.attached(c, None)
 
722
 
 
723
        self.participants[nick] = [p, c]
 
724
 
 
725
    def changeParticipantNick(self, old_nick, new_nick):
 
726
        if not self.participants.has_key(old_nick):
 
727
            return
 
728
 
 
729
        (p, c) = self.participants[old_nick]
 
730
        c.setNick(new_nick)
 
731
 
 
732
        self.participants[new_nick] = self.participants[old_nick]
 
733
        del self.participants[old_nick]
 
734
 
 
735
    def logoutParticipant(self, nick):
 
736
        if not self.participants.has_key(nick):
 
737
            return
 
738
 
 
739
        (p, c) = self.participants[nick]
 
740
        p.detached(c, None)
 
741
        c.tendril = None
 
742
        # XXX: This must change if we ever start giving people 'real'
 
743
        #    perspectives!
 
744
        if not p.identityName:
 
745
            del self.service.participants[p.name]
 
746
        del self.participants[nick]
 
747
 
 
748
    def isThisMine(self, sender):
 
749
        """Returns true if 'sender' is the name of a perspective I'm providing.
 
750
        """
 
751
        if self.perspectiveName == sender:
 
752
            return "That's ME!"
 
753
 
 
754
        for (p, c) in self.participants.values():
 
755
            if p.name == sender:
 
756
                return 1
 
757
        return 0
 
758
 
 
759
 
 
760
    ### Utility
 
761
 
 
762
    def log(self, message, priority=None):
 
763
        """I need to give Twisted a prioritized logging facility one of these days.
 
764
        """
 
765
        if _LOGALL:
 
766
            log.msg(message)
 
767
        elif not (priority in ('dump',)):
 
768
            log.msg(message)
 
769
 
 
770
        if priority in ('info', 'NOTICE', 'ERROR'):
 
771
            self.groupMessage(self.errorGroup, message)
 
772
 
 
773
 
 
774
def channelToGroupName(channelName):
 
775
    """Map an IRC channel name to a Words group name.
 
776
 
 
777
    IRC is case-insensitive, words is not.  Arbitrtarily decree that all
 
778
    IRC channels should be lowercase.
 
779
 
 
780
    Warning: This prevents me from relaying text from IRC to
 
781
    a mixed-case Words group.  That is, any words group I'm
 
782
    in should have an all-lowercase name.
 
783
    """
 
784
 
 
785
    # Normalize case and trim leading '#'
 
786
    groupName = string.lower(channelName[1:])
 
787
    return groupName
 
788
 
 
789
def groupToChannelName(groupName):
 
790
    # Don't add a "#" here, because we do so in the outgoing IRC methods.
 
791
    channelName = groupName
 
792
    return channelName