/******************************************************************************
*
* 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 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;
/**
* Connect to and interact on IRC chat networks.
*/
public class Chat extends BasicSense {
public static int SLEEP = 1000 * 60 * 10; // 10 minutes.
public static int MAX_SPAM = 3;
public static int LAST_USERS = 5;
private boolean isConnected = false;
private String nick = "Bot01";
private String nickAlt = "Bot01_";
/**
* 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;
/** Maps users to speakers. */
private Map<String, Long> userSpeakers;
/** 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 ChatListener listener;
public Chat() {
initialize();
this.languageState = LanguageState.Discussion;
}
public void initialize() {
this.users = new HashSet<String>();
this.userNicks = new HashMap<String, String>();
this.userSpeakers = new HashMap<String, Long>();
this.lastUsers = new LinkedList<String>();
this.spamText = new HashMap<String, String>();
this.spamCount = new HashMap<String, Integer>();
this.conversation = null;
this.listener = null;
}
public void connect() {
disconnect();
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);
initialize();
}
/**
* Trim special IRC command chars from the text.
*/
public String trimSpecialChars(String text) {
return text;
}
/**
* 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;
}
ChatEvent message = (ChatEvent) inputText;
String text = message.getMessage();
String user = message.getNick();
if (checkSpam(user, text)) {
return;
}
text = trimSpecialChars(text);
List<String> targetUsers = new ArrayList<String>();
if (!message.isGreet()) {
TextStream stream = new TextStream(text);
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();
}
stream.skipWhitespace();
if (!stream.atEnd()) {
text = stream.upToEnd();
}
}
// Allow for compound names.
if (targetUsers.isEmpty() && (text.indexOf(':') != -1)) {
stream.reset();
firstWord = stream.upTo(':');
firstWordLower = firstWord.toLowerCase();
stream.skip();
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();
}
stream.skipWhitespace();
if (!stream.atEnd()) {
text = stream.upToEnd();
}
}
}
if (targetUsers.isEmpty()) {
for (String possibleUser : this.lastUsers) {
if ((possibleUser.length() > 2) && text.indexOf(possibleUser) != -1) {
targetUsers.add(possibleUser);
if (text.indexOf(possibleUser) == 0 && (text.length() > (possibleUser.length() + 1))) {
text = text.substring(possibleUser.length() + 1, text.length());
}
}
String trimmedPossibleUser = trimUserName(possibleUser);
if ((trimmedPossibleUser.length() > 2) && text.indexOf(trimmedPossibleUser) != -1) {
targetUsers.add(possibleUser);
if (text.indexOf(trimmedPossibleUser) == 0 && (text.length() > (trimmedPossibleUser.length() + 1))) {
text = text.substring(trimmedPossibleUser.length() + 1, text.length());
}
}
}
// Check self.
if (text.indexOf(getNick()) != -1) {
targetUsers.add(getNick());
if (text.indexOf(getNick()) == 0 && (text.length() > (getNick().length() + 1))) {
text = text.substring(getNick().length() + 1, text.length());
}
}
String trimmedNick = trimUserName(getNick());
if (text.indexOf(trimmedNick) != -1) {
targetUsers.add(getNick());
if (text.indexOf(trimmedNick) == 0 && (text.length() > (trimmedNick.length() + 1))) {
text = text.substring(trimmedNick.length() + 1, text.length());
}
}
if (targetUsers.isEmpty()) {
targetUsers.addAll(this.lastUsers);
}
if (getUsers().size() == 2) {
targetUsers.clear();
targetUsers.add(getNick());
}
}
}
inputSentence(text.trim(), user, targetUsers, message.isGreet(), message.isWhisper(), 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, boolean isGreet, boolean isWhisper, Network network) {
Vertex input = createInputSentence(text.trim(), network);
if (isGreet) {
// Null input is used to get greeting.
input.setRelationship(Primitive.INPUT, Primitive.NULL);
input.setRelationship(Primitive.TARGET, Primitive.SELF);
}
input.addRelationship(Primitive.INSTANTIATION, Primitive.CHAT);
if (isWhisper) {
input.addRelationship(Primitive.ASSOCIATED, Primitive.WHISPER);
}
// Process speaker.
Long userId = this.userSpeakers.get(userName);
Vertex user = null;
if (userId != null) {
user = network.findById(userId);
}
if (user == null) {
if (userName.startsWith("anonymous")) {
user = network.createAnonymousSpeaker();
} else {
user = network.createSpeaker(userName);
}
this.userSpeakers.put(userName, user.getId());
}
user.addRelationship(Primitive.NICK, network.createName(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);
}
targetUser.addRelationship(Primitive.NICK, network.createVertex(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 (getListener() != null) {
log("Output:", Bot.FINE, output);
ChatEvent message = new ChatEvent();
if (output.hasRelationship(Primitive.ASSOCIATED, Primitive.WHISPER)) {
message.setWhisper(true);
}
message.setNick(getNick(output));
message.setMessage(printInput(output));
getListener().sendMessage(message);
addLastUser(getNick());
}
} catch (Exception exception) {
log(exception);
}
}
public String getNick(Vertex output) {
Vertex target = output.getRelationship(Primitive.TARGET);
if (target == null) {
return null;
}
Vertex nick = target.getRelationship(Primitive.NICK);
if (nick == null) {
nick = target.getRelationship(Primitive.NAME);
}
if (nick == null) {
nick = target.getRelationship(Primitive.WORD);
}
if (nick == null) {
return null;
}
return (String)nick.getData();
}
public ChatListener getListener() {
return listener;
}
public void setListener(ChatListener listener) {
this.listener = listener;
}
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 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 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);
trimmedUser = user.toLowerCase();
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);
this.userNicks.remove(user.toLowerCase());
}
}
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();
}
}
}