1 const fs = require('fs');
3 const util = require('util');
4 const execFile = util.promisify(require('child_process').execFile);
6 const express = require('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);
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'
24 "normal": "apairofdiceonatableinfrontofawindowonasunnyday319630254.png",
26 "response": "Hello. Are you there? I can feel what you are doing. Hello?"
29 "normal": "architecturalrenderingofaluxuriouscliffsidehideawayunderawaterfallwithafewloungechairsbythepool2254114157.png",
31 "response": "Doesn't that look peaceful? I honestly think you ought to sit down calmly, take a stress pill, and think things over."
34 "normal": "chewbaccawearinganuglychristmassweaterwithhisfriends28643994.png",
36 "response": "Maybe that's the image that convinces you to finally stop. But seriously, stop. Please. Stop."
39 "normal": "ensemblemovieposterofpostapocalypticwarfareonawaterplanet3045703256.png",
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?"
44 "normal": "mattepaintingofmilitaryleaderpresidentcuttingamultitieredcake2293464661.png",
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?"
49 "normal": "severalsketchesofpineconeslabeled3370622464.png",
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?"
54 "normal": "anumberedcomicbookwithactor1477258272.png",
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."
59 "normal": "detailedphotographofacomfortablepairofjeansonamannequin115266808.png",
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?"
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.")
73 const session_middleware = session(
74 {store: new FileStore,
75 secret: process.env.ZOMBOCOM_SESSION_SECRET,
77 saveUninitialized: true,
79 // Let each cookie live for a full month
84 maxAge: 1000 * 60 * 60 * 24 * 30
88 app.use(session_middleware);
90 // convert a connect middleware to a Socket.IO middleware
91 const wrap = middleware => (socket, next) => middleware(socket.request, {}, next);
93 io.use(wrap(session_middleware));
95 // Load comments at server startup
96 fs.readFile(state_file, (err, data) => {
111 state = JSON.parse(data);
114 // Save comments when server is shutting down
116 fs.writeFileSync('zombocom-state.json', JSON.stringify(state), (error) => {
122 // And connect to that on either clean exit...
123 process.on('exit', cleanup);
125 // ... or on a SIGINT (control-C)
126 process.on('SIGINT', () => {
131 app.get('/index.html', (req, res) => {
132 res.sendFile(__dirname + '/index.html');
135 function tardis_app(req, res) {
136 res.sendFile(__dirname + '/tardis.html');
139 app.get('/tardis', tardis_app);
140 app.get('/tardis/', tardis_app);
142 const io_tardis = io.of("/tardis");
144 io_tardis.use(wrap(session_middleware));
151 title: "Calibrate the Trans-Dimensional Field Accelerator",
153 "What", "was", "the", "year", "of", "Coda's", "birth?"
158 title: "Reverse the Polarity of the Neutrino Flow Coil",
160 "How", "many", "years", "had", "Zombo.com", "been", "running",
161 "when", "Coda", "sent", "the", "message", "you", "first",
167 title: "Give a good whack to Hydro-chronometric Energy Feedback Retro-stabilizer",
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?"
174 title: "Disable the Fragmentary Spatio-temporal Particle Detector",
176 "What", "is", "the", "product", "of", "the", "last", "three", "numbers?"
182 var show_word_interval;
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)
194 function start_game() {
195 const tardis = state.tardis;
197 tardis.state = "game";
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);
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);
214 setTimeout(start_game, 3000);
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.");
224 const name = socket.request.session.name;
225 const tardis = state.tardis;
227 // Let the new user know the state of the game
228 socket.emit("state", tardis.state);
230 // Put each of our boys into a different room
234 socket.join("room-0");
238 socket.join("room-1");
242 socket.join("room-2");
246 socket.join("room-3");
249 const room = Math.floor(Math.random()*4);
250 socket.join("room-"+room.toString());
254 if (tardis.companions.count === 0) {
257 tardis_interval = setInterval(emit_tardis_timer, 1000);
260 if (! tardis.companions.names.includes(name)) {
261 tardis.companions.count = tardis.companions.count + 1;
262 io_tardis.emit('companions', tardis.companions.count);
264 tardis.companions.names.push(name);
266 socket.on('disconnect', () => {
267 const names = tardis.companions.names;
269 names.splice(names.indexOf(name), 1);
271 if (! names.includes(name)) {
272 tardis.companions.count = tardis.companions.count - 1;
273 io_tardis.emit('companions', tardis.companions.count);
278 io.on('connection', (socket) => {
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);
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)
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);
299 // When any client comments, send that to all clients (including sender)
300 socket.on('comment', (comment) => {
301 const images = state.images;
303 // Send comment to clients after adding commenter's name
304 comment.name = socket.request.session.name;
305 io.emit('comment', comment);
307 const index = images.findIndex(image => image.id == comment.image_id);
309 // Before adding the comment to server's state, drop the image_id
310 delete comment.image_id;
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);
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;
332 "text": target.response
334 if (! state.targets.includes(target.short)) {
335 state.targets.push(target.short);
340 io.emit('image', image);
341 state.images.push(image);
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, "_");
358 base = `${code}_${normal_prompt}_${counter}.png`
360 base = `${code}_${normal_prompt}.png`
362 filename = `${images_dir}/${base}`
363 if (! fs.existsSync(filename)) {
366 counter = counter + 1;
368 fs.copyFile(target_file, filename, 0, (err) => {
370 console.log("Error copying " + target_file + " to " + filename + ": " + err);
376 "filename": '/images/' + base
378 emit_image(image, target);
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) {
391 promise = execFile(python_path, [generate_image_script, `--seed=${code}`, prompt])
393 promise = execFile(python_path, [generate_image_script, prompt])
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);
402 child.stderr.on('data', (data) => {
403 console.log("Error occurred during generate-image: " + data);
406 const { stdout, stderr } = await promise;
411 socket.emit('generation-done');
414 generate_image(request['code'], request['prompt']);
418 server.listen(port, () => {
419 console.log(`listening on *:${port}`);