]> git.cworth.org Git - zombocom-ai/blob - index.js
Add an error page if the user has no name set
[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     if (! req.session.name) {
137         res.sendFile(__dirname + '/tardis-error.html');
138     } else {
139         res.sendFile(__dirname + '/tardis.html');
140     }
141 }
142
143 app.get('/tardis', tardis_app);
144 app.get('/tardis/', tardis_app);
145
146 const io_tardis = io.of("/tardis");
147
148 io_tardis.use(wrap(session_middleware));
149
150 var tardis_interval;
151 var game_timer;
152
153 const levels = [
154     {
155         title: "Calibrate the Trans-Dimensional Field Accelerator",
156         words: [
157             "What", "was", "the", "year", "of", "Coda's", "birth?"
158         ],
159         answer: 2098
160     },
161     {
162         title: "Reverse the Polarity of the Neutrino Flow Coil",
163         words: [
164             "How", "many", "years", "had", "Zombo.com", "been", "running",
165             "when", "Coda", "sent", "the", "message", "you", "first",
166             "received?"
167         ],
168         answer: 123
169     },
170     {
171         title: "Give a good whack to Hydro-chronometric Energy Feedback Retro-stabilizer",
172         words: [
173             "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?"
174         ],
175         answer: 111
176     },
177     {
178         title: "Disable the Fragmentary Spatio-temporal Particle Detector",
179         words: [
180             "What", "is", "the", "product", "of", "the", "last", "three", "numbers?"
181         ],
182         answer: 28643994
183     }
184 ];
185
186 var show_word_interval = 0;
187
188 function show_word() {
189     const tardis = state.tardis;
190     const room = "room-" + (tardis.word % 4).toString();
191     const word = levels[tardis.level].words[tardis.word];
192     io_tardis.to(room).emit('show-word', word);
193     tardis.word = tardis.word + 1;
194     if (tardis.word >= levels[tardis.level].words.length)
195         tardis.word = 0;
196 }
197
198 function start_level() {
199     const tardis = state.tardis;
200
201     // Inform all players of the new level
202     io_tardis.emit("level", levels[tardis.level].title);
203
204     // Then start the timer that shows the words
205     show_word_interval = setInterval(show_word, 1200);
206 }
207
208 function level_up() {
209     const tardis = state.tardis;
210
211     if (show_word_interval) {
212         clearInterval(show_word_interval);
213         show_word_interval = 0;
214     }
215
216     if (tardis.state === "game") {
217         tardis.level = tardis.level + 1;
218         tardis.word = 0;
219
220         if (tardis.level >= levels.length) {
221             tardis.state = "over";
222             io_tardis.emit("state", tardis.state);
223         } else {
224             setTimeout(() => {
225                 start_level();
226             }, 2000);
227         }
228
229     }
230 }
231
232 function start_game() {
233     const tardis = state.tardis;
234
235     tardis.state = "game";
236     tardis.level = 0;
237     tardis.word = 0;
238
239     // Let all companions know the state of the game
240     io_tardis.emit("level", levels[tardis.level].title);
241     io_tardis.emit("state", tardis.state);
242
243     start_level();
244 }
245
246 function emit_tardis_timer() {
247     const tardis = state.tardis;
248     io_tardis.emit('timer', tardis.timer);
249     tardis.timer = tardis.timer - 1;
250     if (tardis.timer < 0) {
251         clearInterval(tardis_interval);
252         tardis.timer = 30;
253         setTimeout(start_game, 3000);
254     }
255 }
256
257 function start_welcome_timer() {
258     const tardis = state.tardis;
259     tardis.timer = 30;
260     emit_tardis_timer();
261     tardis_interval = setInterval(emit_tardis_timer, 1000);
262 }
263
264 io_tardis.on("connection", (socket) => {
265     if (! socket.request.session.name) {
266         console.log("Error: Someone showed up at the Tardis without a name.");
267         return;
268     }
269
270     const name = socket.request.session.name;
271     const tardis = state.tardis;
272
273     // Let the new user know the state of the game
274     socket.emit("state", tardis.state);
275
276     // And the level if relevant
277     if (tardis.state === "game") {
278         socket.emit("level", levels[tardis.level].title);
279     }
280
281     // Put each of our boys into a different room
282     switch (name[0]) {
283     case 'C':
284     case 'c':
285         socket.join("room-0");
286         break;
287     case 'H':
288     case' h':
289         socket.join("room-1");
290         break;
291     case 'A':
292     case 'a':
293         socket.join("room-2");
294         break;
295     case 'S':
296     case 's':
297         socket.join("room-3");
298         break;
299     default:
300         const room = Math.floor(Math.random()*4);
301         socket.join("room-"+room.toString());
302         break;
303     }
304
305     if (tardis.companions.count === 0) {
306         start_welcome_timer();
307     }
308
309     if (! tardis.companions.names.includes(name)) {
310         tardis.companions.count = tardis.companions.count + 1;
311         io_tardis.emit('companions', tardis.companions.count);
312     }
313     tardis.companions.names.push(name);
314
315     socket.on('answer', answer => {
316         const tardis = state.tardis;
317
318         if (tardis.state != "game") {
319             return;
320         }
321
322         if (answer == levels[tardis.level].answer) {
323             io_tardis.emit('correct');
324             level_up();
325         } else {
326             io_tardis.emit('incorrect');
327         }
328     });
329
330     socket.on('reboot', () => {
331         const tardis = state.tardis;
332
333         if (show_word_interval) {
334             clearInterval(show_word_interval);
335             show_word_interval = 0;
336         }
337
338         tardis.state = "welcome";
339         io_tardis.emit("state", tardis.state);
340         io_tardis.emit('companions', tardis.companions.count);
341
342         start_welcome_timer();
343     });
344
345     socket.on('disconnect', () => {
346         const names = tardis.companions.names;
347
348         names.splice(names.indexOf(name), 1);
349
350         if (! names.includes(name)) {
351             tardis.companions.count = tardis.companions.count - 1;
352             io_tardis.emit('companions', tardis.companions.count);
353         }
354     });
355 });
356
357 io.on('connection', (socket) => {
358
359     // First things first, tell the client their name (if any)
360     if (socket.request.session.name) {
361         socket.emit('inform-name', socket.request.session.name);
362     }
363
364     // Replay old comments and images to a newly-joining client
365     socket.emit('reset');
366     state.images.forEach((image) => {
367         socket.emit('image', image)
368     });
369
370     socket.on('set-name', (name) => {
371         console.log("Received set-name event: " + name);
372         socket.request.session.name = name;
373         socket.request.session.save();
374         // Complete the round trip to the client
375         socket.emit('inform-name', socket.request.session.name);
376     });
377
378     // When any client comments, send that to all clients (including sender)
379     socket.on('comment', (comment) => {
380         const images = state.images;
381
382         // Send comment to clients after adding commenter's name
383         comment.name = socket.request.session.name;
384         io.emit('comment', comment);
385
386         const index = images.findIndex(image => image.id == comment.image_id);
387
388         // Before adding the comment to server's state, drop the image_id
389         delete comment.image_id;
390
391         // Now add the comment to the image, remove the image from the
392         // images array and then add it back at the end, (so it appears
393         // as the most-recently-modified image for any new clients)
394         const image = images[index];
395         image.comments.push(comment);
396         images.splice(index, 1);
397         images.push(image);
398     });
399
400     // Generate an image when requested
401     socket.on('generate', (request) => {
402         console.log(`Generating image for ${socket.request.session.name} with code=${request['code']} and prompt=${request['prompt']}`);
403         async function generate_image(code, prompt) {
404             function emit_image(image, target) {
405                 image.id = state.images.length;
406                 image.censored = false;
407                 image.link = "";
408                 if (target) {
409                     image.comments = [{
410                         "name": "ZomboCom",
411                         "text": target.response
412                     }];
413                     if (! state.targets.includes(target.short)) {
414                         state.targets.push(target.short);
415                     }
416                 } else {
417                     image.comments = [];
418                 }
419                 io.emit('image', image);
420                 state.images.push(image);
421             }
422
423             var promise;
424
425             // Before doing any generation, check for a target image
426             const normal_target = prompt.replace(/[^a-zA-Z]/g, "").toLowerCase() + code.toString() + ".png";
427             const target_arr = targets.filter(item => item.normal === normal_target);
428             if (target_arr.length) {
429                 const target = target_arr[0];
430                 const target_file = `${targets_dir}/${normal_target}`;
431                 const normal_prompt = prompt.replace(/[^-_.a-zA-Z]/g, "_");
432                 var counter = 1;
433                 var base;
434                 var filename;
435                 while (true) {
436                     if (counter > 1) {
437                         base = `${code}_${normal_prompt}_${counter}.png`
438                     } else {
439                         base = `${code}_${normal_prompt}.png`
440                     }
441                     filename = `${images_dir}/${base}`
442                     if (! fs.existsSync(filename)) {
443                         break;
444                     }
445                     counter = counter + 1;
446                 }
447                 fs.copyFile(target_file, filename, 0, (err) => {
448                     if (err) {
449                         console.log("Error copying " + target_file + " to " + filename + ": " + err);
450                     }
451                 });
452                 const image = {
453                     "code": code,
454                     "prompt": prompt,
455                     "filename": '/images/' + base
456                 };
457                 emit_image(image, target);
458             } else {
459
460                 // Inject the target seed for the "dice" prompt once every
461                 // 4 requests for a random seed (and only if the word
462                 // "dice" does not appear in the prompt).
463                 if (!code && !prompt.toLowerCase().includes("dice")) {
464                     if (state.images.length % 4 == 0) {
465                         code = 319630254;
466                     }
467                 }
468
469                 if (code) {
470                     promise = execFile(python_path, [generate_image_script, `--seed=${code}`, prompt])
471                 } else {
472                     promise = execFile(python_path, [generate_image_script, prompt])
473                 }
474                 const child = promise.child;
475                 child.stdout.on('data', (data) => {
476                     const images = JSON.parse(data);
477                     images.forEach((image) => {
478                         emit_image(image, null);
479                     });
480                 });
481                 child.stderr.on('data', (data) => {
482                     console.log("Error occurred during generate-image: " + data);
483                 });
484                 try {
485                     const { stdout, stderr } = await promise;
486                 } catch(e) {
487                     console.error(e);
488                 }
489             }
490             socket.emit('generation-done');
491         }
492
493         generate_image(request['code'], request['prompt']);
494     });
495 });
496
497 server.listen(port, () => {
498     console.log(`listening on *:${port}`);
499 });