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: | 1286 / 2543 |
| Referenced in: | [show references] |