]> git.cworth.org Git - zombocom-ai/blob - index.js
6aef48c06360b598ef68eb9d5bbaa0e02efce188
[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             bus : {
111                 students: {
112                     names: [],
113                     count: 0,
114                 },
115                 state: "welcome"
116             }
117         };
118     else
119         state = JSON.parse(data);
120 });
121
122 // Save comments when server is shutting down
123 function cleanup() {
124     fs.writeFileSync('zombocom-state.json', JSON.stringify(state), (error) => {
125         if (error)
126             throw error;
127     })
128 }
129
130 // And connect to that on either clean exit...
131 process.on('exit', cleanup);
132
133 // ... or on a SIGINT (control-C)
134 process.on('SIGINT', () => {
135     cleanup();
136     process.exit();
137 });
138
139 app.get('/index.html', (req, res) => {
140     res.sendFile(__dirname + '/index.html');
141 });
142
143 function bus_app(req, res) {
144     res.sendFile(__dirname + '/bus.html');
145 }
146
147 app.get('/bus', bus_app);
148 app.get('/bus/', bus_app);
149
150 const io_bus = io.of("/bus");
151
152 io_bus.use(wrap(session_middleware));
153
154 var bus_interval = 0;
155
156 function start_bus() {
157     const bus = state.bus;
158
159     bus.state = "program";
160
161     // Let all companions know the state of the game
162     io_bus.emit("state", bus.state);
163 }
164
165 function emit_bus_timer() {
166     const bus = state.bus;
167     io_bus.emit('timer', bus.timer);
168     bus.timer = bus.timer - 1;
169     if (bus.timer < 0) {
170         clearInterval(bus_interval);
171         bus.timer = 30;
172         setTimeout(start_bus, 3000);
173     }
174 }
175
176 function start_bus_timer() {
177     const bus = state.bus;
178     bus.timer = 3; // XXX: 30 in production
179     emit_bus_timer();
180     bus_interval = setInterval(emit_bus_timer, 1000);
181 }
182
183 io_bus.on("connection", (socket) => {
184     if (! socket.request.session.name) {
185         console.log("Error: Someone showed up at the Magic School Bus without a name.");
186         return;
187     }
188
189     const name = socket.request.session.name;
190     const bus = state.bus;
191
192     // Let the new user know the state of the bus
193     socket.emit("state", bus.state);
194
195     if (bus.students.count === 0) {
196         start_bus_timer();
197     }
198
199     if (! bus.students.names.includes(name)) {
200         bus.students.count = bus.students.count + 1;
201         io_bus.emit('students', bus.students.count);
202     }
203     bus.students.names.push(name);
204
205     socket.on('run', code => {
206         try {
207             output = child_process.execFileSync(python_path, [interpret_cairo_script, code], { input: code });
208             // Grab just first line of output
209             const nl = output.indexOf("\n");
210             if (nl === -1)
211                 nl = undefined;
212             const filename = output.toString().substring(0, nl);
213             
214             // Give all clients the new image
215             io_bus.emit('output', filename);
216         } catch (e) {
217             console.log("Error executing turtle script: " + e);
218         }
219     });
220
221     socket.on('jumpstart', () => {
222         const bus = state.bus;
223
224         bus.state = "welcome";
225         io_bus.emit("state", bus.state);
226         io_bus.emit('students', bus.students.count);
227
228         start_bus_timer();
229     });
230
231     socket.on('disconnect', () => {
232         const names = bus.students.names;
233
234         names.splice(names.indexOf(name), 1);
235
236         if (! names.includes(name)) {
237             bus.students.count = bus.students.count - 1;
238             io_bus.emit('students', bus.students.count);
239         }
240     });
241 });
242
243 function tardis_app(req, res) {
244     if (! req.session.name) {
245         res.sendFile(__dirname + '/tardis-error.html');
246     } else {
247         res.sendFile(__dirname + '/tardis.html');
248     }
249 }
250
251 app.get('/tardis', tardis_app);
252 app.get('/tardis/', tardis_app);
253
254 const io_tardis = io.of("/tardis");
255
256 io_tardis.use(wrap(session_middleware));
257
258 var tardis_interval;
259 var game_timer;
260
261 const levels = [
262     {
263         title: "Calibrate the Trans-Dimensional Field Accelerator",
264         words: [
265             "What", "was", "the", "year", "of", "Coda's", "birth?"
266         ],
267         answer: 2098
268     },
269     {
270         title: "Reverse the Polarity of the Neutrino Flow Coil",
271         words: [
272             "How", "many", "years", "had", "Zombo.com", "been", "running",
273             "when", "Coda", "sent", "the", "message", "you", "first",
274             "received?"
275         ],
276         answer: 123
277     },
278     {
279         title: "Give a good whack to Hydro-chronometric Energy Feedback Retro-stabilizer",
280         words: [
281             "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?"
282         ],
283         answer: 111
284     },
285     {
286         title: "Disable the Fragmentary Spatio-temporal Particle Detector",
287         words: [
288             "What", "is", "the", "product", "of", "the", "last", "three", "numbers?"
289         ],
290         answer: 28643994
291     }
292 ];
293
294 var show_word_interval = 0;
295
296 function show_word() {
297     const tardis = state.tardis;
298     const room = "room-" + (tardis.word % 4).toString();
299     const word = levels[tardis.level].words[tardis.word];
300     io_tardis.to(room).emit('show-word', word);
301     tardis.word = tardis.word + 1;
302     if (tardis.word >= levels[tardis.level].words.length)
303         tardis.word = 0;
304 }
305
306 function start_level() {
307     const tardis = state.tardis;
308
309     // Inform all players of the new level
310     io_tardis.emit("level", levels[tardis.level].title);
311
312     // Then start the timer that shows the words
313     show_word_interval = setInterval(show_word, 1200);
314 }
315
316 function level_up() {
317     const tardis = state.tardis;
318
319     if (show_word_interval) {
320         clearInterval(show_word_interval);
321         show_word_interval = 0;
322     }
323
324     if (tardis.state === "game") {
325         tardis.level = tardis.level + 1;
326         tardis.word = 0;
327
328         if (tardis.level >= levels.length) {
329             tardis.state = "over";
330             io_tardis.emit("state", tardis.state);
331         } else {
332             setTimeout(() => {
333                 start_level();
334             }, 2000);
335         }
336
337     }
338 }
339
340 function start_game() {
341     const tardis = state.tardis;
342
343     tardis.state = "game";
344     tardis.level = 0;
345     tardis.word = 0;
346
347     // Let all companions know the state of the game
348     io_tardis.emit("level", levels[tardis.level].title);
349     io_tardis.emit("state", tardis.state);
350
351     start_level();
352 }
353
354 function emit_tardis_timer() {
355     const tardis = state.tardis;
356     io_tardis.emit('timer', tardis.timer);
357     tardis.timer = tardis.timer - 1;
358     if (tardis.timer < 0) {
359         clearInterval(tardis_interval);
360         tardis.timer = 30;
361         setTimeout(start_game, 3000);
362     }
363 }
364
365 function start_welcome_timer() {
366     const tardis = state.tardis;
367     tardis.timer = 30;
368     emit_tardis_timer();
369     tardis_interval = setInterval(emit_tardis_timer, 1000);
370 }
371
372 io_tardis.on("connection", (socket) => {
373     if (! socket.request.session.name) {
374         console.log("Error: Someone showed up at the Tardis without a name.");
375         return;
376     }
377
378     const name = socket.request.session.name;
379     const tardis = state.tardis;
380
381     // Let the new user know the state of the game
382     socket.emit("state", tardis.state);
383
384     // And the level if relevant
385     if (tardis.state === "game") {
386         socket.emit("level", levels[tardis.level].title);
387     }
388
389     // Put each of our boys into a different room
390     switch (name[0]) {
391     case 'C':
392     case 'c':
393         socket.join("room-0");
394         break;
395     case 'H':
396     case' h':
397         socket.join("room-1");
398         break;
399     case 'A':
400     case 'a':
401         socket.join("room-2");
402         break;
403     case 'S':
404     case 's':
405         socket.join("room-3");
406         break;
407     default:
408         const room = Math.floor(Math.random()*4);
409         socket.join("room-"+room.toString());
410         break;
411     }
412
413     if (tardis.companions.count === 0) {
414         start_welcome_timer();
415     }
416
417     if (! tardis.companions.names.includes(name)) {
418         tardis.companions.count = tardis.companions.count + 1;
419         io_tardis.emit('companions', tardis.companions.count);
420     }
421     tardis.companions.names.push(name);
422
423     socket.on('answer', answer => {
424         const tardis = state.tardis;
425
426         if (tardis.state != "game") {
427             return;
428         }
429
430         if (answer == levels[tardis.level].answer) {
431             io_tardis.emit('correct');
432             level_up();
433         } else {
434             io_tardis.emit('incorrect');
435         }
436     });
437
438     socket.on('reboot', () => {
439         const tardis = state.tardis;
440
441         if (show_word_interval) {
442             clearInterval(show_word_interval);
443             show_word_interval = 0;
444         }
445
446         tardis.state = "welcome";
447         io_tardis.emit("state", tardis.state);
448         io_tardis.emit('companions', tardis.companions.count);
449
450         start_welcome_timer();
451     });
452
453     socket.on('disconnect', () => {
454         const names = tardis.companions.names;
455
456         names.splice(names.indexOf(name), 1);
457
458         if (! names.includes(name)) {
459             tardis.companions.count = tardis.companions.count - 1;
460             io_tardis.emit('companions', tardis.companions.count);
461         }
462     });
463 });
464
465 io.on('connection', (socket) => {
466
467     // First things first, tell the client their name (if any)
468     if (socket.request.session.name) {
469         socket.emit('inform-name', socket.request.session.name);
470     }
471
472     // Replay old comments and images to a newly-joining client
473     socket.emit('reset');
474     state.images.forEach((image) => {
475         socket.emit('image', image)
476     });
477
478     socket.on('set-name', (name) => {
479         console.log("Received set-name event: " + name);
480         socket.request.session.name = name;
481         socket.request.session.save();
482         // Complete the round trip to the client
483         socket.emit('inform-name', socket.request.session.name);
484     });
485
486     // When any client comments, send that to all clients (including sender)
487     socket.on('comment', (comment) => {
488         const images = state.images;
489
490         // Send comment to clients after adding commenter's name
491         comment.name = socket.request.session.name;
492         io.emit('comment', comment);
493
494         const index = images.findIndex(image => image.id == comment.image_id);
495
496         // Before adding the comment to server's state, drop the image_id
497         delete comment.image_id;
498
499         // Now add the comment to the image, remove the image from the
500         // images array and then add it back at the end, (so it appears
501         // as the most-recently-modified image for any new clients)
502         const image = images[index];
503         image.comments.push(comment);
504         images.splice(index, 1);
505         images.push(image);
506     });
507
508     // Generate an image when requested
509     socket.on('generate', (request) => {
510         console.log(`Generating image for ${socket.request.session.name} with code=${request['code']} and prompt=${request['prompt']}`);
511         async function generate_image(code, prompt) {
512             function emit_image(image, target) {
513                 image.id = state.images.length;
514                 image.censored = false;
515                 image.link = "";
516                 if (target) {
517                     image.comments = [{
518                         "name": "ZomboCom",
519                         "text": target.response
520                     }];
521                     if (! state.targets.includes(target.short)) {
522                         state.targets.push(target.short);
523                     }
524                 } else {
525                     image.comments = [];
526                 }
527                 io.emit('image', image);
528                 state.images.push(image);
529             }
530
531             var promise;
532
533             // Before doing any generation, check for a target image
534             const normal_target = prompt.replace(/[^a-zA-Z]/g, "").toLowerCase() + code.toString() + ".png";
535             const target_arr = targets.filter(item => item.normal === normal_target);
536             if (target_arr.length) {
537                 const target = target_arr[0];
538                 const target_file = `${targets_dir}/${normal_target}`;
539                 const normal_prompt = prompt.replace(/[^-_.a-zA-Z]/g, "_");
540                 var counter = 1;
541                 var base;
542                 var filename;
543                 while (true) {
544                     if (counter > 1) {
545                         base = `${code}_${normal_prompt}_${counter}.png`
546                     } else {
547                         base = `${code}_${normal_prompt}.png`
548                     }
549                     filename = `${images_dir}/${base}`
550                     if (! fs.existsSync(filename)) {
551                         break;
552                     }
553                     counter = counter + 1;
554                 }
555                 fs.copyFile(target_file, filename, 0, (err) => {
556                     if (err) {
557                         console.log("Error copying " + target_file + " to " + filename + ": " + err);
558                     }
559                 });
560                 const image = {
561                     "code": code,
562                     "prompt": prompt,
563                     "filename": '/images/' + base
564                 };
565                 emit_image(image, target);
566             } else {
567
568                 // Inject the target seed for the "dice" prompt once every
569                 // 4 requests for a random seed (and only if the word
570                 // "dice" does not appear in the prompt).
571                 if (!code && !prompt.toLowerCase().includes("dice")) {
572                     if (state.images.length % 4 == 0) {
573                         code = 319630254;
574                     }
575                 }
576
577                 if (code) {
578                     promise = execFile(python_path, [generate_image_script, `--seed=${code}`, prompt])
579                 } else {
580                     promise = execFile(python_path, [generate_image_script, prompt])
581                 }
582                 const child = promise.child;
583                 child.stdout.on('data', (data) => {
584                     const images = JSON.parse(data);
585                     images.forEach((image) => {
586                         emit_image(image, null);
587                     });
588                 });
589                 child.stderr.on('data', (data) => {
590                     console.log("Error occurred during generate-image: " + data);
591                 });
592                 try {
593                     const { stdout, stderr } = await promise;
594                 } catch(e) {
595                     console.error(e);
596                 }
597             }
598             socket.emit('generation-done');
599         }
600
601         generate_image(request['code'], request['prompt']);
602     });
603 });
604
605 server.listen(port, () => {
606     console.log(`listening on *:${port}`);
607 });