Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

1089 lines (956 sloc) 38.001 kb
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.crowd.chat.client {
import com.threerings.util.Arrays;
import com.threerings.util.Log;
import com.threerings.util.Long;
import com.threerings.util.Map;
import com.threerings.util.Maps;
import com.threerings.util.MessageBundle;
import com.threerings.util.MessageManager;
import com.threerings.util.Name;
import com.threerings.util.ObserverList;
import com.threerings.util.ResultListener;
import com.threerings.util.StringUtil;
import com.threerings.util.TimeUtil;
import com.threerings.presents.client.BasicDirector;
import com.threerings.presents.client.Client;
import com.threerings.presents.client.ClientEvent;
import com.threerings.presents.client.InvocationAdapter;
import com.threerings.presents.dobj.DObject;
import com.threerings.presents.dobj.MessageEvent;
import com.threerings.presents.dobj.MessageListener;
import com.threerings.crowd.chat.data.ChatChannel;
import com.threerings.crowd.chat.data.ChatCodes;
import com.threerings.crowd.chat.data.ChatMarshaller;
import com.threerings.crowd.chat.data.ChatMessage;
import com.threerings.crowd.chat.data.SystemMessage;
import com.threerings.crowd.chat.data.TellFeedbackMessage;
import com.threerings.crowd.chat.data.UserMessage;
import com.threerings.crowd.chat.data.UserSystemMessage;
import com.threerings.crowd.client.CrowdClient;
import com.threerings.crowd.data.BodyObject;
import com.threerings.crowd.data.CrowdCodes;
import com.threerings.crowd.data.PlaceObject;
import com.threerings.crowd.util.CrowdContext;
/**
* The chat director is the client side coordinator of all chat related services. It handles both
* place constrained chat as well as direct messaging.
*/
public class ChatDirector extends BasicDirector
implements MessageListener
{
// statically reference classes we require
ChatMarshaller;
/**
* Creates a chat director and initializes it with the supplied context.
*
* @param msgmgr the message manager via which we do our translations.
* @param bundle the message bundle from which we obtain our chat-related translation strings.
*/
public function ChatDirector (ctx :CrowdContext, msgmgr :MessageManager, bundle :String)
{
super(ctx);
// keep the context around
_cctx = ctx;
_msgmgr = msgmgr;
_bundle = bundle;
if (_bundle == null || _msgmgr == null) {
log.warning("Null bundle or message manager given to ChatDirector");
return;
}
var msg :MessageBundle = _msgmgr.getBundle(_bundle);
// register our default chat handlers
registerCommandHandler(msg, "help", new HelpHandler());
registerCommandHandler(msg, "clear", new ClearHandler());
registerCommandHandler(msg, "speak", new SpeakHandler());
registerCommandHandler(msg, "emote", new EmoteHandler());
registerCommandHandler(msg, "think", new ThinkHandler());
// registerCommandHandler(msg, "tell", new TellHandler());
registerCommandHandler(msg, "broadcast", new BroadcastHandler());
}
/**
* Adds the supplied chat display to the front of the chat display list. It will subsequently
* be notified of incoming chat messages as well as tell responses.
*/
public function pushChatDisplay (display :ChatDisplay) :void
{
_displays.add(display, 0);
}
/**
* Adds the supplied chat display to the chat display list. It will subsequently be notified of
* incoming chat messages as well as tell responses.
*/
public function addChatDisplay (display :ChatDisplay) :void
{
_displays.add(display);
}
/**
* Removes the specified chat display from the chat display list. The display will no longer
* receive chat related notifications.
*/
public function removeChatDisplay (display :ChatDisplay) :void
{
_displays.remove(display);
}
/**
* Add the supplied chat snooper to hear about chat arriving from the server, pre-filtered.
*/
public function addChatSnooper (snooper :ChatSnooper) :void
{
_snoopers.add(snooper);
}
/**
* Remove the specified snooper.
*/
public function removeChatSnooper (snooper :ChatSnooper) :void
{
_snoopers.remove(snooper);
}
/**
* Adds the specified chat filter to the list of filters. All chat requests and receipts will
* be filtered with all filters before they being sent or dispatched locally.
*/
public function addChatFilter (filter :ChatFilter) :void
{
_filters.add(filter);
}
/**
* Removes the specified chat validator from the list of chat validators.
*/
public function removeChatFilter (filter :ChatFilter) :void
{
_filters.remove(filter);
}
/**
* Get all registered chat Filters.
*/
public function getChatFilters () :Array
{
var retval :Array = [];
_filters.apply(retval.push);
return retval;
}
/**
* Adds an observer that watches the chatters list, and updates it immediately.
*/
public function addChatterObserver (co :ChatterObserver) :void
{
_chatterObservers.add(co);
co.chattersUpdated(_chatters);
}
/**
* Removes an observer from the list of chatter observers.
*/
public function removeChatterObserver (co :ChatterObserver) :void
{
_chatterObservers.remove(co);
}
/**
* Sets the validator that decides if a username is valid to be added to the chatter list, or
* null if no such filtering is desired.
*/
public function setChatterValidator (validator :ChatterValidator) :void
{
_chatterValidator = validator;
}
/**
* Get a list of the recent users with whom we've chatted. The most recent users will be listed
* first.
*/
public function getChatters () :Array
{
return _chatters;
}
/**
* Adds a chatter to our list of recent chatters. This is normally done automatically by the
* ChatDirector, but may be called to also to forcibly add a chatter. The ChatterValidator will
* have to approve of the chatter.
*/
public function addChatter (name :Name) :void
{
// check to see if the chatter validator approves..
if ((_chatterValidator != null) && (!_chatterValidator.isChatterValid(name))) {
return;
}
var wasThere :Boolean = Arrays.removeAll(_chatters, name);
_chatters.unshift(name);
if (!wasThere) {
if (_chatters.length > MAX_CHATTERS) {
_chatters.length = MAX_CHATTERS; // truncate array
}
// we only notify on a change to the contents, not just reordering
notifyChatterObservers();
}
}
/**
* Registers a chat command handler.
*
* @param msg the message bundle via which the slash command will be translated (as
* <code>c.</code><i>command</i>). If no translation exists the command will be
* <code>/</code><i>command</i>.
* @param command the name of the command that will be used to invoke this handler
* (e.g. <code>tell</code> if the command will be invoked as <code>/tell</code>).
* @param handler the chat command handler itself.
*/
public function registerCommandHandler (
msg :MessageBundle, command :String, handler :CommandHandler) :void
{
for each (var cmd :String in getCommandAliases(msg, command)) {
_handlers.put(cmd, handler);
}
}
/**
* Unregister a chat command handler.
*/
public function unregisterCommandHandler (msg :MessageBundle, command :String) :void
{
for each (var cmd :String in getCommandAliases(msg, command)) {
_handlers.remove(cmd);
}
}
/**
* Return the current size of the history.
*/
public function getCommandHistorySize () :int
{
return _history.length;
}
/**
* Get the chat history entry at the specified index, with 0 being the oldest.
*/
public function getCommandHistory (index :int) :String
{
return (_history[index] as String);
}
/**
* Clear the chat command history.
*/
public function clearCommandHistory () :void
{
_history.length = 0;
}
/**
* Requests that all chat displays clear their contents.
*/
public function clearDisplays () :void
{
_displays.apply(function (disp :ChatDisplay) :void {
disp.clear();
});
}
/**
* Display a system INFO message as if it had come from the server.
*
* Info messages are sent when something happens that was neither directly triggered by the
* user, nor requires direct action.
*/
public function displayInfo (bundle :String, message :String, localtype :String = null) :void
{
if (localtype == null) {
localtype = ChatCodes.PLACE_CHAT_TYPE;
}
displaySystem(bundle, message, SystemMessage.INFO, localtype);
}
/**
* Display a system FEEDBACK message as if it had come from the server. The localtype of the
* message will be PLACE_CHAT_TYPE.
*
* Feedback messages are sent in direct response to a user action, usually to indicate success
* or failure of the user's action.
*/
public function displayFeedback (bundle :String, message :String) :void
{
displaySystem(bundle, message, SystemMessage.FEEDBACK, ChatCodes.PLACE_CHAT_TYPE);
}
/**
* Display a system ATTENTION message as if it had come from the server. The localtype of the
* message will be PLACE_CHAT_TYPE.
*
* Attention messages are sent when something requires user action that did not result from
* direct action by the user.
*/
public function displayAttention (bundle :String, message :String) :void
{
displaySystem(bundle, message, SystemMessage.ATTENTION, ChatCodes.PLACE_CHAT_TYPE);
}
/**
* Dispatches the provided message to our chat displays.
*/
public function dispatchMessage (message :ChatMessage, localType :String) :void
{
setClientInfo(message, localType);
dispatchPreparedMessage(message);
}
/**
* Parses and delivers the supplied chat message. Slash command processing and mogrification
* are performed and the message is added to the chat history if appropriate.
*
* @param speakSvc the SpeakService representing the target dobj of the speak or null if we
* should speak in the "default" way.
* @param text the text to be parsed and sent.
* @param record if text is a command, should it be added to the history?
*
* @return <code>ChatCodes#SUCCESS</code> if the message was parsed and sent correctly, a
* translatable error string if there was some problem.
*/
public function requestChat (speakSvc :SpeakService, text :String, record :Boolean) :String
{
if (text.indexOf("/") == 0) {
// split the text up into a command and arguments
var command :String = text.substring(1).toLowerCase();
var hist :Array = new Array();
var args :String = "";
var sidx :int = text.indexOf(" ");
if (sidx != -1) {
command = text.substring(1, sidx).toLowerCase();
args = StringUtil.trim(text.substring(sidx+1));
}
var possibleCommands :Map = getCommandHandlers(command);
switch (possibleCommands.size()) {
case 0:
return MessageBundle.tcompose("m.unknown_command", text.split(/\s/)[0]);
case 1:
var cmdName :String = possibleCommands.keys()[0];
var cmd :CommandHandler = (possibleCommands.get(cmdName) as CommandHandler);
var result :String = cmd.handleCommand(_cctx, speakSvc, cmdName, args, hist);
if (result != ChatCodes.SUCCESS) {
return result;
}
if (record) {
// get the final history-ready command string
hist[0] = "/" + ((hist[0] == null) ? command : hist[0]);
// remove from history if it was present and add it to the end
addToHistory(hist[0]);
}
return result;
default:
var cmds :Array = possibleCommands.keys();
var alternativeCommands :String = "/" + cmds.join(", /");
return MessageBundle.tcompose("m.unspecific_command", alternativeCommands);
}
}
// if not a command then just speak
var message :String = StringUtil.trim(text);
if (StringUtil.isBlank(message)) {
return ChatCodes.SUCCESS; // report silent failure for now
}
return deliverChat(speakSvc, message, ChatCodes.DEFAULT_MODE);
}
/**
* Requests that a speak message with the specified mode be generated and delivered via the
* supplied speak service instance (which will be associated with a particular "speak object").
* The message will first be validated by all registered {@link ChatFilter}s (and possibly
* vetoed) before being dispatched.
*
* @param speakService the speak service to use when generating the speak request or null if we
* should speak in the current "place".
* @param message the contents of the speak message.
* @param mode a speech mode that will be interpreted by the {@link ChatDisplay}
* implementations that eventually display this speak message.
*/
public function requestSpeak (speakService :SpeakService, message :String, mode :int) :void
{
if (speakService == null) {
if (_place == null) {
return;
}
speakService = _place.speakService;
}
// make sure they can say what they want to say
message = filter(message, null, true);
if (message == null) {
return;
}
// dispatch a speak request using the supplied speak service
speakService.speak(message, mode);
}
/**
* Requests to send a site-wide broadcast message.
*
* @param message the contents of the message.
*/
public function requestBroadcast (message :String) :void
{
// filtering is now done in the broadcast handler, not here. Java code may differ.
var failure :Function = function (reason :String) :void {
reason = MessageBundle.compose("m.broadcast_failed", reason);
displayFeedback(_bundle, reason);
};
_cservice.broadcast(message, new InvocationAdapter(failure));
}
/**
* Requests that a tell message be delivered to the specified target user.
*
* @param target the username of the user to which the tell message should be delivered.
* @param msg the contents of the tell message.
* @param rl an optional result listener if you'd like to be notified of success or failure.
* @param feedbackLocaltype this local type will be used for both TellFeedbackMessages, and
* regular feedback that is dispatched for away/idle messages.
*/
public function requestTell (target :Name, msg :String, rl :ResultListener,
feedbackLocaltype :String = ChatCodes.PLACE_CHAT_TYPE) :void
{
// make sure they can say what they want to say
var message :String = filter(msg, target, true);
if (message == null) {
if (rl != null) {
rl.requestFailed(null);
}
return;
}
var failure :Function = function (reason :String) :void {
var msg :String = MessageBundle.compose(
"m.tell_failed", MessageBundle.taint(target), reason);
var tfm :TellFeedbackMessage = new TellFeedbackMessage(target, msg, true);
tfm.bundle = _bundle;
dispatchMessage(tfm, feedbackLocaltype);
if (rl != null) {
rl.requestFailed(null);
}
};
var success :Function = function (idleTime :Long, awayMessage :String) :void {
dispatchMessage(new TellFeedbackMessage(target, message), feedbackLocaltype);
addChatter(target);
if (rl != null) {
rl.requestCompleted(target);
}
// if they have an away message, report that
if (awayMessage != null) {
awayMessage = filter(awayMessage, target, false);
if (awayMessage != null) {
var msg :String = MessageBundle.tcompose(
"m.recipient_afk", target, awayMessage);
displaySystem(_bundle, msg, SystemMessage.FEEDBACK, feedbackLocaltype);
}
}
// if they are idle, report that
var idle :Number = idleTime.toNumber();
if (idle > 0) {
// adjust by the time it took them to become idle
// TODO: implement
//idle += _cctx.getConfig().getValue(IDLE_TIME_KEY, DEFAULT_IDLE_TIME);
var rmsg :String = MessageBundle.compose(
"m.recipient_idle", MessageBundle.taint(target),
TimeUtil.getTimeOrderString(idle, TimeUtil.MINUTE));
displaySystem(_bundle, rmsg, SystemMessage.FEEDBACK, feedbackLocaltype);
}
};
_cservice.tell(target, message, new TellAdapter(failure, success));
}
/**
* Configures a message that will be automatically reported to anyone that sends a tell message
* to this client to indicate that we are busy or away from the keyboard.
*/
public function setAwayMessage (message :String) :void
{
if (message != null) {
message = filter(message, null, true);
if (message == null) {
// they filtered away their own away message.. change it to something
message = "...";
}
}
// pass the buck right on along
_cservice.away(message);
}
/**
* Adds an additional object via which chat messages may arrive. The chat director assumes the
* caller will be managing the subscription to this object and will remain subscribed to it for
* as long as it remains in effect as an auxiliary chat source.
*
* @param localtype a type to be associated with all chat messages that arrive on the specified
* DObject.
*/
public function addAuxiliarySource (source :DObject, localtype :String) :void
{
source.addListener(this);
_auxes.put(source.getOid(), localtype);
}
/**
* Removes a previously added auxiliary chat source.
*/
public function removeAuxiliarySource (source :DObject) :void
{
source.removeListener(this);
_auxes.remove(source.getOid());
}
/**
* Run a message through all the currently registered filters.
*/
public function filter (msg :String, otherUser :Name, outgoing :Boolean) :String
{
_filters.apply(function (observer :ChatFilter) :void {
if (msg != null) {
msg = observer.filter(msg, otherUser, outgoing);
}
});
return msg;
}
/**
* Runs the supplied message through the various chat mogrifications.
*/
public function mogrifyChat (text :String) :String
{
return mogrifyChatImpl(text, false, true);
}
/**
* Called by a LocationDirector when we enter a new location to configure our place chat.
*/
public function enteredLocation (place :PlaceObject) :void
{
// nix our old location if we have one
if (_place != null) {
leftLocation(_place);
}
// listen to the new object
_place = place;
_place.addListener(this);
}
/**
* Called by a LocationDirector when we leave our current place.
*/
public function leftLocation (place :PlaceObject) :void
{
// if this is our current place chat, then stop listening to it
if (place == _place) {
_place.removeListener(this);
_place = null;
}
}
// documentation inherited from interface MessageListener
public function messageReceived (event :MessageEvent) :void
{
if (ChatCodes.CHAT_NOTIFICATION === event.getName()) {
processReceivedMessage(event.getArgs()[0] as ChatMessage,
getLocalType(event.getTargetOid()));
} else if (ChatCodes.CHAT_CHANNEL_NOTIFICATION === event.getName()) {
processReceivedMessage(event.getArgs()[1] as ChatMessage,
getChannelLocalType(event.getArgs()[0] as ChatChannel));
}
}
// documentation inherited
override public function clientDidLogon (event :ClientEvent) :void
{
super.clientDidLogon(event);
_bodyobj = (event.getClient() as CrowdClient).bodyOf();
// listen on the body object for tells
addAuxiliarySource(_bodyobj, ChatCodes.USER_CHAT_TYPE);
}
// documentation inherited
override public function clientObjectDidChange (event :ClientEvent) :void
{
super.clientObjectDidChange(event);
// change what we're listening to for tells
if (_bodyobj != null) {
removeAuxiliarySource(_bodyobj);
}
_bodyobj = (event.getClient() as CrowdClient).bodyOf();
// listen on the body object for tells
addAuxiliarySource(_bodyobj, ChatCodes.USER_CHAT_TYPE);
if (clearChatOnClientExit()) {
clearDisplays();
}
}
// documentation inherited
override public function clientDidLogoff (event :ClientEvent) :void
{
super.clientDidLogoff(event);
if (_bodyobj != null) {
// stop listening to it for tells
removeAuxiliarySource(_bodyobj);
_bodyobj = null;
}
// in fact, clear out all auxiliary sources
_auxes.clear();
if (clearChatOnClientExit()) {
clearDisplays();
}
// clear out the list of people we've chatted with
_chatters.length = 0;
notifyChatterObservers();
// the location director will call leftLocation() when we logoff
// clear our service
_cservice = null;
}
/**
* Processes and dispatches the specified chat message.
*/
protected function processReceivedMessage (msg :ChatMessage, localtype :String) :void
{
var autoResponse :String = null;
var speaker :Name = null;
var mode :int = -1;
// figure out if the message was triggered by another user
if (msg is UserMessage) {
var umsg :UserMessage = (msg as UserMessage);
speaker = umsg.speaker;
mode = umsg.mode;
} else if (msg is UserSystemMessage) {
speaker = (msg as UserSystemMessage).speaker;
}
// Translate and timestamp the message. This would happen during dispatch but we
// need to do it ahead of filtering.
setClientInfo(msg, localtype);
// inform the "snoopers"
_snoopers.apply(function (snooper :ChatSnooper) :void {
snooper.snoopChat(msg);
});
// if there was an originating speaker, see if we want to hear it
if (speaker != null) {
if ((msg.message = filter(msg.message, speaker, false)) == null) {
return;
}
if (ChatCodes.USER_CHAT_TYPE == localtype && mode == ChatCodes.DEFAULT_MODE) {
// if it was a tell, add the speaker as a chatter
addChatter(speaker);
// note whether or not we have an auto-response
var self :BodyObject = (_cctx.getClient() as CrowdClient).bodyOf();
if (!StringUtil.isBlank(self.awayMessage)) {
autoResponse = self.awayMessage;
}
}
}
// and send it off!
dispatchMessage(msg, localtype);
// if we auto-responded, report as much
if (autoResponse != null) {
var amsg :String = MessageBundle.tcompose(
"m.auto_responded", speaker, autoResponse);
displayFeedback(_bundle, amsg);
}
}
/**
* Dispatch a message to chat displays once it is fully prepared with the clientinfo.
*/
protected function dispatchPreparedMessage (message :ChatMessage) :void
{
_displays.apply(function (disp :ChatDisplay) :void {
disp.displayMessage(message);
});
}
/**
* Called to determine whether we are permitted to post the supplied chat message. Derived
* classes may wish to throttle chat or restrict certain types in certain circumstances for
* whatever reason.
*
* @return null if the chat is permitted, SUCCESS if the chat is permitted and has already been
* dealt with, or a translatable string indicating the reason for rejection if not.
*/
protected function checkCanChat (speakSvc :SpeakService , message :String, mode :int) :String
{
return null;
}
/**
* Delivers a plain chat message (not a slash command) on the specified speak service in the
* specified mode. The message will be mogrified and filtered prior to delivery.
*
* @return {@link ChatCodes#SUCCESS} if the message was delivered or a string indicating why it
* failed.
*/
internal function deliverChat (speakSvc :SpeakService, message :String, mode :int) :String
{
// run the message through our mogrification process
message = mogrifyChatImpl(message, true, mode != ChatCodes.EMOTE_MODE);
// mogrification may result in something being turned into a slash command, in which case
// we have to run everything through again from the start
if (message.indexOf("/") == 0) {
return requestChat(speakSvc, message, false);
}
// make sure this client is not restricted from performing this chat message for some
// reason or other
var errmsg :String = checkCanChat(speakSvc, message, mode);
if (errmsg != null) {
return errmsg;
}
// speak on the specified service
requestSpeak(speakSvc, message, mode);
return ChatCodes.SUCCESS;
}
/**
* Add the specified command to the history.
*/
protected function addToHistory (cmd :String) :void
{
// remove any previous instance of this command
Arrays.removeAll(_history, cmd);
// append it to the end
_history.push(cmd);
// prune the history once it extends beyond max size
if (_history.length > MAX_COMMAND_HISTORY) {
_history.shift();
}
}
/**
* Mogrify common literary crutches into more appealing chat or commands.
*
* @param transformsAllowed if true, the chat may transformed into a different mode. (lol ->
* /emote laughs)
* @param capFirst if true, the first letter of the text is capitalized. This is not desired if
* the chat is already an emote.
*/
protected function mogrifyChatImpl (
text :String, transformsAllowed :Boolean, capFirst :Boolean) :String
{
var tlen :int = text.length;
if (tlen == 0) {
return text;
// check to make sure there aren't too many caps
} else if (tlen > 7 && suppressTooManyCaps()) {
// count caps
var caps :int = 0;
for (var ii :int = 0; ii < tlen; ii++) {
var ch :String = text.charAt(ii);
// do a fucked-up test to see if it's uppercase make sure the uppercase version is
// the same as the orig and the lowercase version is different
var chU :String = ch.toUpperCase();
if (ch == chU && chU != ch.toLowerCase()) {
caps++;
if (caps > (tlen / 2)) {
// lowercase the whole string if there are
text = text.toLowerCase();
break;
}
}
}
}
return mogrifyChatText(text, transformsAllowed, capFirst);
}
/** Helper function for {@link #mogrifyChat}. */
protected function mogrifyChatText (
text :String, transformsAllowed :Boolean, capFirst :Boolean) :String
{
// do the generic mogrifications and translations
text = translatedReplacements("x.mogrifies", text);
// perform themed expansions and transformations
if (transformsAllowed) {
text = translatedReplacements("x.transforms", text);
}
/*
// capitalize the first letter
if (capFirst) {
buf.setCharAt(0, Character.toUpperCase(buf.charAt(0)));
}
// and capitalize any letters after a sentence-ending punctuation
Pattern p = Pattern.compile("([^\\.][\\.\\?\\!](\\s)+\\p{Ll})");
Matcher m = p.matcher(buf);
if (m.find()) {
buf = new StringBuffer();
m.appendReplacement(buf, m.group().toUpperCase());
while (m.find()) {
m.appendReplacement(buf, m.group().toUpperCase());
}
m.appendTail(buf);
}
*/
return text;
}
/**
* Do all the replacements (mogrifications) specified in the translation string specified by
* the key.
*/
protected function translatedReplacements (key :String, text :String) :String
{
var bundle :MessageBundle = _msgmgr.getBundle(_bundle);
if (!bundle.exists(key)) {
return text;
}
var repls :Array = bundle.get(key).split("#");
// apply the replacements to each mogrification that matches
for (var ii :int = 0; ii < repls.length; ii++) {
var pattern :RegExp = new RegExp((repls[ii] as String), "gim");
var replace :String = (repls[++ii] as String);
text = text.replace(pattern, replace);
}
return text;
}
/**
* Return true if we should lowercase messages containing more than half upper-case characters.
*/
protected function suppressTooManyCaps () :Boolean
{
return true;
}
/**
* Check that after mogrification the message is not too long.
* @return an error mesage if it is too long, or null.
*/
internal function checkLength (msg :String) :String
{
return null; // TODO
}
/**
* Return the translated aliases for the specified command.
*/
protected function getCommandAliases (msg :MessageBundle, cmd :String) :Array /* of String */
{
var key :String = "c." + cmd;
if (msg.exists(key)) {
return msg.get(key).split(/\s+/);
}
return [ cmd ];
}
/**
* Returns a hashmap containing all command handlers that match the specified command (i.e. the
* specified command is a prefix of their registered command string).
*/
internal function getCommandHandlers (command :String) :Map
{
var matches :Map = Maps.newMapOf(String);
var user :BodyObject = (_cctx.getClient() as CrowdClient).bodyOf();
var keys :Array = _handlers.keys();
for (var ii :int = 0; ii < keys.length; ii++) {
var cmd :String = (keys[ii] as String);
if (cmd.indexOf(command) != 0) {
continue;
}
var handler :CommandHandler = (_handlers.get(cmd) as CommandHandler);
if (handler.checkAccess(user)) {
matches.put(cmd, handler);
}
}
return matches;
}
internal function accessHistory () :Array
{
return _history;
}
/**
* Notifies all registered {@link ChatterObserver}s that the list of chatters has changed.
*/
protected function notifyChatterObservers () :void
{
_chatterObservers.apply(chatterObserverNotify);
}
/**
* A function to be called by our _chatterObservers list to apply updates to each observer.
*/
protected function chatterObserverNotify (obs :ChatterObserver) :void
{
obs.chattersUpdated(_chatters);
}
/**
* Set the "client info" on the specified message, if not already set.
*/
protected function setClientInfo (msg :ChatMessage, localType :String) :void
{
if (msg.localtype == null) {
msg.setClientInfo(xlate(msg.bundle, msg.message), localType);
}
}
/**
* Translates the specified message using the specified bundle.
*/
protected function xlate (bundle :String, message :String) :String
{
if (bundle != null && _msgmgr != null) {
var msgb :MessageBundle = _msgmgr.getBundle(bundle);
if (msgb == null) {
log.warning("No message bundle available to translate message",
"bundle", bundle, "message", message);
} else {
message = msgb.xlate(message);
}
}
return message;
}
/**
* Display the specified system message as if it had come from the server.
*/
protected function displaySystem (
bundle :String, message :String, attLevel :int, localtype :String) :void
{
// nothing should be untranslated, so pass the default bundle if need be.
if (bundle == null) {
bundle = _bundle;
}
var msg :SystemMessage = new SystemMessage(message, bundle, attLevel);
dispatchMessage(msg, localtype);
}
/**
* Looks up and returns the message type associated with the specified oid.
*/
protected function getLocalType (oid :int) :String
{
var typ :String = (_auxes.get(oid) as String);
return (typ == null) ? ChatCodes.PLACE_CHAT_TYPE : typ;
}
/**
* Returns the local type for a chat channel message.
*/
protected function getChannelLocalType (channel :ChatChannel) :String
{
throw new Error("Users of the chat channel system must implement getChannelLocalType()");
}
// from BasicDirector
override protected function registerServices (client :Client) :void
{
client.addServiceGroup(CrowdCodes.CROWD_GROUP);
}
// from BasicDirector
override protected function fetchServices (client :Client) :void
{
// get a handle on our chat service
_cservice = (client.requireService(ChatService) as ChatService);
}
/** Overrideable. */
protected function clearChatOnClientExit () :Boolean
{
return true;
}
/** Log this! */
protected const log :Log = Log.getLog(this);
/** Our active chat context. */
protected var _cctx :CrowdContext;
/** Provides access to chat-related server-side services. */
protected var _cservice :ChatService;
/** The message manager. */
protected var _msgmgr :MessageManager;
/** The bundle to use for our own internal messages. */
protected var _bundle :String;
/** The place object that we currently occupy. */
protected var _place :PlaceObject;
/** The object that we're listening to for tells. */
protected var _bodyobj :DObject;
/** A list of registered chat displays. */
protected var _displays :ObserverList = new ObserverList();
/** A list of registered chat filters. */
protected var _filters :ObserverList = new ObserverList();
/** A list of registered chat snoopers. */
protected var _snoopers :ObserverList = new ObserverList();
/** A mapping from auxiliary chat objects to the types under which they are registered. */
protected var _auxes :Map = Maps.newMapOf(int);
/** Validator of who may be added to the chatters list. */
protected var _chatterValidator :ChatterValidator;
/** Usernames of users we've recently chatted with. */
protected var _chatters :Array = [];
/** Observers that are watching our chatters list. */
protected var _chatterObservers :ObserverList = new ObserverList();
/** Registered chat command handlers. */
protected const _handlers :Map = Maps.newMapOf(String);
/** A history of chat commands. */
protected const _history :Array = new Array();
/** The maximum number of chatter usernames to track. */
protected static const MAX_CHATTERS :int = 6;
/** The maximum number of commands to keep in the chat history. */
protected static const MAX_COMMAND_HISTORY :int = 10;
}
}
Jump to Line
Something went wrong with that request. Please try again.