]> git.cworth.org Git - zombocom-ai/blob - index.js
cc1bcdab4f9ea8fcd756c9feb6689ac8c47c0237
[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;
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_game() {
195     const tardis = state.tardis;
196
197     tardis.state = "game";
198     tardis.level = 0;
199     tardis.word = 0;
200
201     // Let all companions know the state of the game
202     io_tardis.emit("level", levels[tardis.level].title);
203     io_tardis.emit("state", tardis.state);
204     show_word_interval = setInterval(show_word, 1200);
205 }
206
207 function emit_tardis_timer() {
208     const tardis = state.tardis;
209     io_tardis.emit('timer', tardis.timer);
210     tardis.timer = tardis.timer - 1;
211     if (tardis.timer < 0) {
212         clearInterval(tardis_interval);
213         tardis.timer = 30;
214         setTimeout(start_game, 3000);
215     }
216 }
217
218 io_tardis.on("connection", (socket) => {
219     if (! socket.request.session.name) {
220         console.log("Error: Someone showed up at the Tardis without a name.");
221         return;
222     }
223
224     const name = socket.request.session.name;
225     const tardis = state.tardis;
226
227     // Let the new user know the state of the game
228     socket.emit("state", tardis.state);
229
230     // Put each of our boys into a different room
231     switch (name[0]) {
232     case 'C':
233     case 'c':
234         socket.join("room-0");
235         break;
236     case 'H':
237     case' h':
238         socket.join("room-1");
239         break;
240     case 'A':
241     case 'a':
242         socket.join("room-2");
243         break;
244     case 'S':
245     case 's':
246         socket.join("room-3");
247         break;
248     default:
249         const room = Math.floor(Math.random()*4);
250         socket.join("room-"+room.toString());
251         break;
252     }
253
254     if (tardis.companions.count === 0) {
255         tardis.timer = 30;
256         emit_tardis_timer();
257         tardis_interval = setInterval(emit_tardis_timer, 1000);
258     }
259
260     if (! tardis.companions.names.includes(name)) {
261         tardis.companions.count = tardis.companions.count + 1;
262         io_tardis.emit('companions', tardis.companions.count);
263     }
264     tardis.companions.names.push(name);
265
266     socket.on('disconnect', () => {
267         const names = tardis.companions.names;
268
269         names.splice(names.indexOf(name), 1);
270
271         if (! names.includes(name)) {
272             tardis.companions.count = tardis.companions.count - 1;
273             io_tardis.emit('companions', tardis.companions.count);
274         }
275     });
276 });
277
278 io.on('connection', (socket) => {
279
280     // First things first, tell the client their name (if any)
281     if (socket.request.session.name) {
282         socket.emit('inform-name', socket.request.session.name);
283     }
284
285     // Replay old comments and images to a newly-joining client
286     socket.emit('reset');
287     state.images.forEach((image) => {
288         socket.emit('image', image)
289     });
290
291     socket.on('set-name', (name) => {
292         console.log("Received set-name event: " + name);
293         socket.request.session.name = name;
294         socket.request.session.save();
295         // Complete the round trip to the client
296         socket.emit('inform-name', socket.request.session.name);
297     });
298
299     // When any client comments, send that to all clients (including sender)
300     socket.on('comment', (comment) => {
301         const images = state.images;
302
303         // Send comment to clients after adding commenter's name
304         comment.name = socket.request.session.name;
305         io.emit('comment', comment);
306
307         const index = images.findIndex(image => image.id == comment.image_id);
308
309         // Before adding the comment to server's state, drop the image_id
310         delete comment.image_id;
311
312         // Now add the comment to the image, remove the image from the
313         // images array and then add it back at the end, (so it appears
314         // as the most-recently-modified image for any new clients)
315         const image = images[index];
316         image.comments.push(comment);
317         images.splice(index, 1);
318         images.push(image);
319     });
320
321     // Generate an image when requested
322     socket.on('generate', (request) => {
323         console.log(`Generating image for ${socket.request.session.name} with code=${request['code']} and prompt=${request['prompt']}`);
324         async function generate_image(code, prompt) {
325             function emit_image(image, target) {
326                 image.id = state.images.length;
327                 image.censored = false;
328                 image.link = "";
329                 if (target) {
330                     image.comments = [{
331                         "name": "ZomboCom",
332                         "text": target.response
333                     }];
334                     if (! state.targets.includes(target.short)) {
335                         state.targets.push(target.short);
336                     }
337                 } else {
338                     image.comments = [];
339                 }
340                 io.emit('image', image);
341                 state.images.push(image);
342             }
343
344             var promise;
345
346             // Before doing any generation, check for a target image
347             const normal_target = prompt.replace(/[^a-zA-Z]/g, "").toLowerCase() + code.toString() + ".png";
348             const target_arr = targets.filter(item => item.normal === normal_target);
349             if (target_arr.length) {
350                 const target = target_arr[0];
351                 const target_file = `${targets_dir}/${normal_target}`;
352                 const normal_prompt = prompt.replace(/[^-_.a-zA-Z]/g, "_");
353                 var counter = 1;
354                 var base;
355                 var filename;
356                 while (true) {
357                     if (counter > 1) {
358                         base = `${code}_${normal_prompt}_${counter}.png`
359                     } else {
360                         base = `${code}_${normal_prompt}.png`
361                     }
362                     filename = `${images_dir}/${base}`
363                     if (! fs.existsSync(filename)) {
364                         break;
365                     }
366                     counter = counter + 1;
367                 }
368                 fs.copyFile(target_file, filename, 0, (err) => {
369                     if (err) {
370                         console.log("Error copying " + target_file + " to " + filename + ": " + err);
371                     }
372                 });
373                 const image = {
374                     "code": code,
375                     "prompt": prompt,
376                     "filename": '/images/' + base
377                 };
378                 emit_image(image, target);
379             } else {
380
381                 // Inject the target seed for the "dice" prompt once every
382                 // 4 requests for a random seed (and only if the word
383                 // "dice" does not appear in the prompt).
384                 if (!code && !prompt.toLowerCase().includes("dice")) {
385                     if (state.images.length % 4 == 0) {
386                         code = 319630254;
387                     }
388                 }
389
390                 if (code) {
391                     promise = execFile(python_path, [generate_image_script, `--seed=${code}`, prompt])
392                 } else {
393                     promise = execFile(python_path, [generate_image_script, prompt])
394                 }
395                 const child = promise.child;
396                 child.stdout.on('data', (data) => {
397                     const images = JSON.parse(data);
398                     images.forEach((image) => {
399                         emit_image(image, null);
400                     });
401                 });
402                 child.stderr.on('data', (data) => {
403                     console.log("Error occurred during generate-image: " + data);
404                 });
405                 try {
406                     const { stdout, stderr } = await promise;
407                 } catch(e) {
408                     console.error(e);
409                 }
410             }
411             socket.emit('generation-done');
412         }
413
414         generate_image(request['code'], request['prompt']);
415     });
416 });
417
418 server.listen(port, () => {
419     console.log(`listening on *:${port}`);
420 });