/******************************************************************************
*
* Copyright 2014 Paphus Solutions Inc.
*
* Licensed under the Eclipse Public License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
******************************************************************************/
package org.botlibre.sense.chat;
import java.io.StringWriter;
import java.util.ArrayList;
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.logging.Level;
import org.botlibre.Bot;
import org.botlibre.api.knowledge.Network;
import org.botlibre.api.knowledge.Vertex;
import org.botlibre.knowledge.Primitive;
import org.botlibre.sense.BasicSense;
import org.botlibre.thought.language.Language;
import org.botlibre.thought.language.Language.LanguageState;
import org.botlibre.util.TextStream;
import org.relayirc.chatengine.Channel;
import org.relayirc.chatengine.ChannelEvent;
import org.relayirc.chatengine.ChannelListener;
import org.relayirc.chatengine.Server;
import org.relayirc.chatengine.ServerEvent;
import org.relayirc.chatengine.ServerListener;
/**
* Connect to and interact on IRC chat networks.
*/
public class IRC extends BasicSense {
public static int SLEEP = 1000 * 60 * 10; // 10 minutes.
public static int MAX_SPAM = 3;
public static int LAST_USERS = 5;
private String serverName = "irc.freenode.org";
private String channelName = "#ai";
private int port = 6667;
private String nick = "Bot01";
private String nickAlt = "Bot01_";
private String userName = "Bot01";
private String realName = "Bot01";
private boolean isConnected = false;
private Server server;
private Channel channel;
/**
* Keeps track of the current conversation.
*/
private Long conversation;
/** Keeps track of the users in the chat room. */
private Set<String> users;
/** Maps users to possible nick names. */
private Map<String, String> userNicks;
/** Keeps track of the last users to chat, to link response. */
private List<String> lastUsers;
/** Keeps track of spam messages. */
private Map<String, String> spamText;
/** Keeps track of spam message count. */
private Map<String, Integer> spamCount;
/** Defines number of message repeat to consider message spam. */
private int maxSpam = MAX_SPAM;
private List<ChannelListener> channelListeners = new ArrayList<ChannelListener>();
class IRCServerListener implements ServerListener {
public IRCServerListener() {
log("Connecting:", Bot.FINE, getServerName());
try {
setServer(new Server(getServerName(), getPort(), "n/a", "n/a"));
getServer().addServerListener(this);
getServer().connect(getNick(), getNickAlt(), getUserName(), getRealName());
// Check for a dead connection and reconnect.
Runnable connectionChecker = new Runnable() {
public void run() {
Server server = getServer();
while (isConnected()) {
try {
Thread.sleep(SLEEP);
} catch (Exception exception) {
// Ignore.
}
server = getServer();
if (server == null) {
break;
}
log("Ping:", Level.FINER, server.isConnected());
if (isConnected() && !server.isConnected() && (getServer() != null)) {
log("Connection lost, reconnecting", Bot.WARNING);
connect();
break;
}
}
}
};
Thread thread = new Thread(connectionChecker);
thread.start();
} catch (Exception exception) {
log(exception);
}
}
public String eventToString(ServerEvent event) {
String toString = "";
if (event.getSource() != null) {
toString = toString + " s:" + event.getSource();
}
if (event.getChannelName() != null) {
toString = toString + " c:" + event.getChannelName();
}
if (event.getMessage() != null) {
toString = toString + " m:" + event.getMessage();
}
if (event.getOriginNick() != null) {
toString = toString + " n:" + event.getOriginNick();
}
if (event.getTargetNick() != null) {
toString = toString + " tn:" + event.getTargetNick();
}
if (event.getUser() != null) {
toString = toString + " u:" + event.getUser();
}
if (event.getUsers() != null) {
toString = toString + " us:" + event.getUsers();
}
return toString;
}
public void onConnect(ServerEvent event) {
try {
log("Connected:", Bot.FINE, eventToString(event));
getServer().sendJoin(getChannelName());
} catch (Exception exception) {
log(exception);
}
}
public void onWhoIs(ServerEvent event) {
try {
log("WhoIs:", Bot.FINE, eventToString(event));
} catch (Exception exception) {
log(exception);
}
}
public void onIsOn(ServerEvent event) {
try {
log("IsOn:", Bot.FINE, eventToString(event));
} catch (Exception exception) {
log(exception);
}
}
public void onStatus(ServerEvent event) {
try {
log("Status:", Bot.FINE, eventToString(event));
} catch (Exception exception) {
log(exception);
}
}
public void onChannelPart(ServerEvent event) {
try {
log("ChannelPart:", Bot.FINE, eventToString(event));
} catch (Exception exception) {
log(exception);
}
}
public void onChannelAdd(ServerEvent event) {
try {
log("ChannelAdd:", Bot.FINE, eventToString(event));
Channel channel = (Channel) event.getChannel();
setChannel(channel);
channel.addChannelListener(new IRCChannelListener());
for (ChannelListener listener : getChannelListeners()) {
channel.addChannelListener(listener);
}
} catch (Exception exception) {
log(exception);
}
}
public void onInvite(ServerEvent event) {
try {
log("Invite:", Bot.FINE, eventToString(event));
} catch (Exception exception) {
log(exception);
}
}
public void onChannelJoin(ServerEvent event) {
try {
Channel channel = (Channel) event.getChannel();
setChannel(channel);
log("Joined:", Bot.FINE, channel);
// Wait for add.
} catch (Exception exception) {
log(exception);
}
}
public void onDisconnect(ServerEvent event) {
log("Disconnected:", Bot.FINE, eventToString(event));
}
}
class IRCChannelListener implements ChannelListener {
public String eventToString(ChannelEvent event) {
String toString = "";
if (event.getSource() != null) {
toString = toString + " s:" + event.getSource();
}
if (event.getSubjectAddress() != null) {
toString = toString + " sa:" + event.getSubjectAddress();
}
if (event.getSubjectNick() != null) {
toString = toString + " sn:" + event.getSubjectNick();
}
if (event.getOriginNick() != null) {
toString = toString + " n:" + event.getOriginNick();
}
if (event.getOriginAddress() != null) {
toString = toString + " na:" + event.getOriginAddress();
}
if (event.getValue() != null) {
toString = toString + " v:" + event.getValue();
}
return toString;
}
public void onMessage(ChannelEvent event) {
log("Message:", Level.INFO, eventToString(event));
try {
input(event);
} catch (Exception exception) {
log(exception);
}
}
public void onBan(ChannelEvent event) {
log("Ban:", Bot.FINE, eventToString(event));
}
public void onKick(ChannelEvent event) {
log("Kick:", Bot.FINE, eventToString(event));
}
public void onPart(ChannelEvent event) {
log("Part:", Bot.FINE, eventToString(event));
}
public void onQuit(ChannelEvent event) {
log("Quit:", Bot.FINE, eventToString(event));
removeUser(((String)event.getValue()).trim());
}
public void onConnect(ChannelEvent event) {
log("Connect:", Bot.FINE, eventToString(event));
}
public void onOp(ChannelEvent event) {
log("Op:", Bot.FINE, eventToString(event));
}
public void onDeOp(ChannelEvent event) {
log("DeOp:", Bot.FINE, eventToString(event));
}
public void onJoin(ChannelEvent event) {
log("Join:", Bot.FINE, eventToString(event));
addUser(((String)event.getValue()).trim());
}
public void onJoins(ChannelEvent event) {
log("Join:", Bot.FINE, eventToString(event));
TextStream stream = new TextStream((String)event.getValue());
String user = stream.nextWord();
while (user != null) {
addUser(user);
user = stream.nextWord();
}
}
public void onNick(ChannelEvent event) {
log("Nick:", Bot.FINE, eventToString(event));
}
public void onAction(ChannelEvent event) {
log("Action:", Bot.FINE, eventToString(event));
}
public void onActivation(ChannelEvent event) {
log("Activation:", Bot.FINE, eventToString(event));
}
public void onTopicChange(ChannelEvent event) {
log("TopicChange:", Bot.FINE, eventToString(event));
}
public void onDisconnect(ChannelEvent event) {
log("Disconnect:", Bot.FINE, eventToString(event));
}
}
public IRC() {
initialize();
this.languageState = LanguageState.Discussion;
}
public void initialize() {
this.users = new HashSet<String>();
this.userNicks = new HashMap<String, String>();
this.lastUsers = new LinkedList<String>();
this.spamText = new HashMap<String, String>();
this.spamCount = new HashMap<String, Integer>();
this.conversation = null;
}
public void connect() {
disconnect();
new IRCServerListener();
setConnected(true);
}
/**
* Stop sensing.
*/
@Override
public void shutdown() {
super.shutdown();
disconnect();
}
/**
* Reset state when instance is pooled.
*/
@Override
public void pool() {
disconnect();
}
public void disconnect() {
setConnected(false);
Server server = getServer();
setServer(null);
setChannel(null);
initialize();
if (server != null) {
server.disconnect();
}
}
/**
* Trim special IRC command chars from the text.
*/
public String trimSpecialChars(String text) {
TextStream stream = new TextStream(text);
StringWriter writer = new StringWriter();
while (!stream.atEnd()) {
char next = stream.next();
// char 3 means the next to chars are a command like colour, etc.
if (next == (char)3) {
stream.skip(2);
} else {
writer.append(next);
}
}
return writer.toString();
}
/**
* Trim non-letters and lower case.
*/
public String trimUserName(String text) {
TextStream stream = new TextStream(text);
StringWriter writer = new StringWriter();
while (!stream.atEnd()) {
char next = stream.next();
// char 3 means the next to chars are a command like colour, etc.
if (Character.isLetter(next)) {
writer.append(next);
}
}
return writer.toString().toLowerCase();
}
/**
* Process the input chat event.
* Check the source user and check for a targeted user.
* Ignore if spam.
*/
public void input(Object inputText, Network network) {
if (!isEnabled()) {
return;
}
ChannelEvent event = (ChannelEvent) inputText;
String text = (String)event.getValue();
String user = event.getOriginNick();
if (checkSpam(user, text)) {
return;
}
text = trimSpecialChars(text);
TextStream stream = new TextStream(text);
List<String> targetUsers = new ArrayList<String>();
String firstWord = stream.nextWord();
if (firstWord == null) {
// Ignore empty chat.
return;
}
String firstWordLower = firstWord.toLowerCase();
// Check if a directed question and trim nick.
// Try to avoid matching users with common word names like 'hi', 'lol'
if (getUsers().contains(firstWord) || getUserNicks().containsKey(firstWordLower)) {
if (getUsers().contains(firstWord)) {
targetUsers.add(firstWord);
} else {
targetUsers.add(getUserNicks().get(firstWordLower));
}
if (!stream.atEnd()) {
stream.next();
}
text = stream.upToEnd();
} else {
for (String possibleUser : this.lastUsers) {
if ((possibleUser.length() > 2) && text.indexOf(possibleUser) != -1) {
targetUsers.add(possibleUser);
}
String trimmedPossibleUser = trimUserName(possibleUser);
if ((trimmedPossibleUser.length() > 2) && text.indexOf(trimmedPossibleUser) != -1) {
targetUsers.add(possibleUser);
}
}
// Check self.
if (text.indexOf(getNick()) != -1) {
targetUsers.add(getNick());
}
String trimmedNick = trimUserName(getNick());
if (text.indexOf(trimmedNick) != -1) {
targetUsers.add(getNick());
}
if (targetUsers.isEmpty()) {
targetUsers.addAll(this.lastUsers);
}
}
inputSentence(text.trim(), user, targetUsers, network);
addLastUser(user);
}
/**
* Ignore users that spam the same message repeatedly.
*/
public boolean checkSpam(String user, String text) {
String lastSpam = this.spamText.get(user);
if (text.equals(lastSpam)) {
Integer count = this.spamCount.get(user);
if (count == null) {
count = 0;
}
this.spamCount.put(user, count + 1);
if (count.intValue() > this.maxSpam) {
return true;
}
} else {
this.spamText.put(user, text);
this.spamCount.put(user, 1);
}
if (this.spamText.size() > 50) {
this.spamText.clear();
this.spamCount.clear();
}
return false;
}
/**
* Process the text sentence.
*/
public void inputSentence(String text, String userName, List<String> targetUserNames, Network network) {
Vertex input = createInputSentence(text.trim(), network);
input.addRelationship(Primitive.INSTANTIATION, Primitive.CHAT);
// Process speaker.
Vertex user = network.createSpeaker(userName);
input.addRelationship(Primitive.SPEAKER, user);
// Process target speakers.
Set<String> uniqueTargetUserNames = new HashSet<String>();
for (String targetUserName : targetUserNames) {
if (!targetUserName.equals(userName) && !uniqueTargetUserNames.contains(targetUserName)) {
uniqueTargetUserNames.add(targetUserName);
Vertex targetUser = null;
if (targetUserName.equals(getNick()) || targetUserName.equals(getNickAlt())) {
targetUser = network.createVertex(Primitive.SELF);
} else {
targetUser = network.createSpeaker(targetUserName);
}
input.addRelationship(Primitive.TARGET, targetUser);
}
}
user.addRelationship(Primitive.INPUT, input);
// Process conversation.
Vertex conversation = getConversation(network);
if (conversation == null) {
conversation = network.createInstance(Primitive.CONVERSATION);
conversation.addRelationship(Primitive.TYPE, Primitive.CHAT);
setConversation(conversation);
conversation.addRelationship(Primitive.SPEAKER, Primitive.SELF);
for (String eachUser : getUsers()) {
conversation.addRelationship(Primitive.SPEAKER, network.createSpeaker(eachUser));
}
}
Language.addToConversation(input, conversation);
network.save();
getBot().memory().addActiveMemory(input);
}
/**
* Output the vertex to text.
*/
public void output(Vertex output) {
if (!isEnabled()) {
return;
}
Vertex sense = output.mostConscious(Primitive.SENSE);
// If not output to IRC, ignore.
if ((sense == null) || (!getPrimitive().equals(sense.getData()))) {
return;
}
try {
if (getChannel() != null) {
log("Output:", Bot.FINE, output);
getChannel().sendMessage(printInput(output) + "\n");
for (ChannelListener listener : getChannelListeners()) {
ChannelEvent event = new ChannelEvent(getChannel(), getNick(), getUserName(), printInput(output));
listener.onMessage(event);
}
this.lastUsers.remove(0);
this.lastUsers.add(getNick());
}
} catch (Exception exception) {
log(exception);
}
}
public void addLastUser(String user) {
/*if (this.lastUsers.contains(user)) {
this.lastUsers.remove(user);
} -- only last five messages, not users -- */
if (this.lastUsers.size() > LAST_USERS) {
this.lastUsers.remove(this.lastUsers.size() - 1);
}
this.lastUsers.add(0, user);
}
public String getServerName() {
return serverName;
}
public void setServerName(String serverName) {
this.serverName = serverName;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public String getNick() {
return nick;
}
public void setNick(String nick) {
this.nick = nick;
}
public String getNickAlt() {
return nickAlt;
}
public void setNickAlt(String nickAlt) {
this.nickAlt = nickAlt;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getRealName() {
return realName;
}
public void setRealName(String realName) {
this.realName = realName;
}
public Server getServer() {
return server;
}
public void setServer(Server server) {
this.server = server;
}
public Channel getChannel() {
return channel;
}
public void setChannel(Channel channel) {
this.channel = channel;
}
public String getChannelName() {
return channelName;
}
public void setChannelName(String channelName) {
this.channelName = channelName;
}
public List<ChannelListener> getChannelListeners() {
return channelListeners;
}
public void setChannelListeners(List<ChannelListener> channelListeners) {
this.channelListeners = channelListeners;
}
public void addUser(String user) {
this.users.add(user);
String trimmedUser = trimUserName(user);
if (!trimmedUser.equals(user)) {
this.userNicks.put(trimmedUser, user);
this.userNicks.put(user, trimmedUser);
}
}
public void removeUser(String user) {
this.users.remove(user);
String trimmedUser = trimUserName(user);
if (!trimmedUser.equals(user)) {
this.userNicks.remove(trimmedUser);
this.userNicks.remove(user);
}
}
public Set<String> getUsers() {
return users;
}
public void setUsers(Set<String> users) {
this.users = users;
}
public boolean isConnected() {
return isConnected;
}
public void setConnected(boolean isConnected) {
this.isConnected = isConnected;
}
public Map<String, String> getUserNicks() {
return userNicks;
}
public void setUserNicks(Map<String, String> userNicks) {
this.userNicks = userNicks;
}
/**
* Return the current conversation.
*/
public Vertex getConversation(Network network) {
if (this.conversation == null) {
return null;
}
return network.findById(conversation);
}
/**
* Set the current conversation.
*/
public void setConversation(Vertex conversation) {
if (conversation == null) {
this.conversation = null;
} else {
this.conversation = conversation.getId();
}
}
}