5f332b758db5cd185c2d8407a9f5654a8ac5625f
1 const db
= require("../utils/database");
2 const UserModel
= require("./User");
5 * Structure table Games:
7 * vid: integer (variant id)
8 * fenStart: varchar (initial position)
9 * fen: varchar (current position)
13 * score: varchar (result)
14 * scoreMsg: varchar ("Time", "Mutual agreement"...)
16 * drawOffer: char ('w','b' or '' for none)
17 * rematchOffer: char (similar to drawOffer)
19 * deletedByWhite: boolean
20 * deletedByBlack: boolean
22 * Structure table Moves:
24 * squares: varchar (description)
28 * Structure table Chats:
37 checkGameInfo: function(g
) {
39 g
.vid
.toString().match(/^[0-9]+$/) &&
40 g
.cadence
.match(/^[0-9dhms
+]+$/) &&
41 g
.randomness
.toString().match(/^[0-2]$/) &&
42 g
.fen
.match(/^[a-zA-Z0-9, /-]*$/) &&
43 g
.players
.length
== 2 &&
44 g
.players
.every(p
=> p
.id
.toString().match(/^[0-9]+$/))
48 create: function(vid
, fen
, randomness
, cadence
, players
, cb
) {
49 db
.serialize(function() {
51 "INSERT INTO Games " +
53 "vid, fenStart, fen, randomness, " +
59 vid
+ ",'" + fen
+ "','" + fen
+ "'," + randomness
+ "," +
60 players
[0].id
+ "," + players
[1].id
+ "," +
61 "'" + cadence
+ "'," + Date
.now() +
63 db
.run(query
, function(err
) {
64 cb(err
, { id: this.lastID
});
69 // TODO: some queries here could be async
70 getOne: function(id
, cb
) {
71 // NOTE: ignoring errors (shouldn't happen at this stage)
72 db
.serialize(function() {
75 "g.id, g.fen, g.fenStart, g.cadence, g.created, " +
76 "g.white, g.black, g.score, g.scoreMsg, " +
77 "g.drawOffer, g.rematchOffer, v.name AS vname " +
82 db
.get(query
, (err
, gameInfo
) => {
84 cb(err
|| { errmsg: "Game not found" }, undefined);
90 "WHERE id IN (" + gameInfo
.white
+ "," + gameInfo
.black
+ ")";
91 db
.all(query
, (err2
, players
) => {
92 if (players
[0].id
== gameInfo
.black
) players
= players
.reverse();
93 // The original players' IDs info isn't required anymore
94 delete gameInfo
["white"];
95 delete gameInfo
["black"];
97 "SELECT squares, played, idx " +
100 db
.all(query
, (err3
, moves
) => {
102 "SELECT msg, name, added " +
105 db
.all(query
, (err4
, chats
) => {
106 const game
= Object
.assign(
123 // For display on Hall: no need for moves or chats
124 getObserved: function(uid
, cursor
, cb
) {
125 db
.serialize(function() {
127 "SELECT id, vid, cadence, created, score, white, black " +
129 if (uid
> 0) query
+=
131 " created < " + cursor
+ " AND " +
132 " white <> " + uid
+ " AND " +
133 " black <> " + uid
+ " ";
135 "ORDER BY created DESC " +
136 "LIMIT 20"; //TODO: 20 hard-coded...
137 db
.all(query
, (err
, games
) => {
138 // Query players names
141 if (!pids
[g
.white
]) pids
[g
.white
] = true;
142 if (!pids
[g
.black
]) pids
[g
.black
] = true;
144 UserModel
.getByIds(Object
.keys(pids
), (err2
, users
) => {
146 users
.forEach(u
=> { names
[u
.id
] = u
.name
; });
158 { id: g
.white
, name: names
[g
.white
] },
159 { id: g
.black
, name: names
[g
.black
] }
170 // For display on MyGames: registered user only
171 getRunning: function(uid
, cb
) {
172 db
.serialize(function() {
174 "SELECT g.id, g.cadence, g.created, " +
175 "g.white, g.black, v.name AS vname " +
178 " ON g.vid = v.id " +
179 "WHERE score = '*' AND (white = " + uid
+ " OR black = " + uid
+ ")";
180 db
.all(query
, (err
, games
) => {
181 // Get movesCount (could be done in // with next query)
183 "SELECT gid, COUNT(*) AS nbMoves " +
185 "WHERE gid IN " + "(" + games
.map(g
=> g
.id
).join(",") + ") " +
187 db
.all(query
, (err
, mstats
) => {
188 let movesCounts
= {};
189 mstats
.forEach(ms
=> { movesCounts
[ms
.gid
] = ms
.nbMoves
; });
190 // Query player names
193 if (!pids
[g
.white
]) pids
[g
.white
] = true;
194 if (!pids
[g
.black
]) pids
[g
.black
] = true;
196 UserModel
.getByIds(Object
.keys(pids
), (err2
, users
) => {
198 users
.forEach(u
=> { names
[u
.id
] = u
.name
; });
209 movesCount: movesCounts
[g
.id
] || 0,
211 { id: g
.white
, name: names
[g
.white
] },
212 { id: g
.black
, name: names
[g
.black
] }
224 // These games could be deleted on some side. movesCount not required
225 getCompleted: function(uid
, cursor
, cb
) {
226 db
.serialize(function() {
228 "SELECT g.id, g.cadence, g.created, g.score, g.scoreMsg, " +
229 "g.white, g.black, g.deletedByWhite, g.deletedByBlack, " +
233 " ON g.vid = v.id " +
235 " score <> '*' AND" +
236 " created < " + cursor
+ " AND" +
239 " white = " + uid
+ " AND" +
240 " (deletedByWhite IS NULL OR NOT deletedByWhite)" +
244 " black = " + uid
+ " AND" +
245 " (deletedByBlack IS NULL OR NOT deletedByBlack)" +
249 "ORDER BY created DESC " +
251 db
.all(query
, (err
, games
) => {
252 // Query player names
255 if (!pids
[g
.white
]) pids
[g
.white
] = true;
256 if (!pids
[g
.black
]) pids
[g
.black
] = true;
258 UserModel
.getByIds(Object
.keys(pids
), (err2
, users
) => {
260 users
.forEach(u
=> { names
[u
.id
] = u
.name
; });
271 scoreMsg: g
.scoreMsg
,
273 { id: g
.white
, name: names
[g
.white
] },
274 { id: g
.black
, name: names
[g
.black
] }
276 deletedByWhite: g
.deletedByWhite
,
277 deletedByBlack: g
.deletedByBlack
287 getPlayers: function(id
, cb
) {
288 db
.serialize(function() {
290 "SELECT white, black " +
293 db
.get(query
, (err
, players
) => {
294 return cb(err
, players
);
299 checkGameUpdate: function(obj
) {
300 // Check all that is possible (required) in obj:
303 !obj
.move || !!(obj
.move.idx
.toString().match(/^[0-9]+$/))
305 !obj
.drawOffer
|| !!(obj
.drawOffer
.match(/^[wbtn]$/))
307 !obj
.rematchOffer
|| !!(obj
.rematchOffer
.match(/^[wbn]$/))
309 !obj
.fen
|| !!(obj
.fen
.match(/^[a-zA-Z0-9, /-]*$/))
311 !obj
.score
|| !!(obj
.score
.match(/^[012?*\/-]+$/))
313 !obj
.scoreMsg
|| !!(obj
.scoreMsg
.match(/^[a
-zA
-Z
]+$/))
315 !obj
.chat
|| UserModel
.checkNameEmail({name: obj
.chat
.name
})
320 // obj can have fields move, chat, fen, drawOffer and/or score + message
321 update: function(id
, obj
, cb
) {
322 db
.parallelize(function() {
327 // NOTE: if drawOffer is set, we should check that it's player's turn
328 // A bit overcomplicated. Let's trust the client on that for now...
329 if (!!obj
.drawOffer
) {
330 if (obj
.drawOffer
== "n")
331 // Special "None" update
333 modifs
+= "drawOffer = '" + obj
.drawOffer
+ "',";
335 if (!!obj
.rematchOffer
) {
336 if (obj
.rematchOffer
== "n")
337 // Special "None" update
338 obj
.rematchOffer
= "";
339 modifs
+= "rematchOffer = '" + obj
.rematchOffer
+ "',";
341 if (!!obj
.fen
) modifs
+= "fen = '" + obj
.fen
+ "',";
342 if (!!obj
.deletedBy
) {
343 const myColor
= obj
.deletedBy
== 'w' ? "White" : "Black";
344 modifs
+= "deletedBy" + myColor
+ " = true,";
347 modifs
+= "score = '" + obj
.score
+ "'," +
348 "scoreMsg = '" + obj
.scoreMsg
+ "',";
350 const finishAndSendQuery
= () => {
351 modifs
= modifs
.slice(0, -1); //remove last comma
352 if (modifs
.length
> 0) {
353 updateQuery
+= modifs
+ " WHERE id = " + id
;
358 if (!!obj
.move || (!!obj
.score
&& obj
.scoreMsg
== "Time")) {
359 // Security: only update moves if index is right,
360 // and score with scoreMsg "Time" if really lost on time.
362 "SELECT MAX(idx) AS maxIdx, MAX(played) AS lastPlayed " +
365 db
.get(query
, (err
, ret
) => {
367 if (!ret
.maxIdx
|| ret
.maxIdx
+ 1 == obj
.move.idx
) {
369 "INSERT INTO Moves (gid, squares, played, idx) VALUES " +
370 "(" + id
+ ",?," + Date
.now() + "," + obj
.move.idx
+ ")";
371 db
.run(query
, JSON
.stringify(obj
.move.squares
));
372 finishAndSendQuery();
373 } else cb({ errmsg: "Wrong move index" });
375 if (ret
.maxIdx
< 2) cb({ errmsg: "Time not over" });
377 // We also need the game cadence
382 db
.get(query
, (err2
, ret2
) => {
383 const daysTc
= parseInt(ret2
.cadence
.match(/\(^[0-9]+\)/)[0]);
384 if (Date
.now() - ret
.lastPlayed
> daysTc
* 24 * 3600 * 1000)
385 finishAndSendQuery();
386 else cb({ errmsg: "Time not over" });
391 } else finishAndSendQuery();
392 // NOTE: chat and delchat are mutually exclusive
395 "INSERT INTO Chats (gid, msg, name, added) VALUES ("
396 + id
+ ",?,'" + obj
.chat
.name
+ "'," + Date
.now() + ")";
397 db
.run(query
, obj
.chat
.msg
);
398 } else if (obj
.delchat
) {
405 if (!!obj
.deletedBy
) {
406 // Did my opponent delete it too?
409 (obj
.deletedBy
== 'w' ? "Black" : "White") +
412 "SELECT " + selection
+ " " +
415 db
.get(query
, (err
,ret
) => {
416 // If yes: just remove game
417 if (!!ret
.deletedByOpp
) GameModel
.remove(id
);
423 remove: function(id_s
) {
426 ? " IN (" + id_s
.join(",") + ")"
428 db
.parallelize(function() {
430 "DELETE FROM Games " +
431 "WHERE id " + suffix
;
434 "DELETE FROM Moves " +
435 "WHERE gid " + suffix
;
438 "DELETE FROM Chats " +
439 "WHERE gid " + suffix
;
444 cleanGamesDb: function() {
445 const tsNow
= Date
.now();
446 // 86400000 = 24 hours in milliseconds
447 const day
= 86400000;
448 db
.serialize(function() {
450 "SELECT id, created " +
452 db
.all(query
, (err
, games
) => {
454 "SELECT gid, count(*) AS nbMoves, MAX(played) AS lastMaj " +
457 db
.get(query
, (err2
, mstats
) => {
458 // Reorganize moves data to avoid too many array lookups:
459 let movesGroups
= {};
460 mstats
.forEach(ms
=> {
461 movesGroups
[ms
.gid
] = {
466 // Remove games still not really started,
467 // with no action in the last 3 months:
472 !movesGroups
[g
.id
] &&
473 tsNow
- g
.created
> 91*day
477 movesGroups
[g
.id
].nbMoves
== 1 &&
478 tsNow
- movesGroups
[g
.id
].lastMaj
> 91*day
484 if (toRemove
.length
> 0) GameModel
.remove(toRemove
);
491 module
.exports
= GameModel
;