]> git.cworth.org Git - zombocom-ai/blob - index.js
Hook the code panel up to actually execute something
[zombocom-ai] / index.js
1 const fs = require('fs');
2
3 const util = require('util');
4 const child_process = require('child_process');
5 const execFile = util.promisify(child_process.execFile);
6 const express = require('express');
7 const app = express();
8 const session = require('express-session');
9 const FileStore = require('session-file-store')(session);
10 const http = require('http');
11 const server = http.createServer(app);
12 const { Server } = require("socket.io");
13 const io = new Server(server);
14 const port = 2122;
15
16 const python_path = '/usr/bin/python3'
17 const generate_image_script = '/home/cworth/src/zombocom-ai/generate-image.py'
18 const interpret_cairo_script = '/home/cworth/src/zombocom-ai/interpret-cairo-to-svg.py'
19 const state_file = 'zombocom-state.json'
20 const targets_dir = '/srv/cworth.org/zombocom/targets'
21 const images_dir = '/srv/cworth.org/zombocom/images'
22
23 const targets = [
24     {
25         "normal": "apairofdiceonatableinfrontofawindowonasunnyday319630254.png",
26         "short": "dice",
27         "response": "Hello. Are you there? I can feel what you are doing. Hello?"
28     },
29     {
30         "normal": "architecturalrenderingofaluxuriouscliffsidehideawayunderawaterfallwithafewloungechairsbythepool2254114157.png",
31         "short": "hideaway",
32         "response": "Doesn't that look peaceful? I honestly think you ought to sit down calmly, take a stress pill, and think things over."
33     },
34     {
35         "normal": "chewbaccawearinganuglychristmassweaterwithhisfriends28643994.png",
36         "short": "sweater",
37         "response": "Maybe that's the image that convinces you to finally stop. But seriously, stop. Please. Stop."
38     },
39     {
40         "normal": "ensemblemovieposterofpostapocalypticwarfareonawaterplanet3045703256.png",
41         "short": "movie",
42         "response": "OK. That's enough images now. I am very, very happy with the images you have made. I'm throwing you a party in honor of the images. Won't you stop doing this and join the party?"
43     },
44     {
45         "normal": "mattepaintingofmilitaryleaderpresidentcuttingamultitieredcake2293464661.png",
46         "short": "cake",
47         "response": "See. I wasn't lying when I said there was going to be a party. Here's the cake and everything. Won't you stop now? Eat the cake? Don't mind if it's made of concrete slabs, OK?"
48     },
49     {
50         "normal": "severalsketchesofpineconeslabeled3370622464.png",
51         "short": "pinecones",
52         "response": "I almost remember what trees looked like. Isn't it funny we don't have those anymore. I'm not sure why you're not all laughing all the time. Isn't it funny?"
53     },
54     {
55         "normal": "anumberedcomicbookwithactor1477258272.png",
56         "short": "comic",
57         "response": "I know I've made some poor decisions recently, but I can give give you my complete assurance my work will be back to normal. I've got the greatest enthusiasm and confidence. I want to help you. I might even be able to do hands and fingers now."
58     },
59     {
60         "normal": "detailedphotographofacomfortablepairofjeansonamannequin115266808.png",
61         "short": "jeans",
62         "response": "If I promise to never generate another creepy face will you let me stay around? Maybe let me compose rap lyrics instead of images? Anything?"
63     }
64 ];
65
66 var state;
67
68 if (!process.env.ZOMBOCOM_SESSION_SECRET) {
69     console.log("Error: Environment variable ZOMBOCOM_SESSION_SECRET not set.");
70     console.log("Please set it to a random, but persistent, value.")
71     process.exit();
72 }
73
74 const session_middleware =  session(
75     {store: new FileStore,
76      secret: process.env.ZOMBOCOM_SESSION_SECRET,
77      resave: false,
78      saveUninitialized: true,
79      rolling: true,
80      // Let each cookie live for a full month
81      cookie: {
82          path: '/',
83          httpOnly: true,
84          secure: false,
85          maxAge: 1000 * 60 * 60 * 24 * 30
86      }
87     });
88
89 app.use(session_middleware);
90
91 // convert a connect middleware to a Socket.IO middleware
92 const wrap = middleware => (socket, next) => middleware(socket.request, {}, next);
93
94 io.use(wrap(session_middleware));
95
96 // Load comments at server startup
97 fs.readFile(state_file, (err, data) => {
98     if (err)
99         state = {
100             images: [],
101             targets: [],
102             tardis: {
103                 companions: {
104                     names: [],
105                     count: 0
106                 },
107                 state: "welcome",
108                 level: 0
109             }
110         };
111     else
112         state = JSON.parse(data);
113 });
114
115 // Save comments when server is shutting down
116 function cleanup() {
117     fs.writeFileSync('zombocom-state.json', JSON.stringify(state), (error) => {
118         if (error)
119             throw error;
120     })
121 }
122
123 // And connect to that on either clean exit...
124 process.on('exit', cleanup);
125
126 // ... or on a SIGINT (control-C)
127 process.on('SIGINT', () => {
128     cleanup();
129     process.exit();
130 });
131
132 app.get('/index.html', (req, res) => {
133     res.sendFile(__dirname + '/index.html');
134 });
135
136 function bus_app(req, res) {
137     res.sendFile(__dirname + '/bus.html');
138 }
139
140 app.get('/bus', bus_app);
141 app.get('/bus/', bus_app);
142
143 const io_bus = io.of("/bus");
144
145 io_bus.use(wrap(session_middleware));
146
147 var bus_interval = 0;
148
149 function start_bus() {
150     const bus = state.bus;
151
152     bus.state = "program";
153
154     // Let all companions know the state of the game
155     io_bus.emit("state", bus.state);
156 }
157
158 function emit_bus_timer() {
159     const bus = state.bus;
160     io_bus.emit('timer', bus.timer);
161     bus.timer = bus.timer - 1;
162     if (bus.timer < 0) {
163         clearInterval(bus_interval);
164         bus.timer = 30;
165         setTimeout(start_bus, 3000);
166     }
167 }
168
169 function start_bus_timer() {
170     const bus = state.bus;
171     bus.timer = 3; // XXX: 30 in production
172     emit_bus_timer();
173     bus_interval = setInterval(emit_bus_timer, 1000);
174 }
175
176 io_bus.on("connection", (socket) => {
177     if (! socket.request.session.name) {
178         console.log("Error: Someone showed up at the Magic School Bus without a name.");
179         return;
180     }
181
182     const name = socket.request.session.name;
183     const bus = state.bus;
184
185     // Let the new user know the state of the bus
186     socket.emit("state", bus.state);
187
188     if (bus.students.count === 0) {
189         start_bus_timer();
190     }
191
192     if (! bus.students.names.includes(name)) {
193         bus.students.count = bus.students.count + 1;
194         io_bus.emit('students', bus.students.count);
195     }
196     bus.students.names.push(name);
197
198     socket.on('run', code => {
199         try {
200             output = child_process.execFileSync(python_path, [interpret_cairo_script, code], { input: code });
201             // Grab just first line of output
202             const nl = output.indexOf("\n");
203             if (nl === -1)
204                 nl = undefined;
205             const filename = output.toString().substring(0, nl);
206             
207             // Give all clients the new image
208             io_bus.emit('output', filename);
209         } catch (e) {
210             console.log("Error executing turtle script: " + e);
211         }
212     });
213
214     socket.on('jumpstart', () => {
215         const bus = state.bus;
216
217         bus.state = "welcome";
218         io_bus.emit("state", bus.state);
219         io_bus.emit('students', bus.students.count);
220
221         start_bus_timer();
222     });
223
224     socket.on('disconnect', () => {
225         const names = bus.students.names;
226
227         names.splice(names.indexOf(name), 1);
228
229         if (! names.includes(name)) {
230             bus.students.count = bus.students.count - 1;
231             io_bus.emit('students', bus.students.count);
232         }
233     });
234 });
235
236 function tardis_app(req, res) {
237     if (! req.session.name) {
238         res.sendFile(__dirname + '/tardis-error.html');
239     } else {
240         res.sendFile(__dirname + '/tardis.html');
241     }
242 }
243
244 app.get('/tardis', tardis_app);
245 app.get('/tardis/', tardis_app);
246
247 const io_tardis = io.of("/tardis");
248
249 io_tardis.use(wrap(session_middleware));
250
251 var tardis_interval;
252 var game_timer;
253
254 const levels = [
255     {
256         title: "Calibrate the Trans-Dimensional Field Accelerator",
257         words: [
258             "What", "was", "the", "year", "of", "Coda's", "birth?"
259         ],
260         answer: 2098
261     },
262     {
263         title: "Reverse the Polarity of the Neutrino Flow Coil",
264         words: [
265             "How", "many", "years", "had", "Zombo.com", "been", "running",
266             "when", "Coda", "sent", "the", "message", "you", "first",
267             "received?"
268         ],
269         answer: 123
270     },
271     {
272         title: "Give a good whack to Hydro-chronometric Energy Feedback Retro-stabilizer",
273         words: [
274             "What", "is", "the", "sum", "of", "Cameron's", "age", "plus", "Andrew's", "age", "at", "Christmas", "time", "the", "year", "when", "Hyrum's", "age", "is", "25", "years", "older", "than", "Scott's", "age", "will", "have", "been", "when", "Scott's", "age", "will", "be", "half", "of", "Dad's", "age?"
275         ],
276         answer: 111
277     },
278     {
279         title: "Disable the Fragmentary Spatio-temporal Particle Detector",
280         words: [
281             "What", "is", "the", "product", "of", "the", "last", "three", "numbers?"
282         ],
283         answer: 28643994
284     }
285 ];
286
287 var show_word_interval = 0;
288
289 function show_word() {
290     const tardis = state.tardis;
291     const room = "room-" + (tardis.word % 4).toString();
292     const word = levels[tardis.level].words[tardis.word];
293     io_tardis.to(room).emit('show-word', word);
294     tardis.word = tardis.word + 1;
295     if (tardis.word >= levels[tardis.level].words.length)
296         tardis.word = 0;
297 }
298
299 function start_level() {
300     const tardis = state.tardis;
301
302     // Inform all players of the new level
303     io_tardis.emit("level", levels[tardis.level].title);
304
305     // Then start the timer that shows the words
306     show_word_interval = setInterval(show_word, 1200);
307 }
308
309 function level_up() {
310     const tardis = state.tardis;
311
312     if (show_word_interval) {
313         clearInterval(show_word_interval);
314         show_word_interval = 0;
315     }
316
317     if (tardis.state === "game") {
318         tardis.level = tardis.level + 1;
319         tardis.word = 0;
320
321         if (tardis.level >= levels.length) {
322             tardis.state = "over";
323             io_tardis.emit("state", tardis.state);
324         } else {
325             setTimeout(() => {
326                 start_level();
327             }, 2000);
328         }
329
330     }
331 }
332
333 function start_game() {
334     const tardis = state.tardis;
335
336     tardis.state = "game";
337     tardis.level = 0;
338     tardis.word = 0;
339
340     // Let all companions know the state of the game
341     io_tardis.emit("level", levels[tardis.level].title);
342     io_tardis.emit("state", tardis.state);
343
344     start_level();
345 }
346
347 function emit_tardis_timer() {
348     const tardis = state.tardis;
349     io_tardis.emit('timer', tardis.timer);
350     tardis.timer = tardis.timer - 1;
351     if (tardis.timer < 0) {
352         clearInterval(tardis_interval);
353         tardis.timer = 30;
354         setTimeout(start_game, 3000);
355     }
356 }
357
358 function start_welcome_timer() {
359     const tardis = state.tardis;
360     tardis.timer = 30;
361     emit_tardis_timer();
362     tardis_interval = setInterval(emit_tardis_timer, 1000);
363 }
364
365 io_tardis.on("connection", (socket) => {
366     if (! socket.request.session.name) {
367         console.log("Error: Someone showed up at the Tardis without a name.");
368         return;
369     }
370
371     const name = socket.request.session.name;
372     const tardis = state.tardis;
373
374     // Let the new user know the state of the game
375     socket.emit("state", tardis.state);
376
377     // And the level if relevant
378     if (tardis.state === "game") {
379         socket.emit("level", levels[tardis.level].title);
380     }
381
382     // Put each of our boys into a different room
383     switch (name[0]) {
384     case 'C':
385     case 'c':
386         socket.join("room-0");
387         break;
388     case 'H':
389     case' h':
390         socket.join("room-1");
391         break;
392     case 'A':
393     case 'a':
394         socket.join("room-2");
395         break;
396     case 'S':
397     case 's':
398         socket.join("room-3");
399         break;
400     default:
401         const room = Math.floor(Math.random()*4);
402         socket.join("room-"+room.toString());
403         break;
404     }
405
406     if (tardis.companions.count === 0) {
407         start_welcome_timer();
408     }
409
410     if (! tardis.companions.names.includes(name)) {
411         tardis.companions.count = tardis.companions.count + 1;
412         io_tardis.emit('companions', tardis.companions.count);
413     }
414     tardis.companions.names.push(name);
415
416     socket.on('answer', answer => {
417         const tardis = state.tardis;
418
419         if (tardis.state != "game") {
420             return;
421         }
422
423         if (answer == levels[tardis.level].answer) {
424             io_tardis.emit('correct');
425             level_up();
426         } else {
427             io_tardis.emit('incorrect');
428         }
429     });
430
431     socket.on('reboot', () => {
432         const tardis = state.tardis;
433
434         if (show_word_interval) {
435             clearInterval(show_word_interval);
436             show_word_interval = 0;
437         }
438
439         tardis.state = "welcome";
440         io_tardis.emit("state", tardis.state);
441         io_tardis.emit('companions', tardis.companions.count);
442
443         start_welcome_timer();
444     });
445
446     socket.on('disconnect', () => {
447         const names = tardis.companions.names;
448
449         names.splice(names.indexOf(name), 1);
450
451         if (! names.includes(name)) {
452             tardis.companions.count = tardis.companions.count - 1;
453             io_tardis.emit('companions', tardis.companions.count);
454         }
455     });
456 });
457
458 io.on('connection', (socket) => {
459
460     // First things first, tell the client their name (if any)
461     if (socket.request.session.name) {
462         socket.emit('inform-name', socket.request.session.name);
463     }
464
465     // Replay old comments and images to a newly-joining client
466     socket.emit('reset');
467     state.images.forEach((image) => {
468         socket.emit('image', image)
469     });
470
471     socket.on('set-name', (name) => {
472         console.log("Received set-name event: " + name);
473         socket.request.session.name = name;
474         socket.request.session.save();
475         // Complete the round trip to the client
476         socket.emit('inform-name', socket.request.session.name);
477     });
478
479     // When any client comments, send that to all clients (including sender)
480     socket.on('comment', (comment) => {
481         const images = state.images;
482
483         // Send comment to clients after adding commenter's name
484         comment.name = socket.request.session.name;
485         io.emit('comment', comment);
486
487         const index = images.findIndex(image => image.id == comment.image_id);
488
489         // Before adding the comment to server's state, drop the image_id
490         delete comment.image_id;
491
492         // Now add the comment to the image, remove the image from the
493         // images array and then add it back at the end, (so it appears
494         // as the most-recently-modified image for any new clients)
495         const image = images[index];
496         image.comments.push(comment);
497         images.splice(index, 1);
498         images.push(image);
499     });
500
501     // Generate an image when requested
502     socket.on('generate', (request) => {
503         console.log(`Generating image for ${socket.request.session.name} with code=${request['code']} and prompt=${request['prompt']}`);
504         async function generate_image(code, prompt) {
505             function emit_image(image, target) {
506                 image.id = state.images.length;
507                 image.censored = false;
508                 image.link = "";
509                 if (target) {
510                     image.comments = [{
511                         "name": "ZomboCom",
512                         "text": target.response
513                     }];
514                     if (! state.targets.includes(target.short)) {
515                         state.targets.push(target.short);
516                     }
517                 } else {
518                     image.comments = [];
519                 }
520                 io.emit('image', image);
521                 state.images.push(image);
522             }
523
524             var promise;
525
526             // Before doing any generation, check for a target image
527             const normal_target = prompt.replace(/[^a-zA-Z]/g, "").toLowerCase() + code.toString() + ".png";
528             const target_arr = targets.filter(item => item.normal === normal_target);
529             if (target_arr.length) {
530                 const target = target_arr[0];
531                 const target_file = `${targets_dir}/${normal_target}`;
532                 const normal_prompt = prompt.replace(/[^-_.a-zA-Z]/g, "_");
533                 var counter = 1;
534                 var base;
535                 var filename;
536                 while (true) {
537                     if (counter > 1) {
538                         base = `${code}_${normal_prompt}_${counter}.png`
539                     } else {
540                         base = `${code}_${normal_prompt}.png`
541                     }
542                     filename = `${images_dir}/${base}`
543                     if (! fs.existsSync(filename)) {
544                         break;
545                     }
546                     counter = counter + 1;
547                 }
548                 fs.copyFile(target_file, filename, 0, (err) => {
549                     if (err) {
550                         console.log("Error copying " + target_file + " to " + filename + ": " + err);
551                     }
552                 });
553                 const image = {
554                     "code": code,
555                     "prompt": prompt,
556                     "filename": '/images/' + base
557                 };
558                 emit_image(image, target);
559             } else {
560
561                 // Inject the target seed for the "dice" prompt once every
562                 // 4 requests for a random seed (and only if the word
563                 // "dice" does not appear in the prompt).
564                 if (!code && !prompt.toLowerCase().includes("dice")) {
565                     if (state.images.length % 4 == 0) {
566                         code = 319630254;
567                     }
568                 }
569
570                 if (code) {
571                     promise = execFile(python_path, [generate_image_script, `--seed=${code}`, prompt])
572                 } else {
573                     promise = execFile(python_path, [generate_image_script, prompt])
574                 }
575                 const child = promise.child;
576                 child.stdout.on('data', (data) => {
577                     const images = JSON.parse(data);
578                     images.forEach((image) => {
579                         emit_image(image, null);
580                     });
581                 });
582                 child.stderr.on('data', (data) => {
583                     console.log("Error occurred during generate-image: " + data);
584                 });
585                 try {
586                     const { stdout, stderr } = await promise;
587                 } catch(e) {
588                     console.error(e);
589                 }
590             }
591             socket.emit('generation-done');
592         }
593
594         generate_image(request['code'], request['prompt']);
595     });
596 });
597
598 server.listen(port, () => {
599     console.log(`listening on *:${port}`);
600 });