summaryrefslogtreecommitdiff
path: root/server.js
diff options
context:
space:
mode:
Diffstat (limited to 'server.js')
-rw-r--r--server.js943
1 files changed, 943 insertions, 0 deletions
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..f3af3d7
--- /dev/null
+++ b/server.js
@@ -0,0 +1,943 @@
+"use strict";
+
+const fs = require('fs');
+const express = require('express');
+const express_session = require('express-session');
+const passport = require('passport');
+const passport_local = require('passport-local');
+const passport_socket = require('passport.socketio');
+const body_parser = require('body-parser');
+const connect_flash = require('connect-flash');
+const crypto = require('crypto');
+const sqlite3 = require('better-sqlite3');
+const SQLiteStore = require('./connect-better-sqlite3')(express_session);
+
+require('dotenv').config();
+
+const SESSION_SECRET = "Caesar has a big head!";
+
+const MAX_OPEN_GAMES = 3;
+
+let sessionStore = new SQLiteStore();
+let db = new sqlite3(process.env.DATABASE || "./db");
+let app = express();
+let http_port = process.env.PORT || 8080;
+let https_port = process.env.HTTPS_PORT || 8443;
+let http = require('http').createServer(app);
+let https = require('https').createServer({
+ key: fs.readFileSync(process.env.SSL_KEY || "key.pem"),
+ cert: fs.readFileSync(process.env.SSL_CERT || "cert.pem")
+ }, app);
+let socket_io = require('socket.io');
+let io1 = socket_io(http);
+let io2 = socket_io(https);
+let io = {
+ use: function (fn) { io1.use(fn); io2.use(fn); },
+ on: function (ev,fn) { io1.on(ev,fn); io2.on(ev,fn); },
+};
+
+app.disable('etag');
+app.set('view engine', 'ejs');
+app.use(body_parser.urlencoded({extended:false}));
+app.use(express_session({
+ secret: SESSION_SECRET,
+ resave: false,
+ saveUninitialized: true,
+ store: sessionStore,
+ cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 }
+}));
+app.use(connect_flash());
+
+io.use(passport_socket.authorize({
+ key: 'connect.sid',
+ secret: SESSION_SECRET,
+ store: sessionStore,
+}));
+
+const is_immutable = /\.(svg|png|jpg|jpeg|woff2)$/;
+
+function setHeaders(res, path) {
+ if (is_immutable.test(path))
+ res.set("Cache-Control", "public, max-age=86400, immutable");
+}
+
+app.use(express.static('public', { setHeaders: setHeaders }));
+
+function LOG(req, ...msg) {
+ let name;
+ if (req.isAuthenticated())
+ name = req.user.mail;
+ else
+ name = "guest";
+ let time = new Date().toISOString().substring(0,19).replace("T", " ");
+ console.log(time, req.connection.remoteAddress, name, ...msg);
+}
+
+function SLOG(socket, ...msg) {
+ let name = socket.request.user.mail;
+ let time = new Date().toISOString().substring(0,19).replace("T", " ");
+ console.log(time, socket.request.connection.remoteAddress, name,
+ socket.id, socket.title_id, socket.game_id, socket.role, ...msg);
+}
+
+function human_date(time) {
+ var date = time ? new Date(time + " UTC") : new Date(0);
+ var seconds = (Date.now() - date.getTime()) / 1000;
+ var days = Math.floor(seconds / 86400);
+ if (days == 0) {
+ if (seconds < 60) return "now";
+ if (seconds < 120) return "1 minute ago";
+ if (seconds < 3600) return Math.floor(seconds / 60) + " minutes ago";
+ if (seconds < 7200) return "1 hour ago";
+ if (seconds < 86400) return Math.floor(seconds / 3600) + " hours ago";
+ }
+ if (days == 1) return "Yesterday";
+ if (days < 14) return days + " days ago";
+ if (days < 31) return Math.ceil(days / 7) + " weeks ago";
+ return date.toISOString().substring(0,10);
+}
+
+function humanize(rows) {
+ for (let row of rows) {
+ row.ctime = human_date(row.ctime);
+ row.mtime = human_date(row.mtime);
+ }
+}
+
+function is_email(email) {
+ return email.match(/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/);
+}
+
+function clean_user_name(name) {
+ name = name.replace(/^ */,'').replace(/ *$/,'').replace(/ */g,' ');
+ if (name.length > 50)
+ name = name.substring(0, 50);
+ return name;
+}
+
+function hash_password(password, salt) {
+ let hash = crypto.createHash('sha256');
+ hash.update(password);
+ hash.update(salt);
+ return hash.digest('hex');
+}
+
+function get_avatar(mail) {
+ if (!mail)
+ mail = "foo@example.com";
+ let digest = crypto.createHash('md5').update(mail.trim().toLowerCase()).digest('hex');
+ return '//www.gravatar.com/avatar/' + digest + '?d=mp';
+}
+
+/*
+ * USER PROFILES
+ */
+
+const sql_blacklist_ip = db.prepare("SELECT COUNT(*) FROM blacklist_ip WHERE ip = ?").raw();
+const sql_blacklist_mail = db.prepare("SELECT COUNT(*) AS count FROM blacklist_mail WHERE ? LIKE mail").raw();
+
+function is_blacklisted(ip, mail) {
+ if (sql_blacklist_ip.get(ip)[0] != 0)
+ return true;
+ if (sql_blacklist_mail.get(mail)[0] != 0)
+ return true;
+ return false;
+}
+
+const sql_deserialize_user = db.prepare("SELECT user_id, name, mail FROM users WHERE user_id = ?");
+const sql_update_last_seen = db.prepare("UPDATE users SET aip = ?, atime = datetime('now') WHERE user_id = ?");
+const sql_login_select = db.prepare("SELECT user_id, name, mail, password, salt FROM users WHERE name = ? OR mail = ?");
+
+passport.serializeUser(function (user, done) {
+ return done(null, user.user_id);
+});
+
+passport.deserializeUser(function (user_id, done) {
+ try {
+ let row = sql_deserialize_user.get(user_id);
+ if (!row)
+ return done(null, false);
+ return done(null, row);
+ } catch (err) {
+ console.log(err);
+ return done(null, false);
+ }
+});
+
+function local_login(req, name_or_mail, password, done) {
+ try {
+ if (!is_email(name_or_mail))
+ name_or_mail = clean_user_name(name_or_mail);
+ LOG(req, "POST /login", name_or_mail);
+ let row = sql_login_select.get(name_or_mail, name_or_mail);
+ if (!row)
+ return setTimeout(() => done(null, false, req.flash('message', "User not found.")), 1000);
+ if (is_blacklisted(req.connection.remoteAddress, row.mail))
+ return setTimeout(() => done(null, false, req.flash('message', "Sorry, but this IP or account has been banned.")), 1000);
+ let hash = hash_password(password, row.salt);
+ if (hash != row.password)
+ return setTimeout(() => done(null, false, req.flash('message', "Wrong password.")), 1000);
+ sql_update_last_seen.run(req.connection.remoteAddress, row.user_id);
+ done(null, row);
+ } catch (err) {
+ done(null, false, req.flash('message', err.toString()));
+ }
+}
+
+const sql_signup_check = db.prepare("SELECT user_id, name FROM users WHERE name = ? OR mail = ?");
+const sql_signup_insert = db.prepare("INSERT INTO users (name, mail, password, salt, ctime, cip, atime, aip) VALUES (?,?,?,?,datetime('now'),?,datetime('now'),?)");
+const sql_signup_login = db.prepare("SELECT user_id, name FROM users WHERE name = ? AND password = ?");
+
+function local_signup(req, name, password, done) {
+ try {
+ let mail = req.body.mail;
+ name = clean_user_name(name);
+ LOG(req, "POST /signup", name, mail);
+ if (is_blacklisted(req.connection.remoteAddress, mail))
+ return setTimeout(() => done(null, false, req.flash('message', "Sorry, but this IP or account has been banned.")), 1000);
+ if (password.length < 4)
+ return done(null, false, req.flash('message', "Password is too short!"));
+ if (password.length > 100)
+ return done(null, false, req.flash('message', "Password is too long!"));
+ // TODO: actual verification if process.env.VERIFY_EMAIL
+ if (!is_email(mail))
+ return done(null, false, req.flash('message', "Invalid mail address!"));
+ let row = sql_signup_check.get(name, mail);
+ if (row)
+ return done(null, false, req.flash('message', "User name or mail is already taken."));
+ let salt = crypto.randomBytes(32).toString('hex');
+ let hash = hash_password(password, salt);
+ let ip = req.connection.remoteAddress;
+ sql_signup_insert.run(name, mail, hash, salt, ip, ip);
+ row = sql_signup_login.get(name, hash);
+ done(null, row);
+ } catch (err) {
+ done(null, false, req.flash('message', err.toString()));
+ }
+}
+
+passport.use('local-login', new passport_local.Strategy({ passReqToCallback: true }, local_login));
+passport.use('local-signup', new passport_local.Strategy({ passReqToCallback: true }, local_signup));
+
+app.use(passport.initialize());
+app.use(passport.session());
+
+function update_last_seen(req) {
+ sql_update_last_seen.run(req.connection.remoteAddress, req.user.user_id);
+}
+
+function must_be_logged_in(req, res, next) {
+ if (!req.isAuthenticated())
+ return res.redirect('/login');
+ if (sql_blacklist_ip.get(req.connection.remoteAddress)[0] != 0)
+ return res.redirect('/banned');
+ if (sql_blacklist_mail.get(req.user.mail)[0] != 0)
+ return res.redirect('/banned');
+ update_last_seen(req);
+ return next();
+}
+
+app.get('/favicon.ico', function (req, res) {
+ res.status(204).send();
+});
+
+app.get('/about', function (req, res) {
+ res.render('about.ejs', { user: req.user });
+});
+
+app.get('/logout', function (req, res) {
+ LOG(req, "GET /logout");
+ req.logout();
+ res.redirect('/login');
+});
+
+app.get('/banned', function (req, res) {
+ LOG(req, "GET /banned");
+ res.render('banned.ejs', { user: req.user, message: req.flash('message') });
+});
+
+app.get('/login', function (req, res) {
+ LOG(req, "GET /login");
+ res.render('login.ejs', { user: req.user, message: req.flash('message') });
+});
+
+app.get('/signup', function (req, res) {
+ LOG(req, "GET /signup");
+ res.render('signup.ejs', { user: req.user, message: req.flash('message') });
+});
+
+app.post('/login',
+ passport.authenticate('local-login', {
+ successRedirect: '/',
+ failureRedirect: '/login',
+ failureFlash: true
+ })
+);
+
+app.post('/signup',
+ passport.authenticate('local-signup', {
+ successRedirect: '/',
+ failureRedirect: '/signup',
+ failureFlash: true
+ })
+);
+
+app.get('/users', function (req, res) {
+ LOG(req, "GET /users");
+ let rows = db.prepare("SELECT name, mail, ctime, atime FROM users ORDER BY atime DESC").all();
+ rows.forEach(row => {
+ row.avatar = get_avatar(row.mail);
+ row.ctime = human_date(row.ctime);
+ row.atime = human_date(row.atime);
+ });
+ res.render('users.ejs', { user: req.user, message: req.flash('message'), userList: rows });
+});
+
+app.get('/change_password', must_be_logged_in, function (req, res) {
+ LOG(req, "GET /change_password");
+ res.render('change_password.ejs', { user: req.user, message: req.flash('message') });
+});
+
+app.post('/change_password', must_be_logged_in, function (req, res) {
+ try {
+ let name = clean_user_name(req.user.name);
+ let password = req.body.password;
+ let newpass = req.body.newpass;
+ LOG(req, "POST /change_password", name);
+ if (newpass.length < 4) {
+ req.flash('message', "Password is too short!");
+ return res.redirect('/change_password');
+ }
+ let salt_row = db.prepare("SELECT salt FROM users WHERE name = ?").get(name);
+ if (!salt_row) {
+ req.flash('message', "User not found.");
+ return res.redirect('/change_password');
+ }
+ let salt = salt_row.salt;
+ let hash = hash_password(password, salt);
+ let user_row = db.prepare("SELECT user_id, name FROM users WHERE name = ? AND password = ?").get(name, hash);
+ if (!user_row) {
+ req.flash('message', "Wrong password.");
+ return res.redirect('/change_password');
+ }
+ hash = hash_password(newpass, salt);
+ db.prepare("UPDATE users SET password = ? WHERE user_id = ?").run(hash, user_row.user_id);
+ return res.redirect('/profile');
+ } catch (err) {
+ console.log(err);
+ req.flash('message', err.message);
+ return res.redirect('/change_password');
+ }
+});
+
+/*
+ * GAME LOBBY
+ */
+
+let RULES = {};
+for (let title_id of db.prepare("SELECT * FROM titles").pluck().all()) {
+ console.log("Loading rules for " + title_id);
+ try {
+ RULES[title_id] = require("./public/" + title_id + "/rules.js");
+ } catch (err) {
+ console.log(err);
+ }
+}
+
+const QUERY_LIST_ONE_GAME = db.prepare(`
+ SELECT
+ games.game_id,
+ games.title_id AS title_id,
+ titles.title_name AS title_name,
+ games.scenario AS scenario,
+ games.owner AS owner_id,
+ users.name AS owner_name,
+ games.ctime,
+ games.mtime,
+ games.description,
+ games.status,
+ games.result,
+ games.active
+ FROM games
+ JOIN users ON games.owner = users.user_id
+ JOIN titles ON games.title_id = titles.title_id
+ WHERE game_id = ?
+`);
+
+const QUERY_LIST_PUBLIC_GAMES = db.prepare(`
+ SELECT
+ games.game_id,
+ games.title_id AS title_id,
+ games.scenario AS scenario,
+ games.owner AS owner_id,
+ users.name AS owner_name,
+ games.ctime,
+ games.mtime,
+ games.description,
+ games.status,
+ games.result,
+ games.active
+ FROM games
+ JOIN users ON games.owner = users.user_id
+ WHERE title_id = ? AND private = 0
+ ORDER BY status ASC, mtime DESC
+`);
+
+const QUERY_LIST_USER_GAMES = db.prepare(`
+ SELECT DISTINCT
+ games.game_id,
+ games.title_id,
+ titles.title_name,
+ games.scenario AS scenario,
+ users.name AS owner_name,
+ games.description,
+ games.ctime,
+ games.mtime,
+ games.status,
+ games.result,
+ games.active
+ FROM games
+ LEFT JOIN players ON games.game_id = players.game_id
+ LEFT JOIN users ON games.owner = users.user_id
+ LEFT JOIN titles ON games.title_id = titles.title_id
+ WHERE games.owner = ? OR players.user_id = ?
+ ORDER BY status ASC, mtime DESC
+`);
+
+const QUERY_LIST_ALL_GAMES = db.prepare(`
+ SELECT
+ games.game_id,
+ games.title_id AS title_id,
+ titles.title_name,
+ games.scenario AS scenario,
+ games.owner AS owner_id,
+ users.name AS owner_name,
+ games.ctime,
+ games.mtime,
+ games.description,
+ games.status,
+ games.result,
+ games.active,
+ games.private
+ FROM games
+ JOIN users ON games.owner = users.user_id
+ LEFT JOIN titles ON games.title_id = titles.title_id
+ ORDER BY status ASC, mtime DESC
+`);
+
+const QUERY_PLAYERS = db.prepare(`
+ SELECT
+ players.game_id,
+ players.user_id,
+ players.role,
+ users.name
+ FROM players
+ JOIN users ON players.user_id = users.user_id
+ WHERE players.game_id = ?
+`);
+
+const QUERY_PLAYER_NAMES = db.prepare(`
+ SELECT
+ users.name AS name
+ FROM players
+ JOIN users ON players.user_id = users.user_id
+ WHERE players.game_id = ?
+ ORDER BY players.role
+`).pluck();
+
+const QUERY_TITLE = db.prepare("SELECT * FROM titles WHERE title_id = ?");
+const QUERY_ROLES = db.prepare("SELECT * FROM roles WHERE title_id = ?");
+const QUERY_GAME_OWNER = db.prepare("SELECT * FROM games WHERE game_id = ? AND owner = ?");
+const QUERY_TITLE_FROM_GAME = db.prepare("SELECT title_id FROM games WHERE game_id = ?");
+const QUERY_ROLE_FROM_GAME_AND_USER = db.prepare("SELECT role FROM players WHERE game_id = ? AND user_id = ?");
+
+const QUERY_JOIN_GAME = db.prepare("INSERT INTO players (user_id, game_id, role) VALUES (?,?,?)");
+const QUERY_PART_GAME = db.prepare("DELETE FROM players WHERE game_id = ? AND user_id = ? AND role = ?");
+const QUERY_START_GAME = db.prepare("UPDATE games SET status = 1, state = ?, active = ? WHERE game_id = ?");
+const QUERY_CREATE_GAME = db.prepare(`
+ INSERT INTO games
+ (owner,title_id,scenario,private,ctime,mtime,description,status,state,chat)
+ VALUES
+ (?,?,?,?,datetime('now'),datetime('now'),?,0,NULL,'[]')
+`);
+const QUERY_UPDATE_GAME_SET_PRIVATE = db.prepare("UPDATE games SET private = 1 WHERE game_id = ?");
+
+const QUERY_IS_PLAYER = db.prepare("SELECT COUNT(*) FROM players WHERE game_id = ? AND user_id = ?").pluck();
+const QUERY_IS_ACTIVE = db.prepare("SELECT COUNT(*) FROM players WHERE game_id = ? AND role = ? AND user_id = ?").pluck();
+const QUERY_COUNT_OPEN_GAMES = db.prepare("SELECT COUNT(*) FROM games WHERE owner = ? AND status = 0").pluck();
+const QUERY_DELETE_GAME = db.prepare("DELETE FROM games WHERE game_id = ?");
+
+app.get('/', function (req, res) {
+ res.render('index.ejs', { user: req.user, message: req.flash('message') });
+});
+
+function is_your_turn(game, user) {
+ if (!game.active || game.active == "None")
+ return false;
+ if (game.active == "All" || game.active == "Both")
+ return QUERY_IS_PLAYER.get(game.game_id, user.user_id);
+ return QUERY_IS_ACTIVE.get(game.game_id, game.active, user.user_id);
+}
+
+app.get('/profile', must_be_logged_in, function (req, res) {
+ LOG(req, "GET /profile");
+ let avatar = get_avatar(req.user.mail);
+ let games = QUERY_LIST_USER_GAMES.all(req.user.user_id, req.user.user_id);
+ humanize(games);
+ for (let game of games) {
+ game.players = QUERY_PLAYER_NAMES.all(game.game_id);
+ game.your_turn = is_your_turn(game, req.user);
+ }
+ let open_games = games.filter(game => game.status == 0);
+ let active_games = games.filter(game => game.status == 1);
+ let finished_games = games.filter(game => game.status == 2);
+ res.set("Cache-Control", "no-store");
+ res.render('profile.ejs', { user: req.user, avatar: avatar,
+ open_games: open_games,
+ active_games: active_games,
+ finished_games: finished_games,
+ message: req.flash('message')
+ });
+});
+
+app.get('/info/:title_id', function (req, res) {
+ LOG(req, "GET /info/" + req.params.title_id);
+ let title_id = req.params.title_id;
+ let title = QUERY_TITLE.get(title_id);
+ if (!title) {
+ req.flash('message', 'That title does not exist.');
+ return res.redirect('/');
+ }
+ if (req.isAuthenticated()) {
+ let games = QUERY_LIST_PUBLIC_GAMES.all(title_id);
+ humanize(games);
+ let open_games = games.filter(game => game.status == 0);
+ let active_games = games.filter(game => game.status == 1);
+ for (let game of active_games) {
+ game.players = QUERY_PLAYER_NAMES.all(game.game_id);
+ game.your_turn = is_your_turn(game, req.user);
+ }
+ let finished_games = games.filter(game => game.status == 2);
+ for (let game of finished_games)
+ game.players = QUERY_PLAYER_NAMES.all(game.game_id);
+ res.set("Cache-Control", "no-store");
+ res.render('info.ejs', { user: req.user, title: title,
+ open_games: open_games,
+ active_games: active_games,
+ finished_games: finished_games,
+ message: req.flash('message')
+ });
+ } else {
+ res.set("Cache-Control", "no-store");
+ res.render('info.ejs', { user: req.user, title: title,
+ open_games: [],
+ active_games: [],
+ finished_games: [],
+ message: req.flash('message')
+ });
+ }
+});
+
+app.get('/create/:title_id', must_be_logged_in, function (req, res) {
+ LOG(req, "GET /create/" + req.params.title_id);
+ let title_id = req.params.title_id;
+ let title = QUERY_TITLE.get(title_id);
+ if (!title) {
+ req.flash('message', 'That title does not exist.');
+ return res.redirect('/');
+ }
+ res.render('create.ejs', { user: req.user, message: req.flash('message'), title: title, scenarios: RULES[title_id].scenarios });
+});
+
+app.post('/create/:title_id', must_be_logged_in, function (req, res) {
+ let title_id = req.params.title_id;
+ let descr = req.body.description;
+ let priv = req.body.private == 'private';
+ let scenario = req.body.scenario;
+ let user_id = req.user.user_id;
+ LOG(req, "POST /create/" + req.params.title_id, scenario, priv, JSON.stringify(descr));
+ try {
+ let count = QUERY_COUNT_OPEN_GAMES.get(user_id);
+ if (count >= MAX_OPEN_GAMES) {
+ req.flash('message', "You have too many open games!");
+ return res.redirect('/create/'+title_id);
+ }
+ if (!(title_id in RULES)) {
+ req.flash('message', "That title doesn't exist.");
+ return res.redirect('/');
+ }
+ if (!RULES[title_id].scenarios.includes(scenario)) {
+ req.flash('message', "That scenario doesn't exist.");
+ return res.redirect('/create/'+title_id);
+ }
+ let info = QUERY_CREATE_GAME.run(user_id, title_id, scenario, priv ? 1 : 0, descr);
+ res.redirect('/join/'+info.lastInsertRowid);
+ } catch (err) {
+ req.flash('message', err.toString());
+ return res.redirect('/create/'+title_id);
+ }
+});
+
+app.get('/delete/:game_id', must_be_logged_in, function (req, res) {
+ let game_id = req.params.game_id;
+ LOG(req, "GET /delete/" + game_id);
+ try {
+ let game = QUERY_GAME_OWNER.get(game_id, req.user.user_id);
+ if (!game) {
+ req.flash('message', "Only the game owner can delete the game!");
+ return res.redirect('/join/'+game_id);
+ }
+ QUERY_DELETE_GAME.run(game_id);
+ res.redirect('/info/'+game.title_id);
+ } catch (err) {
+ req.flash('message', err.toString());
+ return res.redirect('/join/'+game_id);
+ }
+});
+
+app.get('/join/:game_id', must_be_logged_in, function (req, res) {
+ LOG(req, "GET /join/" + req.params.game_id);
+ let game_id = req.params.game_id | 0;
+ let game = QUERY_LIST_ONE_GAME.get(game_id);
+ if (!game) {
+ req.flash('message', "That game doesn't exist.");
+ return res.redirect('/');
+ }
+ let roles = QUERY_ROLES.all(game.title_id);
+ let players = QUERY_PLAYERS.all(game_id);
+ res.set("Cache-Control", "no-store");
+ res.render('join.ejs', {
+ user: req.user,
+ game: game,
+ roles: roles,
+ players: players,
+ solo: players.every(p => p.user_id == req.user.user_id),
+ message: req.flash('message')
+ });
+});
+
+app.get('/join/:game_id/:role', must_be_logged_in, function (req, res) {
+ LOG(req, "GET /join/" + req.params.game_id + "/" + req.params.role);
+ let game_id = req.params.game_id | 0;
+ let role = req.params.role;
+ try {
+ QUERY_JOIN_GAME.run(req.user.user_id, game_id, role);
+ return res.redirect('/join/'+game_id);
+ } catch (err) {
+ req.flash('message', err.toString());
+ return res.redirect('/join/'+game_id);
+ }
+});
+
+app.get('/part/:game_id/:part_id/:role', must_be_logged_in, function (req, res) {
+ LOG(req, "GET /part/" + req.params.game_id + "/" + req.params.part_id + "/" + req.params.role);
+ let game_id = req.params.game_id | 0;
+ let part_id = req.params.part_id | 0;
+ let role = req.params.role;
+ try {
+ QUERY_PART_GAME.run(game_id, part_id, role);
+ return res.redirect('/join/'+game_id);
+ } catch (err) {
+ req.flash('message', err.toString());
+ return res.redirect('/join/'+game_id);
+ }
+});
+
+app.get('/start/:game_id', must_be_logged_in, function (req, res) {
+ LOG(req, "GET /start/" + req.params.game_id);
+ let game_id = req.params.game_id | 0;
+ try {
+ let game = QUERY_GAME_OWNER.get(game_id, req.user.user_id);
+ if (!game) {
+ req.flash('message', "Only the game owner can start the game!");
+ return res.redirect('/join/'+game_id);
+ }
+ if (game.status != 0) {
+ req.flash('message', "The game is already started!");
+ return res.redirect('/join/'+game_id);
+ }
+ let players = QUERY_PLAYERS.all(game_id);
+ let state = RULES[game.title_id].setup(game.scenario, players);
+ QUERY_START_GAME.run(JSON.stringify(state), state.active, game_id);
+ let is_solo = players.every(p => p.user_id == players[0].user_id);
+ if (is_solo)
+ QUERY_UPDATE_GAME_SET_PRIVATE.run(game_id);
+ return res.redirect('/join/'+game_id);
+ } catch (err) {
+ req.flash('message', err.toString());
+ return res.redirect('/join/'+game_id);
+ }
+});
+
+app.get('/play/:game_id/:role', must_be_logged_in, function (req, res) {
+ LOG(req, "GET /play/" + req.params.game_id + "/" + req.params.role);
+ let game_id = req.params.game_id | 0;
+ let role = req.params.role;
+ try {
+ let title = QUERY_TITLE_FROM_GAME.get(game_id);
+ if (!title)
+ return res.redirect('/join/'+game_id);
+ res.redirect('/'+title.title_id+'/play.html?game='+game_id+'&role='+role);
+ } catch (err) {
+ req.flash('message', err.toString());
+ return res.redirect('/join/'+game_id);
+ }
+});
+
+app.get('/play/:game_id', must_be_logged_in, function (req, res) {
+ LOG(req, "GET /play/" + req.params.game_id);
+ let game_id = req.params.game_id | 0;
+ let user_id = req.user.user_id | 0;
+ try {
+ let role = QUERY_ROLE_FROM_GAME_AND_USER.get(game_id, user_id);
+ if (!role)
+ return res.redirect('/play/'+game_id+'/Observer');
+ return res.redirect('/play/'+game_id+'/'+role.role);
+ } catch (err) {
+ req.flash('message', err.toString());
+ return res.redirect('/join/'+game_id);
+ }
+});
+
+/*
+ * GAME PLAYING
+ */
+
+const QUERY_SELECT_CHAT = db.prepare("SELECT chat FROM games WHERE game_id = ?");
+const QUERY_UPDATE_CHAT = db.prepare("UPDATE games SET chat = ? WHERE game_id = ?");
+const QUERY_SELECT_GAME_STATE = db.prepare("SELECT state FROM games WHERE game_id = ?");
+const QUERY_UPDATE_GAME_STATE = db.prepare("UPDATE games SET state = ?, active = ?, status = ?, result = ?, mtime = datetime('now') WHERE game_id = ?");
+const QUERY_CONNECT_GAME = db.prepare("SELECT title_id, state FROM games WHERE title_id = ? AND game_id = ?");
+const QUERY_RESTART_GAME = db.prepare("UPDATE games SET state = ?, mtime = datetime('now') WHERE game_id = ?");
+
+let clients = {};
+
+function send_state(socket, state) {
+ try {
+ let view = socket.rules.view(state, socket.role);
+ if (socket.log_length < view.log.length)
+ view.log_start = socket.log_length;
+ else
+ view.log_start = view.log.length;
+ socket.log_length = view.log.length;
+ view.log = view.log.slice(view.log_start);
+ socket.emit('state', view);
+ } catch (err) {
+ console.log(err);
+ return socket.emit('error', err.toString());
+ }
+}
+
+function get_game_state(game_id) {
+ let row = QUERY_SELECT_GAME_STATE.get(game_id);
+ if (!row)
+ throw new Error("No game with that ID");
+ return JSON.parse(row.state);
+}
+
+function put_game_state(game_id, state) {
+ let status = 1;
+ let result = null;
+ if (state.state == 'game_over') {
+ status = 2;
+ result = state.result;
+ }
+ QUERY_UPDATE_GAME_STATE.run(JSON.stringify(state), state.active, status, result, game_id);
+ for (let other of clients[game_id])
+ send_state(other, state);
+}
+
+function on_action(socket, action, arg) {
+ SLOG(socket, "--> ACTION", action, arg);
+ try {
+ let state = get_game_state(socket.game_id);
+ socket.rules.action(state, socket.role, action, arg);
+ put_game_state(socket.game_id, state);
+ } catch (err) {
+ console.log(err);
+ return socket.emit('error', err.toString());
+ }
+}
+
+function on_resign(socket) {
+ SLOG(socket, "--> RESIGN");
+ try {
+ let state = get_game_state(socket.game_id);
+ socket.rules.resign(state, socket.role);
+ put_game_state(socket.game_id, state);
+ } catch (err) {
+ console.log(err);
+ return socket.emit('error', err.toString());
+ }
+}
+
+function send_chat(socket, chat) {
+ if (chat && socket.chat_length < chat.length) {
+ SLOG(socket, "<-- CHAT LOG", socket.chat_length, "..", chat.length);
+ socket.emit('chat', socket.chat_length, chat.slice(socket.chat_length));
+ socket.chat_length = chat.length;
+ }
+}
+
+function on_getchat(socket, old_len) {
+ try {
+ socket.chat_length = old_len;
+ let row = QUERY_SELECT_CHAT.get(socket.game_id);
+ if (!row)
+ return socket.emit('error', "No game with that ID.");
+ let chat = JSON.parse(row.chat);
+ if (!chat)
+ chat = [];
+ send_chat(socket, chat);
+ } catch (err) {
+ console.log(err);
+ return socket.emit('error', err.toString());
+ }
+}
+
+function on_chat(socket, message) {
+ message = message.substring(0,4096);
+ SLOG(socket, "--> CHAT");
+ try {
+ let row = QUERY_SELECT_CHAT.get(socket.game_id);
+ if (!row)
+ return socket.emit('error', "No game with that ID.");
+ let chat = JSON.parse(row.chat);
+ if (!chat)
+ chat = [];
+ chat.push([new Date(), socket.user_name, message]);
+ QUERY_UPDATE_CHAT.run(JSON.stringify(chat), socket.game_id);
+ for (let other of clients[socket.game_id])
+ send_chat(other, chat);
+ } catch (err) {
+ console.log(err);
+ return socket.emit('error', err.toString());
+ }
+}
+
+function on_debug(socket) {
+ SLOG(socket, "<-- DEBUG");
+ try {
+ let row = QUERY_SELECT_GAME_STATE.get(socket.game_id);
+ if (!row)
+ return socket.emit('error', "No game with that ID.");
+ socket.emit('debug', row.state);
+ } catch (err) {
+ console.log(err);
+ return socket.emit('error', err.toString());
+ }
+}
+
+function on_save(socket) {
+ SLOG(socket, "<-- SAVE");
+ try {
+ let row = QUERY_SELECT_GAME_STATE.get(socket.game_id);
+ if (!row)
+ return socket.emit('error', "No game with that ID.");
+ socket.emit('save', row.state);
+ } catch (err) {
+ console.log(err);
+ return socket.emit('error', err.toString());
+ }
+}
+
+function on_restore(socket, state_text) {
+ SLOG(socket, '--> RESTORE', state_text);
+ try {
+ let state = JSON.parse(state_text);
+ QUERY_UPDATE_GAME_STATE.run(state_text, state.active, 1, null, socket.game_id);
+ for (let other of clients[socket.game_id])
+ send_state(other, state);
+ } catch (err) {
+ console.log(err);
+ return socket.emit('error', err.toString());
+ }
+}
+
+function broadcast_presence(game_id) {
+ let presence = {};
+ for (let socket of clients[game_id])
+ presence[socket.role] = true;
+ for (let socket of clients[game_id])
+ socket.emit('presence', presence);
+}
+
+io.on('connection', (socket) => {
+ socket.title_id = socket.handshake.query.title;
+ socket.game_id = socket.handshake.query.game | 0;
+ socket.user_id = socket.request.user.user_id | 0;
+ socket.user_name = socket.request.user.name;
+ socket.role = socket.handshake.query.role;
+ socket.log_length = 0;
+ socket.chat_length = 0;
+ socket.rules = RULES[socket.title_id];
+
+ SLOG(socket, "CONNECT");
+
+ try {
+ let game = QUERY_CONNECT_GAME.get(socket.title_id, socket.game_id);
+ if (!game)
+ return socket.emit('error', "That game does not exist.");
+
+ let players = QUERY_PLAYERS.all(socket.game_id);
+
+ if (socket.role != "Observer") {
+ let me;
+ if (socket.role && socket.role != 'undefined' && socket.role != 'null') {
+ me = players.find(p => p.user_id == socket.user_id && p.role == socket.role);
+ if (!me) {
+ socket.role = "Observer";
+ return socket.emit('error', "You aren't assigned that role!");
+ }
+ } else {
+ me = players.find(p => p.user_id == socket.user_id);
+ socket.role = me ? me.role : "Observer";
+ }
+ }
+
+ socket.emit('roles', socket.role, players);
+
+ if (clients[socket.game_id])
+ clients[socket.game_id].push(socket);
+ else
+ clients[socket.game_id] = [ socket ];
+
+ socket.on('disconnect', () => {
+ SLOG(socket, "DISCONNECT");
+ clients[socket.game_id].splice(clients[socket.game_id].indexOf(socket), 1);
+ if (socket.role != "Observer")
+ broadcast_presence(socket.game_id);
+ });
+
+ if (socket.role != "Observer") {
+ socket.on('action', (action, arg) => on_action(socket, action, arg));
+ socket.on('resign', () => on_resign(socket));
+ socket.on('getchat', (old_len) => on_getchat(socket, old_len));
+ socket.on('chat', (message) => on_chat(socket, message));
+
+ socket.on('debug', () => on_debug(socket));
+ socket.on('save', () => on_save(socket));
+ socket.on('restore', (state) => on_restore(socket, state));
+ socket.on('restart', (scenario) => {
+ let state = socket.rules.setup(scenario, players);
+ for (let other of clients[socket.game_id]) {
+ other.log_length = 0;
+ send_state(other, state);
+ }
+ let state_text = JSON.stringify(state);
+ QUERY_RESTART_GAME.run(state_text, socket.game_id);
+ });
+ }
+
+ broadcast_presence(socket.game_id);
+
+ send_state(socket, JSON.parse(game.state));
+
+ } catch (err) {
+ console.log(err);
+ socket.emit('error', err.message);
+ }
+});
+
+http.listen(http_port, '0.0.0.0', () => { console.log('listening HTTP on *:' + http_port); });
+https.listen(https_port, '0.0.0.0', () => { console.log('listening HTTPS on *:' + https_port); });