diff --git a/astron/dclass/united.dc b/astron/dclass/united.dc index 3f3ee41c..655b2477 100644 --- a/astron/dclass/united.dc +++ b/astron/dclass/united.dc @@ -255,8 +255,10 @@ dclass MagicWordManager : DistributedObject { }; dclass ChatAgent : DistributedObject { - adminChat(uint32, string); - chatMessage(string(0-256)) clsend; + adminChat(uint32 aboutId, string message); + chatMessage(string(0-256) message, uint8 chatMode) clsend; + whisperMessage(uint32 receiverAvId, string(0-256) message) clsend; + sfWhisperMessage(uint32 receiverAvId, string(0-256) message) clsend; }; dclass FriendManager : DistributedObject { diff --git a/otp/chat/ChatAgent.py b/otp/chat/ChatAgent.py index 1b9f8889..32a84a24 100644 --- a/otp/chat/ChatAgent.py +++ b/otp/chat/ChatAgent.py @@ -1,14 +1,17 @@ from direct.distributed.DistributedObjectGlobal import DistributedObjectGlobal from pandac.PandaModules import * from otp.otpbase import OTPGlobals +from otp.ai.MagicWordGlobal import * class ChatAgent(DistributedObjectGlobal): def __init__(self, cr): DistributedObjectGlobal.__init__(self, cr) + self.chatMode = 0 def delete(self): self.ignoreAll() self.cr.chatManager = None + self.cr.chatAgent = None DistributedObjectGlobal.delete(self) return @@ -17,4 +20,35 @@ class ChatAgent(DistributedObjectGlobal): messenger.send('adminChat', [aboutId, message]) def sendChatMessage(self, message): - self.sendUpdate('chatMessage', [message]) + self.sendUpdate('chatMessage', [message, self.chatMode]) + + def sendWhisperMessage(self, receiverAvId, message): + self.sendUpdate('whisperMessage', [receiverAvId, message]) + + def sendSFWhisperMessage(self, receiverAvId, message): + self.sendUpdate('sfWhisperMessage', [receiverAvId, message]) + +@magicWord(category=CATEGORY_MODERATOR, types=[int]) +def chatmode(mode=-1): + """ Set the chat mode of the current avatar. """ + mode2name = { + 0 : "user", + 1 : "moderator", + 2 : "administrator", + 3 : "system administrator", + } + if base.cr.chatAgent is None: + return "No ChatAgent found." + if mode == -1: + return "You are currently talking in the %s chat mode." % mode2name.get(base.cr.chatAgent.chatMode, "N/A") + if not 0 <= mode <= 3: + return "Invalid chat mode specified." + if mode == 3 and spellbook.getInvoker().getAdminAccess() < 500: + return "Chat mode 3 is reserved for system administrators." + if mode == 2 and spellbook.getInvoker().getAdminAccess() < 400: + return "Chat mode 2 is reserved for administrators." + if mode == 1 and spellbook.getInvoker().getAdminAccess() < 200: + # Like this will ever happen, but whatever. + return "Chat mode 1 is reserved for moderators." + base.cr.chatAgent.chatMode = mode + return "You are now talking in the %s chat mode." % mode2name.get(mode, "N/A") diff --git a/otp/chat/ChatAgentUD.py b/otp/chat/ChatAgentUD.py index b90b782d..389678fe 100644 --- a/otp/chat/ChatAgentUD.py +++ b/otp/chat/ChatAgentUD.py @@ -2,41 +2,151 @@ from direct.directnotify import DirectNotifyGlobal from direct.distributed.DistributedObjectGlobalUD import DistributedObjectGlobalUD # TODO: OTP should not depend on Toontown... Hrrm. from toontown.chat.TTWhiteList import TTWhiteList +from toontown.chat.TTSequenceList import TTSequenceList +from otp.distributed import OtpDoGlobals +import time class ChatAgentUD(DistributedObjectGlobalUD): notify = DirectNotifyGlobal.directNotify.newCategory("ChatAgentUD") def announceGenerate(self): DistributedObjectGlobalUD.announceGenerate(self) + self.wantBlacklistSequence = config.GetBool('want-blacklist-sequence', True) + self.wantWhitelist = config.GetBool('want-whitelist', True) + if self.wantWhitelist: + self.whiteList = TTWhiteList() + if self.wantBlacklistSequence: + self.sequenceList = TTSequenceList() + self.chatMode2channel = { + 1 : OtpDoGlobals.OTP_MOD_CHANNEL, + 2 : OtpDoGlobals.OTP_ADMIN_CHANNEL, + 3 : OtpDoGlobals.OTP_SYSADMIN_CHANNEL, + } + self.chatMode2prefix = { + 1 : "[MOD] ", + 2 : "[ADMIN] ", + 3 : "[SYSADMIN] ", + } + + self.muted = {} - self.whiteList = TTWhiteList() + def muteAccount(self, account, howLong): + print ['muteAccount', account, howLong] + self.muted[account] = int(time.time()/60) + howLong - def chatMessage(self, message): + def unmuteAccount(self, account): + print ['unmuteAccount', account] + if account in self.muted: + del self.muted[account] + # Open chat + def chatMessage(self, message, chatMode): sender = self.air.getAvatarIdFromSender() if sender == 0: - self.air.writeServerEvent('suspicious', self.air.getAccountIdFromSender(), - 'Account sent chat without an avatar', message) + self.air.writeServerEvent('suspicious', accId=self.air.getAccountIdFromSender(), + issue='Account sent chat without an avatar', message=message) + return + + if sender in self.muted and int(time.time()/60) < self.muted[sender]: + return + + if self.wantWhitelist: + cleanMessage, modifications = self.cleanWhitelist(message) + else: + cleanMessage, modifications = message, [] + self.air.writeServerEvent('chat-said', avId=sender, chatMode=chatMode, msg=message, cleanMsg=cleanMessage) + + # TODO: The above is probably a little too ugly for my taste... Maybe AIR + # should be given an API for sending updates for unknown objects? + if chatMode != 0: + # Staff messages do not need to be cleaned. [TODO: Blacklist this?] + if message.startswith('.'): + # This is a thought bubble, move the point to the start. + cleanMessage = '.' + self.chatMode2prefix.get(chatMode, "") + message[1:] + else: + cleanMessage = self.chatMode2prefix.get(chatMode, "") + message + modifications = [] + DistributedAvatar = self.air.dclassesByName['DistributedAvatarUD'] + dg = DistributedAvatar.aiFormatUpdate('setTalk', sender, self.chatMode2channel.get(chatMode, sender), + self.air.ourChannel, + [0, 0, '', cleanMessage, modifications, 0]) + self.air.send(dg) + self.air.csm.accountDB.persistChat(sender, message, self.air.ourChannel) + + # Regular filtered chat + def whisperMessage(self, receiverAvId, message): + sender = self.air.getAvatarIdFromSender() + if sender == 0: + self.air.writeServerEvent('suspicious', accId=self.air.getAccountIdFromSender(), + issue='Account sent chat without an avatar', message=message) return + cleanMessage, modifications = self.cleanWhitelist(message) + # Maybe a better "cleaner" way of doing this, but it works + self.air.writeServerEvent('whisper-said', avId=sender, reciever=receiverAvId, msg=message, cleanMsg=cleanMessage) + DistributedAvatar = self.air.dclassesByName['DistributedAvatarUD'] + dg = DistributedAvatar.aiFormatUpdate('setTalkWhisper', receiverAvId, receiverAvId, self.air.ourChannel, + [sender, sender, '', cleanMessage, modifications, 0]) + self.air.send(dg) + + # True friend unfiltered chat + def sfWhisperMessage(self, receiverAvId, message): + sender = self.air.getAvatarIdFromSender() + if sender == 0: + self.air.writeServerEvent('suspicious', accId=self.air.getAccountIdFromSender(), + issue='Account sent chat without an avatar', message=message) + return + + cleanMessage = self.cleanBlacklist(message) + + self.air.writeServerEvent('sf-whisper-said', avId=sender, reciever=receiverAvId, msg=message, cleanMsg=cleanMessage) + DistributedAvatar = self.air.dclassesByName['DistributedAvatarUD'] + dg = DistributedAvatar.aiFormatUpdate('setTalkWhisper', receiverAvId, receiverAvId, self.air.ourChannel, + [sender, sender, '', cleanMessage, [], 0]) + self.air.send(dg) + + # Filter the chat message + def cleanWhitelist(self, message): modifications = [] words = message.split(' ') offset = 0 - WantWhitelist = config.GetBool('want-whitelist', 1) for word in words: - if word and not self.whiteList.isWord(word) and WantWhitelist: + if word and not self.whiteList.isWord(word): modifications.append((offset, offset+len(word)-1)) offset += len(word) + 1 cleanMessage = message + if self.wantBlacklistSequence: + modifications += self.cleanSequences(cleanMessage) + for modStart, modStop in modifications: - cleanMessage = cleanMessage[:modStart] + '*'*(modStop-modStart+1) + cleanMessage[modStop+1:] + # Traverse through modification list and replace the characters of non-whitelisted words and/or blacklisted sequences with asterisks. + cleanMessage = cleanMessage[:modStart] + '*' * (modStop - modStart + 1) + cleanMessage[modStop + 1:] - self.air.writeServerEvent('chat-said', sender, message, cleanMessage) + return (cleanMessage, modifications) - # TODO: The above is probably a little too ugly for my taste... Maybe AIR - # should be given an API for sending updates for unknown objects? - DistributedAvatar = self.air.dclassesByName['DistributedAvatarUD'] - dg = DistributedAvatar.aiFormatUpdate('setTalk', sender, sender, - self.air.ourChannel, - [0, 0, '', cleanMessage, modifications, 0]) - self.air.send(dg) \ No newline at end of file + # Check the black list for black-listed words + def cleanBlacklist(self, message): + # We don't have a black list so we just return the full message + return message + + # Check for black-listed word sequences and scrub accordingly. + def cleanSequences(self, message): + modifications = [] + offset = 0 + words = message.split() + for wordit in xrange(len(words)): + word = words[wordit].lower() + seqlist = self.sequenceList.getList(word) + if len(seqlist) > 0: + for seqit in xrange(len(seqlist)): + sequence = seqlist[seqit] + splitseq = sequence.split() + if len(words) - (wordit + 1) >= len(splitseq): + cmplist = words[wordit + 1:] + del cmplist[len(splitseq):] + cmplist = [word.lower() for word in cmplist] + if cmp(cmplist, splitseq) == 0: + modifications.append((offset, offset + len(word) + len(sequence) - 1)) + offset += len(word) + 1 + + return modifications diff --git a/otp/chat/SequenceList.py b/otp/chat/SequenceList.py new file mode 100644 index 00000000..0021efd3 --- /dev/null +++ b/otp/chat/SequenceList.py @@ -0,0 +1,15 @@ +class SequenceList: + + def __init__(self, wordlist): + self.list = {} + for line in wordlist: + if line is '': + continue + split = line.split(':') + self.list[split[0].lower()] = [word.rstrip('\r\n').lower() for word in split[1].split(',')] + + def getList(self, word): + if word in self.list: + return self.list[word] + else: + return [] \ No newline at end of file diff --git a/otp/chat/WhiteList.py b/otp/chat/WhiteList.py index 8be965a6..0011183e 100644 --- a/otp/chat/WhiteList.py +++ b/otp/chat/WhiteList.py @@ -1,22 +1,55 @@ from bisect import bisect_left +import string +import sys +import os class WhiteList: - def __init__(self, words): - self.words = words + + def __init__(self, wordlist): + self.words = [] + for line in wordlist: + self.words.append(line.strip('\n\r').lower()) + + self.words.sort() self.numWords = len(self.words) def cleanText(self, text): text = text.strip('.,?!') - return text.lower() + text = text.lower() + return text def isWord(self, text): - return self.cleanText(text) in self.words + try: + text = self.cleanText(text) + i = bisect_left(self.words, text) + if i == self.numWords: + return False + return self.words[i] == text + except UnicodeDecodeError: + return False # Lets not open ourselves up to obscure keyboards... + def isPrefix(self, text): text = self.cleanText(text) i = bisect_left(self.words, text) - if i == self.numWords: return False + return self.words[i].startswith(text) - return self.words[i].startswith(text) \ No newline at end of file + def prefixCount(self, text): + text = self.cleanText(text) + i = bisect_left(self.words, text) + j = i + while j < self.numWords and self.words[j].startswith(text): + j += 1 + + return j - i + + def prefixList(self, text): + text = self.cleanText(text) + i = bisect_left(self.words, text) + j = i + while j < self.numWords and self.words[j].startswith(text): + j += 1 + + return self.words[i:j] diff --git a/otp/distributed/DCClassImports.py b/otp/distributed/DCClassImports.py index 8b6c09a8..08fd1b8b 100644 --- a/otp/distributed/DCClassImports.py +++ b/otp/distributed/DCClassImports.py @@ -2,7 +2,7 @@ from pandac.PandaModules import * -hashVal = 598642574 +hashVal = 4270694562L from toontown.coghq import DistributedCashbotBossSafe, DistributedCashbotBossCrane, DistributedBattleFactory, DistributedCashbotBossTreasure, DistributedCogHQDoor, DistributedSellbotHQDoor, DistributedFactoryElevatorExt, DistributedMintElevatorExt, DistributedLawOfficeElevatorExt, DistributedLawOfficeElevatorInt, LobbyManager, DistributedMegaCorp, DistributedFactory, DistributedLawOffice, DistributedLawOfficeFloor, DistributedLift, DistributedDoorEntity, DistributedSwitch, DistributedButton, DistributedTrigger, DistributedCrushableEntity, DistributedCrusherEntity, DistributedStomper, DistributedStomperPair, DistributedLaserField, DistributedGolfGreenGame, DistributedSecurityCamera, DistributedMover, DistributedElevatorMarker, DistributedBarrelBase, DistributedGagBarrel, DistributedBeanBarrel, DistributedHealBarrel, DistributedGrid, ActiveCell, DirectionalCell, CrusherCell, DistributedCrate, DistributedSinkingPlatform, BattleBlocker, DistributedMint, DistributedMintRoom, DistributedMintBattle, DistributedStage, DistributedStageRoom, DistributedStageBattle, DistributedLawbotBossGavel, DistributedLawbotCannon, DistributedLawbotChair, DistributedCogKart, DistributedCountryClub, DistributedCountryClubRoom, DistributedMoleField, DistributedCountryClubBattle, DistributedMaze, DistributedFoodBelt, DistributedBanquetTable, DistributedGolfSpot diff --git a/otp/distributed/OtpDoGlobals.py b/otp/distributed/OtpDoGlobals.py index f665ced4..1371df4a 100644 --- a/otp/distributed/OtpDoGlobals.py +++ b/otp/distributed/OtpDoGlobals.py @@ -88,4 +88,6 @@ OTP_ZONE_ID_DISTRICTS = 3 OTP_ZONE_ID_DISTRICTS_STATS = 4 OTP_ZONE_ID_ELEMENTS = 5 OTP_NET_MESSENGER_CHANNEL = (OTP_DO_ID_UBER_DOG << 32) + OTP_ZONE_ID_MANAGEMENT -OTP_STAFF_CHANNEL = 6200 +OTP_MOD_CHANNEL = 6200 +OTP_ADMIN_CHANNEL = 6400 +OTP_SYSADMIN_CHANNEL = 6500 diff --git a/toontown/ai/ServiceStart.py b/toontown/ai/ServiceStart.py index 4850f968..02df1a35 100644 --- a/toontown/ai/ServiceStart.py +++ b/toontown/ai/ServiceStart.py @@ -35,6 +35,7 @@ if args.astron_ip: localconfig += 'air-connect %s\n' % args.astron_ip if args.eventlogger_ip: localconfig += 'eventlog-host %s\n' % args.eventlogger_ip loadPrcFileData('Command-line', localconfig) + from otp.ai.AIBaseGlobal import * from toontown.ai.ToontownAIRepository import ToontownAIRepository diff --git a/toontown/uberdog/ClientServicesManagerUD.py b/toontown/uberdog/ClientServicesManagerUD.py index c8b3e188..98b25865 100644 --- a/toontown/uberdog/ClientServicesManagerUD.py +++ b/toontown/uberdog/ClientServicesManagerUD.py @@ -422,13 +422,26 @@ class LoginAccountFSM(OperationFSM): datagram.addChannel(self.csm.GetAccountConnectionChannel(self.accountId)) self.csm.air.send(datagram) - # Add this connection to extra channels which may be useful: - if self.accessLevel > 100: - datagram = PyDatagram() - datagram.addServerHeader(self.target, self.csm.air.ourChannel, - CLIENTAGENT_OPEN_CHANNEL) - datagram.addChannel(OtpDoGlobals.OTP_STAFF_CHANNEL) - self.csm.air.send(datagram) + # Subscribe to any "staff" channels that the account has access to. + access = self.account.get('ADMIN_ACCESS', 0) + if access >= 200: + # Subscribe to the moderator channel. + dg = PyDatagram() + dg.addServerHeader(self.target, self.csm.air.ourChannel, CLIENTAGENT_OPEN_CHANNEL) + dg.addChannel(OtpDoGlobals.OTP_MOD_CHANNEL) + self.csm.air.send(dg) + if access >= 400: + # Subscribe to the administrator channel. + dg = PyDatagram() + dg.addServerHeader(self.target, self.csm.air.ourChannel, CLIENTAGENT_OPEN_CHANNEL) + dg.addChannel(OtpDoGlobals.OTP_ADMIN_CHANNEL) + self.csm.air.send(dg) + if access >= 500: + # Subscribe to the system administrator channel. + dg = PyDatagram() + dg.addServerHeader(self.target, self.csm.air.ourChannel, CLIENTAGENT_OPEN_CHANNEL) + dg.addChannel(OtpDoGlobals.OTP_SYSADMIN_CHANNEL) + self.csm.air.send(dg) # Now set their sender channel to represent their account affiliation: datagram = PyDatagram()