da748f0642fd12b60a94cb48ad03f83fb30effeb
[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 messageListener = (objtxt) => {
56 let obj = JSON.parse(objtxt);
57 switch (obj.code)
58 {
59 // Wait for "connect" message to notify connection to the room,
60 // because if game loading is slow the message listener might
61 // not be ready too early.
62 case "connect":
63 {
64 notifyRoom(page, "connect");
65 if (page.indexOf("/game/") >= 0)
66 notifyRoom("/", "gconnect", {page:page});
67 break;
68 }
69 case "disconnect":
70 // When page changes:
71 deleteConnexion();
72 if (!clients[page] || !clients[page][sid])
73 {
74 // I effectively disconnected from this page:
75 notifyRoom(page, "disconnect");
76 if (page.indexOf("/game/") >= 0)
77 notifyRoom("/", "gdisconnect", {page:page});
78 }
79 break;
80 case "killme":
81 {
82 // Self multi-connect: manual removal + disconnect
83 const doKill = (pg) => {
84 Object.keys(clients[pg][obj.sid]).forEach(x => {
85 send(clients[pg][obj.sid][x], {code: "killed"});
86 });
87 delete clients[pg][obj.sid];
88 };
89 const disconnectFromOtherConnexion = (pg,code,o={}) => {
90 Object.keys(clients[pg]).forEach(k => {
91 if (k != obj.sid)
92 {
93 Object.keys(clients[pg][k]).forEach(x => {
94 send(clients[pg][k][x], Object.assign({code:code, from:obj.sid}, o));
95 });
96 }
97 });
98 };
99 Object.keys(clients).forEach(pg => {
100 if (!!clients[pg][obj.sid])
101 {
102 doKill(pg);
103 disconnectFromOtherConnexion(pg, "disconnect");
104 if (pg.indexOf("/game/") >= 0 && !!clients["/"])
105 disconnectFromOtherConnexion("/", "gdisconnect", {page:pg});
106 }
107 });
108 break;
109 }
110 case "pollclients": //from Hall or Game
111 {
112 let sockIds = [];
113 Object.keys(clients[page]).forEach(k => {
114 // Avoid polling myself: no new information to get
115 if (k != sid)
116 sockIds.push(k);
117 });
118 send(socket, {code:"pollclients", sockIds:sockIds});
119 break;
120 }
121 case "pollclientsandgamers": //from Hall
122 {
123 let sockIds = [];
124 Object.keys(clients["/"]).forEach(k => {
125 // Avoid polling myself: no new information to get
126 if (k != sid)
127 sockIds.push({sid:k});
128 });
129 // NOTE: a "gamer" could also just be an observer
130 Object.keys(clients).forEach(p => {
131 if (p != "/")
132 {
133 Object.keys(clients[p]).forEach(k => {
134 if (k != sid)
135 sockIds.push({sid:k, page:p}); //page needed for gamers
136 });
137 }
138 });
139 send(socket, {code:"pollclientsandgamers", sockIds:sockIds});
140 break;
141 }
142
143 // Asking something: from is fully identified,
144 // but the requested resource can be from any tmpId (except current!)
145 case "askidentity":
146 case "asklastate":
147 case "askchallenge":
148 case "askgame":
149 case "askfullgame":
150 {
151 // DEBUG:
152 //console.log(sid + " " + page + " " + obj.code + " " + obj.target + " " + obj.page);
153 //console.log(clients);
154 const pg = obj.page || page; //required for askidentity and askgame
155 const tmpIds = Object.keys(clients[pg][obj.target]);
156 if (obj.target == sid) //targetting myself
157 {
158 const idx_myTmpid = tmpIds.findIndex(x => x == tmpId);
159 if (idx_myTmpid >= 0)
160 tmpIds.splice(idx_myTmpid, 1);
161 }
162 const tmpId_idx = Math.floor(Math.random() * tmpIds.length);
163 send(clients[pg][obj.target][tmpIds[tmpId_idx]], {code:obj.code, from:[sid,tmpId,page]});
164 break;
165 }
166
167 // Some Hall events: target all tmpId's (except mine),
168 case "refusechallenge":
169 case "startgame":
170 Object.keys(clients[page][obj.target]).forEach(x => {
171 if (obj.target != sid || x != tmpId)
172 send(clients[page][obj.target][x], {code:obj.code, data:obj.data});
173 });
174 break;
175
176 // Notify all room: mostly game events
177 case "newchat":
178 case "newchallenge":
179 case "newgame":
180 case "deletechallenge":
181 case "newmove":
182 case "resign":
183 case "abort":
184 case "drawoffer":
185 case "draw":
186 notifyRoom(page, obj.code, {data:obj.data});
187 break;
188
189 // Passing, relaying something: from isn't needed,
190 // but target is fully identified (sid + tmpId)
191 case "challenge":
192 case "fullgame":
193 case "game":
194 case "identity":
195 case "lastate":
196 {
197 const pg = obj.target[2] || page; //required for identity and game
198 send(clients[pg][obj.target[0]][obj.target[1]], {code:obj.code, data:obj.data});
199 break;
200 }
201 }
202 };
203 const closeListener = () => {
204 // For tab or browser closing:
205 deleteConnexion();
206 };
207 // Update clients object: add new connexion
208 if (!clients[page])
209 clients[page] = {[sid]: {[tmpId]: socket}};
210 else if (!clients[page][sid])
211 clients[page][sid] = {[tmpId]: socket};
212 else
213 clients[page][sid][tmpId] = socket;
214 socket.on("message", messageListener);
215 socket.on("close", closeListener);
216 });
217 }