520b8b70753eac9aca1f301aab31d0aa79db4e0b
1 const Discord
= require('discord.js');
2 const { token
, channel
} = require('./config/discord.json');
4 // Node version in Ubuntu 16.04 does not know about URL class
5 function getJsonFromUrl(url
) {
6 const query
= url
.substr(2); //starts with "/?"
8 query
.split("&").forEach((part
) => {
9 const item
= part
.split("=");
10 result
[item
[0]] = decodeURIComponent(item
[1]);
15 // Helper to safe-send some message through a (web-)socket:
16 function send(socket
, message
) {
17 if (!!socket
&& socket
.readyState
== 1)
18 socket
.send(JSON
.stringify(message
));
21 // https://www.npmjs.com/package/ws - detect lost connections...
23 function heartbeat() {
27 module
.exports = function(wss
) {
28 // Associative array page --> sid --> tmpId --> socket
29 // "page" is either "/" for hall or "/game/some_gid" for Game,
30 // or "/mygames" for Mygames page (simpler: no 'people' array).
31 // tmpId is required if a same user (browser) has different tabs
33 // NOTE: only purpose of sidToPages = know when to delete keys in idToSid
36 const discordClient
= new Discord
.Client();
37 let discordChannel
= null;
38 if (token
.length
> 0) {
39 discordClient
.login(token
);
40 discordClient
.once("ready", () => {
41 discordChannel
= discordClient
.channels
.cache
.get(channel
);
44 wss
.on("connection", (socket
, req
) => {
45 socket
.isAlive
= true;
46 socket
.on('pong', heartbeat
);
47 const query
= getJsonFromUrl(req
.url
);
48 const sid
= query
["sid"];
49 const id
= query
["id"];
50 const tmpId
= query
["tmpId"];
51 const page
= query
["page"];
52 const notifyRoom
= (page
, code
, obj
={}, except
) => {
53 if (!clients
[page
]) return;
54 except
= except
|| [];
55 Object
.keys(clients
[page
]).forEach(k
=> {
56 if (except
.includes(k
)) return;
57 Object
.keys(clients
[page
][k
]).forEach(x
=> {
58 if (k
== sid
&& x
== tmpId
) return;
60 clients
[page
][k
][x
].socket
,
61 Object
.assign({ code: code
, from: [sid
, tmpId
] }, obj
)
66 const deleteConnexion
= () => {
67 if (!clients
[page
] || !clients
[page
][sid
] || !clients
[page
][sid
][tmpId
])
68 return; //job already done
69 delete clients
[page
][sid
][tmpId
];
70 if (Object
.keys(clients
[page
][sid
]).length
== 0) {
71 delete clients
[page
][sid
];
72 const pgIndex
= sidToPages
[sid
].findIndex(pg
=> pg
== page
);
73 sidToPages
[sid
].splice(pgIndex
, 1);
74 if (Object
.keys(clients
[page
]).length
== 0)
76 // Am I totally offline?
77 if (sidToPages
[sid
].length
== 0) {
78 delete sidToPages
[sid
];
84 const doDisconnect
= () => {
86 // Nothing to notify when disconnecting from MyGames page:
87 if (page
!= "/mygames") {
88 notifyRoom(page
, "disconnect");
89 if (page
.indexOf("/game/") >= 0)
90 notifyRoom("/", "gdisconnect", { page: page
});
93 const messageListener
= (objtxt
) => {
94 let obj
= JSON
.parse(objtxt
);
96 // Wait for "connect" message to notify connection to the room,
97 // because if game loading is slow the message listener might
98 // not be ready too early.
100 notifyRoom(page
, "connect");
101 if (page
.indexOf("/game/") >= 0)
102 notifyRoom("/", "gconnect", { page: page
});
106 // When page changes:
109 case "pollclients": {
112 Object
.keys(clients
[page
]).forEach(k
=> {
114 Object
.keys(clients
[page
][k
]).forEach(x
=> {
115 // Avoid polling my tmpId: no information to get
116 if (k
!= sid
|| x
!= tmpId
)
117 sockIds
[k
][x
] = { focus: clients
[page
][k
][x
].focus
};
120 send(socket
, { code: "pollclients", sockIds: sockIds
});
123 case "pollclientsandgamers": {
126 Object
.keys(clients
["/"]).forEach(k
=> {
128 Object
.keys(clients
[page
][k
]).forEach(x
=> {
129 // Avoid polling my tmpId: no information to get
130 if (k
!= sid
|| x
!= tmpId
) {
133 focus: clients
[page
][k
][x
].focus
138 // NOTE: a "gamer" could also just be an observer
139 Object
.keys(clients
).forEach(p
=> {
140 if (p
.indexOf("/game/") >= 0) {
141 Object
.keys(clients
[p
]).forEach(k
=> {
142 if (!sockIds
[k
]) sockIds
[k
] = {};
143 Object
.keys(clients
[p
][k
]).forEach(x
=> {
144 if (k
!= sid
|| x
!= tmpId
) {
147 focus: clients
[p
][k
][x
].focus
154 send(socket
, { code: "pollclientsandgamers", sockIds: sockIds
});
158 // Asking something: from is fully identified,
159 // but the requested resource can be from any tmpId (except current!)
162 case "askchallenges":
164 const pg
= obj
.page
|| page
; //required for askidentity and askgame
165 if (!!clients
[pg
] && !!clients
[pg
][obj
.target
]) {
166 let tmpIds
= Object
.keys(clients
[pg
][obj
.target
]);
167 if (obj
.target
== sid
) {
169 const idx_myTmpid
= tmpIds
.findIndex(x
=> x
== tmpId
);
170 if (idx_myTmpid
>= 0) tmpIds
.splice(idx_myTmpid
, 1);
172 if (tmpIds
.length
> 0) {
173 const ttmpId
= tmpIds
[Math
.floor(Math
.random() * tmpIds
.length
)];
175 clients
[pg
][obj
.target
][ttmpId
].socket
,
176 { code: obj
.code
, from: [sid
,tmpId
,page
] }
183 // Special situation of the previous "case":
184 // Full game can be asked to any observer.
185 case "askfullgame": {
186 if (!!clients
[page
]) {
187 let sids
= Object
.keys(clients
[page
]).filter(k
=> k
!= sid
);
188 if (sids
.length
> 0) {
189 // Pick a SID at random in this set, and ask full game:
190 const rid
= sids
[Math
.floor(Math
.random() * sids
.length
)];
191 // ..to a random tmpId:
192 const tmpIds
= Object
.keys(clients
[page
][rid
]);
193 const rtmpId
= tmpIds
[Math
.floor(Math
.random() * tmpIds
.length
)];
195 clients
[page
][rid
][rtmpId
].socket
,
196 { code: "askfullgame", from: [sid
,tmpId
] }
199 // I'm the only person who have the game for the moment:
200 send(socket
, { code: "fullgame", data: { empty: true } });
206 // Some Hall events: target all tmpId's (except mine),
207 case "refusechallenge":
209 Object
.keys(clients
[page
][obj
.target
]).forEach(x
=> {
210 if (obj
.target
!= sid
|| x
!= tmpId
)
212 clients
[page
][obj
.target
][x
].socket
,
213 { code: obj
.code
, data: obj
.data
}
218 // Notify all room: mostly game events
221 case "deletechallenge_s":
228 // "newgame" message can provide a page (corr Game --> Hall)
229 if (obj
.code
== "newchallenge") {
230 // Filter out targeted challenges and correspondance games:
231 if (!obj
.data
.to
&& obj
.data
.cadence
.indexOf('d') < 0) {
233 (obj
.data
.sender
|| "@nonymous") + " : " +
234 "**" + obj
.data
.vname
+ "** " +
235 "[" + obj
.data
.cadence
+ "] "
237 if (!!discordChannel
) discordChannel
.send(challMsg
);
239 // Log when running locally (dev, debug):
240 console
.log(challMsg
);
242 delete obj
.data
["sender"];
245 obj
.page
|| page
, obj
.code
, {data: obj
.data
}, obj
.excluded
);
249 // A rematch game started:
250 notifyRoom(page
, "newgame", {data: obj
.data
});
251 // Explicitely notify Hall if gametype == corr.
252 // Live games will be polled from Hall after gconnect event.
253 if (obj
.data
.cadence
.indexOf('d') >= 0)
254 notifyRoom("/", "newgame", {data: obj
.data
});
258 const dataWithFrom
= { from: [sid
,tmpId
], data: obj
.data
};
259 // Special case re-send newmove only to opponent:
260 if (!!obj
.target
&& !!clients
[page
][obj
.target
]) {
261 Object
.keys(clients
[page
][obj
.target
]).forEach(x
=> {
263 clients
[page
][obj
.target
][x
].socket
,
264 Object
.assign({ code: "newmove" }, dataWithFrom
)
268 // NOTE: data.from is useful only to opponent
269 notifyRoom(page
, "newmove", dataWithFrom
);
275 !!clients
[page
][obj
.target
[0]] &&
276 !!clients
[page
][obj
.target
[0]][obj
.target
[1]]
279 clients
[page
][obj
.target
[0]][obj
.target
[1]].socket
,
286 // Special case: notify all, 'transroom': Game --> Hall
287 notifyRoom("/", "result", { gid: obj
.gid
, score: obj
.score
});
291 const gamePg
= "/game/" + obj
.gid
;
292 if (!!clients
[gamePg
] && !!clients
[gamePg
][obj
.target
]) {
293 Object
.keys(clients
[gamePg
][obj
.target
]).forEach(x
=> {
295 clients
[gamePg
][obj
.target
][x
].socket
,
305 case "notifynewgame":
306 if (!!clients
["/mygames"]) {
307 obj
.targets
.forEach(t
=> {
308 const k
= t
.sid
|| idToSid
[t
.id
];
309 if (!!clients
["/mygames"][k
]) {
310 Object
.keys(clients
["/mygames"][k
]).forEach(x
=> {
312 clients
["/mygames"][k
][x
].socket
,
313 { code: obj
.code
, data: obj
.data
}
325 !!clients
[page
][sid
] &&
326 !!clients
[page
][sid
][tmpId
]
328 clients
[page
][sid
][tmpId
].focus
= (obj
.code
== "getfocus");
330 if (page
== "/") notifyRoom("/", obj
.code
, { page: "/" }, [sid
]);
332 // Notify game room + Hall:
333 notifyRoom(page
, obj
.code
, {}, [sid
]);
334 notifyRoom("/", obj
.code
, { page: page
}, [sid
]);
338 // Passing, relaying something: from isn't needed,
339 // but target is fully identified (sid + tmpId)
346 const pg
= obj
.target
[2] || page
; //required for identity and game
347 // NOTE: if in game we ask identity to opponent still in Hall, but
348 // leaving Hall, clients[pg] or clients[pg][target] could be undef.
349 if (!!clients
[pg
] && !!clients
[pg
][obj
.target
[0]]) {
351 clients
[pg
][obj
.target
[0]][obj
.target
[1]].socket
,
352 { code:obj
.code
, data:obj
.data
}
359 const closeListener
= () => {
360 // For browser or tab closing (including page reload):
363 // Update clients object: add new connexion
364 const newElt
= { socket: socket
, focus: true };
366 clients
[page
] = { [sid
]: {[tmpId
]: newElt
} };
367 else if (!clients
[page
][sid
])
368 clients
[page
][sid
] = { [tmpId
]: newElt
};
370 clients
[page
][sid
][tmpId
] = newElt
;
371 // Also update helper correspondances
372 if (!idToSid
[id
]) idToSid
[id
] = sid
;
373 if (!sidToPages
[sid
]) sidToPages
[sid
] = [];
374 const pgIndex
= sidToPages
[sid
].findIndex(pg
=> pg
== page
);
375 if (pgIndex
=== -1) sidToPages
[sid
].push(page
);
376 socket
.on("message", messageListener
);
377 socket
.on("close", closeListener
);
379 const interval
= setInterval(
381 wss
.clients
.forEach(ws
=> {
382 if (ws
.isAlive
=== false) return ws
.terminate();
389 wss
.on('close', () => clearInterval(interval
));