Christian Fraß commited on 2021-11-18 23:54:20
Zeige 7 geänderte Dateien mit 510 Einfügungen und 0 Löschungen.
| ... | ... |
@@ -0,0 +1 @@ |
| 1 |
+build/ |
| ... | ... |
@@ -0,0 +1,16 @@ |
| 1 |
+{
|
|
| 2 |
+ "backend": {
|
|
| 3 |
+ "scheme": "http", |
|
| 4 |
+ "host": "localhost", |
|
| 5 |
+ "port": 7979 |
|
| 6 |
+ }, |
|
| 7 |
+ "settings": {
|
|
| 8 |
+ "poll_interval_in_milliseconds": 2000 |
|
| 9 |
+ }, |
|
| 10 |
+ "irc": {
|
|
| 11 |
+ "server": "irc.abtreff.de", |
|
| 12 |
+ "predefined_channel": "#ab", |
|
| 13 |
+ "predefined_nickname_prefix": "wichtel_" |
|
| 14 |
+ } |
|
| 15 |
+} |
|
| 16 |
+ |
| ... | ... |
@@ -0,0 +1,41 @@ |
| 1 |
+<!DOCTYPE html> |
|
| 2 |
+<html> |
|
| 3 |
+ <head> |
|
| 4 |
+ <meta charset="utf-8"/> |
|
| 5 |
+ <title>web-irc</title> |
|
| 6 |
+ <link rel="stylesheet" type="text/css" href="style.css"/> |
|
| 7 |
+ <script type="text/javascript" src="logic.js"></script> |
|
| 8 |
+ <script type="text/javascript">init();</script> |
|
| 9 |
+ </head> |
|
| 10 |
+ <body class="offline"> |
|
| 11 |
+ <div id="connect"> |
|
| 12 |
+ <form action="#"> |
|
| 13 |
+ <div class="field"> |
|
| 14 |
+ <label>nickname</label> |
|
| 15 |
+ <input type="text" id="nickname" placeholder="nickname"/> |
|
| 16 |
+ </div> |
|
| 17 |
+ <div class="field"> |
|
| 18 |
+ <label>channel</label> |
|
| 19 |
+ <input type="text" id="channel" placeholder="channel"/> |
|
| 20 |
+ </div> |
|
| 21 |
+ <input type="submit" value="connect"/> |
|
| 22 |
+ </form> |
|
| 23 |
+ </div> |
|
| 24 |
+ <div id="wait"> |
|
| 25 |
+ <span>loading …</span> |
|
| 26 |
+ </div> |
|
| 27 |
+ <div id="main"> |
|
| 28 |
+ <div id="head"> |
|
| 29 |
+ <button id="disconnect">exit</button> |
|
| 30 |
+ </div> |
|
| 31 |
+ <div id="middle"> |
|
| 32 |
+ <ul class="pane" id="history"></ul> |
|
| 33 |
+ <ul class="pane" id="users"></ul> |
|
| 34 |
+ </div> |
|
| 35 |
+ <form action="#"> |
|
| 36 |
+ <input type="text" id="message" placeholder="…"/> |
|
| 37 |
+ <input type="submit" value="send"/> |
|
| 38 |
+ </form> |
|
| 39 |
+ </template> |
|
| 40 |
+ </body> |
|
| 41 |
+</html> |
| ... | ... |
@@ -0,0 +1,236 @@ |
| 1 |
+type int = number; |
|
| 2 |
+type float = number; |
|
| 3 |
+ |
|
| 4 |
+var _conf: any = null; |
|
| 5 |
+var _state: (null | string) = null; |
|
| 6 |
+var _channel: (null | string) = null; |
|
| 7 |
+var _nickname: (null | string) = null; |
|
| 8 |
+var _userhash: (null | string) = null; |
|
| 9 |
+ |
|
| 10 |
+function get_timestamp(): int |
|
| 11 |
+{
|
|
| 12 |
+ return Math.floor(Date.now()/1000); |
|
| 13 |
+} |
|
| 14 |
+ |
|
| 15 |
+function hash_string_to_unit(x: string): float |
|
| 16 |
+{
|
|
| 17 |
+ return (x.split("").reduce((x, y) => ((x + y.charCodeAt(0)) % 32), 0) / 32);
|
|
| 18 |
+} |
|
| 19 |
+ |
|
| 20 |
+function get_usercolor(name: string): string |
|
| 21 |
+{
|
|
| 22 |
+ const hue: float = hash_string_to_unit(name); |
|
| 23 |
+ return `hsl(${(hue*360).toFixed(2)},50%,75%)`;
|
|
| 24 |
+} |
|
| 25 |
+ |
|
| 26 |
+async function backend_call(action: string, data: any): Promise<any> |
|
| 27 |
+{
|
|
| 28 |
+ const response = await fetch( |
|
| 29 |
+ `${_conf.backend.scheme}://${_conf.backend.host}:${_conf.backend.port.toFixed(0)}`,
|
|
| 30 |
+ {
|
|
| 31 |
+ "method": "POST", |
|
| 32 |
+ "body": JSON.stringify({"action": action, "data": data}),
|
|
| 33 |
+ } |
|
| 34 |
+ ); |
|
| 35 |
+ if (response.ok) {
|
|
| 36 |
+ return response.json(); |
|
| 37 |
+ } |
|
| 38 |
+ else {
|
|
| 39 |
+ console.error(response.text()); |
|
| 40 |
+ return Promise.reject<any>(new Error("backend call failed"));
|
|
| 41 |
+ } |
|
| 42 |
+} |
|
| 43 |
+ |
|
| 44 |
+function update_state(): void |
|
| 45 |
+{
|
|
| 46 |
+ document.querySelector("body").setAttribute("class", _state);
|
|
| 47 |
+} |
|
| 48 |
+ |
|
| 49 |
+function update_history(events): void |
|
| 50 |
+{
|
|
| 51 |
+ let dom_history: HTMLUListElement = document.querySelector("#history");
|
|
| 52 |
+ for (const event of events) {
|
|
| 53 |
+ const timestring: string = (new Date(event["timestamp"]*1000)).toISOString().slice(11, 19); |
|
| 54 |
+ let dom_event: HTMLLIElement = document.createElement("li");
|
|
| 55 |
+ dom_event.classList.add("event");
|
|
| 56 |
+ switch (event["kind"]) {
|
|
| 57 |
+ default: |
|
| 58 |
+ dom_event.textContent = ("-- unhandled event: " + JSON.stringify(event));
|
|
| 59 |
+ break; |
|
| 60 |
+ case "channel_message": |
|
| 61 |
+ {
|
|
| 62 |
+ let dom_time: HTMLDivElement = document.createElement("div");
|
|
| 63 |
+ dom_time.classList.add("event_time");
|
|
| 64 |
+ dom_time.textContent = timestring; |
|
| 65 |
+ dom_event.appendChild(dom_time); |
|
| 66 |
+ } |
|
| 67 |
+ {
|
|
| 68 |
+ let dom_sender: HTMLDivElement = document.createElement("div");
|
|
| 69 |
+ dom_sender.classList.add("event_sender");
|
|
| 70 |
+ dom_sender.style.color = get_usercolor(event["data"]["from"] ?? ""); |
|
| 71 |
+ dom_sender.textContent = event["data"]["from"]; |
|
| 72 |
+ dom_event.appendChild(dom_sender); |
|
| 73 |
+ } |
|
| 74 |
+ {
|
|
| 75 |
+ let dom_message: HTMLDivElement = document.createElement("div");
|
|
| 76 |
+ dom_message.classList.add("event_message");
|
|
| 77 |
+ dom_message.textContent = event["data"]["message"]; |
|
| 78 |
+ dom_event.appendChild(dom_message); |
|
| 79 |
+ } |
|
| 80 |
+ break; |
|
| 81 |
+ } |
|
| 82 |
+ dom_history.appendChild(dom_event); |
|
| 83 |
+ } |
|
| 84 |
+ dom_history.scrollTo(0, dom_history["scrollTopMax"]); |
|
| 85 |
+} |
|
| 86 |
+ |
|
| 87 |
+function update_users(users: Array<{name: string; role: string;}>): void
|
|
| 88 |
+{
|
|
| 89 |
+ let dom_users: HTMLUListElement = document.querySelector("#users");
|
|
| 90 |
+ dom_users.textContent = ""; |
|
| 91 |
+ const users_sorted: Array<{name: string; role: string;}> = users.sort(
|
|
| 92 |
+ (x, y) => ( |
|
| 93 |
+ (x.role >= y.role) |
|
| 94 |
+ ? -1 |
|
| 95 |
+ : ( |
|
| 96 |
+ (x.role === y.role) |
|
| 97 |
+ ? ((x.name < y.name) ? -1 : +1) |
|
| 98 |
+ : +1 |
|
| 99 |
+ ) |
|
| 100 |
+ ) |
|
| 101 |
+ ); |
|
| 102 |
+ for (const user of users_sorted) {
|
|
| 103 |
+ let dom_user: HTMLLIElement = document.createElement("li");
|
|
| 104 |
+ dom_user.classList.add("user");
|
|
| 105 |
+ {
|
|
| 106 |
+ let dom_role: HTMLSpanElement = document.createElement("span");
|
|
| 107 |
+ dom_role.textContent = user.role; |
|
| 108 |
+ dom_user.appendChild(dom_role); |
|
| 109 |
+ } |
|
| 110 |
+ {
|
|
| 111 |
+ let dom_name: HTMLSpanElement = document.createElement("span");
|
|
| 112 |
+ dom_name.textContent = user.name; |
|
| 113 |
+ dom_name.style.color = get_usercolor(user.name); |
|
| 114 |
+ dom_user.appendChild(dom_name); |
|
| 115 |
+ } |
|
| 116 |
+ // dom_user.textContent = `${user.role}${user.name}`;
|
|
| 117 |
+ dom_users.appendChild(dom_user); |
|
| 118 |
+ } |
|
| 119 |
+} |
|
| 120 |
+ |
|
| 121 |
+function set_state(state: string): void |
|
| 122 |
+{
|
|
| 123 |
+ _state = state; |
|
| 124 |
+ update_state(); |
|
| 125 |
+} |
|
| 126 |
+ |
|
| 127 |
+function setup_view(): void |
|
| 128 |
+{
|
|
| 129 |
+ document.querySelector<HTMLInputElement>("#channel").value = _conf["irc"]["predefined_channel"];
|
|
| 130 |
+ document.querySelector<HTMLInputElement>("#nickname").value = (_conf["irc"]["predefined_nickname_prefix"] + (Math.random()*100).toFixed(0));
|
|
| 131 |
+ setInterval( |
|
| 132 |
+ async () => {
|
|
| 133 |
+ switch (_state) {
|
|
| 134 |
+ case "offline": |
|
| 135 |
+ // do nothing |
|
| 136 |
+ break; |
|
| 137 |
+ case "checking": |
|
| 138 |
+ const ready: boolean = await backend_call("check", null);
|
|
| 139 |
+ if (ready) {
|
|
| 140 |
+ set_state("online");
|
|
| 141 |
+ } |
|
| 142 |
+ break; |
|
| 143 |
+ case "online": |
|
| 144 |
+ const stuff: any = await backend_call("fetch", null);
|
|
| 145 |
+ update_history(stuff["events"]); |
|
| 146 |
+ const userhash: string = btoa(JSON.stringify(stuff["users"])); |
|
| 147 |
+ if (_userhash !== userhash) {
|
|
| 148 |
+ _userhash = userhash; |
|
| 149 |
+ update_users(stuff["users"]); |
|
| 150 |
+ } |
|
| 151 |
+ break; |
|
| 152 |
+ } |
|
| 153 |
+ }, |
|
| 154 |
+ _conf["settings"]["poll_interval_in_milliseconds"] |
|
| 155 |
+ ); |
|
| 156 |
+ set_state("offline");
|
|
| 157 |
+} |
|
| 158 |
+ |
|
| 159 |
+function setup_control(): void |
|
| 160 |
+{
|
|
| 161 |
+ document.querySelector("#connect > form").addEventListener(
|
|
| 162 |
+ "submit", |
|
| 163 |
+ async (event) => {
|
|
| 164 |
+ let dom_nickname: HTMLInputElement = document.querySelector<HTMLInputElement>("#nickname");
|
|
| 165 |
+ let dom_channel: HTMLInputElement = document.querySelector<HTMLInputElement>("#channel");
|
|
| 166 |
+ const nickname: string = dom_nickname.value; |
|
| 167 |
+ const channel: string = dom_channel.value; |
|
| 168 |
+ await backend_call( |
|
| 169 |
+ "connect", |
|
| 170 |
+ {
|
|
| 171 |
+ "server": _conf["irc"]["server"], |
|
| 172 |
+ "channels": [channel], |
|
| 173 |
+ "nickname": nickname, |
|
| 174 |
+ } |
|
| 175 |
+ ); |
|
| 176 |
+ _channel = channel; |
|
| 177 |
+ _nickname = nickname; |
|
| 178 |
+ set_state("checking");
|
|
| 179 |
+ } |
|
| 180 |
+ ); |
|
| 181 |
+ document.querySelector("#disconnect").addEventListener(
|
|
| 182 |
+ "click", |
|
| 183 |
+ async (event) => {
|
|
| 184 |
+ await backend_call( |
|
| 185 |
+ "disconnect", |
|
| 186 |
+ null |
|
| 187 |
+ ); |
|
| 188 |
+ set_state("offline");
|
|
| 189 |
+ } |
|
| 190 |
+ ); |
|
| 191 |
+ document.querySelector("#main > form").addEventListener(
|
|
| 192 |
+ "submit", |
|
| 193 |
+ async (event) => {
|
|
| 194 |
+ event.preventDefault(); |
|
| 195 |
+ let dom_message: HTMLInputElement = document.querySelector<HTMLInputElement>("#message");
|
|
| 196 |
+ const message: string = dom_message.value; |
|
| 197 |
+ dom_message.value = ""; |
|
| 198 |
+ dom_message.focus(); |
|
| 199 |
+ const event_: any = {
|
|
| 200 |
+ "timestamp": get_timestamp(), |
|
| 201 |
+ "kind": "channel_message", |
|
| 202 |
+ "data": {
|
|
| 203 |
+ "from": _nickname, |
|
| 204 |
+ "to": _channel, |
|
| 205 |
+ "message": message, |
|
| 206 |
+ } |
|
| 207 |
+ }; |
|
| 208 |
+ update_history([event_]); |
|
| 209 |
+ await backend_call( |
|
| 210 |
+ "say", |
|
| 211 |
+ {
|
|
| 212 |
+ "channel": _channel, |
|
| 213 |
+ "message": message, |
|
| 214 |
+ } |
|
| 215 |
+ ); |
|
| 216 |
+ } |
|
| 217 |
+ ); |
|
| 218 |
+} |
|
| 219 |
+ |
|
| 220 |
+async function main(): Promise<void> |
|
| 221 |
+{
|
|
| 222 |
+ _conf = await fetch("conf.json").then(x => x.json());
|
|
| 223 |
+ setup_view(); |
|
| 224 |
+ setup_control(); |
|
| 225 |
+} |
|
| 226 |
+ |
|
| 227 |
+function init(): void |
|
| 228 |
+{
|
|
| 229 |
+ document.addEventListener( |
|
| 230 |
+ "DOMContentLoaded", |
|
| 231 |
+ (event) => {
|
|
| 232 |
+ main(); |
|
| 233 |
+ } |
|
| 234 |
+ ); |
|
| 235 |
+} |
|
| 236 |
+ |
| ... | ... |
@@ -0,0 +1,160 @@ |
| 1 |
+@hue: 120; |
|
| 2 |
+ |
|
| 3 |
+body |
|
| 4 |
+{
|
|
| 5 |
+ background-color: hsl(@hue, 0%, 6.125%); |
|
| 6 |
+ color: hsl(@hue, 0%, 93.75%); |
|
| 7 |
+ |
|
| 8 |
+ font-family: monospace; |
|
| 9 |
+ font-size: 1.25em; |
|
| 10 |
+} |
|
| 11 |
+ |
|
| 12 |
+input,textarea,button |
|
| 13 |
+{
|
|
| 14 |
+ font-size: 1.25em; |
|
| 15 |
+} |
|
| 16 |
+ |
|
| 17 |
+button, input[type="submit"] |
|
| 18 |
+{
|
|
| 19 |
+ background-color: hsl(@hue, 50%, 37.5%); |
|
| 20 |
+ color: hsl(@hue, 0%, 100%); |
|
| 21 |
+ |
|
| 22 |
+ border: none; |
|
| 23 |
+ border-radius: 4px; |
|
| 24 |
+ |
|
| 25 |
+ padding: 8px; |
|
| 26 |
+ |
|
| 27 |
+ cursor: pointer; |
|
| 28 |
+} |
|
| 29 |
+ |
|
| 30 |
+label |
|
| 31 |
+{
|
|
| 32 |
+ display: block; |
|
| 33 |
+ font-size: 1.0em; |
|
| 34 |
+ font-weight: bold; |
|
| 35 |
+ text-transform: capitalize; |
|
| 36 |
+} |
|
| 37 |
+ |
|
| 38 |
+.field |
|
| 39 |
+{
|
|
| 40 |
+ display: block; |
|
| 41 |
+ margin: 16px 0; |
|
| 42 |
+} |
|
| 43 |
+ |
|
| 44 |
+.event > * |
|
| 45 |
+{
|
|
| 46 |
+ display: inline-block; |
|
| 47 |
+} |
|
| 48 |
+ |
|
| 49 |
+.event_time |
|
| 50 |
+{
|
|
| 51 |
+ margin: 0 2px; |
|
| 52 |
+ |
|
| 53 |
+ color: hsl(@hue, 0%, 75%); |
|
| 54 |
+ |
|
| 55 |
+ &:before |
|
| 56 |
+ {
|
|
| 57 |
+ content: "<"; |
|
| 58 |
+ } |
|
| 59 |
+ |
|
| 60 |
+ &:after |
|
| 61 |
+ {
|
|
| 62 |
+ content: ">"; |
|
| 63 |
+ } |
|
| 64 |
+} |
|
| 65 |
+ |
|
| 66 |
+.event_sender |
|
| 67 |
+{
|
|
| 68 |
+ margin: 0 2px; |
|
| 69 |
+ |
|
| 70 |
+ font-weight: bold; |
|
| 71 |
+ |
|
| 72 |
+ &:before |
|
| 73 |
+ {
|
|
| 74 |
+ content: "["; |
|
| 75 |
+ } |
|
| 76 |
+ |
|
| 77 |
+ &:after |
|
| 78 |
+ {
|
|
| 79 |
+ content: "]"; |
|
| 80 |
+ } |
|
| 81 |
+} |
|
| 82 |
+ |
|
| 83 |
+.pane |
|
| 84 |
+{
|
|
| 85 |
+ background-color: hsl(@hue, 0%, 12.5%); |
|
| 86 |
+ |
|
| 87 |
+ height: 75vh; |
|
| 88 |
+ overflow-y: auto; |
|
| 89 |
+ |
|
| 90 |
+ list-style-type: none; |
|
| 91 |
+ |
|
| 92 |
+ border: 1px solid hsl(@hue, 0%, 50%); |
|
| 93 |
+ margin: 4px; |
|
| 94 |
+ padding: 8px; |
|
| 95 |
+ |
|
| 96 |
+ & > li |
|
| 97 |
+ {
|
|
| 98 |
+ margin: 4px 0; |
|
| 99 |
+ } |
|
| 100 |
+} |
|
| 101 |
+ |
|
| 102 |
+#head |
|
| 103 |
+{
|
|
| 104 |
+ text-align: right; |
|
| 105 |
+} |
|
| 106 |
+ |
|
| 107 |
+#middle |
|
| 108 |
+{
|
|
| 109 |
+ display: flex; |
|
| 110 |
+ flex-direction: row; |
|
| 111 |
+ |
|
| 112 |
+ & #history {flex: 4;}
|
|
| 113 |
+ & #users {flex: 1;}
|
|
| 114 |
+} |
|
| 115 |
+ |
|
| 116 |
+#message |
|
| 117 |
+{
|
|
| 118 |
+ // height: 40px; |
|
| 119 |
+ width: 80%; |
|
| 120 |
+ |
|
| 121 |
+ border: none; |
|
| 122 |
+ |
|
| 123 |
+ background-color: hsl(@hue, 0%, 25%); |
|
| 124 |
+ color: hsl(@hue, 0%, 100%); |
|
| 125 |
+ |
|
| 126 |
+ padding: 8px; |
|
| 127 |
+ margin: 4px; |
|
| 128 |
+} |
|
| 129 |
+ |
|
| 130 |
+body |
|
| 131 |
+{
|
|
| 132 |
+ &:not(.offline):not(.checking):not(.online) |
|
| 133 |
+ {
|
|
| 134 |
+ & #connect {display: none;}
|
|
| 135 |
+ & #wait {display: none;}
|
|
| 136 |
+ & #main {display: none;}
|
|
| 137 |
+ } |
|
| 138 |
+ |
|
| 139 |
+ &.offline |
|
| 140 |
+ {
|
|
| 141 |
+ & #connect {}
|
|
| 142 |
+ & #wait {display: none;}
|
|
| 143 |
+ & #main {display: none;}
|
|
| 144 |
+ } |
|
| 145 |
+ |
|
| 146 |
+ &.checking |
|
| 147 |
+ {
|
|
| 148 |
+ & #connect {display: none;}
|
|
| 149 |
+ & #wait {}
|
|
| 150 |
+ & #main {display: none;}
|
|
| 151 |
+ } |
|
| 152 |
+ |
|
| 153 |
+ &.online |
|
| 154 |
+ {
|
|
| 155 |
+ & #connect {display: none;}
|
|
| 156 |
+ & #wait {display: none;}
|
|
| 157 |
+ & #main {}
|
|
| 158 |
+ } |
|
| 159 |
+} |
|
| 160 |
+ |
| ... | ... |
@@ -0,0 +1,52 @@ |
| 1 |
+## directories |
|
| 2 |
+ |
|
| 3 |
+dir_source := source |
|
| 4 |
+dir_build := build |
|
| 5 |
+ |
|
| 6 |
+ |
|
| 7 |
+## commands |
|
| 8 |
+ |
|
| 9 |
+cmd_log := echo "--" |
|
| 10 |
+cmd_tsc := tsc --lib es2015,dom |
|
| 11 |
+cmd_cp := cp |
|
| 12 |
+cmd_lessc := lessc |
|
| 13 |
+cmd_mkdir := mkdir -p |
|
| 14 |
+ |
|
| 15 |
+ |
|
| 16 |
+## rules |
|
| 17 |
+ |
|
| 18 |
+all: structure logic style conf |
|
| 19 |
+.PHONY: all |
|
| 20 |
+ |
|
| 21 |
+structure: ${dir_build}/index.html
|
|
| 22 |
+.PHONY: structure |
|
| 23 |
+ |
|
| 24 |
+${dir_build}/index.html: ${dir_source}/index.html
|
|
| 25 |
+ @ ${cmd_log} "structure …"
|
|
| 26 |
+ @ ${cmd_mkdir} $(dir $@)
|
|
| 27 |
+ @ ${cmd_cp} -ru $^ $@
|
|
| 28 |
+ |
|
| 29 |
+logic: ${dir_build}/logic.js
|
|
| 30 |
+.PHONY: logic |
|
| 31 |
+ |
|
| 32 |
+${dir_build}/logic.js: ${dir_source}/logic.ts
|
|
| 33 |
+ @ ${cmd_log} "logic …"
|
|
| 34 |
+ @ ${cmd_mkdir} $(dir $@)
|
|
| 35 |
+ @ ${cmd_tsc} $^ --outFile $@
|
|
| 36 |
+ |
|
| 37 |
+style: ${dir_build}/style.css
|
|
| 38 |
+.PHONY: style |
|
| 39 |
+ |
|
| 40 |
+${dir_build}/style.css: ${dir_source}/style.less
|
|
| 41 |
+ @ ${cmd_log} "style …"
|
|
| 42 |
+ @ ${cmd_mkdir} $(dir $@)
|
|
| 43 |
+ @ ${cmd_lessc} $^ > $@
|
|
| 44 |
+ |
|
| 45 |
+conf: ${dir_build}/conf.json
|
|
| 46 |
+.PHONY: conf |
|
| 47 |
+ |
|
| 48 |
+${dir_build}/conf.json: ${dir_source}/conf.json
|
|
| 49 |
+ @ ${cmd_log} "conf …"
|
|
| 50 |
+ @ ${cmd_mkdir} $(dir $@)
|
|
| 51 |
+ @ ${cmd_cp} -ru $^ $@
|
|
| 52 |
+ |
|
| 0 | 53 |