static int slackSpeed = 500, slackTimeout = 20000; static int historySuckSize = 10; static L attnPrefixes = litlist("!" /*, "eleutheria", "eleu"*/); static new L channels; static class Channel { S channelName; // includes the site S channelID, token; boolean generalRelease, postAsUser, dedicated; boolean firstTime = true; new Map userInfos; S lastQuestion, lastAnswer; S lastAnswerTimestamp; // only for posted in this session int lastAnswerScore; void tendTo() { if (lastAnswerTimestamp != null) { // Poll reactions to our last answer - is there a more efficient way? int score = slackReactionScore(token, channelID, lastAnswerTimestamp); if (score != lastAnswerScore) { print("Reaction score changes to " + score); recordFeedback(lastQuestion, lastAnswer, lastAnswerTimestamp, "emoji", score-lastAnswerScore); lastAnswerScore = score; } } L qs = getNewQuestions(); //print(l(qs) + " question(s)."); //printList(qs); for (SlackMsg msg : qs) if (!isMyAnswer(msg)) { S q = msg.text; S userName = getUserName(msg.user); Map userInfo = getUserInfo(msg.user); if (userInfo != null && eq(true, userInfo.get("is_bot"))) { print("Skipping question from bot " + userName + ": " + quote(q)); continue; } q = q.trim(); print("Processing question: " + quote(q)); if (lastAnswer != null) { // TODO: maybe check the time delay int score = feedbackScore(q); if (score != 0) { recordFeedback(lastQuestion /*lastPostByUser.get(userName)*/, lastAnswer, lastAnswerTimestamp, q, score); if (score > 0) pcall { slackSmile(token, channelID, msg.ts); } else pcall { slackReact(token, channelID, msg.ts, "thinking_face"); } } lastAnswer = null; } //if (userName != null) lastPostByUser.put(userName, msg.text); pcall { originalLine.set(q); main.userName.set(userName); S dialogID = "slack-" + userName; main.dialogID.set(dialogID); main.slackTS.set(msg.ts); main.channelName.set(channelName); // attn yes/no? main.dedicated.set(dedicated); attn.set(dedicated); pcall { if (!dedicated && attnPrefixes.contains(get(javaTok(q), 1))) { attn.set(true); q = join(subList(javaTok(q), 3)).trim(); } } S a = answer(q, generalRelease); if (a != null) { // slack snippets have some size limit - let's forget that /*if (l(a) > maxAnswerLength) a = "```" + a + "```";*/ // post long stuff to tinybrain instead if (l(a) > maxAnswerLength) { S title = "Answer for " + userName + " (>> " + q + ")"; S id = ntUpload("eleutheria-for-user", title, unSlackSnippet(a)); boolean isSnippet = a.contains("```"); a = shorten(a, shortenTo) + (isSnippet ? "```" : "") + "\nFull answer here: tinybrain.de/" + parseSnippetID(id); } if (userName != null) a = "<@" + userName + "> " + a; print("Answering with: " + quote(a)); lastAnswer = a; lastQuestion = q; if (actuallyPost) { S answerTime = postAnswer(a); Map logEntry = litmap("question", q, "answertime", answerTime, "answer", a, "user", userName, "dedicated", dedicated); historyAdd(logEntry); } } } } } Map getUserInfo(S userID) { if (userID == null) ret null; Map userInfo = userInfos.get(userID); if (userInfo == null) pcall { userInfo = slackGetUserInfo(token, userID); userInfos.put(userID, userInfo); } ret userInfo; } S getUserName(S userID) { Map userInfo = getUserInfo(userID); ret userInfo == null ? null : (S) userInfo.get("name"); } // returns the slack timestamp S postAnswer(S text) { S postAs = main.postAs.get(); S username = or(postAs, botUserName); S botIcon = main.botIcon.get(); S url = "https://slack.com/api/chat.postMessage"; Map postData = litmap( "token", token, "channel", channelID, "text", text, "username", username/*, "parse", "none", "link_names", "1"*/, "icon_url", botIcon); if (postAsUser && postAs == null) postData.put("as_user", "true"); printStructure(postData); S data = doPostWithTimeout(postData, url, slackTimeout); Map map = jsonDecodeMap(data); lastAnswerTimestamp = (S) map.get("ts"); lastAnswerScore = 0; print("postAnswer: " + data); ret lastAnswerTimestamp; } S getNewQuestions_lastSeen; L getNewQuestions() { // Get historySuckSize on first call (history before bot start) // and 1000 thereafter (just grab anything new) int limit = getNewQuestions_lastSeen == null ? historySuckSize : 1000; L msgs = slackReadChannel(channelID, token, limit, getNewQuestions_lastSeen); if (!msgs.isEmpty()) getNewQuestions_lastSeen = last(msgs).ts; int i = indexOfLastBotAnswer(msgs); if (i >= 0) { print("Last bot answer at " + (i+1) + "/" + l(msgs) + ": " + structure(msgs.get(i))); // update variables lastAnswer = msgs.get(i).text; for (int j = 0; j < i; j++) { SlackMsg msg = msgs.get(j); S userName = getUserName(msg.user); //if (userName != null) lastPostByUser.put(userName, msg.text); } if (firstTime) msgs = subList(msgs, i+1); } firstTime = false; ret msgs; } } // end of class Channel static boolean slackBot_inited; static void initSlackBot() { if (slackBot_inited) ret; slackBot_inited = true; //S devChannelToken = devChannelToken(); S nlbotsToken = loadSecretTextFile("#1001925", "nlbots-slack-token").trim(); S eleuToken = loadSecretTextFile("#1001925", "eleu-slack-token").trim(); S devchattesttoken = loadSecretTextFile("#1001925", "devchat-test-token").trim(); Channel c; /*c = new Channel; c.channelName = "devchannel, #talkingbots"; c.channelID = "C0H0SR40J"; c.token = devChannelToken; c.postAsUser = true; channels.add(c); c = new Channel; c.channelName = "devchannel, #eleudedi"; c.channelID = "C0MDDQEJY"; c.token = devChannelToken; c.postAsUser = true; c.dedicated = true; channels.add(c);*/ /*c = new Channel; c.channelName = "eleu, #general"; c.channelID = "C0MLVC346"; c.token = eleuToken; c.postAsUser = true; c.dedicated = true; channels.add(c);*/ /*c = new Channel; c.channelName = "devchannel, #general"; c.channelID = "C0H0Q0J3D"; c.token = devChannelToken; c.postAsUser = true; channels.add(c);*/ /*c = new Channel; c.channelName = "devchannel, #programming"; c.channelID = "C0H0MG0F6"; c.token = devChannelToken; c.postAsUser = true; channels.add(c);*/ /*c = new Channel; c.channelName = "devchannel, #c_sharp"; c.channelID = "C0H0T0EQ6"; c.token = devChannelToken; c.postAsUser = true; channels.add(c);*/ c = new Channel; c.channelName = "nlbots, #general"; c.channelID = "C0FHTG6SY"; // nlbots, #general c.token = nlbotsToken; channels.add(c); /*c = new Channel; c.channelID = "C0G5GPEMR"; // nlbots, #talkingbots c.token = nlbotsToken; channels.add(c);*/ /*c = new Channel; c.channelID = "G0HLW98RY"; // devchannel, #bot_test c.token = devChannelToken; c.postAsUser = true; channels.add(c);*/ c = new Channel; c.channelName = "devchat-test, #general"; c.channelID = "C0QK003PV"; c.token = devchattesttoken; c.postAsUser = true; channels.add(c); } static int indexOfLastBotAnswer(L msgs) { for (int i = l(msgs)-1; i >= 0; i--) if (isMyAnswer(msgs.get(i))) ret i; ret -1; } static boolean isMyAnswer(SlackMsg msg) { ret msg.botName != null || actualBotUserNames.contains(msg.userName); } static void slackBotLoop() { slackSetTimeout(slackTimeout); try { long lastPrinted = now(); while true { for (Channel c : channels) pcall { c.tendTo(); } sleep(slackSpeed); if (now() > lastPrinted + 60*1000) { lastPrinted = now(); print("Slack loop still going. " + unixTime()); } } } finally { print("Slack bot loop exit!?"); } } static void sayInDedicated(S s) { for (Channel c : channels) pcall { if (c.dedicated) c.postAnswer(s); } } static void dediSay(S s) { sayInDedicated(s); }