namespace ns_model { /** * adds a listener for a certain incident */ export function listen ( model: type_model, incident: string, handler: (details?: any)=>void ): void { if (! model.listeners.hasOwnProperty(incident)) { model.listeners[incident] = []; } else { // do nothing } model.listeners[incident].push(handler); } /** * sends a notification to all listeners for a certain incident */ function notify ( model: type_model, incident: string, details: any = null ): void { if (model.listeners.hasOwnProperty(incident)) { for (const handler of model.listeners[incident]) { handler(details); } } } /** * sets the state */ export function set_state ( model: type_model, state: enum_state ): void { model.state = state; notify(model, "state_changed"); } /** * sets the active spot (channel or query) */ export function set_active ( model: type_model, spot: type_spot ): void { model.active = spot; notify(model, "spots_changed"); notify(model, "entries_changed"); notify(model, "users_changed"); } /** * updates the model according to a list of events */ function process_events ( model: type_model, events: Array ): void { let shall_update_spots: boolean = false; let shall_update_entries: boolean = false; let shall_update_users: boolean = false; for (const event of events) { switch (event.kind) { default: { console.warn("unhandled event kind: " + event.kind); break; } case "user_list": { model.channels[event.data["channel"]].users = event.data["users"]; shall_update_users = true; break; } case "user_joined": { if (model.channels.hasOwnProperty(event.data["channel"])) { model.channels[event.data["channel"]].users.push({"name": event.data["user_name"], "role": ""}); model.channels[event.data["channel"]].entries.push ({ "timestamp": event.timestamp, "kind": enum_entrykind.info, "sender": null, "content": `${event.data["user_name"]} joined`, }); shall_update_users = true; shall_update_entries = true; } else { // do nothing } break; } case "user_parted": { if (model.channels.hasOwnProperty(event.data["channel"])) { model.channels[event.data["channel"]].users = model.channels[event.data["channel"]].users.filter ( (user) => (user.name != event.data["user_name"]) ); model.channels[event.data["channel"]].entries.push ({ "timestamp": event.timestamp, "kind": enum_entrykind.info, "sender": null, "content": `${event.data["user_name"]} left`, }); shall_update_users = true; shall_update_entries = true; } else { // do nothing } break; } case "user_kicked": { if (model.channels.hasOwnProperty(event.data["channel"])) { model.channels[event.data["channel"]].users = model.channels[event.data["channel"]].users.filter ( (user) => (user.name != event.data["user_name"]) ); model.channels[event.data["channel"]].entries.push ({ "timestamp": event.timestamp, "kind": enum_entrykind.info, "sender": null, "content": `${event.data["user_name"]} was kicked by ${event.data["op_name"]}: ${event.data["reason"]}`, }); shall_update_users = true; shall_update_entries = true; } else { // do nothing } break; } case "user_quit": { for (let channel_name of event.data["channels"]) { if (model.channels.hasOwnProperty(channel_name)) { model.channels[channel_name].users = model.channels[channel_name].users.filter ( (user) => (user.name != event.data["user_name"]) ); model.channels[channel_name].entries.push ({ "timestamp": event.timestamp, "kind": enum_entrykind.info, "sender": null, "content": `${event.data["user_name"]} quit`, }); shall_update_entries = true; shall_update_users = true; } } break; } case "topic": { if (model.channels.hasOwnProperty(event.data["channel"])) { // model.channels[event.data["channel"]].topic = event.data["content"]; model.channels[event.data["channel"]].entries.push ({ "timestamp": event.timestamp, "kind": enum_entrykind.info, "sender": null, "content": `channel topic: ${event.data["content"]}`, }); shall_update_entries = true; } else { // do nothing } break; } case "message_channel": { model.channels[event.data["channel"]].entries.push ({ "timestamp": event.timestamp, "kind": enum_entrykind.message, "sender": (event.data["sender"] ?? model.nickname), "content": event.data["content"], }); shall_update_entries = true; break; } case "message_query": { if (! model.queries.hasOwnProperty(event.data["user_name"])) { model.queries[event.data["user_name"]] = {"entries": []}; shall_update_spots = true; } else { // do nothing } model.queries[event.data["user_name"]].entries.push ({ "timestamp": event.timestamp, "kind": enum_entrykind.message, "sender": (event.data["sender"] ?? model.nickname), "content": event.data["content"], }); shall_update_entries = true; break; } } } if (shall_update_spots) notify(model, "spots_changed"); if (shall_update_entries) notify(model, "entries_changed"); if (shall_update_users) notify(model, "users_changed"); } /** * establishes the connection */ export async function connect ( conf: type_conf, model: type_model, nickname: string, channel_names: Array ): Promise { set_state(model, enum_state.connecting); const connection_id: string = await backend_call ( conf, model.connection_id, "connect", { "server": conf.irc.server, "channels": channel_names, "nickname": nickname, } ); model.connection_id = connection_id; model.nickname = nickname; for (const channel_name of channel_names) { model.channels[channel_name] = { "users": [], "entries": [], }; } if (channel_names.length > 0) { set_active(model, {"kind": "channel", "name": channel_names[0]}); } return Promise.resolve(undefined); } /** * closes the connection */ export async function disconnect ( conf: type_conf, model: type_model ): Promise { await backend_call ( conf, model.connection_id, "disconnect", null ); set_state(model, enum_state.offline); model.connection_id = null; return Promise.resolve(undefined); } /** * adds a client side message */ export function send ( conf: type_conf, model: type_model, content: string ): void { switch (model.active.kind) { case "channel": { backend_call ( conf, model.connection_id, "send_channel", { "channel": model.active.name, "content": content, } ); /* const event: type_event = { "timestamp": get_timestamp(), "kind": "message_channel", "data": { "channel": model.active.name, "sender": model.nickname, "content": content, } }; process_events(model, [event]); notify(model, "entries_changed"); */ notify(model, "message_sent"); break; } case "query": { backend_call ( conf, model.connection_id, "send_query", { "receiver": model.active.name, "content": content, } ); /* const event: type_event = { "timestamp": get_timestamp(), "kind": "message_query", "data": { "user_name": model.active.name, "sender": model.nickname, "content": content, } }; process_events(model, [event]); notify(model, "entries_changed"); */ notify(model, "message_sent"); break; } } } /** * adds a query to a user as spot */ export function open_query ( model: type_model, user_name: string ): void { if ((user_name !== model.nickname) && (! model.queries.hasOwnProperty(user_name))) { model.queries[user_name] = {"entries": []}; set_active(model, {"kind": "query", "name": user_name}); } else { // do nothing } } /** * sets up the model */ export function setup ( conf: type_conf, model: type_model ): void { setInterval ( async () => { switch (model.state) { default: { throw (new Error(`invalid state '${model.state}'`)); break; } case enum_state.offline: { // do nothing break; } case enum_state.connecting: { const ready: boolean = await backend_call(conf, model.connection_id, "check", null); if (ready) { set_state(model, enum_state.online); } else { // do nothing } break; } case enum_state.online: { const events: Array = await backend_call(conf, model.connection_id, "fetch", null); process_events(model, events); break; } } }, conf.settings.poll_interval_in_milliseconds ); set_state(model, enum_state.offline); } }