cf5ea1b82bbc558f9f87c651f4c16c607b91b48e
[vchess.git] / server / sockets.js
1 const url = require('url');
2
3 // Node version in Ubuntu 16.04 does not know about URL class
4 // NOTE: url is already transformed, without ?xxx=yyy... parts
5 function getJsonFromUrl(url)
6 {
7 const query = url.substr(2); //starts with "/?"
8 let result = {};
9 query.split("&").forEach((part) => {
10 const item = part.split("=");
11 result[item[0]] = decodeURIComponent(item[1]);
12 });
13 return result;
14 }
15
16 // Helper to safe-send some message through a (web-)socket:
17 function send(socket, message)
18 {
19 if (!!socket && socket.readyState == 1)
20 socket.send(JSON.stringify(message));
21 }
22
23 module.exports = function(wss) {
24 // Associative array page --> sid --> tmpId --> socket
25 // "page" is either "/" for hall or "/game/some_gid" for Game,
26 // tmpId is required if a same user (browser) has different tabs
27 let clients = {};
28 wss.on("connection", (socket, req) => {
29 const query = getJsonFromUrl(req.url);
30 const sid = query["sid"];
31 const tmpId = query["tmpId"];
32 const page = query["page"];
33 const notifyRoom = (page,code,obj={}) => {
34 if (!clients[page])
35 return;
36 Object.keys(clients[page]).forEach(k => {
37 Object.keys(clients[page][k]).forEach(x => {
38 if (k == sid && x == tmpId)
39 return;
40 send(clients[page][k][x], Object.assign({code:code, from:sid}, obj));
41 });
42 });
43 };
44 const deleteConnexion = () => {
45 if (!clients[page] || !clients[page][sid] || !clients[page][sid][tmpId])
46 return; //job already done
47 delete clients[page][sid][tmpId];
48 if (Object.keys(clients[page][sid]).length == 0)
49 {
50 delete clients[page][sid];
51 if (Object.keys(clients[page]) == 0)
52 delete clients[page];
53 }
54 };
55 const doDisconnect = () => {
56 deleteConnexion();
57 if (!clients[page] || !clients[page][sid])
58 {
59 // I effectively disconnected from this page:
60 notifyRoom(page, "disconnect");
61 if (page.indexOf("/game/") >= 0)
62 notifyRoom("/", "gdisconnect", {page:page});
63 }
64 };
65 const messageListener = (objtxt) => {
66 let obj = JSON.parse(objtxt);
67 switch (obj.code)
68 {
69 // Wait for "connect" message to notify connection to the room,
70 // because if game loading is slow the message listener might
71 // not be ready too early.
72 case "connect":
73 {
74 notifyRoom(page, "connect");
75 if (page.indexOf("/game/") >= 0)
76 notifyRoom("/", "gconnect", {page:page});
77 break;
78 }
79 case "disconnect":
80 // When page changes:
81 doDisconnect();
82 break;
83 case "killme":
84 {
85 // Self multi-connect: manual removal + disconnect
86 const doKill = (pg) => {
87 Object.keys(clients[pg][obj.sid]).forEach(x => {
88 send(clients[pg][obj.sid][x], {code: "killed"});
89 });
90 delete clients[pg][obj.sid];
91 };
92 const disconnectFromOtherConnexion = (pg,code,o={}) => {
93 Object.keys(clients[pg]).forEach(k => {
94 if (k != obj.sid)
95 {
96 Object.keys(clients[pg][k]).forEach(x => {
97 send(clients[pg][k][x], Object.assign({code:code, from:obj.sid}, o));
98 });
99 }
100 });
101 };
102 Object.keys(clients).forEach(pg => {
103 if (!!clients[pg][obj.sid])
104 {
105 doKill(pg);
106 disconnectFromOtherConnexion(pg, "disconnect");
107 if (pg.indexOf("/game/") >= 0 && !!clients["/"])
108 disconnectFromOtherConnexion("/", "gdisconnect", {page:pg});
109 }
110 });
111 break;
112 }
113 case "pollclients": //from Hall or Game
114 {
115 let sockIds = [];
116 Object.keys(clients[page]).forEach(k => {
117 // Avoid polling myself: no new information to get
118 if (k != sid)
119 sockIds.push(k);
120 });
121 send(socket, {code:"pollclients", sockIds:sockIds});
122 break;
123 }
124 case "pollclientsandgamers": //from Hall
125 {
126 let sockIds = [];
127 Object.keys(clients["/"]).forEach(k => {
128 // Avoid polling myself: no new information to get
129 if (k != sid)
130 sockIds.push({sid:k});
131 });
132 // NOTE: a "gamer" could also just be an observer
133 Object.keys(clients).forEach(p => {
134 if (p != "/")
135 {
136 Object.keys(clients[p]).forEach(k => {
137 if (k != sid)
138 sockIds.push({sid:k, page:p}); //page needed for gamers
139 });
140 }
141 });
142 send(socket, {code:"pollclientsandgamers", sockIds:sockIds});
143 break;
144 }
145
146 // Asking something: from is fully identified,
147 // but the requested resource can be from any tmpId (except current!)
148 case "askidentity":
149 case "asklastate":
150 case "askchallenge":
151 case "askgame":
152 case "askfullgame":
153 {
154 const pg = obj.page || page; //required for askidentity and askgame
155 // In cas askfullgame to wrong SID for example, would crash:
156 if (clients[pg] && clients[pg][obj.target])
157 {
158 const tmpIds = Object.keys(clients[pg][obj.target]);
159 if (obj.target == sid) //targetting myself
160 {
161 const idx_myTmpid = tmpIds.findIndex(x => x == tmpId);
162 if (idx_myTmpid >= 0)
163 tmpIds.splice(idx_myTmpid, 1);
164 }
165 const tmpId_idx = Math.floor(Math.random() * tmpIds.length);
166 send(
167 clients[pg][obj.target][tmpIds[tmpId_idx]],
168 {code:obj.code, from:[sid,tmpId,page]}
169 );
170 }
171 break;
172 }
173
174 // Some Hall events: target all tmpId's (except mine),
175 case "refusechallenge":
176 case "startgame":
177 Object.keys(clients[page][obj.target]).forEach(x => {
178 if (obj.target != sid || x != tmpId)
179 send(clients[page][obj.target][x], {code:obj.code, data:obj.data});
180 });
181 break;
182
183 // Notify all room: mostly game events
184 case "newchat":
185 case "newchallenge":
186 case "newgame":
187 case "deletechallenge":
188 case "newmove":
189 case "resign":
190 case "abort":
191 case "drawoffer":
192 case "draw":
193 {
194 notifyRoom(page, obj.code, {data:obj.data});
195 const mygamesPg = "/mygames";
196 if (obj.code == "newmove" && clients[mygamesPg])
197 {
198 // Relay newmove info to myGames page
199 // NOTE: the move itself is not needed (for now at least)
200 const newmoveForMygames = {
201 gid: page.split("/")[2] //format is "/game/gid"
202 };
203 obj.data.players.forEach(pSid => {
204 if (clients[mygamesPg][pSid])
205 {
206 Object.keys(clients[mygamesPg][pSid]).forEach(x => {
207 send(
208 clients[mygamesPg][pSid][x],
209 {code:"newmove", data:newmoveForMygames}
210 );
211 });
212 }
213 });
214 }
215 break;
216 }
217
218 case "result":
219 // Special case: notify all, 'transroom': Game --> Hall
220 notifyRoom("/", "result", {gid:obj.gid, score:obj.score});
221 break;
222
223 // Passing, relaying something: from isn't needed,
224 // but target is fully identified (sid + tmpId)
225 case "challenge":
226 case "fullgame":
227 case "game":
228 case "identity":
229 case "lastate":
230 {
231 const pg = obj.target[2] || page; //required for identity and game
232 // NOTE: if in game we ask identity to opponent still in Hall,
233 // but leaving Hall, clients[pg] or clients[pg][target] could be ndefined
234 if (clients[pg] && clients[pg][obj.target[0]])
235 send(clients[pg][obj.target[0]][obj.target[1]], {code:obj.code, data:obj.data});
236 break;
237 }
238 }
239 };
240 const closeListener = () => {
241 // For browser or tab closing (including page reload):
242 doDisconnect();
243 };
244 // Update clients object: add new connexion
245 if (!clients[page])
246 clients[page] = {[sid]: {[tmpId]: socket}};
247 else if (!clients[page][sid])
248 clients[page][sid] = {[tmpId]: socket};
249 else
250 clients[page][sid][tmpId] = socket;
251 socket.on("message", messageListener);
252 socket.on("close", closeListener);
253 });
254 }