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 |