package de.skuzzle.polly.core.internal.irc;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.apache.log4j.Logger;
import org.jibble.pircbot.IrcException;
import org.jibble.pircbot.NickAlreadyInUseException;
import org.jibble.pircbot.PircBot;
import org.jibble.pircbot.User;
import de.skuzzle.jeve.EventProvider;
import de.skuzzle.polly.sdk.AbstractDisposable;
import de.skuzzle.polly.sdk.Configuration;
import de.skuzzle.polly.sdk.Disposable;
import de.skuzzle.polly.sdk.IrcManager;
import de.skuzzle.polly.sdk.eventlistener.ChannelEvent;
import de.skuzzle.polly.sdk.eventlistener.ChannelModeEvent;
import de.skuzzle.polly.sdk.eventlistener.ChannelModeListener;
import de.skuzzle.polly.sdk.eventlistener.ConnectionEvent;
import de.skuzzle.polly.sdk.eventlistener.ConnectionListener;
import de.skuzzle.polly.sdk.eventlistener.IrcUser;
import de.skuzzle.polly.sdk.eventlistener.JoinPartListener;
import de.skuzzle.polly.sdk.eventlistener.MessageEvent;
import de.skuzzle.polly.sdk.eventlistener.MessageListener;
import de.skuzzle.polly.sdk.eventlistener.MessageSendListener;
import de.skuzzle.polly.sdk.eventlistener.MessageType;
import de.skuzzle.polly.sdk.eventlistener.NickChangeEvent;
import de.skuzzle.polly.sdk.eventlistener.NickChangeListener;
import de.skuzzle.polly.sdk.eventlistener.OwnMessageEvent;
import de.skuzzle.polly.sdk.eventlistener.QuitEvent;
import de.skuzzle.polly.sdk.eventlistener.QuitListener;
import de.skuzzle.polly.sdk.eventlistener.SpotEvent;
import de.skuzzle.polly.sdk.eventlistener.UserSpottedListener;
import de.skuzzle.polly.sdk.exceptions.DisposingException;
import de.skuzzle.polly.tools.iterators.WrapIterator;
/**
*
* @author Simon
* @version 27.07.2011 ae73250
*/
public class IrcManagerImpl extends AbstractDisposable implements IrcManager, Disposable {
private static Logger logger = Logger.getLogger(IrcManagerImpl.class.getName());
private PircBot bot = new PircBot() {
@Override
protected void onConnect() {
IrcManagerImpl.this.fireConnectionEstablished(
new ConnectionEvent(IrcManagerImpl.this));
};
protected void onDisconnect() {
IrcManagerImpl.this.onlineUsers.clear();
IrcManagerImpl.this.topics.clear();
IrcManagerImpl.this.retry();
};
@Override
protected void onNickChange(String oldNick, String login, String hostname,
String newNick) {
String oldIrcName = IrcManagerImpl.this.stripNickname(oldNick);
String newIrcName = IrcManagerImpl.this.stripNickname(newNick);
IrcUser oldUser = new IrcUser(oldIrcName, login, hostname);
IrcUser newUser = new IrcUser(newIrcName, login, hostname);
NickChangeEvent e = new NickChangeEvent(
IrcManagerImpl.this, oldUser, newUser);
synchronized (IrcManagerImpl.this.onlineUsers) {
IrcManagerImpl.this.onlineUsers.remove(oldIrcName);
IrcManagerImpl.this.onlineUsers.add(newIrcName);
}
IrcManagerImpl.this.fireNickChange(e);
}
@Override
protected void onJoin(String channel, String sender, String login,
String hostname) {
String nickName = IrcManagerImpl.this.stripNickname(sender);
IrcUser user = new IrcUser(nickName, login, hostname);
ChannelEvent e = new ChannelEvent(IrcManagerImpl.this, user, channel);
this.checkUserSpotted(channel, nickName, SpotEvent.USER_JOINED);
IrcManagerImpl.this.fireJoin(e);
}
@Override
protected synchronized void onPart(String channel, String sender, String login,
String hostname) {
String nickName = IrcManagerImpl.this.stripNickname(sender);
IrcUser user = new IrcUser(nickName, login, hostname);
ChannelEvent e = new ChannelEvent(IrcManagerImpl.this, user, channel);
this.checkUserLost(channel, user);
// XXX: not sure if this makes sense or not
IrcManagerImpl.this.topics.remove(channel);
IrcManagerImpl.this.firePart(e);
}
@Override
protected void onKick(String channel, String kickerNick, String kickerLogin,
String kickerHostname, String recipientNick, String reason) {
IrcUser user = new IrcUser(recipientNick, "", "");
this.checkUserLost(channel, user);
// CONSIDER: add kick event to polly api
};
private void checkUserLost(String channel, IrcUser user) {
/* ISSUE: 0000002 && 0000026*/
boolean known = false;
for (String c : this.getChannels()) {
// HACK: continue on own channel as pircbot may not have removed the user
// from its data structure
if (c.equals(channel)) {
continue;
}
for (User u : this.getUsers(c)) {
String uStripped = IrcManagerImpl.this.stripNickname(u.getNick());
if (uStripped.equals(user.getNickName())) {
known = true;
break;
}
}
}
if (!known) {
SpotEvent e1 = new SpotEvent(IrcManagerImpl.this, user, channel,
SpotEvent.USER_PART);
IrcManagerImpl.this.onlineUsers.remove(user.getNickName());
IrcManagerImpl.this.fireUserLost(e1);
}
}
@Override
protected void onQuit(String sourceNick, String sourceLogin,
String sourceHostname, String reason) {
String nickName = IrcManagerImpl.this.stripNickname(sourceNick);
IrcUser user = new IrcUser(nickName, sourceLogin, sourceHostname);
QuitEvent e = new QuitEvent(IrcManagerImpl.this, user, reason);
SpotEvent e1 = new SpotEvent(e);
IrcManagerImpl.this.onlineUsers.remove(nickName);
IrcManagerImpl.this.fireQuit(e);
IrcManagerImpl.this.fireUserLost(e1);
}
@Override
protected void onMessage(String channel, String sender, String login,
String hostname, String message) {
String nickName = IrcManagerImpl.this.stripNickname(sender);
IrcUser user = new IrcUser(nickName, login, hostname);
MessageEvent e = new MessageEvent(IrcManagerImpl.this, user,
MessageType.PUBLIC, channel, message);
IrcManagerImpl.this.firePublicMessageEvent(e);
}
protected void onNotice(String sourceNick, String sourceLogin,
String sourceHostname, String target, String notice) {
String nickName = IrcManagerImpl.this.stripNickname(sourceNick);
IrcUser user = new IrcUser(nickName, sourceLogin, sourceHostname);
MessageEvent e = new MessageEvent(IrcManagerImpl.this, user,
MessageType.NOTICE, target, notice);
IrcManagerImpl.this.fireNoticeMessageEvent(e);
};
@Override
protected void onPrivateMessage(String sender, String login, String hostname,
String message) {
String nickName = IrcManagerImpl.this.stripNickname(sender);
IrcUser user = new IrcUser(nickName, login, hostname);
MessageEvent e = new MessageEvent(IrcManagerImpl.this, user,
MessageType.PRIVATE, nickName, message);
this.checkUserSpotted(nickName, nickName, SpotEvent.PRIVATE_MSG);
IrcManagerImpl.this.firePrivateMessageEvent(e);
}
@Override
protected void onAction(String sender, String login,
String hostname, String target, String action) {
String nickName = IrcManagerImpl.this.stripNickname(sender);
IrcUser user = new IrcUser(nickName, login, hostname);
MessageEvent e = new MessageEvent(IrcManagerImpl.this, user,
MessageType.ACTION, target, action);
IrcManagerImpl.this.fireActionMessageEvent(e);
}
@Override
protected void onMode(String channel, String sourceNick, String sourceLogin,
String sourceHostname, String mode) {
String nickName = IrcManagerImpl.this.stripNickname(sourceNick);
IrcUser user = new IrcUser(nickName, sourceLogin, sourceHostname);
ChannelModeEvent e = new ChannelModeEvent(IrcManagerImpl.this, user,
channel, mode);
IrcManagerImpl.this.fireChannelModeEvent(e);
};
protected void onUserList(String channel, org.jibble.pircbot.User[] users) {
for (int i = 0; i < users.length; ++i) {
String nickName = IrcManagerImpl.this.stripNickname(users[i].getNick());
this.checkUserSpotted(channel, nickName, SpotEvent.USER_JOINED);
}
}
private void checkUserSpotted(String channel, String nickName, int type) {
synchronized (IrcManagerImpl.this.onlineUsers) {
if (IrcManagerImpl.this.onlineUsers.add(nickName)) {
IrcUser user = new IrcUser(nickName, channel, "");
SpotEvent e = new SpotEvent(IrcManagerImpl.this, user, channel,
type);
IrcManagerImpl.this.fireUserSpotted(e);
}
}
}
protected void onChannelInfo(String channel, int userCount, String topic) {
IrcManagerImpl.this.topics.put(channel, topic);
}
};
private EventProvider eventProvider;
private Set<String> onlineUsers;
private Map<String, String> topics;
private Configuration config;
private boolean disconnect;
private BotConnectionSettings recent;
private MessageScheduler messageScheduler;
public IrcManagerImpl(String ircName, EventProvider eventProvider,
Configuration config, String encodingName) {
this.config = config;
this.onlineUsers = Collections.synchronizedSet(new TreeSet<String>());
this.topics = new HashMap<String, String>();
this.eventProvider = eventProvider;
this.bot.changeNick(ircName);
try {
this.bot.setEncoding(encodingName);
} catch (UnsupportedEncodingException e) {
logger.error("Encoding exception", e);
}
// Message thread has its own delay
this.bot.setMessageDelay(0);
/*
* IRC Verbose output
*/
this.bot.setVerbose(false);
this.messageScheduler = new RoundRobinScheduler(this,
config.readInt(Configuration.MESSAGE_DELAY));
this.messageScheduler.start();
}
private String stripNickname(String nickName) {
// HACK: this method is a relict, but i did not dare to remove it completely
return nickName;
}
public void connect(BotConnectionSettings e)
throws NickAlreadyInUseException, IOException, IrcException {
if (this.isConnected()) {
return;
}
int port = e.getPort();
logger.info("Connecting to " + e.getHostName() + ":" + port);
this.bot.setAutoNickChange(true);
this.bot.connect(e.getHostName(), e.getPort());
this.bot.sendMessage("nickserv", "ghost " + e.getNickName() + " " + e.getIdentity());
this.setAndIdentifyDefaultNickname();
if (this.config.readBoolean(Configuration.JOIN_ON_CONNECT)) {
this.joinChannels(
e.getChannels().toArray(new String[e.getChannels().size()]));
}
this.recent = e;
this.sendRawCommand("MODE " + e.getNickName() + " " + e.getModes());
}
@Override
public void reconnect() throws IOException {
this.disconnect();
final long ts = System.currentTimeMillis();
while (this.isConnected() && System.currentTimeMillis() - ts < 5000);
if (this.isConnected()) {
throw new IOException("could not disconnect");
}
try {
this.connect(this.recent);
} catch (NickAlreadyInUseException e) {
e.printStackTrace();
throw new IOException(e);
} catch (IrcException e) {
throw new IOException(e);
}
}
@Override
public void quit(String message) {
if (this.isConnected()) {
this.disconnect = true;
this.bot.quitServer(message);
}
}
@Override
public void quit() {
this.quit("");
}
@Override
public boolean isOnline(String nickName) {
synchronized (this.onlineUsers) {
return this.onlineUsers.contains(nickName);
}
}
@Override
public boolean isOnlineIgnoreCase(String nickName) {
synchronized (this.onlineUsers) {
for (String s : this.onlineUsers) {
if (s.equalsIgnoreCase(nickName)) {
return true;
}
}
}
return false;
}
@Override
public List<String> getChannelUser(String channel) {
Set<String> result = new HashSet<String>();
User[] users = this.bot.getUsers(channel);
for (User user : users) {
result.add(this.stripNickname(user.getNick()));
}
// ISSUE: 0000037
// HACK: first, add user to set, then create list from set so that it contains
// no duplicates
// CONSIDER: Make return Value Set<String>
return new LinkedList<String>(result);
}
@Override
public Set<String> getOnlineUsers() {
return this.onlineUsers;
}
@Override
public void disconnect() {
this.disconnect = true;
this.bot.disconnect();
}
@Override
public boolean isConnected() {
return this.bot.isConnected();
}
@Override
public List<String> getChannels() {
return Arrays.asList(this.bot.getChannels());
}
@Override
public boolean isOnChannel(String channel, String nickName) {
User[] users = this.bot.getUsers(channel);
for (User user : users) {
if (this.stripNickname(user.getNick()).equalsIgnoreCase(nickName)) {
return true;
}
}
return false;
}
@Override
public void rejoinDefaultChannels() {
this.joinChannels(this.recent.getChannels().toArray(
new String[this.recent.getChannels().size()]));
}
@Override
public void joinChannel(String channel, String password) {
if (!channel.startsWith("#")) {
throw new IllegalArgumentException("Channel must be preceded with '#'");
}
if (this.isConnected()) {
this.bot.joinChannel(channel, password);
}
}
@Override
public void joinChannels(String...channels) {
for (String channel : channels) {
this.joinChannel(channel, "");
}
}
@Override
public void partChannel(String channel, String message) {
if (this.isConnected()) {
this.bot.partChannel(channel, message);
}
}
@Override
public void kick(String channel, String nickName, String reason) {
logger.info("Kicking user " + nickName + " from " + channel +
". Reason: " + reason);
this.bot.kick(channel, nickName, reason);
}
@Override
public void op(String channel, String nickName) {
this.bot.op(channel, nickName);
}
@Override
public void deop(String channel, String nickName) {
this.bot.deOp(channel, nickName);
}
@Override
public void sendMessage(String channel, String message, Object source) {
final String[] lines = message.split("\n");
for (final String line : lines) {
this.messageScheduler.addMessage(channel, line, source);
}
OwnMessageEvent e = new OwnMessageEvent(this,
new IrcUser(this.getNickname(), "", ""), MessageType.OWN, channel, message, source);
this.fireMessageSend(e);
}
@Override
public void sendMessage(String channel, String message) {
if (this.isConnected()) {
for (String line : new WrapIterator(message,
this.config.readInt(Configuration.LINE_LENGTH))) {
this.bot.sendMessage(channel, line);
}
}
}
@Override
public synchronized void sendAction(String channel, String message) {
if (this.isConnected()) {
for (String line : new WrapIterator(message,
this.config.readInt(Configuration.LINE_LENGTH))) {
this.bot.sendAction(channel, line);
}
}
}
@Override
public void sendRawCommand(String command) {
if (this.isConnected()) {
this.bot.sendRawLineViaQueue(command);
}
}
@Override
public void setTopic(String channel, String topic) {
if (this.isConnected()) {
this.bot.setTopic(channel, topic);
}
}
@Override
public String getTopic(String channel) {
if (this.isConnected()) {
return this.topics.get(channel) == null ? "" : this.topics.get(channel);
}
return "";
}
@Override
public String getNickname() {
return this.bot.getNick();
}
@Override
public void setNickname(String nickname) {
this.bot.changeNick(nickname);
}
@Override
public void setAndIdentifyDefaultNickname() {
this.bot.changeNick(this.config.readString(Configuration.NICKNAME));
this.bot.identify(this.config.readString(Configuration.IDENT));
}
public void retry() {
this.fireConnectionLost(new ConnectionEvent(this, disconnect));
if (this.disconnect) {
return;
}
int attempts = 1;
logger.warn("IRC Connection lost. Trying to reconnect...");
while (!this.isConnected()) {
try {
this.connect(this.recent);
this.disconnect = false;
logger.info("IRC Connection reestablished.");
} catch (Exception e) {
logger.warn("Reconnect failed. Starting attempt " + ++attempts +"...", e);
try {
Thread.sleep(this.config.readInt(Configuration.RECONNECT_DELAY));
} catch (InterruptedException e1) {
logger.error("", e1);
}
}
}
}
@Override
public void addMessageSendListener(MessageSendListener listener) {
this.eventProvider.addListener(MessageSendListener.class, listener);
}
@Override
public void removeMessageSendListener(MessageSendListener listener) {
this.eventProvider.removeListener(MessageSendListener.class, listener);
}
@Override
public void addNickChangeListener(NickChangeListener listener) {
this.eventProvider.addListener(NickChangeListener.class, listener);
}
@Override
public void removeNickChangeListener(NickChangeListener listener) {
this.eventProvider.removeListener(NickChangeListener.class, listener);
}
@Override
public void addJoinPartListener(JoinPartListener listener) {
this.eventProvider.addListener(JoinPartListener.class, listener);
}
@Override
public void removeJoinPartListener(JoinPartListener listener) {
this.eventProvider.removeListener(JoinPartListener.class, listener);
}
@Override
public void addQuitListener(QuitListener listener) {
this.eventProvider.addListener(QuitListener.class, listener);
}
@Override
public void removeQuitListener(QuitListener listener) {
this.eventProvider.removeListener(QuitListener.class, listener);
}
@Override
public void addMessageListener(MessageListener listener) {
this.eventProvider.addListener(MessageListener.class, listener);
}
@Override
public void removeMessageListener(MessageListener listener) {
this.eventProvider.removeListener(MessageListener.class, listener);
}
@Override
public void addChannelModeListener(ChannelModeListener listener) {
this.eventProvider.addListener(ChannelModeListener.class, listener);
}
@Override
public void removeChannelModeListener(ChannelModeListener listener) {
this.eventProvider.removeListener(ChannelModeListener.class, listener);
}
@Override
public void addUserSpottedListener(UserSpottedListener listener) {
this.eventProvider.addListener(UserSpottedListener.class, listener);
}
@Override
public void removeUserSpottedListener(UserSpottedListener listener) {
this.eventProvider.removeListener(UserSpottedListener.class, listener);
}
@Override
public void addConnectionListener(ConnectionListener listener) {
this.eventProvider.addListener(ConnectionListener.class, listener);
}
@Override
public void removeConnectionListener(ConnectionListener listener) {
this.eventProvider.removeListener(ConnectionListener.class, listener);
}
protected void fireMessageSend(final OwnMessageEvent e) {
this.eventProvider.dispatch(MessageSendListener.class, e,
MessageSendListener::messageSent);
}
protected void fireNickChange(final NickChangeEvent e) {
this.eventProvider.dispatch(NickChangeListener.class, e,
NickChangeListener::nickChanged);
}
protected void fireJoin(final ChannelEvent e) {
this.eventProvider.dispatch(JoinPartListener.class, e,
JoinPartListener::channelJoined);
}
protected void firePart(final ChannelEvent e) {
this.eventProvider.dispatch(JoinPartListener.class, e,
JoinPartListener::channelParted);
}
protected void fireQuit(final QuitEvent e) {
this.eventProvider.dispatch(QuitListener.class, e,
QuitListener::quited);
}
protected void firePublicMessageEvent(final MessageEvent e) {
this.eventProvider.dispatch(MessageListener.class, e,
MessageListener::publicMessage);
}
protected void firePrivateMessageEvent(final MessageEvent e) {
this.eventProvider.dispatch(MessageListener.class, e,
MessageListener::privateMessage);
}
protected void fireActionMessageEvent(final MessageEvent e) {
this.eventProvider.dispatch(MessageListener.class, e,
MessageListener::actionMessage);
}
protected void fireNoticeMessageEvent(final MessageEvent e) {
this.eventProvider.dispatch(MessageListener.class, e,
MessageListener::noticeMessage);
}
protected void fireChannelModeEvent(final ChannelModeEvent e) {
this.eventProvider.dispatch(ChannelModeListener.class, e,
ChannelModeListener::channelModeChanged);
}
protected void fireUserSpotted(final SpotEvent e) {
this.eventProvider.dispatch(UserSpottedListener.class, e,
UserSpottedListener::userSpotted);
}
protected void fireUserLost(final SpotEvent e) {
this.eventProvider.dispatch(UserSpottedListener.class, e,
UserSpottedListener::userLost);
}
protected void fireConnectionEstablished(ConnectionEvent e) {
this.eventProvider.dispatch(ConnectionListener.class, e,
ConnectionListener::ircConnectionEstablished);
}
protected void fireConnectionLost(ConnectionEvent e) {
this.eventProvider.dispatch(ConnectionListener.class, e,
ConnectionListener::ircConnectionLost);
}
@Override
protected void actualDispose() throws DisposingException {
if (this.isConnected()) {
logger.debug("Closing irc connection using default quit message.");
this.quit();
} else {
logger.debug("Irc connection already closed.");
}
logger.trace("Shutting down message scheduler");
this.messageScheduler.dispose();
logger.trace("Shutting down irc bot.");
if (this.bot != null) {
this.bot.dispose();
}
}
}