static int slackSpeed = 1000; static int historySuckSize = 10; static L attnPrefixes = litlist("!", "bot"); static new L channels; static class Channel { S teamName; S channelName; // includes the site S channelID; S tokenPath; boolean generalRelease, postAsUser; 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? attn.set(false); pcall { if (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); 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 username = botUserName; S url = "https://slack.com/api/chat.postMessage"; Map postData = litmap( "token", token, "channel", channelID, "text", text, "username", username/*, "parse", "none", "link_names", "1"*/); if (postAsUser) postData.put("as_user", "true"); printStructure(postData); S data = doPost(postData, url); 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 void initSlackBot() { S relpToken = loadSecretTextFileMandatory("#1001889", "relp-slack-botstuff-token").trim(); S devChannelToken = devChannelToken(); S nlbotsToken = loadSecretTextFileMandatory("#1001925", "nlbots-slack-token").trim(); Channel 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, #talkingbots"; c.channelID = "C0H0SR40J"; 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);*/ } 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 || eq(msg.userName, actualBotUserName); } static void slackBotLoop() { 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!?"); } }