[mod] stateless
Christian Fraß

Christian Fraß commited on 2021-11-20 15:05:30
Zeige 4 geänderte Dateien mit 255 Einfügungen und 72 Löschungen.

... ...
@@ -1,5 +1,9 @@
1 1
 {
2 2
 	"port": 7979,
3
-	"verbosity": 1
3
+	"verbosity": 1,
4
+	"cleaning": {
5
+		"timeout_in_seconds": 120,
6
+		"worker_interval_in_seconds": 60
7
+	}
4 8
 }
5 9
 
... ...
@@ -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