Christian Fraß commited on 2021-11-20 15:05:30
Zeige 4 geänderte Dateien mit 255 Einfügungen und 72 Löschungen.
... | ... |
@@ -34,10 +34,17 @@ Example: `curl 'http://localhost:7979' -d '{"id":"foo1bar2",action":"send","data |
34 | 34 |
- output type: `boolean` |
35 | 35 |
|
36 | 36 |
|
37 |
-### `send` |
|
37 |
+### `send_channel` |
|
38 | 38 |
|
39 |
-- description: sends a message |
|
40 |
-- input type: `record<channel:string,message:string>` |
|
39 |
+- description: sends a message to a channel |
|
40 |
+- input type: `record<channel:string,content:string>` |
|
41 |
+- output type: `void` |
|
42 |
+ |
|
43 |
+ |
|
44 |
+### `send_query` |
|
45 |
+ |
|
46 |
+- description: sends a message to a query |
|
47 |
+- input type: `record<receiver:string,content:string>` |
|
41 | 48 |
- output type: `void` |
42 | 49 |
|
43 | 50 |
|
... | ... |
@@ -3,6 +3,11 @@ |
3 | 3 |
A simple mediator between an IRC server and a web application |
4 | 4 |
|
5 | 5 |
|
6 |
+# Dependencies |
|
7 |
+ |
|
8 |
+- [node irc](https://node-irc.readthedocs.io/en/latest/) |
|
9 |
+ |
|
10 |
+ |
|
6 | 11 |
# Licence |
7 | 12 |
|
8 | 13 |
- model: GPLv3 |
... | ... |
@@ -31,4 +36,7 @@ After building just execute `cd build && ./wirc`! |
31 | 36 |
# Plans and ToDos |
32 | 37 |
|
33 | 38 |
- support commands (e.g. `/nick new_name`) |
39 |
+- support for actions (e.g. `/me smiles`) |
|
40 |
+- add join and part events to `fetch` |
|
41 |
+- support topic |
|
34 | 42 |
|
... | ... |
@@ -1,3 +1,22 @@ |
1 |
+declare var setInterval: any; |
|
2 |
+ |
|
3 |
+ |
|
4 |
+type type_conf = |
|
5 |
+( |
|
6 |
+ null |
|
7 |
+ | |
|
8 |
+ { |
|
9 |
+ port: int; |
|
10 |
+ verbosity: int; |
|
11 |
+ cleaning: |
|
12 |
+ { |
|
13 |
+ timeout_in_seconds: int; |
|
14 |
+ worker_interval_in_seconds: int; |
|
15 |
+ }; |
|
16 |
+ } |
|
17 |
+); |
|
18 |
+ |
|
19 |
+ |
|
1 | 20 |
type type_event = |
2 | 21 |
{ |
3 | 22 |
timestamp: int; |
... | ... |
@@ -15,15 +34,25 @@ type type_user = |
15 | 34 |
|
16 | 35 |
type type_connection = |
17 | 36 |
{ |
18 |
- events: Array<type_event>; |
|
19 |
- users: Array<type_user>; |
|
20 | 37 |
client: any; |
38 |
+ eventqueue: Array<type_event>; |
|
39 |
+ termination: (null | int); |
|
40 |
+}; |
|
41 |
+ |
|
42 |
+ |
|
43 |
+type type_id = string; |
|
44 |
+ |
|
45 |
+ |
|
46 |
+type type_model = |
|
47 |
+{ |
|
48 |
+ counter: int; |
|
49 |
+ connections: Record<type_id, type_connection>; |
|
21 | 50 |
}; |
22 | 51 |
|
23 | 52 |
|
24 | 53 |
type type_internal_request = |
25 | 54 |
{ |
26 |
- id: (null | string); |
|
55 |
+ id: (null | type_id); |
|
27 | 56 |
action: string; |
28 | 57 |
data: any; |
29 | 58 |
}; |
... | ... |
@@ -32,66 +61,128 @@ type type_internal_request = |
32 | 61 |
type type_internal_response = any; |
33 | 62 |
|
34 | 63 |
|
35 |
-function get_timestamp(): int |
|
64 |
+/** |
|
65 |
+ * the node module "irc" |
|
66 |
+ */ |
|
67 |
+var nm_irc: any; |
|
68 |
+ |
|
69 |
+ |
|
70 |
+/** |
|
71 |
+ * gets the current UNIX timestamp |
|
72 |
+ */ |
|
73 |
+function get_timestamp |
|
74 |
+( |
|
75 |
+): int |
|
36 | 76 |
{ |
37 | 77 |
return Math.floor(Date.now()/1000); |
38 | 78 |
} |
39 | 79 |
|
40 |
-function generate_id(): string |
|
80 |
+ |
|
81 |
+/** |
|
82 |
+ * generates a unique 8 digit long string |
|
83 |
+ */ |
|
84 |
+function generate_id |
|
85 |
+( |
|
86 |
+ model: type_model |
|
87 |
+): type_id |
|
41 | 88 |
{ |
42 |
- return (Math.random() * (1 << 24)).toFixed(0).padStart(8, '0'); |
|
89 |
+ model.counter += 1; |
|
90 |
+ return model.counter.toFixed(0).padStart(8, '0'); |
|
43 | 91 |
} |
44 | 92 |
|
45 | 93 |
|
46 |
-var nm_irc: any = require("irc"); |
|
47 |
- |
|
48 |
- |
|
49 |
-var _connections: Record<string, type_connection> = {}; |
|
50 |
- |
|
51 |
- |
|
52 |
-var _conf: any = {}; |
|
94 |
+/** |
|
95 |
+ * writes a message to stderr |
|
96 |
+ */ |
|
97 |
+function log |
|
98 |
+( |
|
99 |
+ conf: type_conf, |
|
100 |
+ level: int, |
|
101 |
+ incident: string, |
|
102 |
+ details: Record<string, any> = {} |
|
103 |
+): void |
|
104 |
+{ |
|
105 |
+ if (level <= conf.verbosity) |
|
106 |
+ { |
|
107 |
+ process.stderr.write(`-- ${incident} | ${lib_json.encode(details)}\n`); |
|
108 |
+ } |
|
109 |
+ else |
|
110 |
+ { |
|
111 |
+ // do nothing |
|
112 |
+ } |
|
113 |
+} |
|
53 | 114 |
|
54 | 115 |
|
55 |
-function log(level: int, incident: string, details: Record<string, any> = {}): void |
|
116 |
+/** |
|
117 |
+ * updates the termination timestamp of a connection, prolonging its lifetime |
|
118 |
+ */ |
|
119 |
+function connection_touch |
|
120 |
+( |
|
121 |
+ conf: type_conf, |
|
122 |
+ connection: type_connection |
|
123 |
+): void |
|
56 | 124 |
{ |
57 |
- if (level <= _conf["verbosity"]) |
|
125 |
+ if (conf.cleaning.timeout_in_seconds !== null) |
|
58 | 126 |
{ |
59 |
- process.stderr.write(`-- ${incident} | ${lib_json.encode(details)}\n`); |
|
127 |
+ connection.termination = (get_timestamp() + conf.cleaning.timeout_in_seconds); |
|
128 |
+ } |
|
129 |
+ else |
|
130 |
+ { |
|
131 |
+ connection.termination = null; |
|
60 | 132 |
} |
61 | 133 |
} |
62 | 134 |
|
63 | 135 |
|
64 |
-function get_connection(id: string): type_connection |
|
136 |
+/** |
|
137 |
+ * gets a connection by its ID |
|
138 |
+ */ |
|
139 |
+function get_connection |
|
140 |
+( |
|
141 |
+ conf: type_conf, |
|
142 |
+ model: type_model, |
|
143 |
+ id: type_id |
|
144 |
+): type_connection |
|
65 | 145 |
{ |
66 |
- if (! _connections.hasOwnProperty(id)) |
|
146 |
+ if (! model.connections.hasOwnProperty(id)) |
|
67 | 147 |
{ |
68 |
- throw (new Error("no connection for ID '" + id + "'")); |
|
148 |
+ throw (new Error(`no connection for ID '${id}'`)); |
|
69 | 149 |
} |
70 | 150 |
else |
71 | 151 |
{ |
72 |
- return _connections[id]; |
|
152 |
+ const connection: type_connection = model.connections[id]; |
|
153 |
+ connection_touch(conf, connection); |
|
154 |
+ return connection; |
|
73 | 155 |
} |
74 | 156 |
} |
75 | 157 |
|
76 | 158 |
|
77 |
-async function execute(internal_request: type_internal_request, ip_address: string): Promise<type_internal_response> |
|
159 |
+/** |
|
160 |
+ * executes a request |
|
161 |
+ */ |
|
162 |
+async function execute |
|
163 |
+( |
|
164 |
+ conf: type_conf, |
|
165 |
+ model: type_model, |
|
166 |
+ internal_request: type_internal_request, |
|
167 |
+ ip_address: string |
|
168 |
+): Promise<type_internal_response> |
|
78 | 169 |
{ |
79 | 170 |
switch (internal_request.action) |
80 | 171 |
{ |
81 | 172 |
default: |
82 | 173 |
{ |
83 |
- throw (new Error("unhandled action: " + internal_request.action)); |
|
174 |
+ throw (new Error(`unhandled action '${internal_request.action}'`)); |
|
84 | 175 |
break; |
85 | 176 |
} |
86 | 177 |
case "connect": |
87 | 178 |
{ |
88 |
- if (_connections.hasOwnProperty(internal_request.id)) |
|
179 |
+ if (model.connections.hasOwnProperty(internal_request.id)) |
|
89 | 180 |
{ |
90 | 181 |
throw (new Error("already connected")); |
91 | 182 |
} |
92 | 183 |
else |
93 | 184 |
{ |
94 |
- const id: string = generate_id(); |
|
185 |
+ const id: type_id = generate_id(model); |
|
95 | 186 |
const client = new nm_irc.Client |
96 | 187 |
( |
97 | 188 |
internal_request.data["server"], |
... | ... |
@@ -107,32 +198,32 @@ async function execute(internal_request: type_internal_request, ip_address: stri |
107 | 198 |
let connection: type_connection = |
108 | 199 |
{ |
109 | 200 |
"client": client, |
110 |
- "events": [], |
|
111 |
- "users": [], |
|
201 |
+ "eventqueue": [], |
|
202 |
+ "termination": null, |
|
112 | 203 |
}; |
113 | 204 |
client.addListener |
114 | 205 |
( |
115 |
- "message", |
|
116 |
- (from, to, message) => |
|
206 |
+ "message#", |
|
207 |
+ (nick, to, text, message) => |
|
117 | 208 |
{ |
118 |
- connection.events.push |
|
209 |
+ connection.eventqueue.push |
|
119 | 210 |
({ |
120 | 211 |
"timestamp": get_timestamp(), |
121 |
- "kind": "channel_message", |
|
122 |
- "data": {"from": from, "to": to, "message": message} |
|
212 |
+ "kind": "message_channel", |
|
213 |
+ "data": {"channel": to, "sender": nick, "content": text} |
|
123 | 214 |
}); |
124 | 215 |
} |
125 | 216 |
); |
126 | 217 |
client.addListener |
127 | 218 |
( |
128 | 219 |
"pm", |
129 |
- (from, message) => |
|
220 |
+ (nick, text, message) => |
|
130 | 221 |
{ |
131 |
- connection.events.push |
|
222 |
+ connection.eventqueue.push |
|
132 | 223 |
({ |
133 | 224 |
"timestamp": get_timestamp(), |
134 |
- "kind": "private_message", |
|
135 |
- "data": {"from": from, "message": message} |
|
225 |
+ "kind": "message_query", |
|
226 |
+ "data": {"user_name": nick, "sender": nick, "content": text} |
|
136 | 227 |
}); |
137 | 228 |
} |
138 | 229 |
); |
... | ... |
@@ -141,7 +232,13 @@ async function execute(internal_request: type_internal_request, ip_address: stri |
141 | 232 |
"names", |
142 | 233 |
(channel, users) => |
143 | 234 |
{ |
144 |
- connection.users = Object.entries(users).map(([key, value]) => ({"name": key, "role": value.toString()})); |
|
235 |
+ |
|
236 |
+ connection.eventqueue.push |
|
237 |
+ ({ |
|
238 |
+ "timestamp": get_timestamp(), |
|
239 |
+ "kind": "userlist", |
|
240 |
+ "data": {"channel": channel, "users": Object.entries(users).map(([name, role]) => ({"name": name, "role": role}))} |
|
241 |
+ }); |
|
145 | 242 |
} |
146 | 243 |
); |
147 | 244 |
client.addListener |
... | ... |
@@ -149,7 +246,7 @@ async function execute(internal_request: type_internal_request, ip_address: stri |
149 | 246 |
"error", |
150 | 247 |
(error) => |
151 | 248 |
{ |
152 |
- log(0, "irc_error", {"reason": error.message}); |
|
249 |
+ log(conf, 0, "irc_error", {"reason": error.message}); |
|
153 | 250 |
} |
154 | 251 |
); |
155 | 252 |
client.connect |
... | ... |
@@ -157,7 +254,8 @@ async function execute(internal_request: type_internal_request, ip_address: stri |
157 | 254 |
3, |
158 | 255 |
() => |
159 | 256 |
{ |
160 |
- _connections[id] = connection; |
|
257 |
+ model.connections[id] = connection; |
|
258 |
+ connection_touch(conf, connection); |
|
161 | 259 |
} |
162 | 260 |
); |
163 | 261 |
return Promise.resolve<type_internal_response>(id); |
... | ... |
@@ -168,7 +266,7 @@ async function execute(internal_request: type_internal_request, ip_address: stri |
168 | 266 |
{ |
169 | 267 |
try |
170 | 268 |
{ |
171 |
- get_connection(internal_request.id); |
|
269 |
+ get_connection(conf, model, internal_request.id); |
|
172 | 270 |
return Promise.resolve<type_internal_response>(true); |
173 | 271 |
} |
174 | 272 |
catch (error) |
... | ... |
@@ -179,51 +277,91 @@ async function execute(internal_request: type_internal_request, ip_address: stri |
179 | 277 |
} |
180 | 278 |
case "disconnect": |
181 | 279 |
{ |
182 |
- const connection: type_connection = get_connection(internal_request.id); |
|
183 |
- delete _connections[internal_request.id]; |
|
280 |
+ const connection: type_connection = get_connection(conf, model, internal_request.id); |
|
281 |
+ delete model.connections[internal_request.id]; |
|
184 | 282 |
connection.client.disconnect("", () => {}); |
185 | 283 |
return Promise.resolve<type_internal_response>(null); |
186 | 284 |
break; |
187 | 285 |
} |
188 |
- case "send": |
|
286 |
+ case "send_channel": |
|
189 | 287 |
{ |
190 |
- const connection: type_connection = get_connection(internal_request.id); |
|
191 |
- connection.client.say(internal_request.data["channel"], internal_request.data["message"]); |
|
288 |
+ const connection: type_connection = get_connection(conf, model, internal_request.id); |
|
289 |
+ connection.client.say(internal_request.data["channel"], internal_request.data["content"]); |
|
192 | 290 |
return Promise.resolve<type_internal_response>(null); |
193 | 291 |
break; |
194 | 292 |
} |
195 |
- case "fetch": |
|
293 |
+ case "send_query": |
|
196 | 294 |
{ |
197 |
- const connection: type_connection = get_connection(internal_request.id); |
|
198 |
- const internal_response: type_internal_response = |
|
295 |
+ const connection: type_connection = get_connection(conf, model, internal_request.id); |
|
296 |
+ connection.client.say(internal_request.data["receiver"], internal_request.data["content"]); |
|
297 |
+ return Promise.resolve<type_internal_response>(null); |
|
298 |
+ break; |
|
299 |
+ } |
|
300 |
+ case "fetch": |
|
199 | 301 |
{ |
200 |
- "users": connection.users, |
|
201 |
- "events": connection.events, |
|
202 |
- }; |
|
203 |
- connection.events = []; |
|
302 |
+ const connection: type_connection = get_connection(conf, model, internal_request.id); |
|
303 |
+ const internal_response: type_internal_response = connection.eventqueue; |
|
304 |
+ connection.eventqueue = []; |
|
204 | 305 |
return Promise.resolve<type_internal_response>(internal_response); |
205 | 306 |
break; |
206 | 307 |
} |
207 | 308 |
} |
208 | 309 |
} |
209 | 310 |
|
210 |
-async function main(): Promise<void> |
|
311 |
+ |
|
312 |
+/** |
|
313 |
+ * sets up the worker for terminating old connections |
|
314 |
+ */ |
|
315 |
+function setup_cleaner |
|
316 |
+( |
|
317 |
+ conf: type_conf, |
|
318 |
+ model: type_model |
|
319 |
+): Promise<void> |
|
320 |
+{ |
|
321 |
+ setInterval |
|
322 |
+ ( |
|
323 |
+ () => |
|
324 |
+ { |
|
325 |
+ const now: int = get_timestamp(); |
|
326 |
+ for (const [id, connection] of Object.entries(model.connections)) |
|
327 |
+ { |
|
328 |
+ if ((connection.termination !== null) && (now > connection.termination)) |
|
329 |
+ { |
|
330 |
+ delete model.connections[id]; |
|
331 |
+ connection.client.disconnect("timeout", () => {}); |
|
332 |
+ log(conf, 1, "connection terminated after timeout", {"id": id}); |
|
333 |
+ } |
|
334 |
+ } |
|
335 |
+ }, |
|
336 |
+ (conf.cleaning.worker_interval_in_seconds * 1000) |
|
337 |
+ ); |
|
338 |
+ return Promise.resolve<void>(undefined); |
|
339 |
+} |
|
340 |
+ |
|
341 |
+ |
|
342 |
+/** |
|
343 |
+ * sets up the server, accepting HTTP request |
|
344 |
+ */ |
|
345 |
+function setup_server |
|
346 |
+( |
|
347 |
+ conf: type_conf, |
|
348 |
+ model: type_model |
|
349 |
+): Promise<void> |
|
211 | 350 |
{ |
212 |
- _conf = await lib_plankton.file.read("conf.json").then<any>(x => lib_json.decode(x)); |
|
213 | 351 |
const server: lib_server.class_server = new lib_server.class_server |
214 | 352 |
( |
215 |
- _conf["port"], |
|
353 |
+ conf.port, |
|
216 | 354 |
async (input: string, metadata?: lib_server.type_metadata): Promise<string> => |
217 | 355 |
{ |
218 | 356 |
const http_request: lib_http.type_request = lib_http.decode_request(input); |
219 |
- log(2, "http_request", http_request); |
|
357 |
+ log(conf, 2, "http_request", http_request); |
|
220 | 358 |
const internal_request: type_internal_request = lib_json.decode(http_request.body); |
221 |
- log(1, "internal_request", internal_request); |
|
359 |
+ log(conf, 1, "internal_request", internal_request); |
|
222 | 360 |
let internal_response: type_internal_response; |
223 | 361 |
let error: (null | Error); |
224 | 362 |
try |
225 | 363 |
{ |
226 |
- internal_response = await execute(internal_request, metadata.ip_address); |
|
364 |
+ internal_response = await execute(conf, model, internal_request, metadata.ip_address); |
|
227 | 365 |
error = null; |
228 | 366 |
} |
229 | 367 |
catch (error_) |
... | ... |
@@ -231,32 +369,58 @@ async function main(): Promise<void> |
231 | 369 |
internal_response = null; |
232 | 370 |
error = error_; |
233 | 371 |
} |
372 |
+ let http_response: lib_http.type_response; |
|
234 | 373 |
if (error !== null) |
235 | 374 |
{ |
236 |
- log(0, "error_in_execution", {"reason": error.toString()}); |
|
237 |
- } |
|
238 |
- const http_response: lib_http.type_response = |
|
239 |
- ( |
|
240 |
- (error !== null) |
|
241 |
- ? { |
|
375 |
+ log(conf, 0, "error_in_execution", {"reason": error.toString()}); |
|
376 |
+ http_response = |
|
377 |
+ { |
|
242 | 378 |
"statuscode": 500, |
243 | 379 |
"headers": {"Access-Control-Allow-Origin": "*", "Content-Type": "text/plain"}, |
244 | 380 |
"body": "error executing the request; check the server logs for details", |
381 |
+ }; |
|
245 | 382 |
} |
246 |
- : { |
|
383 |
+ else |
|
384 |
+ { |
|
385 |
+ log(conf, 1, "internal_response", {"value": internal_response}); |
|
386 |
+ http_response = |
|
387 |
+ { |
|
247 | 388 |
"statuscode": 200, |
248 | 389 |
"headers": {"Access-Control-Allow-Origin": "*", "Content-Type": "application/json"}, |
249 | 390 |
"body": lib_json.encode(internal_response) |
250 | 391 |
} |
251 |
- ); |
|
252 |
- log(2, "http_response", http_response); |
|
392 |
+ } |
|
393 |
+ log(conf, 2, "http_response", http_response); |
|
253 | 394 |
const output: string = lib_http.encode_response(http_response); |
254 | 395 |
return Promise.resolve<string>(output); |
255 | 396 |
} |
256 | 397 |
); |
257 |
- return server.start(); |
|
398 |
+ server.start(); |
|
399 |
+ return Promise.resolve<void>(undefined); |
|
400 |
+} |
|
401 |
+ |
|
402 |
+ |
|
403 |
+/** |
|
404 |
+ * initializes and starts the whole system |
|
405 |
+ */ |
|
406 |
+async function main |
|
407 |
+( |
|
408 |
+): Promise<void> |
|
409 |
+{ |
|
410 |
+ nm_irc = require("irc"); |
|
411 |
+ const conf: type_conf = await lib_plankton.file.read("conf.json").then<type_conf>(lib_json.decode); |
|
412 |
+ let model: type_model = |
|
413 |
+ { |
|
414 |
+ "counter": 0, |
|
415 |
+ "connections": {}, |
|
416 |
+ }; |
|
417 |
+ await Promise.all([ |
|
418 |
+ setup_cleaner(conf, model), |
|
419 |
+ setup_server(conf, model), |
|
420 |
+ ]); |
|
421 |
+ return Promise.resolve<void>(undefined); |
|
258 | 422 |
} |
259 | 423 |
|
260 | 424 |
|
261 |
-main(); |
|
425 |
+main().then(() => {}).catch((reason) => process.stderr.write(reason.toString())); |
|
262 | 426 |
|
263 | 427 |