commit 5fe7ca30fd15f634322ea5e5d519a54c4f77662f Author: Failure Date: Sat Jan 17 01:24:48 2026 -0800 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84d623e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/messages.db +/config.json \ No newline at end of file diff --git a/assets/code.js b/assets/code.js new file mode 100644 index 0000000..9a21ea1 --- /dev/null +++ b/assets/code.js @@ -0,0 +1,110 @@ +const input = document.getElementById("input") +const display = document.getElementById("displayBody") +const glitch = document.getElementById("glitch") +let lastMessage = undefined +let listener + +const addMessage = (from, content) => { + const tr = document.createElement("tr") + const fromTd = document.createElement("td") + fromTd.innerText = from + fromTd.setAttribute("class", from === "COMP/CON" ? "from compcon" : "from") + + const textTd = document.createElement("td") + textTd.innerText = content + textTd.setAttribute("class", "content") + + tr.append(fromTd, textTd) + + display.appendChild(tr) + tr.scrollIntoView() +} + +const makeConnection = () => { + const ws = new WebSocket((window.location.protocol === "https:" ? "wss://" : "ws://") + window.location.host + "/comm") + + ws.onmessage = (msg) => { + const message = JSON.parse(msg.data) + switch (message.type) { + case "error": + addMessage("*", message.content) + break + case "message": + addMessage(message.user, message.content) + break + case "history": + addMessage(message.user, message.content) + break + case "ready": + display.innerHTML = "" + if (lastMessage) { + localStorage.setItem("username", lastMessage) + } + addMessage("*", message.content) + } + } + + ws.onerror = () => { + setTimeout(makeConnection, 1000) + } + ws.onclose = () => { + setTimeout(makeConnection, 1000) + } + + ws.onopen = () => { + if (localStorage.getItem("username")) { + ws.send(JSON.stringify({ + msg: localStorage.getItem("username") + })) + } + } + listener = ws +} + +makeConnection() + + +input.addEventListener("keydown", (e) => { + if (e.key === "Enter" && !e.shiftKey) { + listener.send(JSON.stringify({ + msg: input.innerText + })) + lastMessage = input.innerText + e.preventDefault() + input.innerText = "" + } +}) + +const fun = (pool) => { + const len = Math.ceil(3 + (Math.random() * 15)) + let s = "" + for (let i = 0; i < len; i++) { + s = s + pool.at(Math.floor(Math.random() * pool.length)) + } + return s +} + +const pools = [ + [1, () => ["LIGHT THE FIRES", "green"],], + [5, () => [fun("◯◉"), "cyan"],], + [10, () => [fun("🮗🮖▓▧🮐█🮙▩▥▦🮕▤🮘▨░"), "white"],], + [12, () => ["🯁🯂🯃🮲🮳", "white"]], + [100, () => [fun(" !\"#$%&\\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþ'"), "gray"]], +] + +const spaz = () => { + const r = Math.random() * 100 + for (const [chance, func] of pools) { + if (r < chance) { + const [text, color] = func() + glitch.style.color = color + glitch.innerText = text + break + } + } +} +spaz() + +window.addEventListener("keydown", () => { + spaz() +}) \ No newline at end of file diff --git a/assets/index.html b/assets/index.html new file mode 100644 index 0000000..6a4ad42 --- /dev/null +++ b/assets/index.html @@ -0,0 +1,58 @@ + + + + + CENTCOM + + + + +
+
+ +
+
+LICENSE OWNER: 
+RELEASE:
+TRIAL EXPIRY:
+COMPCON BUILD: 
+
+
TODD (EVALUATION COPY)
+
234-22b
+
NaN
+
ERR
+
+
+████◤
+███◤
+██◤
+█◤
+
+
+
+
+ CENTCOM RESOURCES ARE AVAILABLE AT: HANDBOOK + BARONIES + MORE + +
+
+
+ + + + + + + +
*Please enter your callsign. It will be remembered.
+
+
+ > + +
+ + \ No newline at end of file diff --git a/assets/style.css b/assets/style.css new file mode 100644 index 0000000..8249499 --- /dev/null +++ b/assets/style.css @@ -0,0 +1,218 @@ +@font-face { + font-family: "Unscii"; + src: url("unscii-16.woff"); +} + +@keyframes converge { + 0% {text-shadow: rgba(0, 255, 255, 0.8361107273433098) 6.0px 0 0, rgba(253, 72, 253, 0.9569224401158167) -5.0px 0 0;} + 1% {text-shadow: rgba(0, 255, 255, 0.8619150110069808) 5.95px 0 0, rgba(253, 72, 253, 0.9579050158210693) -4.95px 0 0;} + 2% {text-shadow: rgba(0, 255, 255, 0.854336177145079) 5.9px 0 0, rgba(253, 72, 253, 0.8426867483104116) -4.9px 0 0;} + 3% {text-shadow: rgba(0, 255, 255, 0.9236866898385188) 5.85px 0 0, rgba(253, 72, 253, 0.821628447103927) -4.85px 0 0;} + 4% {text-shadow: rgba(0, 255, 255, 0.8585164669305969) 5.8px 0 0, rgba(253, 72, 253, 0.9886992839559834) -4.8px 0 0;} + 5% {text-shadow: rgba(0, 255, 255, 0.8219830480816586) 5.75px 0 0, rgba(253, 72, 253, 0.9484480449115142) -4.75px 0 0;} + 6% {text-shadow: rgba(0, 255, 255, 0.8003809923844546) 5.7px 0 0, rgba(253, 72, 253, 0.8487031108723073) -4.7px 0 0;} + 7% {text-shadow: rgba(0, 255, 255, 0.8219776552014745) 5.65px 0 0, rgba(253, 72, 253, 0.9113050127726174) -4.65px 0 0;} + 8% {text-shadow: rgba(0, 255, 255, 0.9183556894624699) 5.6px 0 0, rgba(253, 72, 253, 0.8550701555053435) -4.6px 0 0;} + 9% {text-shadow: rgba(0, 255, 255, 0.805410457966203) 5.55px 0 0, rgba(253, 72, 253, 0.8329987799719435) -4.55px 0 0;} + 10% {text-shadow: rgba(0, 255, 255, 0.8398715925070842) 5.5px 0 0, rgba(253, 72, 253, 0.9482139656058877) -4.5px 0 0;} + 11% {text-shadow: rgba(0, 255, 255, 0.967386840612134) 5.45px 0 0, rgba(253, 72, 253, 0.8766333722942649) -4.45px 0 0;} + 12% {text-shadow: rgba(0, 255, 255, 0.897129710183972) 5.4px 0 0, rgba(253, 72, 253, 0.8057922325743391) -4.4px 0 0;} + 13% {text-shadow: rgba(0, 255, 255, 0.9053727693999201) 5.35px 0 0, rgba(253, 72, 253, 0.9054686470293101) -4.35px 0 0;} + 14% {text-shadow: rgba(0, 255, 255, 0.82040606118395) 5.3px 0 0, rgba(253, 72, 253, 0.8027565893856392) -4.3px 0 0;} + 15% {text-shadow: rgba(0, 255, 255, 0.9088155190766086) 5.25px 0 0, rgba(253, 72, 253, 0.8311901733209509) -4.25px 0 0;} + 16% {text-shadow: rgba(0, 255, 255, 0.830204171315196) 5.2px 0 0, rgba(253, 72, 253, 0.8610423937277876) -4.2px 0 0;} + 17% {text-shadow: rgba(0, 255, 255, 0.9993440956157889) 5.15px 0 0, rgba(253, 72, 253, 0.8785756628537958) -4.15px 0 0;} + 18% {text-shadow: rgba(0, 255, 255, 0.8440951076104983) 5.1px 0 0, rgba(253, 72, 253, 0.8367897342791965) -4.1px 0 0;} + 19% {text-shadow: rgba(0, 255, 255, 0.8794669548632236) 5.05px 0 0, rgba(253, 72, 253, 0.8658150502454226) -4.05px 0 0;} + 20% {text-shadow: rgba(0, 255, 255, 0.8296208755041845) 5.0px 0 0, rgba(253, 72, 253, 0.8508951332041956) -4.0px 0 0;} + 21% {text-shadow: rgba(0, 255, 255, 0.9374895228216961) 4.95px 0 0, rgba(253, 72, 253, 0.9407681729537316) -3.95px 0 0;} + 22% {text-shadow: rgba(0, 255, 255, 0.9472441467097896) 4.9px 0 0, rgba(253, 72, 253, 0.8108230906405419) -3.9px 0 0;} + 23% {text-shadow: rgba(0, 255, 255, 0.8841608212385841) 4.85px 0 0, rgba(253, 72, 253, 0.9467742323921511) -3.8499999999999996px 0 0;} + 24% {text-shadow: rgba(0, 255, 255, 0.9857281595178842) 4.8px 0 0, rgba(253, 72, 253, 0.983746771430065) -3.8px 0 0;} + 25% {text-shadow: rgba(0, 255, 255, 0.8901276017385813) 4.75px 0 0, rgba(253, 72, 253, 0.9006720333978879) -3.75px 0 0;} + 26% {text-shadow: rgba(0, 255, 255, 0.8420623794073254) 4.7px 0 0, rgba(253, 72, 253, 0.841030258954916) -3.7px 0 0;} + 27% {text-shadow: rgba(0, 255, 255, 0.80500475794336) 4.65px 0 0, rgba(253, 72, 253, 0.8719997374695452) -3.65px 0 0;} + 28% {text-shadow: rgba(0, 255, 255, 0.8263800795911168) 4.6px 0 0, rgba(253, 72, 253, 0.9560571433379201) -3.5999999999999996px 0 0;} + 29% {text-shadow: rgba(0, 255, 255, 0.9522238613350336) 4.55px 0 0, rgba(253, 72, 253, 0.8677264298768816) -3.55px 0 0;} + 30% {text-shadow: rgba(0, 255, 255, 0.8633010908515628) 4.5px 0 0, rgba(253, 72, 253, 0.8370655185763168) -3.5px 0 0;} + 31% {text-shadow: rgba(0, 255, 255, 0.833147207138388) 4.45px 0 0, rgba(253, 72, 253, 0.9032649254449511) -3.45px 0 0;} + 32% {text-shadow: rgba(0, 255, 255, 0.911593155082442) 4.4px 0 0, rgba(253, 72, 253, 0.9704127292549265) -3.4px 0 0;} + 33% {text-shadow: rgba(0, 255, 255, 0.8730770169688556) 4.35px 0 0, rgba(253, 72, 253, 0.8418315619280776) -3.3499999999999996px 0 0;} + 34% {text-shadow: rgba(0, 255, 255, 0.8254344860492141) 4.3px 0 0, rgba(253, 72, 253, 0.945122258350945) -3.3px 0 0;} + 35% {text-shadow: rgba(0, 255, 255, 0.8994536950486153) 4.25px 0 0, rgba(253, 72, 253, 0.9635174671899864) -3.25px 0 0;} + 36% {text-shadow: rgba(0, 255, 255, 0.8059786830820536) 4.2px 0 0, rgba(253, 72, 253, 0.8107823840459341) -3.2px 0 0;} + 37% {text-shadow: rgba(0, 255, 255, 0.8356685072101536) 4.15px 0 0, rgba(253, 72, 253, 0.8490396342365222) -3.15px 0 0;} + 38% {text-shadow: rgba(0, 255, 255, 0.870306753026597) 4.1px 0 0, rgba(253, 72, 253, 0.8684046884770916) -3.0999999999999996px 0 0;} + 39% {text-shadow: rgba(0, 255, 255, 0.829512957206739) 4.05px 0 0, rgba(253, 72, 253, 0.920924832912171) -3.05px 0 0;} + 40% {text-shadow: rgba(0, 255, 255, 0.981042227568678) 4.0px 0 0, rgba(253, 72, 253, 0.8478604614587707) -3.0px 0 0;} + 41% {text-shadow: rgba(0, 255, 255, 0.9028953642095886) 3.9499999999999997px 0 0, rgba(253, 72, 253, 0.9836375058993726) -2.9499999999999997px 0 0;} + 42% {text-shadow: rgba(0, 255, 255, 0.8298506090024876) 3.9px 0 0, rgba(253, 72, 253, 0.8122960604729693) -2.9px 0 0;} + 43% {text-shadow: rgba(0, 255, 255, 0.9574737190797513) 3.85px 0 0, rgba(253, 72, 253, 0.8207214451650879) -2.85px 0 0;} + 44% {text-shadow: rgba(0, 255, 255, 0.9912354008852822) 3.8px 0 0, rgba(253, 72, 253, 0.9258944470696935) -2.8px 0 0;} + 45% {text-shadow: rgba(0, 255, 255, 0.964617559155446) 3.75px 0 0, rgba(253, 72, 253, 0.8413653094926936) -2.75px 0 0;} + 46% {text-shadow: rgba(0, 255, 255, 0.8556860472070148) 3.6999999999999997px 0 0, rgba(253, 72, 253, 0.8039703790715326) -2.6999999999999997px 0 0;} + 47% {text-shadow: rgba(0, 255, 255, 0.9786225078037258) 3.65px 0 0, rgba(253, 72, 253, 0.8532601491001363) -2.65px 0 0;} + 48% {text-shadow: rgba(0, 255, 255, 0.8136939294439446) 3.5999999999999996px 0 0, rgba(253, 72, 253, 0.9066287030351223) -2.5999999999999996px 0 0;} + 49% {text-shadow: rgba(0, 255, 255, 0.9115638029179485) 3.55px 0 0, rgba(253, 72, 253, 0.8155810075227841) -2.55px 0 0;} + 50% {text-shadow: rgba(0, 255, 255, 0.8486201913333553) 3.5px 0 0, rgba(253, 72, 253, 0.8457649243118975) -2.5px 0 0;} + 51% {text-shadow: rgba(0, 255, 255, 0.8414566201644388) 3.4499999999999997px 0 0, rgba(253, 72, 253, 0.9361224309069724) -2.4499999999999997px 0 0;} + 52% {text-shadow: rgba(0, 255, 255, 0.8359647720401094) 3.4px 0 0, rgba(253, 72, 253, 0.8149059343179613) -2.4px 0 0;} + 53% {text-shadow: rgba(0, 255, 255, 0.9116885963738237) 3.3499999999999996px 0 0, rgba(253, 72, 253, 0.8478810737968121) -2.3499999999999996px 0 0;} + 54% {text-shadow: rgba(0, 255, 255, 0.9156512221006986) 3.3px 0 0, rgba(253, 72, 253, 0.8255903859323139) -2.3px 0 0;} + 55% {text-shadow: rgba(0, 255, 255, 0.8153528842487034) 3.25px 0 0, rgba(253, 72, 253, 0.8994146728664133) -2.25px 0 0;} + 56% {text-shadow: rgba(0, 255, 255, 0.8635217426110132) 3.1999999999999997px 0 0, rgba(253, 72, 253, 0.8100068480800539) -2.1999999999999997px 0 0;} + 57% {text-shadow: rgba(0, 255, 255, 0.8694106443899178) 3.15px 0 0, rgba(253, 72, 253, 0.8650065669920302) -2.15px 0 0;} + 58% {text-shadow: rgba(0, 255, 255, 0.9205282486640333) 3.0999999999999996px 0 0, rgba(253, 72, 253, 0.9146371017699718) -2.0999999999999996px 0 0;} + 59% {text-shadow: rgba(0, 255, 255, 0.9216335826736727) 3.05px 0 0, rgba(253, 72, 253, 0.9535489883131107) -2.05px 0 0;} + 60% {text-shadow: rgba(0, 255, 255, 0.8678550560876669) 3.0px 0 0, rgba(253, 72, 253, 0.9922895844184012) -2.0px 0 0;} + 61% {text-shadow: rgba(0, 255, 255, 0.8047208304855602) 2.9499999999999997px 0 0, rgba(253, 72, 253, 0.8072053478662209) -1.9499999999999997px 0 0;} + 62% {text-shadow: rgba(0, 255, 255, 0.8742226362552165) 2.9px 0 0, rgba(253, 72, 253, 0.9882488598219596) -1.9px 0 0;} + 63% {text-shadow: rgba(0, 255, 255, 0.8187279699943009) 2.8499999999999996px 0 0, rgba(253, 72, 253, 0.847193828631679) -1.8499999999999996px 0 0;} + 64% {text-shadow: rgba(0, 255, 255, 0.915873594484333) 2.8px 0 0, rgba(253, 72, 253, 0.9951780311982585) -1.7999999999999998px 0 0;} + 65% {text-shadow: rgba(0, 255, 255, 0.9091957667405316) 2.75px 0 0, rgba(253, 72, 253, 0.9785125139107118) -1.75px 0 0;} + 66% {text-shadow: rgba(0, 255, 255, 0.9732074805184896) 2.6999999999999997px 0 0, rgba(253, 72, 253, 0.8549161753597928) -1.6999999999999997px 0 0;} + 67% {text-shadow: rgba(0, 255, 255, 0.9942565533650556) 2.65px 0 0, rgba(253, 72, 253, 0.8763648231855105) -1.65px 0 0;} + 68% {text-shadow: rgba(0, 255, 255, 0.9965568662919517) 2.5999999999999996px 0 0, rgba(253, 72, 253, 0.9761753235501034) -1.5999999999999996px 0 0;} + 69% {text-shadow: rgba(0, 255, 255, 0.955938836304943) 2.55px 0 0, rgba(253, 72, 253, 0.9003910147622634) -1.5499999999999998px 0 0;} + 70% {text-shadow: rgba(0, 255, 255, 0.9485273044201737) 2.5px 0 0, rgba(253, 72, 253, 0.9807777183990174) -1.5px 0 0;} + 71% {text-shadow: rgba(0, 255, 255, 0.9674912652650286) 2.4499999999999997px 0 0, rgba(253, 72, 253, 0.9022210932456923) -1.4499999999999997px 0 0;} + 72% {text-shadow: rgba(0, 255, 255, 0.9717372249876183) 2.4px 0 0, rgba(253, 72, 253, 0.8765635698101958) -1.4px 0 0;} + 73% {text-shadow: rgba(0, 255, 255, 0.9645985514947042) 2.3499999999999996px 0 0, rgba(253, 72, 253, 0.8633552189493626) -1.3499999999999996px 0 0;} + 74% {text-shadow: rgba(0, 255, 255, 0.9930937453603206) 2.3px 0 0, rgba(253, 72, 253, 0.8051761331103191) -1.2999999999999998px 0 0;} + 75% {text-shadow: rgba(0, 255, 255, 0.9967694300664716) 2.25px 0 0, rgba(253, 72, 253, 0.8850713876932538) -1.25px 0 0;} + 76% {text-shadow: rgba(0, 255, 255, 0.857931878599814) 2.1999999999999997px 0 0, rgba(253, 72, 253, 0.9484567518701348) -1.1999999999999997px 0 0;} + 77% {text-shadow: rgba(0, 255, 255, 0.8065303268761479) 2.15px 0 0, rgba(253, 72, 253, 0.9861716026231071) -1.15px 0 0;} + 78% {text-shadow: rgba(0, 255, 255, 0.8797138323979175) 2.0999999999999996px 0 0, rgba(253, 72, 253, 0.9327605217362944) -1.0999999999999996px 0 0;} + 79% {text-shadow: rgba(0, 255, 255, 0.8064192594575359) 2.05px 0 0, rgba(253, 72, 253, 0.9752686628797701) -1.0499999999999998px 0 0;} + 80% {text-shadow: rgba(0, 255, 255, 0.8578695134814783) 2.0px 0 0, rgba(253, 72, 253, 0.9597123400872934) -1.0px 0 0;} + 81% {text-shadow: rgba(0, 255, 255, 0.8465689527222937) 1.9500000000000002px 0 0, rgba(253, 72, 253, 0.9362544272288554) -0.9500000000000002px 0 0;} + 82% {text-shadow: rgba(0, 255, 255, 0.9600859379791397) 1.8999999999999995px 0 0, rgba(253, 72, 253, 0.9445462866232998) -0.8999999999999995px 0 0;} + 83% {text-shadow: rgba(0, 255, 255, 0.9741472675714937) 1.8499999999999996px 0 0, rgba(253, 72, 253, 0.9979583491429695) -0.8499999999999996px 0 0;} + 84% {text-shadow: rgba(0, 255, 255, 0.8522887397519873) 1.7999999999999998px 0 0, rgba(253, 72, 253, 0.8642087114124946) -0.7999999999999998px 0 0;} + 85% {text-shadow: rgba(0, 255, 255, 0.8935576284804552) 1.75px 0 0, rgba(253, 72, 253, 0.9995037786832606) -0.75px 0 0;} + 86% {text-shadow: rgba(0, 255, 255, 0.9591551924825271) 1.7000000000000002px 0 0, rgba(253, 72, 253, 0.8895031911672213) -0.7000000000000002px 0 0;} + 87% {text-shadow: rgba(0, 255, 255, 0.847221976351772) 1.6499999999999995px 0 0, rgba(253, 72, 253, 0.8897083979984515) -0.6499999999999995px 0 0;} + 88% {text-shadow: rgba(0, 255, 255, 0.982831513382503) 1.5999999999999996px 0 0, rgba(253, 72, 253, 0.8938824711432813) -0.5999999999999996px 0 0;} + 89% {text-shadow: rgba(0, 255, 255, 0.8543613277389507) 1.5499999999999998px 0 0, rgba(253, 72, 253, 0.9537350423795994) -0.5499999999999998px 0 0;} + 90% {text-shadow: rgba(0, 255, 255, 0.893017776047433) 1.5px 0 0, rgba(253, 72, 253, 0.9956040213561244) -0.5px 0 0;} + 91% {text-shadow: rgba(0, 255, 255, 0.9679434395903915) 1.4500000000000002px 0 0, rgba(253, 72, 253, 0.8782724557074016) -0.4500000000000002px 0 0;} + 92% {text-shadow: rgba(0, 255, 255, 0.8726705168167862) 1.3999999999999995px 0 0, rgba(253, 72, 253, 0.8826217500327997) -0.39999999999999947px 0 0;} + 93% {text-shadow: rgba(0, 255, 255, 0.9966087235235863) 1.3499999999999996px 0 0, rgba(253, 72, 253, 0.8018138119722085) -0.34999999999999964px 0 0;} + 94% {text-shadow: rgba(0, 255, 255, 0.9491747745601509) 1.2999999999999998px 0 0, rgba(253, 72, 253, 0.829179137123354) -0.2999999999999998px 0 0;} + 95% {text-shadow: rgba(0, 255, 255, 0.9127441956297994) 1.25px 0 0, rgba(253, 72, 253, 0.8577438912497287) -0.25px 0 0;} + 96% {text-shadow: rgba(0, 255, 255, 0.9301915605785521) 1.1999999999999993px 0 0, rgba(253, 72, 253, 0.8271745353254621) -0.1999999999999993px 0 0;} + 97% {text-shadow: rgba(0, 255, 255, 0.8860706345525079) 1.1499999999999995px 0 0, rgba(253, 72, 253, 0.9882951178389682) -0.14999999999999947px 0 0;} + 98% {text-shadow: rgba(0, 255, 255, 0.8376062092672816) 1.0999999999999996px 0 0, rgba(253, 72, 253, 0.9615234693412842) -0.09999999999999964px 0 0;} + 99% {text-shadow: rgba(0, 255, 255, 0.8046174532158784) 1.0499999999999998px 0 0, rgba(253, 72, 253, 0.8547331608286676) -0.04999999999999982px 0 0;} + 100% { + text-shadow: rgba(0, 255, 255, 0.8492590734360018) 1.0px 0 0, rgba(253, 72, 253, 0.8562549485674519) -0.0px 0 0; + color: rgba(253, 72, 253); + } +} + +* { + box-sizing: border-box; +} + +html, +body { + font-family: Unscii, monospace; + font-size: 16px; + background: #000; + margin: 0; + color: red; + padding: 0; + line-height: 16px; +} + +pre { + font-family: Unscii, monospace; + padding: 0; + margin: 0; +} + + +.header { + display: flex; +} + +.bgWrap { + background: black; +} + +.yellow { + color: #e3f44d; +} + +.logo, .trailerSlide { + background: #1e992a; + color: black; + display: inline; +} + +.trailer { + height: 64px; + background: #1e992a; + flex-grow: 1; +} +@keyframes blink { + 50% { + opacity: 0; + } +} +.syserror { + color: red; + animation: 1s blink step-start infinite; +} + +.display { + height: 256px; + overflow: scroll; +} + +#displayTable { + display: flex; + flex-direction: column; +} + +.cmdinput { + color: white; + display: flex; + flex-direction: row; +} +.cmdinput > span:first-child { + width: 16px; +} + +#input { + flex-grow: 1; + outline: 0; +} + +a, a:link, a:visited { + color: #8686ff; + text-decoration: none; + text-underline: none; +} + +a:link:before { + content: "▹"; +} + +.padder { + height: 230px; +} + +.from { + color: #ff8f02; + text-align: right; + border-right: transparent 8px solid; +} + +.content { + color: white; +} + +.compcon { + animation: converge 1.4s ease-out forwards; +} \ No newline at end of file diff --git a/assets/unscii-16.woff b/assets/unscii-16.woff new file mode 100644 index 0000000..ffc98be Binary files /dev/null and b/assets/unscii-16.woff differ diff --git a/centcom.go b/centcom.go new file mode 100644 index 0000000..9808b39 --- /dev/null +++ b/centcom.go @@ -0,0 +1,338 @@ +package main + +import ( + "bytes" + "database/sql" + "encoding/base64" + "encoding/json" + "flag" + "fmt" + "log" + "net/http" + "os" + "slices" + "strconv" + "strings" + "sync" + "time" + + "github.com/gorilla/websocket" + _ "github.com/mattn/go-sqlite3" +) + +const CONFIGFILE = "./config.json" + +type Client struct { + Conn *websocket.Conn + Name string + Mu sync.Mutex +} + +type Server struct { + Mu sync.Mutex + Clients []*Client + Db *sql.DB + Cef *CEF +} + +type CEF struct { + Conn *websocket.Conn + FromServer chan string + ToServer chan string + Kill chan error +} + +type Message struct { + Type string `json:"type"` + User string `json:"user"` + Content string `json:"content"` +} + +type Config struct { + Listen string `json:"listen"` + Webhook string `json:"webhook"` + Cef struct { + Channel string `json:"channel"` + Username string `json:"username"` + Password string `json:"password"` + } `json:"cef"` + Users []string `json:"users"` +} + +var config Config +var server = Server{ + Clients: make([]*Client, 0), +} + +func (s *Server) KillClient(client *Client) { + s.Mu.Lock() + defer s.Mu.Unlock() + for i, _ := range s.Clients { + if client == s.Clients[i] { + s.Clients = append(s.Clients[:i], s.Clients[i+1:]...) + return + } + } +} + +func (c *Client) Send(struc any) { + c.Mu.Lock() + defer c.Mu.Unlock() + err := c.Conn.WriteJSON(struc) + if err != nil { + server.KillClient(c) + } +} + +func Webhook(message Message) { + jsonValue, _ := json.Marshal(struct { + Content string `json:"content"` + Username string `json:"username"` + }{ + Content: message.Content, + Username: message.User, + }) + _, err := http.Post(config.Webhook, "application/json", bytes.NewBuffer(jsonValue)) + if err != nil { + log.Println("webhook", err) + } +} + +func (s *Server) BroadcastMessage(from string, content string) { + message := Message{ + Type: "message", + User: from, + Content: content, + } + for _, c := range s.Clients { + c.Send(message) + } + tx, err := s.Db.Begin() + stmt, err := tx.Prepare("insert into messages(`type`, `user`, `content`) values(?, ?, ?)") + if err != nil { + log.Fatal(err) + return + } + _, err = stmt.Exec("message", from, content) + if err != nil { + log.Println(stmt) + return + } + tx.Commit() + go Webhook(message) + go s.CEFSend(message) +} + +func (s *Server) CEFSend(message Message) { + msg := fmt.Sprintf("NPC %s %s :%s", config.Cef.Channel, message.User, message.Content) + select { + case s.Cef.ToServer <- msg: + default: + } +} + +func (s *Server) SendHistory(client *Client) { + // Blast + rows, err := s.Db.Query("SELECT `user`, `content` FROM messages ORDER BY rowid ASC") + if err != nil { + log.Println(err) + return + } + defer rows.Close() + var user, content string + + for rows.Next() { + err = rows.Scan(&user, &content) + if err != nil { + log.Panicln(err) + } + client.Send(Message{ + Type: "history", + User: user, + Content: content, + }) + + } + + client.Send(Message{ + Type: "ready", + User: strconv.Itoa(len(server.Clients)), + Content: "Welcome back, " + client.Name, + }) + +} + +func loadConfig() { + data, err := os.ReadFile(CONFIGFILE) + if err != nil { + log.Panicln("Could not read config, ", err) + } + err = json.Unmarshal(data, &config) + if err != nil { + log.Panicln("Could not load config, ", err) + } +} + +func watchConfig() { + initialStat, _ := os.Stat(CONFIGFILE) + + for { + stat, _ := os.Stat(CONFIGFILE) + + if stat.Size() != initialStat.Size() || stat.ModTime() != initialStat.ModTime() { + loadConfig() + } + + time.Sleep(1 * time.Second) + } +} + +func (c *CEF) Send(m string) { + log.Println("[CEF-O]", m) + err := c.Conn.WriteMessage(websocket.TextMessage, []byte(m)) + if err != nil { + log.Println("[CEF-S]", err) + c.Kill <- err + return + } +} + +func (c *CEF) Loop() { + for { + _, message, err := c.Conn.ReadMessage() + log.Println("[CEF]", string(message), err) + if err != nil { + c.Kill <- err + return + } + c.FromServer <- string(message) + } +} + +func BasicCef() { + c, _, err := websocket.DefaultDialer.Dial("wss://cef.icu/chat", nil) + defer c.Close() + if err != nil { + return + } + cef := &CEF{ + Conn: c, + FromServer: make(chan string), + ToServer: make(chan string), + Kill: make(chan error), + } + defer close(cef.FromServer) + defer close(cef.ToServer) + defer close(cef.Kill) + server.Cef = cef + cef.Send("CAP REQ :account-notify account-tag away-notify batch chghost cef/extended-names draft/chathistory draft/multiline draft/event-playback draft/relaymsg echo-message extended-join invite-notify labeled-response message-tags multi-prefix sasl server-time setname userhost-in-names") + cef.Send("NICK " + config.Cef.Username) + cef.Send("USER " + config.Cef.Username + " . . :cool dude") + cef.Send("AUTHENTICATE PLAIN") + auth := fmt.Sprintf("%s\000%s\000%s", config.Cef.Username, config.Cef.Username, config.Cef.Password) + cef.Send("AUTHENTICATE " + base64.StdEncoding.EncodeToString([]byte(auth))) + cef.Send("CAP END") + // cef.Send("JOIN " + config.Cef.Channel) + + go cef.Loop() + for { + select { + case msg := <-cef.FromServer: + split := strings.Split(msg, " ") + if len(split) > 2 { + if split[1] == "PING" { + cef.Send("PONG " + split[2]) + } + } + case msg := <-cef.ToServer: + cef.Send(msg) + case killError := <-cef.Kill: + log.Println("[CEF]", killError) + break + } + } + +} + +var upgrader = websocket.Upgrader{} // use default options + +func comm(w http.ResponseWriter, r *http.Request) { + c, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Print("upgrade:", err) + return + } + defer c.Close() + var client = &Client{ + Conn: c, + Name: "", + } + setup := false + var inbound struct { + Msg string `json:"msg"` + } + for { + _, message, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + err = json.Unmarshal(message, &inbound) + if err != nil { + log.Println("unmarshal", err) + return + } + log.Printf("recv: %s", message) + if !setup { + if slices.Contains(config.Users, inbound.Msg) { + server.Mu.Lock() + server.Clients = append(server.Clients, client) + server.Mu.Unlock() + defer server.KillClient(client) + setup = true + client.Name = inbound.Msg + server.SendHistory(client) + } else { + err := c.WriteJSON(Message{ + Type: "error", + User: "", + Content: "LANCER NOT FOUND", + }) + if err != nil { + log.Println("writejson", err) + return + } + } + } else { + server.BroadcastMessage(client.Name, inbound.Msg) + } + } +} + +func CefDaemon() { + for { + BasicCef() + time.Sleep(60) + } +} + +func main() { + loadConfig() + go watchConfig() + var addr = flag.String("addr", config.Listen, "http service address") + db, err := sql.Open("sqlite3", "./messages.db") + if err != nil { + log.Fatal(err) + } + server.Db = db + db.Exec("CREATE TABLE messages(`type` text, `user` text, `content` text)") + defer db.Close() + go CefDaemon() + + flag.Parse() + log.SetFlags(0) + fs := http.FileServer(http.Dir("./assets")) + http.Handle("/", fs) + http.HandleFunc("/comm", comm) + log.Fatal(http.ListenAndServe(*addr, nil)) +} diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..6489c48 --- /dev/null +++ b/config.example.json @@ -0,0 +1,13 @@ +{ + "listen": "0.0.0.0:7305", + "webhook": "", + "cef": { + "channel": "#test", + "username": "Centcom", + "password": "Password" + }, + "users": [ + "COMP/CON", + "Guest" + ] +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6a92e87 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module CENTCOM + +go 1.24 + +require ( + github.com/gorilla/websocket v1.5.3 + github.com/mattn/go-sqlite3 v1.14.33 +)