/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package net.java.sip.communicator.impl.protocol.irc;
import java.util.*;
import java.util.Map.Entry;
import net.java.sip.communicator.impl.protocol.irc.ModeParser.ModeEntry;
import net.java.sip.communicator.impl.protocol.irc.exception.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.util.*;
import com.ircclouds.irc.api.*;
import com.ircclouds.irc.api.domain.*;
import com.ircclouds.irc.api.domain.messages.*;
import com.ircclouds.irc.api.domain.messages.interfaces.*;
import com.ircclouds.irc.api.listeners.*;
import com.ircclouds.irc.api.state.*;
/**
* Channel manager.
*
* TODO Implement channel services (ChanServ - channel related services) that
* can be used for accessing remove channel facilities.
*
* TODO Do we need to cancel any join channel operations still in progress?
*
* @author Danny van Heumen
*/
public class ChannelManager
{
/**
* Logger.
*/
private static final Logger LOGGER = Logger.getLogger(ChannelManager.class);
/**
* IRCApi instance.
*
* Instance must be thread-safe!
*/
private final IRCApi irc;
/**
* Connection state.
*/
private final IIRCState connectionState;
/**
* Provider.
*/
private final ProtocolProviderServiceIrcImpl provider;
/**
* Container for joined channels.
*
* There are two different cases:
*
*
* - null value: joining is initiated but still in progress.
* - non-null value: joining is finished, chat room instance is available.
*
*/
private final Map joined = Collections
.synchronizedMap(new HashMap());
/**
* Maximum channel name length according to server ISUPPORT instructions.
*
* This value is not guaranteed, so it may be null .
*/
private final Integer isupportChannelLen;
/**
* Maximum topic length according to server ISUPPORT instructions.
*
* This value is not guaranteed, so it may be null .
*/
private final Integer isupportTopicLen;
/**
* Maximum kick message length according to server ISUPPORT instructions.
*
* This value is not guaranteed, so it may be null .
*/
private final Integer isupportKickLen;
/**
* Maximum number of joined channels according to server ISUPPORT
* instructions. Limits are stored per channel type (#, &, etc.)
*
* This value is not guaranteed, so it may be null .
*/
private final Map isupportChanLimit
= new HashMap();
/**
* Constructor.
*
* @param irc thread-safe IRCApi instance
* @param connectionState the connection state
* @param provider the provider instance
*/
public ChannelManager(final IRCApi irc, final IIRCState connectionState,
final ProtocolProviderServiceIrcImpl provider)
{
if (irc == null)
{
throw new IllegalArgumentException("irc instance cannot be null");
}
this.irc = irc;
if (connectionState == null)
{
throw new IllegalArgumentException(
"connectionState cannot be null");
}
this.connectionState = connectionState;
if (provider == null)
{
throw new IllegalArgumentException("provider cannot be null");
}
this.provider = provider;
this.irc.addListener(new ManagerListener());
// parse ISUPPORT parameters
this.isupportChannelLen = parseISupportInteger(this.connectionState,
ISupport.CHANNELLEN);
this.isupportTopicLen = parseISupportInteger(this.connectionState,
ISupport.TOPICLEN);
this.isupportKickLen = parseISupportInteger(this.connectionState,
ISupport.KICKLEN);
parseISupportChanLimit(this.isupportChanLimit, this.connectionState);
}
/**
* Parse the ISUPPORT parameter for Integer value.
*
* @param state the connection state
* @return returns instance with parameter value or null if
* not specified.
*/
private static Integer parseISupportInteger(final IIRCState state,
final ISupport param)
{
final String value = state.getServerOptions().getKey(param.name());
if (value == null)
{
return null;
}
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Setting ISUPPORT parameter " + param.name() + " to "
+ value);
}
return new Integer(value);
}
/**
* Parse the raw ISUPPORT CHANLIMIT value, extract its values into the
* destination map.
*
* @param destination the destination map
* @param state the IRC connection state
*/
private static void parseISupportChanLimit(
final Map destination, final IIRCState state)
{
final String rawChanLimitValue =
state.getServerOptions().getKey(ISupport.CHANLIMIT.name());
ISupport.parseChanLimit(destination, rawChanLimitValue);
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Parsed ISUPPORT CHANLIMIT parameter: "
+ rawChanLimitValue);
for (Entry e : destination.entrySet())
{
LOGGER.debug(e.getKey() + ":" + e.getValue());
}
}
}
/**
* Get a set of channel type indicators.
*
* @return returns set of channel type indicators.
*/
public Set getChannelTypes()
{
return this.connectionState.getServerOptions().getChanTypes();
}
/**
* Check whether the user has joined a particular chat room.
*
* @param chatroom Chat room to check for.
* @return Returns true in case the user is already joined, or false if the
* user has not joined.
*/
public boolean isJoined(final ChatRoomIrcImpl chatroom)
{
return this.joined.get(chatroom.getIdentifier()) != null;
}
/**
* Join a particular chat room.
*
* @param chatroom Chat room to join.
* @throws OperationFailedException failed to join the chat room
*/
public void join(final ChatRoomIrcImpl chatroom)
throws OperationFailedException
{
join(chatroom, "");
}
/**
* Join a particular chat room.
*
* Issue a join channel IRC operation and wait for the join operation to
* complete (either successfully or failing).
*
* @param chatroom The chatroom to join.
* @param password Optionally, a password that may be required for some
* channels.
* @throws OperationFailedException failed to join the chat room
*/
public void join(final ChatRoomIrcImpl chatroom, final String password)
throws OperationFailedException
{
if (!this.connectionState.isConnected())
{
throw new IllegalStateException(
"Please connect to an IRC server first");
}
if (chatroom == null)
{
throw new IllegalArgumentException("chatroom cannot be null");
}
if (password == null)
{
throw new IllegalArgumentException("password cannot be null");
}
final String chatRoomId = chatroom.getIdentifier();
if (this.joined.containsKey(chatRoomId))
{
// If we already joined this particular chatroom, no further action
// is required.
return;
}
if (this.isupportChannelLen != null
&& chatRoomId.length() > this.isupportChannelLen)
{
throw new IllegalArgumentException("the channel name must not be "
+ "longer than " + this.isupportChannelLen.intValue()
+ " characters according to server parameters.");
}
// Verify max channel limit based on server parameters (ISupport)
final Integer limit = this.isupportChanLimit.get(chatRoomId.charAt(0));
if (limit != null && this.joined.size() >= limit)
{
throw new IllegalStateException("already joined to the maximum "
+ "allowed number of channels ("
+ this.isupportChanLimit.toString() + ") according to "
+ "server parameters.");
}
LOGGER.trace("Start joining channel " + chatRoomId);
final Result joinSignal =
new Result();
synchronized (joinSignal)
{
LOGGER.trace("Issue join channel command to IRC library and wait "
+ "for join operation to complete (un)successfully.");
this.joined.put(chatRoomId, null);
// TODO Refactor this ridiculous nesting of functions and
// classes.
this.irc.joinChannel(chatRoomId, password,
new Callback()
{
@Override
public void onSuccess(final IRCChannel channel)
{
if (LOGGER.isTraceEnabled())
{
LOGGER.trace("Started callback for successful "
+ "join of channel '"
+ chatroom.getIdentifier() + "'.");
}
boolean isRequestedChatRoom =
channel.getName().equalsIgnoreCase(chatRoomId);
synchronized (joinSignal)
{
if (!isRequestedChatRoom)
{
// We joined another chat room than the one
// we requested initially.
if (LOGGER.isTraceEnabled())
{
LOGGER.trace("Callback for successful "
+ "join finished prematurely "
+ "since we got forwarded from '"
+ chatRoomId + "' to '"
+ channel.getName()
+ "'. Joining of forwarded channel "
+ "gets handled by Server Listener "
+ "since that channel was not "
+ "announced.");
}
// Remove original chat room id from
// joined-list since we aren't actually
// attempting to join this room anymore.
ChannelManager.this.joined.remove(chatRoomId);
ChannelManager.this.provider
.getMUC()
.fireLocalUserPresenceEvent(
chatroom,
LocalUserChatRoomPresenceChangeEvent
.LOCAL_USER_JOIN_FAILED,
"We got forwarded to channel '"
+ channel.getName() + "'.");
// Notify waiting threads of finished
// execution.
joinSignal.setDone();
joinSignal.notifyAll();
// The channel that we were forwarded to
// will be handled by the Server Listener,
// since the channel join was unannounced,
// and we are done here.
return;
}
try
{
ChannelManager.this.joined.put(chatRoomId,
chatroom);
ChannelManager.this.irc
.addListener(
new ChatRoomListener(chatroom));
prepareChatRoom(chatroom, channel);
}
finally
{
// In any case, issue the local user
// presence, since the irc library notified
// us of a successful join. We should wait
// as long as possible though. First we need
// to fill the list of chat room members and
// other chat room properties.
ChannelManager.this.provider
.getMUC()
.fireLocalUserPresenceEvent(
chatroom,
LocalUserChatRoomPresenceChangeEvent
.LOCAL_USER_JOINED,
null);
if (LOGGER.isTraceEnabled())
{
LOGGER.trace("Finished successful join "
+ "callback for channel '" + chatRoomId
+ "'. Waking up original thread.");
}
// Notify waiting threads of finished
// execution.
joinSignal.setDone();
joinSignal.notifyAll();
}
}
}
@Override
public void onFailure(final Exception e)
{
LOGGER.trace("Started callback for failed attempt to "
+ "join channel '" + chatRoomId + "'.");
synchronized (joinSignal)
{
try
{
ChannelManager.this.joined.remove(chatRoomId);
ChannelManager.this.provider
.getMUC()
.fireLocalUserPresenceEvent(
chatroom,
LocalUserChatRoomPresenceChangeEvent
.LOCAL_USER_JOIN_FAILED,
e.getMessage());
}
finally
{
if (LOGGER.isTraceEnabled())
{
LOGGER.trace("Finished callback for "
+ "failed attempt to join "
+ "channel '" + chatRoomId
+ "'. Waking up original thread.");
}
// Notify waiting threads of finished
// execution
joinSignal.setDone(e);
joinSignal.notifyAll();
}
}
}
});
try
{
while (!joinSignal.isDone())
{
LOGGER.trace("Waiting for channel join message ...");
// Wait until async channel join operation has finished.
joinSignal.wait();
}
LOGGER
.trace("Finished waiting for join operation for channel '"
+ chatroom.getIdentifier() + "' to complete.");
// TODO How to handle 480 (+j): Channel throttle exceeded?
}
catch (InterruptedException e)
{
LOGGER.error("Wait for join operation was interrupted.", e);
throw new OperationFailedException(e.getMessage(),
OperationFailedException.INTERNAL_ERROR, e);
}
}
}
/**
* Prepare a chat room for initial opening.
*
* @param channel The IRC channel which is the source of data.
* @param chatRoom The chatroom to prepare.
*/
private void prepareChatRoom(final ChatRoomIrcImpl chatRoom,
final IRCChannel channel)
{
final IRCTopic topic = channel.getTopic();
chatRoom.updateSubject(topic.getValue());
for (final IRCUser user : channel.getUsers())
{
final ChatRoomMemberIrcImpl member =
new ChatRoomMemberIrcImpl(this.provider, chatRoom,
user.getNick(), user.getIdent(), user.getHostname(),
ChatRoomMemberRole.SILENT_MEMBER);
ChatRoomMemberRole role;
for (final IRCUserStatus status : channel.getStatusesForUser(user))
{
try
{
role = convertMemberMode(status.getChanModeType());
member.addRole(role);
}
catch (UnknownModeException e)
{
LOGGER.info(
"Unknown mode encountered. This mode will be ignored.",
e);
}
}
chatRoom.addChatRoomMember(member.getContactAddress(), member);
if (this.connectionState.getNickname().equals(user.getNick()))
{
chatRoom.setLocalUser(member);
if (member.getRole() != ChatRoomMemberRole.SILENT_MEMBER)
{
ChatRoomLocalUserRoleChangeEvent event =
new ChatRoomLocalUserRoleChangeEvent(chatRoom,
ChatRoomMemberRole.SILENT_MEMBER, member.getRole(),
true);
chatRoom.fireLocalUserRoleChangedEvent(event);
}
}
}
}
/**
* Set the subject of the specified chat room.
*
* @param chatroom The chat room for which to set the subject.
* @param subject The subject.
*/
public void setSubject(final ChatRoomIrcImpl chatroom, final String subject)
{
if (!this.connectionState.isConnected())
{
throw new IllegalStateException(
"Please connect to an IRC server first.");
}
if (chatroom == null)
{
throw new IllegalArgumentException("Cannot have a null chatroom");
}
if (this.isupportTopicLen != null
&& subject.length() > this.isupportTopicLen)
{
throw new IllegalArgumentException("the topic length must not be "
+ "longer than " + this.isupportTopicLen
+ " characters according to server parameters.");
}
LOGGER.trace("Setting chat room topic to '" + subject + "'");
this.irc.changeTopic(chatroom.getIdentifier(), subject == null ? ""
: subject);
}
/**
* Part from a joined chat room.
*
* @param chatroom The chat room to part from.
*/
public void leave(final ChatRoomIrcImpl chatroom)
{
LOGGER.trace("Leaving chat room '" + chatroom.getIdentifier() + "'.");
leave(chatroom.getIdentifier());
}
/**
* Part from a joined chat room.
*
* @param chatRoomName The chat room to part from.
*/
private void leave(final String chatRoomName)
{
if (!this.connectionState.isConnected())
{
throw new IllegalStateException("Not connected to an IRC server.");
}
try
{
this.irc.leaveChannel(chatRoomName);
}
catch (ApiException e)
{
LOGGER.warn("exception occurred while leaving channel", e);
}
}
/**
* Grant user permissions to specified user.
*
* @param chatRoom chat room to grant permissions for
* @param userAddress user to grant permissions to
* @param mode mode to grant
*/
public void grant(final ChatRoomIrcImpl chatRoom, final String userAddress,
final Mode mode)
{
if (!this.connectionState.isConnected())
{
throw new IllegalStateException("Not connected to an IRC server.");
}
if (mode.getRole() == null)
{
throw new IllegalArgumentException(
"This mode does not modify user permissions.");
}
this.irc.changeMode(chatRoom.getIdentifier() + " +" + mode.getSymbol()
+ " " + userAddress);
}
/**
* Revoke user permissions of chat room for user.
*
* @param chatRoom chat room
* @param userAddress user
* @param mode mode
*/
public void revoke(final ChatRoomIrcImpl chatRoom,
final String userAddress, final Mode mode)
{
if (!this.connectionState.isConnected())
{
throw new IllegalStateException("Not connected to an IRC server.");
}
if (mode.getRole() == null)
{
throw new IllegalArgumentException(
"This mode does not modify user permissions.");
}
this.irc.changeMode(chatRoom.getIdentifier() + " -" + mode.getSymbol()
+ " " + userAddress);
}
/**
* Ban chat room member.
*
* @param chatroom chat room to ban from
* @param member member to ban
* @param reason reason for banning
* @throws OperationFailedException throws operation failed in case of
* trouble.
*/
public void banParticipant(final ChatRoomIrcImpl chatroom,
final ChatRoomMemberIrcImpl member, final String reason)
throws OperationFailedException
{
if (!this.connectionState.isConnected())
{
return;
}
kickParticipant(chatroom, member, reason);
this.irc.changeMode(String.format("%s +b %s!%s@%s",
chatroom.getIdentifier(), "*",
member.getIdent(), member.getHostname()));
}
/**
* Kick channel member.
*
* @param chatroom channel to kick from
* @param member member to kick
* @param reason kick message to deliver
*/
public void kickParticipant(final ChatRoomIrcImpl chatroom,
final ChatRoomMember member, final String reason)
{
if (!this.connectionState.isConnected())
{
return;
}
if (this.isupportKickLen != null
&& reason.length() > this.isupportKickLen)
{
throw new IllegalArgumentException("the kick reason must not be "
+ "longer than " + this.isupportKickLen.intValue()
+ " characters according to server parameters.");
}
this.irc.kick(chatroom.getIdentifier(), member.getContactAddress(),
reason);
}
/**
* Issue invite command to IRC server.
*
* @param memberId member to invite
* @param chatroom channel to invite to
*/
public void invite(final String memberId, final ChatRoomIrcImpl chatroom)
{
if (!this.connectionState.isConnected())
{
throw new IllegalStateException("Not connected to an IRC server.");
}
this.irc.rawMessage("INVITE " + memberId + " "
+ chatroom.getIdentifier());
}
/**
* Convert a member mode character to a ChatRoomMemberRole instance.
*
* @param modeSymbol The member mode character.
* @return Return the instance of ChatRoomMemberRole corresponding to the
* member mode character.
* @throws UnknownModeException returns UnknownModeException in case unknown
* mode is encountered
*/
private static ChatRoomMemberRole convertMemberMode(final char modeSymbol)
throws UnknownModeException
{
return Mode.bySymbol(modeSymbol).getRole();
}
/**
* The channel manager listener. This listener is used for any events that
* are not directly related to an open, managed chat room. This includes
* events signaling that a channel has been joined on initiative of the IRC
* server, such that it isn't managed yet.
*
* @author Danny van Heumen
*/
private final class ManagerListener extends VariousMessageListenerAdapter
{
/**
* IRC reply code for end of list.
*/
private static final int RPL_LISTEND =
IRCServerNumerics.CHANNEL_NICKS_END_OF_LIST;
/**
* Quit message event.
*
* @param msg QuitMessage
*/
@Override
public void onUserQuit(final QuitMessage msg)
{
final String user = msg.getSource().getNick();
if (ChannelManager.this.connectionState.getNickname().equals(user))
{
LOGGER.debug("Local user QUIT message received: removing "
+ "channel manager listener.");
ChannelManager.this.irc.deleteListener(this);
}
}
/**
* In case a fatal error occurs, remove the ChannelManager listener.
*/
@Override
public void onError(final ErrorMessage aMsg)
{
// Errors signal fatal situation, so unregister and assume
// connection lost.
LOGGER.debug("Local user received ERROR message: removing "
+ "channel manager listener.");
ChannelManager.this.irc.deleteListener(this);
}
/**
* Server numeric message.
*
* @param msg server numeric message
*/
@Override
public void onServerNumericMessage(final ServerNumericMessage msg)
{
switch (msg.getNumericCode().intValue())
{
case RPL_LISTEND:
// CHANNEL_NICKS_END_OF_LIST indicates the end of a nick list as
// you will receive when joining a channel. This is used as the
// indicator that we have joined a channel. Now we have to
// determine whether or not we already know about this
// particular join attempt. If not, we continue to inform Jitsi
// and to create a listener for this new chat room.
final String text = msg.getText();
final String channelName = text.substring(0, text.indexOf(' '));
final ChatRoomIrcImpl chatRoom;
final IRCChannel channel;
synchronized (ChannelManager.this.joined)
{
// Synchronize the section that checks then adds a chat
// room. This way we can be sure that there are no 2
// simultaneous creation events.
if (ChannelManager.this.joined.containsKey(channelName))
{
LOGGER.trace("Chat room '" + channelName
+ "' join event was announced or already "
+ "finished. Stop handling this event.");
break;
}
// We aren't currently attempting to join, so this join is
// unannounced.
LOGGER.trace("Starting unannounced join of chat room '"
+ channelName);
// Assuming that at the time that NICKS_END_OF_LIST is
// propagated, the channel join event has been completely
// handled by IRCApi.
channel =
ChannelManager.this.connectionState
.getChannelByName(channelName);
chatRoom =
new ChatRoomIrcImpl(channelName,
ChannelManager.this.provider);
ChannelManager.this.joined.put(channelName, chatRoom);
}
ChannelManager.this.irc.addListener(new ChatRoomListener(
chatRoom));
try
{
ChannelManager.this.provider.getMUC().openChatRoomWindow(
chatRoom);
}
catch (NullPointerException e)
{
LOGGER.error("failed to open chat room window", e);
}
ChannelManager.this.prepareChatRoom(chatRoom, channel);
ChannelManager.this.provider.getMUC()
.fireLocalUserPresenceEvent(chatRoom,
LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_JOINED,
null);
LOGGER.trace("Unannounced join of chat room '" + channelName
+ "' completed.");
break;
default:
break;
}
}
}
/**
* A chat room listener.
*
* A chat room listener is registered for each chat room that we join. The
* chat room listener updates chat room data and fires events based on IRC
* messages that report state changes for the specified channel.
*
* @author Danny van Heumen
*/
private final class ChatRoomListener
extends VariousMessageListenerAdapter
{
/**
* IRC error code for case when user cannot send a message to the
* channel, for example when this channel is moderated and user does not
* have VOICE (+v).
*/
private static final int IRC_ERR_CANNOTSENDTOCHAN = 404;
/**
* IRC error code for case where user is not joined to that channel.
*/
private static final int IRC_ERR_NOTONCHANNEL = 442;
/**
* Chat room for which this listener is working.
*/
private final ChatRoomIrcImpl chatroom;
/**
* Constructor. Instantiate listener for the provided chat room.
*
* @param chatroom the chat room
*/
private ChatRoomListener(final ChatRoomIrcImpl chatroom)
{
if (chatroom == null)
{
throw new IllegalArgumentException("chatroom cannot be null");
}
this.chatroom = chatroom;
}
/**
* Event in case of topic change.
*
* @param msg topic change message
*/
@Override
public void onTopicChange(final TopicMessage msg)
{
if (!isThisChatRoom(msg.getChannelName()))
{
return;
}
this.chatroom.updateSubject(msg.getTopic().getValue());
}
/**
* Event in case of channel mode changes.
*
* @param msg channel mode message
*/
@Override
public void onChannelMode(final ChannelModeMessage msg)
{
if (!isThisChatRoom(msg.getChannelName()))
{
return;
}
processModeMessage(msg);
}
/**
* Event in case of channel join message.
*
* @param msg channel join message
*/
@Override
public void onChannelJoin(final ChanJoinMessage msg)
{
if (!isThisChatRoom(msg.getChannelName()))
{
return;
}
final String user = msg.getSource().getNick();
final String ident = msg.getSource().getIdent();
final String host = msg.getSource().getHostname();
final ChatRoomMemberIrcImpl member =
new ChatRoomMemberIrcImpl(ChannelManager.this.provider,
this.chatroom, user, ident, host,
ChatRoomMemberRole.SILENT_MEMBER);
this.chatroom.fireMemberPresenceEvent(member, null,
ChatRoomMemberPresenceChangeEvent.MEMBER_JOINED, null);
}
/**
* Event in case of channel part.
*
* @param msg channel part message
*/
@Override
public void onChannelPart(final ChanPartMessage msg)
{
if (!isThisChatRoom(msg.getChannelName()))
{
return;
}
final IRCUser user = msg.getSource();
if (isMe(user))
{
leaveChatRoom();
return;
}
final String userNick = msg.getSource().getNick();
final ChatRoomMember member =
this.chatroom.getChatRoomMember(userNick);
if (member != null)
{
// When the account has been disabled, the chat room may return
// null. If that is NOT the case, continue handling.
try
{
this.chatroom.fireMemberPresenceEvent(member, null,
ChatRoomMemberPresenceChangeEvent.MEMBER_LEFT,
msg.getPartMsg());
}
catch (NullPointerException e)
{
LOGGER.warn(
"This should not have happened. Please report this "
+ "as it is a bug.", e);
}
}
}
/**
* Some of the generic message are relevant to us, so keep an eye on
* general numeric messages.
*
* @param msg IRC server numeric message
*/
public void onServerNumericMessage(final ServerNumericMessage msg)
{
final Integer code = msg.getNumericCode();
if (code == null)
{
return;
}
final String raw = msg.getText();
switch (code.intValue())
{
case IRC_ERR_NOTONCHANNEL:
final String channel = raw.substring(0, raw.indexOf(" "));
if (isThisChatRoom(channel))
{
LOGGER
.warn("Just discovered that we are no longer joined to "
+ "channel "
+ channel
+ ". Leaving quietly. (This is most likely due to a"
+ " bug in the implementation.)");
// If for some reason we missed the message that we aren't
// joined (anymore) to this particular chat room, correct
// our problem ASAP.
leaveChatRoom();
}
break;
case IRC_ERR_CANNOTSENDTOCHAN:
final String cannotSendChannel =
raw.substring(0, raw.indexOf(" "));
if (isThisChatRoom(cannotSendChannel))
{
final MessageIrcImpl message =
new MessageIrcImpl("", "text/plain", "UTF-8", null);
this.chatroom.fireMessageDeliveryFailedEvent(
ChatRoomMessageDeliveryFailedEvent.FORBIDDEN,
"This channel is moderated.", new Date(), message);
}
break;
default:
break;
}
}
/**
* Event in case of channel kick.
*
* @param msg channel kick message
*/
@Override
public void onChannelKick(final ChannelKick msg)
{
if (!isThisChatRoom(msg.getChannelName()))
{
return;
}
if (!ChannelManager.this.connectionState.isConnected())
{
LOGGER.error("Not currently connected to IRC Server. "
+ "Aborting message handling.");
return;
}
final String kickedUser = msg.getKickedNickname();
final ChatRoomMember kickedMember =
this.chatroom.getChatRoomMember(kickedUser);
final String user = msg.getSource().getNick();
if (kickedMember != null)
{
ChatRoomMember kicker = this.chatroom.getChatRoomMember(user);
this.chatroom.fireMemberPresenceEvent(kickedMember, kicker,
ChatRoomMemberPresenceChangeEvent.MEMBER_KICKED,
msg.getText());
}
if (isMe(kickedUser))
{
LOGGER.debug(
"Local user is kicked. Removing chat room listener.");
ChannelManager.this.irc.deleteListener(this);
ChannelManager.this.joined
.remove(this.chatroom.getIdentifier());
ChannelManager.this.provider.getMUC()
.fireLocalUserPresenceEvent(this.chatroom,
LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_KICKED,
msg.getText());
}
}
/**
* Event in case of user quit.
*
* @param msg user quit message
*/
@Override
public void onUserQuit(final QuitMessage msg)
{
String user = msg.getSource().getNick();
if (user == null)
{
return;
}
if (isMe(user))
{
LOGGER.debug("Local user QUIT message received: removing chat "
+ "room listener.");
ChannelManager.this.irc.deleteListener(this);
return;
}
final ChatRoomMember member = this.chatroom.getChatRoomMember(user);
if (member != null)
{
this.chatroom.fireMemberPresenceEvent(member, null,
ChatRoomMemberPresenceChangeEvent.MEMBER_QUIT,
msg.getQuitMsg());
}
}
/**
* In case a fatal error occurs, remove the ChatRoomListener.
*/
@Override
public void onError(final ErrorMessage aMsg)
{
// Errors signal fatal situation, so unregister and assume
// connection lost.
LOGGER.debug("Local user received ERROR message: removing "
+ "chat room listener.");
ChannelManager.this.irc.deleteListener(this);
}
/**
* Event in case of nick change.
*
* @param msg nick change message
*/
@Override
public void onNickChange(final NickMessage msg)
{
if (msg == null)
{
return;
}
final String oldNick = msg.getSource().getNick();
final String newNick = msg.getNewNick();
final ChatRoomMemberIrcImpl member =
(ChatRoomMemberIrcImpl) this.chatroom
.getChatRoomMember(oldNick);
if (member != null)
{
member.setName(newNick);
this.chatroom.updateChatRoomMemberName(oldNick);
ChatRoomMemberPropertyChangeEvent evt =
new ChatRoomMemberPropertyChangeEvent(member,
this.chatroom,
ChatRoomMemberPropertyChangeEvent.MEMBER_NICKNAME,
oldNick, newNick);
this.chatroom.fireMemberPropertyChangeEvent(evt);
}
}
/**
* Event in case of channel message arrival.
*
* @param msg channel message
*/
@Override
public void onChannelMessage(final ChannelPrivMsg msg)
{
if (!isThisChatRoom(msg.getChannelName()))
{
return;
}
final MessageIrcImpl message =
MessageIrcImpl.newMessageFromIRC(msg.getText());
final ChatRoomMemberIrcImpl member =
new ChatRoomMemberIrcImpl(ChannelManager.this.provider,
this.chatroom, msg.getSource().getNick(), msg.getSource()
.getIdent(), msg.getSource().getHostname(),
ChatRoomMemberRole.MEMBER);
this.chatroom.fireMessageReceivedEvent(message, member, new Date(),
ChatRoomMessageReceivedEvent.CONVERSATION_MESSAGE_RECEIVED);
}
/**
* Event in case of channel action message arrival.
*
* @param msg channel action message
*/
@Override
public void onChannelAction(final ChannelActionMsg msg)
{
if (!isThisChatRoom(msg.getChannelName()))
{
return;
}
String userNick = msg.getSource().getNick();
ChatRoomMemberIrcImpl member =
new ChatRoomMemberIrcImpl(ChannelManager.this.provider,
this.chatroom, userNick, msg.getSource().getIdent(), msg
.getSource().getHostname(), ChatRoomMemberRole.MEMBER);
MessageIrcImpl message =
MessageIrcImpl.newActionFromIRC(member, msg.getText());
this.chatroom.fireMessageReceivedEvent(message, member, new Date(),
ChatRoomMessageReceivedEvent.CONVERSATION_MESSAGE_RECEIVED);
}
/**
* Event in case of channel notice message arrival.
*
* @param msg channel notice message
*/
@Override
public void onChannelNotice(final ChannelNotice msg)
{
if (!isThisChatRoom(msg.getChannelName()))
{
return;
}
final String userNick = msg.getSource().getNick();
final ChatRoomMemberIrcImpl member =
new ChatRoomMemberIrcImpl(ChannelManager.this.provider,
this.chatroom, userNick, msg.getSource().getIdent(), msg
.getSource().getHostname(), ChatRoomMemberRole.MEMBER);
final MessageIrcImpl message =
MessageIrcImpl.newNoticeFromIRC(member, msg.getText());
this.chatroom.fireMessageReceivedEvent(message, member, new Date(),
ChatRoomMessageReceivedEvent.CONVERSATION_MESSAGE_RECEIVED);
}
/**
* Leave this chat room.
*/
private void leaveChatRoom()
{
ChannelManager.this.irc.deleteListener(this);
ChannelManager.this.joined.remove(this.chatroom.getIdentifier());
LOGGER.debug("Leaving chat room " + this.chatroom.getIdentifier()
+ ". Chat room listener removed.");
ChannelManager.this.provider.getMUC().fireLocalUserPresenceEvent(
this.chatroom,
LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_LEFT, null);
}
/**
* Process mode changes.
*
* @param msg raw mode message
*/
private void processModeMessage(final ChannelModeMessage msg)
{
final ChatRoomMemberIrcImpl source = extractChatRoomMember(msg);
final ModeParser parser = new ModeParser(msg.getModeStr());
for (ModeEntry mode : parser.getModes())
{
switch (mode.getMode())
{
case OWNER:
case OPERATOR:
case HALFOP:
case VOICE:
processRoleChange(source, mode);
break;
case LIMIT:
processLimitChange(source, mode);
break;
case BAN:
processBanChange(source, mode);
break;
case UNKNOWN:
if (LOGGER.isInfoEnabled())
{
LOGGER.info("Unknown mode: "
+ (mode.isAdded() ? "+" : "-")
+ mode.getParams()[0] + ". Original mode string: '"
+ msg.getModeStr() + "'");
}
break;
default:
if (LOGGER.isInfoEnabled())
{
LOGGER.info("Unsupported mode '"
+ (mode.isAdded() ? "+" : "-") + mode.getMode()
+ "' (from modestring '" + msg.getModeStr() + "')");
}
break;
}
}
}
/**
* Process changes for ban patterns.
*
* @param sourceMember the originating member
* @param mode the ban mode change
*/
private void processBanChange(final ChatRoomMemberIrcImpl sourceMember,
final ModeEntry mode)
{
final MessageIrcImpl banMessage =
new MessageIrcImpl(
"channel ban mask was "
+ (mode.isAdded() ? "added" : "removed")
+ ": "
+ mode.getParams()[0]
+ " by "
+ (sourceMember.getContactAddress().length() == 0
? "server"
: sourceMember.getContactAddress()),
MessageIrcImpl.DEFAULT_MIME_TYPE,
MessageIrcImpl.DEFAULT_MIME_ENCODING, null);
this.chatroom.fireMessageReceivedEvent(banMessage, sourceMember,
new Date(),
ChatRoomMessageReceivedEvent.SYSTEM_MESSAGE_RECEIVED);
}
/**
* Process mode changes resulting in role manipulation.
*
* @param sourceMember the originating member
* @param mode the mode change
*/
private void processRoleChange(
final ChatRoomMemberIrcImpl sourceMember, final ModeEntry mode)
{
final String targetNick = mode.getParams()[0];
final ChatRoomMemberIrcImpl targetMember =
(ChatRoomMemberIrcImpl) this.chatroom
.getChatRoomMember(targetNick);
final ChatRoomMemberRole originalRole = targetMember.getRole();
if (mode.isAdded())
{
targetMember.addRole(mode.getMode().getRole());
}
else
{
targetMember.removeRole(mode.getMode().getRole());
}
final ChatRoomMemberRole newRole = targetMember.getRole();
if (newRole != originalRole)
{
// Mode change actually caused a role change.
final ChatRoomLocalUserRoleChangeEvent event =
new ChatRoomLocalUserRoleChangeEvent(this.chatroom,
originalRole, newRole, false);
if (isMe(targetMember.getContactAddress()))
{
this.chatroom.fireLocalUserRoleChangedEvent(event);
}
else
{
this.chatroom.fireMemberRoleEvent(targetMember,
newRole);
}
}
else
{
// Mode change did not cause an immediate role change.
// Display a system message for the mode change.
final String text =
sourceMember.getName()
+ (mode.isAdded() ? " gives "
+ mode.getMode().name().toLowerCase()
+ " to " : " removes "
+ mode.getMode().name().toLowerCase()
+ " from ") + targetMember.getName();
final MessageIrcImpl message =
new MessageIrcImpl(text,
MessageIrcImpl.DEFAULT_MIME_TYPE,
MessageIrcImpl.DEFAULT_MIME_ENCODING, null);
this.chatroom
.fireMessageReceivedEvent(
message,
sourceMember,
new Date(),
ChatRoomMessageReceivedEvent.SYSTEM_MESSAGE_RECEIVED);
}
}
/**
* Process mode change that represents a channel limit modification.
*
* @param sourceMember the originating member
* @param mode the limit mode change
*/
private void processLimitChange(
final ChatRoomMemberIrcImpl sourceMember, final ModeEntry mode)
{
final MessageIrcImpl limitMessage;
if (mode.isAdded())
{
try
{
limitMessage =
new MessageIrcImpl(
"channel limit set to "
+ Integer.parseInt(mode.getParams()[0])
+ " by "
+ (sourceMember.getContactAddress()
.length() == 0
? "server"
: sourceMember.getContactAddress()),
"text/plain", "UTF-8", null);
}
catch (NumberFormatException e)
{
LOGGER.warn("server sent incorrect limit: "
+ "limit is not a number", e);
return;
}
}
else
{
// TODO "server" is now easily fakeable if someone
// calls himself server. There should be some other way
// to represent the server if a message comes from
// something other than a normal chat room member.
limitMessage =
new MessageIrcImpl(
"channel limit removed by "
+ (sourceMember.getContactAddress().length() == 0
? "server"
: sourceMember.getContactAddress()),
"text/plain", "UTF-8", null);
}
this.chatroom.fireMessageReceivedEvent(limitMessage, sourceMember,
new Date(),
ChatRoomMessageReceivedEvent.SYSTEM_MESSAGE_RECEIVED);
}
/**
* Extract chat room member identifier from message.
*
* @param msg raw mode message
* @return returns member instance
*/
private ChatRoomMemberIrcImpl extractChatRoomMember(
final ChannelModeMessage msg)
{
ChatRoomMemberIrcImpl member;
ISource source = msg.getSource();
if (source instanceof IRCServer)
{
// TODO Created chat room member with creepy empty contact ID.
// Interacting with this contact might screw up other sections
// of code which is not good. Is there a better way to represent
// an IRC server as a chat room member?
member =
new ChatRoomMemberIrcImpl(ChannelManager.this.provider,
this.chatroom, "", "", "",
ChatRoomMemberRole.ADMINISTRATOR);
}
else if (source instanceof IRCUser)
{
String nick = ((IRCUser) source).getNick();
member =
(ChatRoomMemberIrcImpl) this.chatroom
.getChatRoomMember(nick);
}
else
{
throw new IllegalArgumentException("Unknown source type: "
+ source.getClass().getName());
}
return member;
}
/**
* Test whether this listener corresponds to the chat room.
*
* @param chatRoomName chat room name
* @return returns true if this listener applies, false otherwise
*/
private boolean isThisChatRoom(final String chatRoomName)
{
return this.chatroom.getIdentifier().equalsIgnoreCase(chatRoomName);
}
/**
* Test whether the source user is this user.
*
* @param user the source user
* @return returns true if this use, or false otherwise
*/
private boolean isMe(final IRCUser user)
{
return isMe(user.getNick());
}
/**
* Test whether the user nick is this user.
*
* @param name nick of the user
* @return returns true if so, false otherwise
*/
private boolean isMe(final String name)
{
final String userNick =
ChannelManager.this.connectionState.getNickname();
if (userNick == null)
{
return false;
}
return userNick.equals(name);
}
}
}