Transpiled version (5456L) is out of date.
1 | !752 |
2 | |
3 | static boolean actuallyPost = true; |
4 | |
5 | static L<S> botIDs = litlist("#1001890", "#1001923", "#1001937", "#1001942", "#1001945", "#1001951" /*, "#1001747"*/, |
6 | "#1001989", "#1001861", "#1001991", "#1001994", "#1002007"); |
7 | static new L<Class> bots; |
8 | static new L<S> activeBots; |
9 | static Map<S, Class> botsByID = synchroTreeMap(); |
10 | |
11 | static S channelID = "C0FH9PY8J"; // relp, #talkingbots |
12 | static S token; |
13 | |
14 | //static new Map<S, S> lastPostByUser; |
15 | static new Map<S, S> userNames; |
16 | |
17 | static S lastQuestion, lastAnswer; |
18 | static S lastAnswerTimestamp; // only for posted in this session |
19 | static int lastAnswerScore; |
20 | |
21 | static int webServerPort = 80; // set to 0 for no web serving |
22 | static long hitCount; |
23 | |
24 | /* |
25 | static class Interaction { |
26 | S question, user, answertime, answer; |
27 | long realtime; |
28 | int score; // not used yet |
29 | } |
30 | |
31 | static new L<Interaction> interactions; |
32 | */ |
33 | static new L<Map> history; // stores all processed user questions (not whole chat log) |
34 | |
35 | p { |
36 | if (token == null) |
37 | token = loadSecretTextFileMandatory("#1001889", "relp-slack-botstuff-token").trim(); |
38 | |
39 | reload(); |
40 | readLog(); |
41 | |
42 | if (webServerPort != 0) { |
43 | readLocally("hitCount"); |
44 | serveHttp(webServerPort); |
45 | } |
46 | |
47 | while true { |
48 | pcall { |
49 | if (lastAnswerTimestamp != null) { |
50 | // Poll reactions to our last answer - is there a more efficient way? |
51 | int score = slackReactionScore(token, channelID, lastAnswerTimestamp); |
52 | if (score != lastAnswerScore) { |
53 | print("Reaction score changes to " + score); |
54 | recordFeedback(lastQuestion, lastAnswer, lastAnswerTimestamp, "emoji", score-lastAnswerScore); |
55 | lastAnswerScore = score; |
56 | } |
57 | } |
58 | |
59 | L<SlackMsg> qs = getNewQuestions(); |
60 | //print(l(qs) + " question(s)."); |
61 | //printList(qs); |
62 | for (SlackMsg msg : qs) { |
63 | S q = msg.text; |
64 | S userName = getUserName(msg.user); |
65 | print("Processing question: " + quote(q)); |
66 | if (lastAnswer != null) { // TODO: maybe check the time delay |
67 | int score = feedbackScore(q); |
68 | if (score != 0) { |
69 | recordFeedback(lastQuestion /*lastPostByUser.get(userName)*/, lastAnswer, lastAnswerTimestamp, q, score); |
70 | if (score > 0) |
71 | pcall { slackSmile(token, channelID, msg.ts); } |
72 | else |
73 | pcall { slackReact(token, channelID, msg.ts, "thinking_face"); } |
74 | } |
75 | lastAnswer = null; |
76 | } |
77 | //if (userName != null) lastPostByUser.put(userName, msg.text); |
78 | pcall { |
79 | S a = answer(q); |
80 | if (a != null) { |
81 | if (userName != null) |
82 | a = "<@" + userName + "> " + a; |
83 | print("Answering with: " + quote(a)); |
84 | lastAnswer = a; |
85 | lastQuestion = q; |
86 | if (actuallyPost) { |
87 | S answerTime = postAnswer(a); |
88 | Map logEntry = litmap("question", q, "user", userName, "answertime", answerTime, "answer", a, "realtime", now()); |
89 | history.add(logEntry); |
90 | logQuoted(new File(programDir(), "memory-log"), structure(logEntry)); |
91 | } |
92 | } |
93 | } |
94 | } |
95 | } |
96 | sleepSeconds(5); |
97 | } |
98 | } |
99 | |
100 | // returns the slack timestamp |
101 | static S postAnswer(S text) { |
102 | S username = "botstuff"; |
103 | S url = "https://slack.com/api/chat.postMessage"; |
104 | S postData = "token=" + urlencode(token) + "&channel=" + urlencode(channelID) + "&text=" + urlencode(text) |
105 | + "&username=" + urlencode(username); |
106 | print(postData); |
107 | S data = doPost(postData, url); |
108 | Map map = jsonDecodeMap(data); |
109 | lastAnswerTimestamp = (S) map.get("ts"); |
110 | lastAnswerScore = 0; |
111 | print("postAnswer: " + data); |
112 | ret lastAnswerTimestamp; |
113 | } |
114 | |
115 | static S getNewQuestions_lastSeen; |
116 | |
117 | static L<SlackMsg> getNewQuestions() { |
118 | // Get 150 on first call (history before bot start) |
119 | // and 1000 thereafter (just grab anything new) |
120 | int limit = getNewQuestions_lastSeen == null ? 150 : 1000; |
121 | L<SlackMsg> msgs = slackReadChannel(channelID, token, limit, getNewQuestions_lastSeen); |
122 | if (!msgs.isEmpty()) getNewQuestions_lastSeen = last(msgs).ts; |
123 | int i = indexOfLastBotAnswer(msgs); |
124 | if (i >= 0) { |
125 | print("Last bot answer at " + (i+1) + "/" + l(msgs) + ": " + structure(msgs.get(i))); |
126 | |
127 | // update variables |
128 | lastAnswer = msgs.get(i).text; |
129 | for (int j = 0; j < i; j++) { |
130 | SlackMsg msg = msgs.get(j); |
131 | S userName = getUserName(msg.user); |
132 | //if (userName != null) lastPostByUser.put(userName, msg.text); |
133 | } |
134 | |
135 | msgs = subList(msgs, i+1); |
136 | } |
137 | ret msgs; |
138 | } |
139 | |
140 | static int indexOfLastBotAnswer(L<SlackMsg> msgs) { |
141 | for (int i = l(msgs)-1; i >= 0; i--) |
142 | if (msgs.get(i).botName != null) ret i; |
143 | ret -1; |
144 | } |
145 | |
146 | static synchronized S answer(S s) { |
147 | new Matches m; |
148 | |
149 | if (match("reload", s)) { |
150 | reload(); |
151 | ret l(bots) + "/" + l(botIDs) + " bots reloaded."; |
152 | } |
153 | |
154 | if (match("reload *", s, m)) { |
155 | reload(m.unq(0)); |
156 | ret "Bot " + formatSnippetID(m.unq(0)) + " reloaded."; |
157 | } |
158 | |
159 | if (match("list bots", s)) { |
160 | S answer = "Active bots: " + structure(activeBots); |
161 | L<S> inactiveBots = diff(botIDs, activeBots); |
162 | if (!inactiveBots.isEmpty()) |
163 | answer += ". Inactive bots: " + structure(inactiveBots); |
164 | ret answer; |
165 | } |
166 | |
167 | if (match("test", s)) |
168 | ret "blah!"; |
169 | |
170 | ret callStaticAnswerMethod(bots, s); |
171 | } |
172 | |
173 | static void recordFeedback(S probableQuestion, S answer, S answerTime, S feedback, int score) { |
174 | Map data = litmap("realtime", now(), "question", probableQuestion, "answer", answer, "answertime", answerTime, "feedback", feedback, "score", score); |
175 | print("Recording feedback: " + data); |
176 | logQuoted(new File(programDir(), "feedback-log"), structure(data)); |
177 | logQuoted(new File(programDir(), "memory-log"), structure(data)); |
178 | } |
179 | |
180 | static void reload() { |
181 | botIDs = or((L<S>) loadVariableDefinition("botIDs"), botIDs); |
182 | cleanUp(bots); |
183 | activeBots = new ArrayList<S>(); |
184 | botsByID.clear(); |
185 | for (S botID : botIDs) |
186 | pcall { |
187 | loadBot(botID); |
188 | } |
189 | } |
190 | |
191 | static void reload(S botID) { |
192 | S parsedID = "" + parseSnippetID(botID); |
193 | Class bot = botsByID.get(parsedID); |
194 | if (bot == null) ret; |
195 | cleanUp(bot); |
196 | botsByID.remove(parsedID); |
197 | activeBots.remove(bot); |
198 | loadBot(botID); |
199 | } |
200 | |
201 | static void loadBot(S botID) { |
202 | print("Loading bot: " + botID); |
203 | Class c = run(botID); |
204 | bots.add(c); |
205 | activeBots.add(botID); // only if loading doesn't fail |
206 | botsByID.put("" + parseSnippetID(botID), c); |
207 | print("Loaded bot: " + botID + ", bots now: " + l(activeBots)); |
208 | } |
209 | |
210 | static S getUserName(S userID) { |
211 | if (userID == null) ret null; |
212 | S userName = userNames.get(userID); |
213 | if (userName == null) pcall { |
214 | userName = (S) slackGetUserInfo(token, userID).get("name"); |
215 | userNames.put(userID, userName); |
216 | } |
217 | ret userName; |
218 | } |
219 | |
220 | static NanoHTTPD.Response serve(S uri, NanoHTTPD.Method method, |
221 | Map<S,S> header, Map<S,S> parms, Map<S,S> files) { |
222 | print("Serving HTTP " + quote(uri)); |
223 | ++hitCount; |
224 | saveLocally("hitCount"); |
225 | uri = dropPrefixMandatory("/", uri); |
226 | if (eq(uri, "")) |
227 | ret serveHomePage(); |
228 | if (eq(uri, "favicon.ico")) |
229 | ret serve404(); |
230 | |
231 | int i = uri.indexOf('/'); |
232 | S firstPart = i >= 0 ? uri.substring(0, i) : uri; |
233 | S rest = i >= 0 ? substr(uri, i) : "/"; // rest always starts with "/" |
234 | ret serveBot(firstPart, rest); |
235 | } |
236 | |
237 | static NanoHTTPD.Response serveBot(S botID, S subUri) { |
238 | Class bot = botsByID.get("" + parseSnippetID(botID)); |
239 | boolean raw = subUri.equals("/raw") || subUri.startsWith("/raw/"); |
240 | if (raw) { |
241 | subUri = substr(subUri, "/raw".length()); |
242 | if (subUri.length() == 0) subUri = "/"; |
243 | } |
244 | if (bot != null) { |
245 | S botHtml = callHtmlMethod(bot, subUri); |
246 | if (raw) ret serveHTML(unnull(botHtml)); |
247 | if (botHtml == null) |
248 | botHtml = "Bot has no HTML output."; |
249 | S title = "TinyBrain Bot " + formatSnippetID(botID); |
250 | S html = "<html><head><title>" + title + "</title></head>" + |
251 | "<body><h3>Bot <a href=\"http://tinybrain.de/" + parseSnippetID(botID) + "\">" + formatSnippetID(botID) + "</a></h3>\n" + botHtml + |
252 | "\n</body></html>"; |
253 | ret serveHTML(html); |
254 | } |
255 | |
256 | ret serve404(); |
257 | } |
258 | |
259 | static NanoHTTPD.Response serveHomePage() { |
260 | new StringBuilder buf; |
261 | buf.append("<html>"); |
262 | buf.append("<head><title>TinyBrain Chat Bots</title></head>"); |
263 | buf.append("<body>"); |
264 | buf.append("<h3><a href=" + htmlQuote("http://tinybrain.de") + ">TinyBrain</a> Bots</h3>"); |
265 | buf.append("<ul>"); |
266 | for (S botID : activeBots) { |
267 | botID = formatSnippetID(botID); |
268 | S parsedID = "" + parseSnippetID(botID); |
269 | S title = "?"; |
270 | pcall { title = getSnippetTitle_overBot(botID); } |
271 | S name = botID + " - " + htmlencode(title); |
272 | buf.append("<li><a href=" + htmlQuote(parsedID) + ">" + name + "</a> ("); |
273 | buf.append("<a href=" + htmlQuote("http://tinybrain.de/" + parsedID) + ">source</a>)</li>"); |
274 | } |
275 | buf.append("</ul>"); |
276 | buf.append("<p>Hit count: " + hitCount + "</p>"); |
277 | |
278 | buf.append("<p>Last interactions:</p>"); |
279 | |
280 | int maxInteractionsToPrint = 10; |
281 | |
282 | for (int i = l(history)-1; i >= 0 && i >= l(history)-maxInteractionsToPrint; i--) { |
283 | Map m = history.get(i); |
284 | buf.append("<p>" + htmlencode(m.get("question")) + "<br> " + htmlencode(m.get("answer")) + "</p>"); |
285 | } |
286 | |
287 | buf.append("</body>"); |
288 | buf.append("</html>"); |
289 | ret serveHTML(buf); |
290 | } |
291 | |
292 | static S getSnippetTitle_overBot(S snippetID) { |
293 | startBot("Snippet Title Bot", "#1001747"); |
294 | // Use local version |
295 | //S s = sendToLocalBot(getVMPort() + "/Snippet Title Bot", "what is the title of snippet *", snippetID); |
296 | // Use bot VM version |
297 | S s = sendToLocalBot_cached("Snippet Title Bot", "what is the title of snippet *", snippetID); |
298 | new Matches m; |
299 | if (match("The title of snippet * is *.", s, m)) |
300 | ret m.unq(1); |
301 | throw fail("Bot said: " + s); |
302 | } |
303 | |
304 | static void readLog() { |
305 | for (S s : scanLog("#1001915", "memory-log")) pcall { |
306 | history.add((Map) safeUnstructure(s)); |
307 | } |
308 | print(l(history) + " history entries."); |
309 | } |
Began life as a copy of #1001912
download show line numbers debug dex old transpilations
Travelled to 16 computer(s): aoiabmzegqzx, bhatertpkbcr, cbybwowwnfue, cfunsshuasjs, gwrvuhgaqvyk, ishqpsrjomds, jtubtzbbkimh, lpdgvwnxivlt, mqqgnosmbjvj, onxytkatvevr, pyentgdyhuwx, pzhvpgtvlbxg, teubizvjbppd, tslmcundralx, tvejysmllsmz, vouqrxazstgt
No comments. add comment
Snippet ID: | #1001915 |
Snippet name: | Slack Bot! (not used anymore. relp.slack.com, #talkingbots, can be redirected to other channel) |
Eternal ID of this version: | #1001915/1 |
Text MD5: | 18ec645b1afc97e73478c26771190c76 |
Author: | stefan |
Category: | |
Type: | JavaX source code |
Public (visible to everyone): | Yes |
Archived (hidden from active list): | No |
Created/modified: | 2015-12-14 19:21:12 |
Source code size: | 10137 bytes / 309 lines |
Pitched / IR pitched: | No / Yes |
Views / Downloads: | 960 / 2169 |
Referenced in: | [show references] |