/******************************************************************************
*
* Copyright 2016 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.slack;
import java.net.URL;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import org.botlibre.api.knowledge.Network;
import org.botlibre.api.knowledge.Relationship;
import org.botlibre.api.knowledge.Vertex;
import org.botlibre.emotion.EmotionalState;
import org.botlibre.knowledge.Primitive;
import org.botlibre.self.SelfCompiler;
import org.botlibre.sense.BasicSense;
import org.botlibre.sense.http.Http;
import org.botlibre.sense.slack.SlackListener;
import org.botlibre.thought.language.Language;
import org.botlibre.thought.language.Language.LanguageState;
import org.botlibre.util.TextStream;
import org.botlibre.util.Utils;
import net.sf.json.JSONObject;
import net.sf.json.JSONSerializer;
/**
* Enables receiving a sending messages through Slack.
*/
public class Slack extends BasicSense {
public static int MAX_WAIT = 60 * 1000; // 1 minute
protected int maxErrors = 5;
protected int errors;
protected String token = "";
protected String botUsername = "";
protected String incomingWebhook = "";
protected String appToken = "";
protected boolean autoPost = false;
protected int autoPostHours = 24;
protected String autoPostChannel;
protected String autoPostUsername;
protected int maxFeed = 20;
protected List<String> postRSS = new ArrayList<String>();
protected List<String> rssKeywords = new ArrayList<String>();
protected String rssChannel;
protected String rssUsername;
protected int messagesProcessed;
protected int posts;
protected boolean initProperties;
protected SlackListener listener;
private boolean enableEmotions = true;
public Slack(boolean enabled) {
this.isEnabled = enabled;
this.languageState = LanguageState.Discussion;
}
public Slack() {
this(false);
}
/**
* Start sensing.
*/
@Override
public void awake() {
this.token = this.bot.memory().getProperty("Slack.token");
if (this.token == null) {
this.token = "";
}
if (!this.token.isEmpty()) {
setIsEnabled(true);
}
this.botUsername = this.bot.memory().getProperty("Slack.botUsername");
if (this.botUsername == null) {
this.botUsername = "";
}
this.incomingWebhook = this.bot.memory().getProperty("Slack.incomingWebhook");
if (this.incomingWebhook == null) {
this.incomingWebhook = "";
}
this.rssUsername = this.bot.memory().getProperty("Slack.rssUsername");
if (this.rssUsername == null) {
this.rssUsername = "";
}
this.rssChannel = this.bot.memory().getProperty("Slack.rssChannel");
if (this.rssChannel == null) {
this.rssChannel = "";
}
this.autoPostUsername = this.bot.memory().getProperty("Slack.autoPostUsername");
if (this.autoPostUsername == null) {
this.autoPostUsername = "";
}
this.autoPostChannel = this.bot.memory().getProperty("Slack.autoPostChannel");
if (this.autoPostChannel == null) {
this.autoPostChannel = "";
}
this.appToken = this.bot.memory().getProperty("Slack.appToken");
if (this.appToken == null) {
this.appToken = "";
}
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getBotUsername() {
return botUsername;
}
public void setBotUsername(String botUsername) {
this.botUsername = botUsername;
}
public String getRssUsername() {
return rssUsername;
}
public void setRssUsername(String rssUsername) {
this.rssUsername = rssUsername;
}
public String getRssChannel() {
return rssChannel;
}
public void setRssChannel(String rssChannel) {
this.rssChannel = rssChannel;
}
public String getAutoPostUsername() {
return autoPostUsername;
}
public void setAutoPostUsername(String autoPostUsername) {
this.autoPostUsername = autoPostUsername;
}
public String getAutoPostChannel() {
return autoPostChannel;
}
public void setAutoPostChannel(String rssChannel) {
this.autoPostChannel = rssChannel;
}
public String getIncomingWebhook() {
return incomingWebhook;
}
public void setIncomingWebhook(String incomingWebhook) {
this.incomingWebhook = incomingWebhook;
}
public int getMaxFeed() {
return maxFeed;
}
public void setMaxFeed(int maxFeed) {
this.maxFeed = maxFeed;
}
public List<String> getRssKeywords() {
initProperties();
return rssKeywords;
}
public void setRssKeywords(List<String> rssKeywords) {
initProperties();
this.rssKeywords = rssKeywords;
}
public List<String> getPostRSS() {
initProperties();
return postRSS;
}
public void setPostRSS(List<String> postRSS) {
initProperties();
this.postRSS = postRSS;
}
public boolean getAutoPost() {
initProperties();
return autoPost;
}
public void setAutoPost(boolean autoPost) {
initProperties();
this.autoPost = autoPost;
}
public int getAutoPostHours() {
initProperties();
return autoPostHours;
}
public void setAutoPostHours(int autoPostHours) {
initProperties();
this.autoPostHours = autoPostHours;
}
public List<Vertex> getAutoPosts(Network network) {
return network.createVertex(getPrimitive()).orderedRelations(Primitive.AUTOPOSTS);
}
public String getAppToken() {
return appToken;
}
public void setAppToken(String appToken) {
this.appToken = appToken;
}
/**
* Load settings.
*/
public void initProperties() {
if (this.initProperties) {
return;
}
synchronized (this) {
if (this.initProperties) {
return;
}
getBot().memory().loadProperties("Slack");
Network memory = getBot().memory().newMemory();
Vertex slack = memory.createVertex(getPrimitive());
String property = this.bot.memory().getProperty("Slack.token");
if (property != null) {
this.token = property;
}
property = this.bot.memory().getProperty("Slack.botUsername");
if (property != null) {
this.botUsername = property;
}
property = this.bot.memory().getProperty("Slack.incomingWebhook");
if (property != null) {
this.incomingWebhook = property;
}
property = this.bot.memory().getProperty("Slack.autoPost");
if (property != null) {
this.autoPost = Boolean.valueOf(property);
}
property = this.bot.memory().getProperty("Slack.autoPostHours");
if (property != null) {
this.autoPostHours = Integer.valueOf(property);
}
property = this.bot.memory().getProperty("Slack.rssUsername");
if (property != null) {
this.rssUsername = property;
}
property = this.bot.memory().getProperty("Slack.rssChannel");
if (property != null) {
this.rssChannel = property;
}
property = this.bot.memory().getProperty("Slack.autoPostUsername");
if (property != null) {
this.autoPostUsername = property;
}
property = this.bot.memory().getProperty("Slack.autoPostChannel");
if (property != null) {
this.autoPostChannel = property;
}
property = this.bot.memory().getProperty("Slack.appToken");
if (property != null) {
this.appToken = property;
}
this.postRSS = new ArrayList<String>();
List<Relationship> rss = slack.orderedRelationships(Primitive.RSS);
if (rss != null) {
for (Relationship relationship : rss) {
String text = ((String)relationship.getTarget().getData()).trim();
if (!text.isEmpty()) {
this.postRSS.add(text);
}
}
}
this.rssKeywords = new ArrayList<String>();
List<Relationship> keywords = slack.orderedRelationships(Primitive.RSSKEYWORDS);
if (keywords != null) {
for (Relationship relationship : keywords) {
String text = ((String)relationship.getTarget().getData()).trim();
this.rssKeywords.add(text);
}
}
this.initProperties = true;
}
}
public void saveProperties(List<String> autoPosts) {
Network memory = getBot().memory().newMemory();
memory.saveProperty("Slack.token", this.token, true);
memory.saveProperty("Slack.botUsername", this.botUsername, true);
memory.saveProperty("Slack.incomingWebhook", this.incomingWebhook, true);
memory.saveProperty("Slack.autoPost", String.valueOf(this.autoPost), false);
memory.saveProperty("Slack.autoPostHours", String.valueOf(this.autoPostHours), false);
memory.saveProperty("Slack.rssUsername", this.rssUsername, true);
memory.saveProperty("Slack.rssChannel", this.rssChannel, true);
memory.saveProperty("Slack.autoPostUsername", this.autoPostUsername, true);
memory.saveProperty("Slack.autoPostChannel", this.autoPostChannel, true);
memory.saveProperty("Slack.appToken", this.appToken, true);
Vertex slack = memory.createVertex(getPrimitive());
slack.unpinChildren();
slack.internalRemoveRelationships(Primitive.RSS);
for (String text : this.postRSS) {
Vertex rss = memory.createVertex(text);
slack.addRelationship(Primitive.RSS, rss);
}
slack.internalRemoveRelationships(Primitive.RSSKEYWORDS);
for (String text : this.rssKeywords) {
Vertex keywords = memory.createVertex(text);
slack.addRelationship(Primitive.RSSKEYWORDS, keywords);
}
if (autoPosts != null) {
Collection<Relationship> old = slack.getRelationships(Primitive.AUTOPOSTS);
if (old != null) {
for (Relationship post : old) {
if (post.getTarget().instanceOf(Primitive.FORMULA)) {
SelfCompiler.getCompiler().unpin(post.getTarget());
}
}
}
slack.internalRemoveRelationships(Primitive.AUTOPOSTS);
for (String text : autoPosts) {
Vertex post = memory.createSentence(text);
if (post.instanceOf(Primitive.FORMULA)) {
SelfCompiler.getCompiler().pin(post);
}
post.addRelationship(Primitive.INSTANTIATION, Primitive.TWEET);
slack.addRelationship(Primitive.AUTOPOSTS, post);
}
}
slack.pinChildren();
memory.save();
}
/**
* Process to the message and reply synchronously.
*/
public String processMessage(String from, String id, String message, String token) {
log("Processing message", Level.INFO, from, message);
if(!token.equals(this.token) && !token.equals(this.appToken))
return "";
String targetUsername = "";
//Check if message is directed to the bot
if(botUsername.contains(" ") && message.toLowerCase().contains(botUsername.toLowerCase())) {
targetUsername = botUsername;
} else {
TextStream stream = new TextStream(message);
String word = stream.nextWord();
while (word != null) {
word.replace("@", "");
word.replace(":", "");
word.replace(",", "");
word.replace(".", "");
word.replace("?", "");
if(word.toLowerCase().equals(botUsername.toLowerCase())) {
targetUsername = botUsername;
break;
}
word = stream.nextWord();
}
}
//Check if message is directed to someone other than the bot
if(targetUsername.isEmpty()) {
if(message.startsWith("@") && message.contains(" ")) {
targetUsername = message.substring(1, message.indexOf(" "));
if(targetUsername.endsWith(":"))
targetUsername = targetUsername.substring(0, targetUsername.length()-2);
}
}
//Check if target is special slack target
if(targetUsername.equals("everyone") || targetUsername.equals("here") || targetUsername.equals("channel"))
targetUsername = botUsername;
this.listener = new SlackListener();
Network memory = bot.memory().newMemory();
this.messagesProcessed++;
inputSentence(message, from, targetUsername, id, memory);
memory.save();
String reply = null;
synchronized (this.listener) {
if (this.listener.reply == null) {
try {
this.listener.wait(MAX_WAIT);
} catch (Exception exception) {
log(exception);
return "";
}
}
reply = this.listener.reply;
this.listener = null;
}
return reply;
}
/**
* Process the text sentence.
*/
public void inputSentence(String text, String userName, String targetUsername, String id, Network network) {
Vertex input = createInput(text.trim(), network);
Vertex user = network.createSpeaker(userName);
Vertex self = network.createVertex(Primitive.SELF);
input.addRelationship(Primitive.SPEAKER, user);
if(targetUsername.equals(botUsername)) {
input.addRelationship(Primitive.TARGET, self);
} else if(!targetUsername.isEmpty()) {
input.addRelationship(Primitive.TARGET, network.createSpeaker(targetUsername));
}
user.addRelationship(Primitive.INPUT, input);
Vertex conversation = network.createVertex(id);
conversation.addRelationship(Primitive.INSTANTIATION, Primitive.CONVERSATION);
conversation.addRelationship(Primitive.TYPE, Primitive.SLACK);
conversation.addRelationship(Primitive.ID, network.createVertex(id));
conversation.addRelationship(Primitive.SPEAKER, user);
conversation.addRelationship(Primitive.SPEAKER, self);
Language.addToConversation(input, conversation);
network.save();
getBot().memory().addActiveMemory(input);
}
/**
* Create an input based on the sentence.
*/
protected Vertex createInput(String text, Network network) {
Vertex sentence = network.createSentence(text);
Vertex input = network.createInstance(Primitive.INPUT);
input.setName(text);
input.addRelationship(Primitive.SENSE, getPrimitive());
input.addRelationship(Primitive.INPUT, sentence);
sentence.addRelationship(Primitive.INSTANTIATION, Primitive.SLACK);
return input;
}
/**
* Output the Slack message.
*/
@Override
public void output(Vertex output) {
if (!isEnabled()) {
return;
}
Vertex sense = output.mostConscious(Primitive.SENSE);
// If not output to slack, ignore.
if ((sense == null) || (!getPrimitive().equals(sense.getData()))) {
return;
}
String text = printInput(output);
if(this.enableEmotions) {
text += addEmotion(output);
}
if (this.listener == null) {
return;
}
this.listener.reply = text;
Vertex conversation = output.getRelationship(Primitive.CONVERSATION);
if (conversation != null) {
this.listener.conversation = conversation.getDataValue();
}
synchronized (this.listener) {
this.listener.notifyAll();
}
}
public SlackListener getListener() {
return listener;
}
public void setListener(SlackListener listener) {
this.listener = listener;
}
/**
* Auto post to channel.
*/
public void checkProfile() {
log("Checking profile.", Level.INFO);
try {
initProperties();
checkRSS();
checkAutoPost();
} catch (Exception exception) {
log(exception);
}
log("Done checking profile.", Level.INFO);
}
/**
* Check RSS feed.
*/
public void checkRSS() {
if (getPostRSS().isEmpty()) {
return;
}
log("Processing RSS", Level.FINE, getPostRSS());
try {
Network memory = getBot().memory().newMemory();
Vertex slack = memory.createVertex(getPrimitive());
Vertex vertex = slack.getRelationship(Primitive.LASTRSS);
long last = 0;
if (vertex != null) {
last = ((Number)vertex.getData()).longValue();
}
for (String rss : getPostRSS()) {
TextStream stream = new TextStream(rss);
String prefix = stream.upToAll("http").trim();
if (prefix.isEmpty()) {
prefix = "";
}
prefix = prefix + " ";
String url = stream.nextWord();
String postfix = " " + stream.upToEnd().trim();
List<Map<String, Object>> feed = getBot().awareness().getSense(Http.class).parseRSSFeed(new URL(url), last);
if (feed != null) {
long max = 0;
int count = 0;
this.errors = 0;
for (int index = feed.size() - 1; index >= 0; index--) {
Map<String, Object> entry = feed.get(index);
long time = (Long)entry.get("published");
if ((System.currentTimeMillis() - time) > DAY) {
continue;
}
if (time > last) {
if (count > this.maxFeed) {
break;
}
if (this.errors > this.maxErrors) {
break;
}
String text = (String)entry.get("title");
if (!getRssKeywords().isEmpty()) {
boolean match = false;
List<String> words = new TextStream(text.toLowerCase()).allWords();
for (String keywords : getRssKeywords()) {
List<String> keyWords = new TextStream(keywords.toLowerCase()).allWords();
if (!keyWords.isEmpty()) {
if (words.containsAll(keyWords)) {
match = true;
break;
}
}
}
if (!match) {
log("Skipping RSS, missing keywords", Level.FINE, text);
continue;
}
}
log("Posting RSS", Level.FINE, entry.get("title"));
text = prefix + text + postfix;
if (text.length() > 120) {
text = text.substring(0, 120);
}
post(text + " " + entry.get("link"), rssUsername, rssChannel);
Utils.sleep(500);
count++;
if (time > max) {
max = time;
}
}
}
if (max != 0) {
slack.setRelationship(Primitive.LASTRSS, memory.createVertex(max));
memory.save();
}
}
}
} catch (Exception exception) {
log(exception);
}
}
/**
* Auto post.
*/
public void checkAutoPost() {
if (!getAutoPost()) {
return;
}
log("Autoposting", Level.FINE);
try {
Network memory = getBot().memory().newMemory();
Vertex slack = memory.createVertex(getPrimitive());
Vertex vertex = slack.getRelationship(Primitive.LASTPOST);
long last = 0;
if (vertex != null) {
last = ((Timestamp)vertex.getData()).getTime();
}
long millis = getAutoPostHours() * 60 * 60 * 1000;
if ((System.currentTimeMillis() - last) < millis) {
log("Autoposting hours not reached", Level.FINE, getAutoPostHours());
return;
}
List<Vertex> autoposts = getAutoPosts(memory);
if (autoposts != null && !autoposts.isEmpty()) {
int index = Utils.random().nextInt(autoposts.size());
Vertex post = autoposts.get(index);
String text = null;
// Check for labels and formulas
if (post.instanceOf(Primitive.LABEL)) {
post = post.mostConscious(Primitive.RESPONSE);
}
if (post.instanceOf(Primitive.FORMULA)) {
Map<Vertex, Vertex> variables = new HashMap<Vertex, Vertex>();
SelfCompiler.addGlobalVariables(memory.createInstance(Primitive.INPUT), null, memory, variables);
Vertex result = getBot().mind().getThought(Language.class).evaluateFormula(post, variables, memory);
if (result != null) {
text = getBot().mind().getThought(Language.class).getWord(result, memory).getDataValue();
} else {
log("Invalid autopost template formula", Level.WARNING, post);
text = null;
}
} else {
text = post.printString();
}
if (text != null) {
log("Autoposting", Level.INFO, post);
post(text, autoPostUsername, autoPostChannel);
Utils.sleep(100);
slack.setRelationship(Primitive.LASTPOST, memory.createTimestamp());
memory.save();
}
}
} catch (Exception exception) {
log(exception);
}
}
public void post(String text, String username, String channel) {
this.posts++;
initProperties();
log("Posting ", Level.INFO, text);
try {
JSONObject jsonPayload = new JSONObject();
if(text!=null && !text.isEmpty()) {
jsonPayload.put("text", text);
} else {
return;
}
if(username!=null && !username.isEmpty()) {
jsonPayload.put("username", username);
}
if(channel!=null && !channel.isEmpty()) {
jsonPayload.put("channel", channel);
}
Map<String, String> params = new HashMap<String, String>();
params.put("payload", jsonPayload.toString());
Utils.httpPOST(this.incomingWebhook, params);
} catch (Exception exception) {
this.errors++;
log(exception);
}
}
public void post(String text)
{
post(text, null, null);
}
public int getPosts() {
return posts;
}
public void setPosts(int posts) {
this.posts = posts;
}
public int getMessagesProcessed() {
return messagesProcessed;
}
public void setMessagesProcessed(int messagesProcessed) {
this.messagesProcessed = messagesProcessed;
}
private String addEmotion(Vertex output){
//EmotionalState emotion = this.getEmotionalState();
EmotionalState emotion = this.bot.mood().evaluateEmotionalState(output);
switch(emotion)
{
case NONE:
return "";
case AFRAID:
return " :scream:";
case ANGER:
return " :angry:";
case BORED:
return " :sleepy:";
case CALM:
return " :pensive:";
case CONFIDENT:
return " :sunglasses:";
case COURAGEOUS:
return " :triumph:";
case CRYING:
return " :cry:";
case DISLIKE:
return " :unamused:";
case ECSTATIC:
return " :joy:";
case HAPPY:
return " :smile:";
case HATE:
return " :disappointed:";
case LAUGHTER:
return " :laughing:";
case LIKE:
return " :smiley:";
case LOVE:
return " :heart_eyes:";
case PANIC:
return " :cold_sweat:";
case RAGE:
return " :rage:";
case SAD:
return " :slightly_frowning_face:";
case SERENE:
return " :relieved:";
case SERIOUS:
return " :neutral_face:";
case SURPRISE:
return " :open_mouth:";
default:
return "";
}
}
public void processSlackEvent(String json) {
JSONObject root = (JSONObject)JSONSerializer.toJSON(json);
JSONObject event = root.getJSONObject("event");
String token = root.getString("token");
if(event.getString("type").equals("message")) {
String user = event.getString("user");
String channel = event.getString("channel");
String text = event.getString("text");
String reply = this.processMessage(user, channel, text, token);
if(reply == null || reply.isEmpty())
return;
String data = "token=" + this.appToken;
data += "&channel=" + channel;
data += "&text=" + "@" + user + " " + reply;
this.callSlackWebAPI("chat.postMessage", data);
}
}
private void callSlackWebAPI(String function, String data) {
try {
Utils.httpPOST("https://slack.com/api/" + function, "application/x-www-form-urlencoded", data);
} catch (Exception exception) {
this.errors++;
log(exception);
}
}
}