Add Fanorona
[vchess.git] / server / sockets.js
1 // Node version in Ubuntu 16.04 does not know about URL class
2 function getJsonFromUrl(url) {
3 const query = url.substr(2); //starts with "/?"
4 let result = {};
5 query.split("&").forEach((part) => {
6 const item = part.split("=");
7 result[item[0]] = decodeURIComponent(item[1]);
8 });
9 return result;
10 }
11
12 // Helper to safe-send some message through a (web-)socket:
13 function send(socket, message) {
14 if (!!socket && socket.readyState == 1)
15 socket.send(JSON.stringify(message));
16 }
17
18 module.exports = function(wss) {
19 // Associative array page --> sid --> tmpId --> socket
20 // "page" is either "/" for hall or "/game/some_gid" for Game,
21 // or "/mygames" for Mygames page (simpler: no 'people' array).
22 // tmpId is required if a same user (browser) has different tabs
23 let clients = {};
24 // NOTE: only purpose of sidToPages = know when to delete keys in idToSid
25 let sidToPages = {};
26 let idToSid = {};
27 wss.on("connection", (socket, req) => {
28 const query = getJsonFromUrl(req.url);
29 const sid = query["sid"];
30 const id = query["id"];
31 const tmpId = query["tmpId"];
32 const page = query["page"];
33 const notifyRoom = (page, code, obj={}, except) => {
34 if (!clients[page]) return;
35 except = except || [];
36 Object.keys(clients[page]).forEach(k => {
37 if (except.includes(k)) return;
38 Object.keys(clients[page][k]).forEach(x => {
39 if (k == sid && x == tmpId) return;
40 send(
41 clients[page][k][x].socket,
42 Object.assign({ code: code, from: [sid, tmpId] }, obj)
43 );
44 });
45 });
46 };
47 const deleteConnexion = () => {
48 if (!clients[page] || !clients[page][sid] || !clients[page][sid][tmpId])
49 return; //job already done
50 delete clients[page][sid][tmpId];
51 if (Object.keys(clients[page][sid]).length == 0) {
52 delete clients[page][sid];
53 const pgIndex = sidToPages[sid].findIndex(pg => pg == page);
54 sidToPages[sid].splice(pgIndex, 1);
55 if (Object.keys(clients[page]).length == 0)
56 delete clients[page];
57 // Am I totally offline?
58 if (sidToPages[sid].length == 0) {
59 delete sidToPages[sid];
60 delete idToSid[id];
61 }
62 }
63 };
64
65 const doDisconnect = () => {
66 deleteConnexion();
67 // Nothing to notify when disconnecting from MyGames page:
68 if (page != "/mygames") {
69 notifyRoom(page, "disconnect");
70 if (page.indexOf("/game/") >= 0)
71 notifyRoom("/", "gdisconnect", { page: page });
72 }
73 };
74 const messageListener = (objtxt) => {
75 let obj = JSON.parse(objtxt);
76 switch (obj.code) {
77 // Wait for "connect" message to notify connection to the room,
78 // because if game loading is slow the message listener might
79 // not be ready too early.
80 case "connect": {
81 notifyRoom(page, "connect");
82 if (page.indexOf("/game/") >= 0)
83 notifyRoom("/", "gconnect", { page: page });
84 break;
85 }
86 case "disconnect":
87 // When page changes:
88 doDisconnect();
89 break;
90 case "pollclients": {
91 // From Game
92 let sockIds = {};
93 Object.keys(clients[page]).forEach(k => {
94 sockIds[k] = {};
95 Object.keys(clients[page][k]).forEach(x => {
96 // Avoid polling my tmpId: no information to get
97 if (k != sid || x != tmpId)
98 sockIds[k][x] = { focus: clients[page][k][x].focus };
99 });
100 });
101 send(socket, { code: "pollclients", sockIds: sockIds });
102 break;
103 }
104 case "pollclientsandgamers": {
105 // From Hall
106 let sockIds = {};
107 Object.keys(clients["/"]).forEach(k => {
108 sockIds[k] = {};
109 Object.keys(clients[page][k]).forEach(x => {
110 // Avoid polling my tmpId: no information to get
111 if (k != sid || x != tmpId) {
112 sockIds[k][x] = {
113 page: "/",
114 focus: clients[page][k][x].focus
115 };
116 }
117 });
118 });
119 // NOTE: a "gamer" could also just be an observer
120 Object.keys(clients).forEach(p => {
121 if (p.indexOf("/game/") >= 0) {
122 Object.keys(clients[p]).forEach(k => {
123 if (!sockIds[k]) sockIds[k] = {};
124 Object.keys(clients[p][k]).forEach(x => {
125 if (k != sid || x != tmpId) {
126 sockIds[k][x] = {
127 page: p,
128 focus: clients[p][k][x].focus
129 };
130 }
131 });
132 });
133 }
134 });
135 send(socket, { code: "pollclientsandgamers", sockIds: sockIds });
136 break;
137 }
138
139 // Asking something: from is fully identified,
140 // but the requested resource can be from any tmpId (except current!)
141 case "askidentity":
142 case "asklastate":
143 case "askchallenges":
144 case "askgame": {
145 const pg = obj.page || page; //required for askidentity and askgame
146 if (!!clients[pg] && !!clients[pg][obj.target]) {
147 let tmpIds = Object.keys(clients[pg][obj.target]);
148 if (obj.target == sid) {
149 // Targetting myself
150 const idx_myTmpid = tmpIds.findIndex(x => x == tmpId);
151 if (idx_myTmpid >= 0) tmpIds.splice(idx_myTmpid, 1);
152 }
153 if (tmpIds.length > 0) {
154 const ttmpId = tmpIds[Math.floor(Math.random() * tmpIds.length)];
155 send(
156 clients[pg][obj.target][ttmpId].socket,
157 { code: obj.code, from: [sid,tmpId,page] }
158 );
159 }
160 }
161 break;
162 }
163
164 // Special situation of the previous "case":
165 // Full game can be asked to any observer.
166 case "askfullgame": {
167 if (!!clients[page]) {
168 let sids = Object.keys(clients[page]).filter(k => k != sid);
169 if (sids.length > 0) {
170 // Pick a SID at random in this set, and ask full game:
171 const rid = sids[Math.floor(Math.random() * sids.length)];
172 // ..to a random tmpId:
173 const tmpIds = Object.keys(clients[page][rid]);
174 const rtmpId = tmpIds[Math.floor(Math.random() * tmpIds.length)];
175 send(
176 clients[page][rid][rtmpId].socket,
177 { code: "askfullgame", from: [sid,tmpId] }
178 );
179 } else {
180 // I'm the only person who have the game for the moment:
181 send(socket, { code: "fullgame", data: { empty: true } });
182 }
183 }
184 break;
185 }
186
187 // Some Hall events: target all tmpId's (except mine),
188 case "refusechallenge":
189 case "startgame":
190 Object.keys(clients[page][obj.target]).forEach(x => {
191 if (obj.target != sid || x != tmpId)
192 send(
193 clients[page][obj.target][x].socket,
194 { code: obj.code, data: obj.data }
195 );
196 });
197 break;
198
199 // Notify all room: mostly game events
200 case "newchat":
201 case "newchallenge":
202 case "deletechallenge_s":
203 case "newgame":
204 case "resign":
205 case "abort":
206 case "drawoffer":
207 case "rematchoffer":
208 case "draw":
209 // "newgame" message can provide a page (corr Game --> Hall)
210 notifyRoom(
211 obj.page || page, obj.code, {data: obj.data}, obj.excluded);
212 break;
213
214 case "rnewgame":
215 // A rematch game started:
216 notifyRoom(page, "newgame", {data: obj.data});
217 // Explicitely notify Hall if gametype == corr.
218 // Live games will be polled from Hall after gconnect event.
219 if (obj.data.cadence.indexOf('d') >= 0)
220 notifyRoom("/", "newgame", {data: obj.data});
221 break;
222
223 case "newmove": {
224 const dataWithFrom = { from: [sid,tmpId], data: obj.data };
225 // Special case re-send newmove only to opponent:
226 if (!!obj.target && !!clients[page][obj.target]) {
227 Object.keys(clients[page][obj.target]).forEach(x => {
228 send(
229 clients[page][obj.target][x].socket,
230 Object.assign({ code: "newmove" }, dataWithFrom)
231 );
232 });
233 } else {
234 // NOTE: data.from is useful only to opponent
235 notifyRoom(page, "newmove", dataWithFrom);
236 }
237 break;
238 }
239 case "gotmove":
240 if (
241 !!clients[page][obj.target[0]] &&
242 !!clients[page][obj.target[0]][obj.target[1]]
243 ) {
244 send(
245 clients[page][obj.target[0]][obj.target[1]].socket,
246 { code: "gotmove" }
247 );
248 }
249 break;
250
251 case "result":
252 // Special case: notify all, 'transroom': Game --> Hall
253 notifyRoom("/", "result", { gid: obj.gid, score: obj.score });
254 break;
255
256 case "mabort": {
257 const gamePg = "/game/" + obj.gid;
258 if (!!clients[gamePg] && !!clients[gamePg][obj.target]) {
259 Object.keys(clients[gamePg][obj.target]).forEach(x => {
260 send(
261 clients[gamePg][obj.target][x].socket,
262 { code: "abort" }
263 );
264 });
265 }
266 break;
267 }
268
269 case "notifyscore":
270 case "notifyturn":
271 case "notifynewgame":
272 if (!!clients["/mygames"]) {
273 obj.targets.forEach(t => {
274 const k = t.sid || idToSid[t.id];
275 if (!!clients["/mygames"][k]) {
276 Object.keys(clients["/mygames"][k]).forEach(x => {
277 send(
278 clients["/mygames"][k][x].socket,
279 { code: obj.code, data: obj.data }
280 );
281 });
282 }
283 });
284 }
285 break;
286
287 case "getfocus":
288 case "losefocus":
289 if (
290 !!clients[page] &&
291 !!clients[page][sid] &&
292 !!clients[page][sid][tmpId]
293 ) {
294 clients[page][sid][tmpId].focus = (obj.code == "getfocus");
295 }
296 if (page == "/") notifyRoom("/", obj.code, { page: "/" }, [sid]);
297 else {
298 // Notify game room + Hall:
299 notifyRoom(page, obj.code, {}, [sid]);
300 notifyRoom("/", obj.code, { page: page }, [sid]);
301 }
302 break;
303
304 // Passing, relaying something: from isn't needed,
305 // but target is fully identified (sid + tmpId)
306 case "challenges":
307 case "fullgame":
308 case "game":
309 case "identity":
310 case "lastate":
311 {
312 const pg = obj.target[2] || page; //required for identity and game
313 // NOTE: if in game we ask identity to opponent still in Hall, but
314 // leaving Hall, clients[pg] or clients[pg][target] could be undef.
315 if (!!clients[pg] && !!clients[pg][obj.target[0]]) {
316 send(
317 clients[pg][obj.target[0]][obj.target[1]].socket,
318 { code:obj.code, data:obj.data }
319 );
320 }
321 break;
322 }
323 }
324 };
325 const closeListener = () => {
326 // For browser or tab closing (including page reload):
327 doDisconnect();
328 };
329 // Update clients object: add new connexion
330 const newElt = { socket: socket, focus: true };
331 if (!clients[page])
332 clients[page] = { [sid]: {[tmpId]: newElt } };
333 else if (!clients[page][sid])
334 clients[page][sid] = { [tmpId]: newElt };
335 else
336 clients[page][sid][tmpId] = newElt;
337 // Also update helper correspondances
338 if (!idToSid[id]) idToSid[id] = sid;
339 if (!sidToPages[sid]) sidToPages[sid] = [];
340 const pgIndex = sidToPages[sid].findIndex(pg => pg == page);
341 if (pgIndex === -1) sidToPages[sid].push(page);
342 socket.on("message", messageListener);
343 socket.on("close", closeListener);
344 });
345 }