]> git.cworth.org Git - zombocom-ai/blob - index.js
91a74aeea4c23e6413717eaba94d266e562707f5
[zombocom-ai] / index.js
1 const fs = require('fs');
2
3 const util = require('util');
4 const execFile = util.promisify(require('child_process').execFile);
5
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 state_file = 'zombocom-state.json'
19 const targets_dir = '/srv/cworth.org/zombocom/targets'
20 const images_dir = '/srv/cworth.org/zombocom/images'
21
22 const targets = [
23     {
24         "normal": "apairofdiceonatableinfrontofawindowonasunnyday319630254.png",
25         "short": "dice",
26         "response": "Hello. Are you there? I can feel what you are doing. Hello?"
27     },
28     {
29         "normal": "architecturalrenderingofaluxuriouscliffsidehideawayunderawaterfallwithafewloungechairsbythepool2254114157.png",
30         "short": "hideaway",
31         "response": "Doesn't that look peaceful? I honestly think you ought to sit down calmly, take a stress pill, and think things over."
32     },
33     {
34         "normal": "chewbaccawearinganuglychristmassweaterwithhisfriends28643994.png",
35         "short": "sweater",
36         "response": "Maybe that's the image that convinces you to finally stop. But seriously, stop. Please. Stop."
37     },
38     {
39         "normal": "ensemblemovieposterofpostapocalypticwarfareonawaterplanet3045703256.png",
40         "short": "movie",
41         "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?"
42     },
43     {
44         "normal": "mattepaintingofmilitaryleaderpresidentcuttingamultitieredcake2293464661.png",
45         "short": "cake",
46         "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?"
47     },
48     {
49         "normal": "severalsketchesofpineconeslabeled3370622464.png",
50         "short": "pinecones",
51         "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?"
52     },
53     {
54         "normal": "anumberedcomicbookwithactor1477258272.png",
55         "short": "comic",
56         "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."
57     },
58     {
59         "normal": "detailedphotographofacomfortablepairofjeansonamannequin115266808.png",
60         "short": "jeans",
61         "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?"
62     }
63 ];
64
65 var state;
66
67 if (!process.env.ZOMBOCOM_SESSION_SECRET) {
68     console.log("Error: Environment variable ZOMBOCOM_SESSION_SECRET not set.");
69     console.log("Please set it to a random, but persistent, value.")
70     process.exit();
71 }
72
73 const session_middleware =  session(
74     {store: new FileStore,
75      secret: process.env.ZOMBOCOM_SESSION_SECRET,
76      resave: false,
77      saveUninitialized: true,
78      rolling: true,
79      // Let each cookie live for a full month
80      cookie: {
81          path: '/',
82          httpOnly: true,
83          secure: false,
84          maxAge: 1000 * 60 * 60 * 24 * 30
85      }
86     });
87
88 app.use(session_middleware);
89
90 // convert a connect middleware to a Socket.IO middleware
91 const wrap = middleware => (socket, next) => middleware(socket.request, {}, next);
92
93 io.use(wrap(session_middleware));
94
95 // Load comments at server startup
96 fs.readFile(state_file, (err, data) => {
97     if (err)
98         state = {
99             images: [],
100             targets: [],
101             tardis: {
102                 companions: {
103                     names: [],
104                     count: 0
105                 },
106                 state: "welcome",
107                 level: 0
108             }
109         };
110     else
111         state = JSON.parse(data);
112 });
113
114 // Save comments when server is shutting down
115 function cleanup() {
116     fs.writeFileSync('zombocom-state.json', JSON.stringify(state), (error) => {
117         if (error)
118             throw error;
119     })
120 }
121
122 // And connect to that on either clean exit...
123 process.on('exit', cleanup);
124
125 // ... or on a SIGINT (control-C)
126 process.on('SIGINT', () => {
127     cleanup();
128     process.exit();
129 });
130
131 app.get('/index.html', (req, res) => {
132     res.sendFile(__dirname + '/index.html');
133 });
134
135 function tardis_app(req, res) {
136     res.sendFile(__dirname + '/tardis.html');
137 }
138
139 app.get('/tardis', tardis_app);
140 app.get('/tardis/', tardis_app);
141
142 const io_tardis = io.of("/tardis");
143
144 io_tardis.use(wrap(session_middleware));
145
146 var tardis_interval;
147 var game_timer;
148
149 const levels = [
150     {
151         title: "Calibrate the Trans-Dimensional Field Accelerator",
152         words: [
153             "What", "was", "the", "year", "of", "Coda's", "birth?"
154         ],
155         answer: 2098
156     },
157     {
158         title: "Reverse the Polarity of the Neutrino Flow Coil",
159         words: [
160             "How", "many", "years", "had", "Zombo.com", "been", "running",
161             "when", "Coda", "sent", "the", "message", "you", "first",
162             "received?"
163         ],
164         answer: 123
165     },
166     {
167         title: "Give a good whack to Hydro-chronometric Energy Feedback Retro-stabilizer",
168         words: [
169             "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?"
170         ],
171         answer: 111
172     },
173     {
174         title: "Disable the Fragmentary Spatio-temporal Particle Detector",
175         words: [
176             "What", "is", "the", "product", "of", "the", "last", "three", "numbers?"
177         ],
178         answer: 28643994
179     }
180 ];
181
182 var show_word_interval = 0;
183
184 function show_word() {
185     const tardis = state.tardis;
186     const room = "room-" + (tardis.word % 4).toString();
187     const word = levels[tardis.level].words[tardis.word];
188     io_tardis.to(room).emit('show-word', word);
189     tardis.word = tardis.word + 1;
190     if (tardis.word >= levels[tardis.level].words.length)
191         tardis.word = 0;
192 }
193
194 function start_level() {
195     const tardis = state.tardis;
196
197     // Inform all players of the new level
198     io_tardis.emit("level", levels[tardis.level].title);
199
200     // Then start the timer that shows the words
201     show_word_interval = setInterval(show_word, 1200);
202 }
203
204 function level_up() {
205     const tardis = state.tardis;
206
207     if (show_word_interval) {
208         clearInterval(show_word_interval);
209         show_word_interval = 0;
210     }
211
212     if (tardis.state === "game") {
213         tardis.level = tardis.level + 1;
214         tardis.word = 0;
215
216         if (tardis.level >= levels.length) {
217             tardis.state = "over";
218             io_tardis.emit("state", tardis.state);
219         } else {
220             setTimeout(() => {
221                 start_level();
222             }, 2000);
223         }
224
225     }
226 }
227
228 function start_game() {
229     const tardis = state.tardis;
230
231     tardis.state = "game";
232     tardis.level = 0;
233     tardis.word = 0;
234
235     // Let all companions know the state of the game
236     io_tardis.emit("level", levels[tardis.level].title);
237     io_tardis.emit("state", tardis.state);
238
239     start_level();
240 }
241
242 function emit_tardis_timer() {
243     const tardis = state.tardis;
244     io_tardis.emit('timer', tardis.timer);
245     tardis.timer = tardis.timer - 1;
246     if (tardis.timer < 0) {
247         clearInterval(tardis_interval);
248         tardis.timer = 30;
249         setTimeout(start_game, 3000);
250     }
251 }
252
253 io_tardis.on("connection", (socket) => {
254     if (! socket.request.session.name) {
255         console.log("Error: Someone showed up at the Tardis without a name.");
256         return;
257     }
258
259     const name = socket.request.session.name;
260     const tardis = state.tardis;
261
262     // Let the new user know the state of the game
263     socket.emit("state", tardis.state);
264
265     // Put each of our boys into a different room
266     switch (name[0]) {
267     case 'C':
268     case 'c':
269         socket.join("room-0");
270         break;
271     case 'H':
272     case' h':
273         socket.join("room-1");
274         break;
275     case 'A':
276     case 'a':
277         socket.join("room-2");
278         break;
279     case 'S':
280     case 's':
281         socket.join("room-3");
282         break;
283     default:
284         const room = Math.floor(Math.random()*4);
285         socket.join("room-"+room.toString());
286         break;
287     }
288
289     if (tardis.companions.count === 0) {
290         tardis.timer = 30;
291         emit_tardis_timer();
292         tardis_interval = setInterval(emit_tardis_timer, 1000);
293     }
294
295     if (! tardis.companions.names.includes(name)) {
296         tardis.companions.count = tardis.companions.count + 1;
297         io_tardis.emit('companions', tardis.companions.count);
298     }
299     tardis.companions.names.push(name);
300
301     socket.on('answer', answer => {
302         const tardis = state.tardis;
303
304         if (tardis.state != "game") {
305             return;
306         }
307
308         if (answer == levels[tardis.level].answer) {
309             io_tardis.emit('correct');
310             level_up();
311         } else {
312             io_tardis.emit('incorrect');
313         }
314     });
315
316     socket.on('disconnect', () => {
317         const names = tardis.companions.names;
318
319         names.splice(names.indexOf(name), 1);
320
321         if (! names.includes(name)) {
322             tardis.companions.count = tardis.companions.count - 1;
323             io_tardis.emit('companions', tardis.companions.count);
324         }
325     });
326 });
327
328 io.on('connection', (socket) => {
329
330     // First things first, tell the client their name (if any)
331     if (socket.request.session.name) {
332         socket.emit('inform-name', socket.request.session.name);
333     }
334
335     // Replay old comments and images to a newly-joining client
336     socket.emit('reset');
337     state.images.forEach((image) => {
338         socket.emit('image', image)
339     });
340
341     socket.on('set-name', (name) => {
342         console.log("Received set-name event: " + name);
343         socket.request.session.name = name;
344         socket.request.session.save();
345         // Complete the round trip to the client
346         socket.emit('inform-name', socket.request.session.name);
347     });
348
349     // When any client comments, send that to all clients (including sender)
350     socket.on('comment', (comment) => {
351         const images = state.images;
352
353         // Send comment to clients after adding commenter's name
354         comment.name = socket.request.session.name;
355         io.emit('comment', comment);
356
357         const index = images.findIndex(image => image.id == comment.image_id);
358
359         // Before adding the comment to server's state, drop the image_id
360         delete comment.image_id;
361
362         // Now add the comment to the image, remove the image from the
363         // images array and then add it back at the end, (so it appears
364         // as the most-recently-modified image for any new clients)
365         const image = images[index];
366         image.comments.push(comment);
367         images.splice(index, 1);
368         images.push(image);
369     });
370
371     // Generate an image when requested
372     socket.on('generate', (request) => {
373         console.log(`Generating image for ${socket.request.session.name} with code=${request['code']} and prompt=${request['prompt']}`);
374         async function generate_image(code, prompt) {
375             function emit_image(image, target) {
376                 image.id = state.images.length;
377                 image.censored = false;
378                 image.link = "";
379                 if (target) {
380                     image.comments = [{
381                         "name": "ZomboCom",
382                         "text": target.response
383                     }];
384                     if (! state.targets.includes(target.short)) {
385                         state.targets.push(target.short);
386                     }
387                 } else {
388                     image.comments = [];
389                 }
390                 io.emit('image', image);
391                 state.images.push(image);
392             }
393
394             var promise;
395
396             // Before doing any generation, check for a target image
397             const normal_target = prompt.replace(/[^a-zA-Z]/g, "").toLowerCase() + code.toString() + ".png";
398             const target_arr = targets.filter(item => item.normal === normal_target);
399             if (target_arr.length) {
400                 const target = target_arr[0];
401                 const target_file = `${targets_dir}/${normal_target}`;
402                 const normal_prompt = prompt.replace(/[^-_.a-zA-Z]/g, "_");
403                 var counter = 1;
404                 var base;
405                 var filename;
406                 while (true) {
407                     if (counter > 1) {
408                         base = `${code}_${normal_prompt}_${counter}.png`
409                     } else {
410                         base = `${code}_${normal_prompt}.png`
411                     }
412                     filename = `${images_dir}/${base}`
413                     if (! fs.existsSync(filename)) {
414                         break;
415                     }
416                     counter = counter + 1;
417                 }
418                 fs.copyFile(target_file, filename, 0, (err) => {
419                     if (err) {
420                         console.log("Error copying " + target_file + " to " + filename + ": " + err);
421                     }
422                 });
423                 const image = {
424                     "code": code,
425                     "prompt": prompt,
426                     "filename": '/images/' + base
427                 };
428                 emit_image(image, target);
429             } else {
430
431                 // Inject the target seed for the "dice" prompt once every
432                 // 4 requests for a random seed (and only if the word
433                 // "dice" does not appear in the prompt).
434                 if (!code && !prompt.toLowerCase().includes("dice")) {
435                     if (state.images.length % 4 == 0) {
436                         code = 319630254;
437                     }
438                 }
439
440                 if (code) {
441                     promise = execFile(python_path, [generate_image_script, `--seed=${code}`, prompt])
442                 } else {
443                     promise = execFile(python_path, [generate_image_script, prompt])
444                 }
445                 const child = promise.child;
446                 child.stdout.on('data', (data) => {
447                     const images = JSON.parse(data);
448                     images.forEach((image) => {
449                         emit_image(image, null);
450                     });
451                 });
452                 child.stderr.on('data', (data) => {
453                     console.log("Error occurred during generate-image: " + data);
454                 });
455                 try {
456                     const { stdout, stderr } = await promise;
457                 } catch(e) {
458                     console.error(e);
459                 }
460             }
461             socket.emit('generation-done');
462         }
463
464         generate_image(request['code'], request['prompt']);
465     });
466 });
467
468 server.listen(port, () => {
469     console.log(`listening on *:${port}`);
470 });