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.
7
"""Tendril between Words and IRC servers.
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.
15
How to Start a Tendril
16
======================
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
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)
31
Stability: No more stable than L{words<twisted.words.service>}.
33
Future plans: Use \"L{Policy<twisted.words.service.Policy>}\" to get
36
@author: U{Kevin Turner<acapnotic@twistedmatrix.com>}
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
48
wordsService = service
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
63
class TendrilFactory(protocol.ReconnectingClientFactory, reflect.Accessor):
64
"""I build Tendril clients for a words service.
66
All of a tendril's configurable state is stored here with me.
74
perspectiveName = None
76
protocol = None # will be set to TendrilIRC as soon as it's defined.
77
_groupList = ['tendril_test']
78
_errorGroup = 'TendrilErrors'
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!",
86
def __init__(self, service):
87
"""Initialize this factory with a words service."""
88
self.reallySet('wordsService', service)
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)
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))
102
proto = protocol.ClientFactory.buildProtocol(self, addr)
103
self.wordsclient.setIrc(proto)
106
## self.protocol.__dict__.update(self.getConfiguration())
107
for k in ('nickname', 'helptext'):
108
setattr(proto, k, getattr(self, k))
112
def __getstate__(self):
113
state = self.__dict__.copy()
115
del state["wordsclient"]
120
def set_wordsService(self, service):
121
raise TypeError, "%s.wordsService is a read-only attribute." % (repr(self),)
123
def set_groupList(self, groupList):
125
oldlist = self.wordsclient.groupList
126
if groupList != oldlist:
127
newgroups = filter(lambda g, ol=oldlist: g not in ol,
129
deadgroups = filter(lambda o, gl=groupList: o not in gl,
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
140
def get_groupList(self):
142
return self.wordsclient.groupList
144
return self._groupList
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)
151
def set_errorGroup(self, errorGroup):
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
160
def get_errorGroup(self):
162
return self.wordsclient.errorGroup
164
return self._errorGroup
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)
174
class ProxiedParticipant(wordsService.WordsClient,
176
"""I'm the client of a participant who is connected through Tendril.
182
def __init__(self, tendril, nickname):
183
self.tendril = tendril
184
self.nickname = nickname
186
def setNick(self, nickname):
187
self.nickname = nickname
189
def receiveDirectMessage(self, sender, message, metadata=None):
190
"""Pass this message through tendril to my IRC counterpart.
192
self.tendril.msgFromWords(self.nickname,
193
sender, message, metadata)
196
class TendrilIRC(irc.IRCClient, styles.Ephemeral):
197
"""I connect to the IRC server and broker traffic.
201
versionName = 'Tendril'
202
versionNum = '$Revision: 1.32 $'[11:-2]
203
versionEnv = copyright.longversion
205
helptext = TendrilFactory.helptext
210
"""Create a new Tendril IRC client."""
211
self.dcc_sessions = {}
213
### Protocol-level methods
215
def connectionLost(self, reason):
216
"""When I lose a connection, log out all my IRC participants.
218
self.log("%s: Connection lost: %s" % (self.transport, reason), 'info')
219
self.words.ircConnectionLost()
221
### Protocol LineReceiver-level methods
223
def lineReceived(self, line):
225
irc.IRCClient.lineReceived(self, line)
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.
233
def sendLine(self, line):
234
"""Send a line through my transport, unless my transport isn't up.
236
if (not self.transport) or (not self.transport.connected):
239
self.log(line, 'dump')
240
irc.IRCClient.sendLine(self, line)
242
### Protocol IRCClient server->client methods
244
def irc_JOIN(self, prefix, params):
245
"""Join IRC user to the corresponding group.
247
nick = string.split(prefix,'!')[0]
248
groupName = channelToGroupName(params[0])
249
if nick == self.nickname:
250
self.words.joinGroup(groupName)
252
self.words._getParticipant(nick).joinGroup(groupName)
254
def irc_NICK(self, prefix, params):
255
"""When an IRC user changes their nickname
257
this does *not* change the name of their perspectivee, just my
258
nickname->perspective and client->nickname mappings.
260
old_nick = string.split(prefix,'!')[0]
262
if old_nick == self.nickname:
263
self.nickname = new_nick
265
self.words.changeParticipantNick(old_nick, new_nick)
267
def irc_PART(self, prefix, params):
268
"""Parting IRC members leave the correspoding group.
270
nick = string.split(prefix,'!')[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)
279
self.words.ircPartParticipant(nick, groupName)
281
def irc_QUIT(self, prefix, params):
282
"""When a user quits IRC, log out their participant.
284
nick = string.split(prefix,'!')[0]
285
if nick == self.nickname:
288
self.words.logoutParticipant(nick)
290
def irc_KICK(self, prefix, params):
291
"""Kicked? Who? Not me, I hope.
293
nick = string.split(prefix,'!')[0]
296
group = channelToGroupName(channel)
297
if string.lower(kicked) == string.lower(self.nickname):
299
if self.words.participants.has_key(nick):
300
wordsname = " (%s)" % (self.words._getParticipant(nick).name,)
304
reason = ' "%s"' % (params[2],)
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)
315
self.words.ircPartParticipant(kicked, group)
317
def irc_INVITE(self, prefix, params):
318
"""Accept an invitation, if it's in my groupList.
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))
326
def irc_TOPIC(self, prefix, params):
327
"""Announce the new topic.
329
# XXX: words groups *do* have topics, but they're currently
330
# not used. Should we use them?
331
nick = string.split(prefix,'!')[0]
334
self.words.groupMessage(channelToGroupName(channel),
335
"%s has just decreed the topic to be: %s"
336
% (self.words._getParticipant(nick).name,
339
def irc_ERR_BANNEDFROMCHAN(self, prefix, params):
340
"""When I can't get on a channel, report it.
342
self.log("Join failed: %s %s" % (prefix, params), 'NOTICE')
344
irc_ERR_CHANNELISFULL = \
345
irc_ERR_UNAVAILRESOURCE = \
346
irc_ERR_INVITEONLYCHAN =\
347
irc_ERR_NOSUCHCHANNEL = \
348
irc_ERR_BADCHANNELKEY = irc_ERR_BANNEDFROMCHAN
350
def irc_ERR_NOTREGISTERED(self, prefix, params):
351
self.log("Got ERR_NOTREGISTERED, re-running connectionMade().",
353
self.connectionMade()
356
### Client-To-Client-Protocol methods
358
def ctcpQuery_DCC(self, user, channel, data):
359
"""Accept DCC handshakes, for passing on to others.
361
nick = string.split(user,"!")[0]
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
366
if len(string.split(data)) < 4:
367
self.ctcpMakeReply(nick, [('ERRMSG',
368
'DCC %s :Malformed DCC request.'
372
dcc_text = irc.dccDescribe(data)
374
self.notice(nick, "Got your DCC %s"
375
% (irc.dccDescribe(data),))
377
pName = self.words._getParticipant(nick).name
378
self.dcc_sessions[pName] = (user, dcc_text, data)
380
self.notice(nick, "If I should pass it on to another user, "
381
"/msg %s DCC PASSTO theirNick" % (self.nickname,))
384
### IRCClient client event methods
387
"""Join my groupList once I've signed on.
389
self.log("Welcomed by IRC server.", 'info')
390
self.factory.resetDelay()
391
for group in self.words.groupList:
392
self.join(groupToChannelName(group))
394
def privmsg(self, user, channel, message):
395
"""Dispatch privmsg as a groupMessage or a command, as appropriate.
397
nick = string.split(user,'!')[0]
398
if nick == self.nickname:
401
if string.lower(channel) == string.lower(self.nickname):
402
parts = string.split(message, ' ', 1)
409
method = getattr(self, "bot_%s" % cmd, None)
410
if method is not None:
411
method(user, remainder)
413
self.botUnknown(user, channel, message)
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)
419
def noticed(self, user, channel, message):
420
"""Pass channel notices on to the group.
422
nick = string.split(user,'!')[0]
423
if nick == self.nickname:
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.
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)
436
def action(self, user, channel, message):
437
"""Speak about a participant in third-person.
439
group = channelToGroupName(channel)
440
nick = string.split(user,'!',1)[0]
441
self.words.ircParticipantMsg(nick, group, message, emote=True)
443
### Bot event methods
445
def bot_msg(self, sender, params):
446
"""Pass along a message as a directMessage to a words Participant
448
(nick, message) = string.split(params, ' ', 1)
449
sender = string.split(sender, '!', 1)[0]
451
self.words._getParticipant(sender).directMessage(nick, message)
452
except wordsService.WordsError, e:
453
self.notice(sender, "msg to %s failed: %s" % (nick, e))
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,
463
def botUnknown(self, user, channel, message):
464
parts = string.split(message, ' ', 1)
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)
477
# But if the msg would be empty, don't that.
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))
484
def bot_DCC(self, user, params):
485
"""Commands for brokering DCC handshakes.
487
DCC -- I'll tell you if I'm holding a DCC request from you.
489
DCC PASSTO nick -- give the DCC request you gave me to this nick.
491
DCC FORGET -- forget any DCC requests you offered to me.
493
nick = string.split(user,"!")[0]
494
pName = self.words._getParticipant(nick).name
497
# Do I have a DCC from you?
498
if self.dcc_sessions.has_key(pName):
499
dcc_text = self.dcc_sessions[pName][1]
501
"I have an offer from you for DCC %s"
504
self.notice(nick, "I have no DCC offer from you.")
507
params = string.split(params)
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]
515
dcc_text = " for " + dcc_text
519
ctcpMsg = irc.ctcpStringify([('DCC',orig_data)])
521
self.words._getParticipant(nick).directMessage(dst,
523
except wordsService.WordsError, e:
524
self.notice(nick, "DCC offer to %s failed: %s"
527
self.notice(nick, "DCC offer%s extended to %s."
529
del self.dcc_sessions[pName]
532
self.notice(nick, "I don't have an active DCC"
533
" handshake from you.")
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"
542
"Valid DCC commands are: "
543
"DCC, DCC PASSTO <nick>, DCC FORGET")
549
def log(self, message, priority=None):
550
"""I need to give Twisted a prioritized logging facility one of these days.
554
elif not (priority in ('dump',)):
557
if priority in ('info', 'NOTICE', 'ERROR'):
558
self.words.groupMessage(self.words.errorGroup, message)
560
TendrilFactory.protocol = TendrilIRC
562
class TendrilWords(wordsService.WordsClient):
564
networkSuffix = '@opn'
565
perspectiveName = nickname + networkSuffix
570
def __init__(self, service, ircFactory,
571
nickname=None, networkSuffix=None, perspectiveName=None,
572
groupList=None, errorGroup=None):
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
579
groupList -- a list of strings naming groups on the Words
580
service to join and bridge to their counterparts on the IRC
583
nickname -- a string to use as my nickname on the IRC network.
585
networkSuffix -- a string to append to the nickname of the
586
Participants I bring in through IRC, e.g. \"@opn\".
588
perspectiveName -- the name of my perspective with this
589
service. Defaults to nickname + networkSuffix.
591
self.service = service
592
self.ircFactory = ircFactory
593
self.participants = {}
596
self.nickname = nickname
598
self.networkSuffix = networkSuffix
601
self.perspectiveName = perspectiveName
603
self.perspectiveName = self.nickname + self.networkSuffix
606
self.groupList = groupList
608
# Copy the class default's list so as to not modify the original.
609
self.groupList = self.groupList[:]
612
self.errorGroup = errorGroup
616
def setIrc(self, ircProtocol):
617
self.irc = ircProtocol
618
self.irc.realname = 'Tendril to %s' % (self.service.serviceName,)
619
self.irc.words = self
621
def setupBot(self, perspective):
622
self.perspective = perspective
623
self.joinGroup(self.errorGroup)
625
def attachToWords(self):
626
"""Get my perspective on the Words service; attach as a client.
629
self.service.addBot(self.perspectiveName, self)
631
# XXX: Decide how much of this code belongs in words..Service.addBot
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"
642
# self.perspectiveName))
644
# if self.perspective.client is self:
645
# log.msg("I seem to be already attached.")
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)
661
### WordsClient methods
662
## Words.Group --> IRC
664
def memberJoined(self, member, group):
665
"""Tell the IRC Channel when someone joins the Words group.
667
if (group == self.errorGroup) or self.isThisMine(member):
669
self.irc.say(groupToChannelName(group), "%s joined." % (member,))
671
def memberLeft(self, member, group):
672
"""Tell the IRC Channel when someone leaves the Words group.
674
if (group == self.errorGroup) or self.isThisMine(member):
676
self.irc.say(groupToChannelName(group), "%s left." % (member,))
678
def receiveGroupMessage(self, sender, group, message, metadata=None):
679
"""Pass a message from the Words group on to IRC.
681
Or, if it's in our errorGroup, recognize some debugging commands.
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
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']:
694
self.irc.say(channel, "* %s %s" % (sender, data))
696
# Not an action. Repackage the chunk,
697
msg = "%(X)s%(tag)s %(data)s%(X)s" % {
702
# ctcpQuote it to render it harmless,
703
msg = irc.ctcpQuote(msg)
704
# and let it continue on.
705
c['normal'].append(msg)
707
for msg in c['normal']:
708
self.irc.say(channel, "<%s> %s" % (sender, msg))
711
elif irc.X_DELIM in message:
712
message = irc.ctcpQuote(message)
714
if metadata and metadata.has_key('style'):
715
if metadata['style'] == "emote":
716
self.irc.say(channel, "* %s %s" % (sender, message))
719
self.irc.say(channel, "<%s> %s" % (sender, message))
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"))
735
self.groupMessage(group, s)
738
### My methods as a Participant
739
### (Shortcuts for self.perspective.foo())
741
def joinGroup(self, groupName):
742
return self.perspective.joinGroup(groupName)
744
def leaveGroup(self, groupName):
745
return self.perspective.leaveGroup(groupName)
747
def groupMessage(self, groupName, message):
748
return self.perspective.groupMessage(groupName, message)
750
def directMessage(self, recipientName, message):
751
return self.perspective.directMessage(recipientName, message)
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.)
758
self.perspective.attached(self, None)
761
"""Pull everyone off Words, sign off, cut the IRC connection.
763
if not (self is getattr(self.perspective,'client')):
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()
778
### Participant event methods
779
## Words.Participant --> IRC
781
def msgFromWords(self, toNick, sender, message, metadata=None):
782
"""Deliver a directMessage as a privmsg over IRC.
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)
789
self.irc.msg(toNick, '<%s> %s' % (sender, message))
791
# If there is a CTCP delimeter at the beginning of the
792
# message, let's leave it there to accomidate not-so-
795
if message[1:5] == 'DCC ':
796
dcc_query = irc.ctcpExtract(message)['extended'][0]
797
dcc_data = dcc_query[1]
800
desc = "DCC " + irc.dccDescribe(dcc_data)
802
desc = "CTCP request"
804
self.irc.msg(toNick, 'The following %s is from %s'
806
self.irc.msg(toNick, '%s' % (message,))
809
# IRC Participant Management
811
def ircConnectionLost(self):
812
for nick in self.participants.keys()[:]:
813
self.logoutParticipant(nick)
815
def ircPartParticipant(self, nick, groupName):
816
participant = self._getParticipant(nick)
818
participant.leaveGroup(groupName)
819
except wordsService.NotInGroupError:
822
if not participant.groups:
823
self.logoutParticipant(nick)
825
def ircParticipantMsg(self, nick, groupName, message, emote=False):
826
participant = self._getParticipant(nick)
828
metadata = {'style': 'emote'}
832
participant.groupMessage(groupName, message, metadata)
833
except wordsService.NotInGroupError:
834
participant.joinGroup(groupName)
835
participant.groupMessage(groupName, message, metadata)
837
def evacuateGroup(self, groupName):
838
"""Pull all of my Participants out of this group.
840
# XXX: This marks another place where we get a little
841
# overly cozy with the words service.
842
group = self.service.getGroup(groupName)
844
allMyMembers = map(lambda m: m[0], self.participants.values())
845
groupMembers = filter(lambda m, a=allMyMembers: m in a,
848
for m in groupMembers:
849
m.leaveGroup(groupName)
851
def _getParticipant(self, nick):
852
"""Get a Perspective (words.service.Participant) for a IRC user.
854
And if I don't have one around, I'll make one.
856
if not self.participants.has_key(nick):
857
self._newParticipant(nick)
859
return self.participants[nick][0]
861
def _getClient(self, nick):
862
if not self.participants.has_key(nick):
863
self._newParticipant(nick)
864
return self.participants[nick][1]
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.
870
def _newParticipant(self, nick):
872
p = self.service.getPerspectiveNamed(nick + self.networkSuffix)
873
except wordsService.UserNonexistantError:
874
p = self.service.createParticipant(nick + self.networkSuffix)
876
raise wordsService.wordsError("Eeek! Couldn't get OR "
877
"make a perspective for "
879
(nick, self.networkSuffix))
881
c = ProxiedParticipant(self, nick)
882
p.attached(LocalAsyncForwarder(c, wordsService.IWordsClient, 1),
884
# p.attached(c, None)
886
self.participants[nick] = [p, c]
888
def changeParticipantNick(self, old_nick, new_nick):
889
if not self.participants.has_key(old_nick):
892
(p, c) = self.participants[old_nick]
895
self.participants[new_nick] = self.participants[old_nick]
896
del self.participants[old_nick]
898
def logoutParticipant(self, nick):
899
if not self.participants.has_key(nick):
902
(p, c) = self.participants[nick]
905
# XXX: This must change if we ever start giving people 'real'
907
if not p.identityName:
908
self.service.uncachePerspective(p)
909
del self.participants[nick]
911
def isThisMine(self, sender):
912
"""Returns true if 'sender' is the name of a perspective I'm providing.
914
if self.perspectiveName == sender:
917
for (p, c) in self.participants.values():
923
def channelToGroupName(channelName):
924
"""Map an IRC channel name to a Words group name.
926
IRC is case-insensitive, words is not. Arbitrtarily decree that all
927
IRC channels should be lowercase.
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.
934
# Normalize case and trim leading '#'
935
groupName = string.lower(channelName[1:])
938
def groupToChannelName(groupName):
939
# Don't add a "#" here, because we do so in the outgoing IRC methods.
940
channelName = groupName