initial commit

This commit is contained in:
Failure 2026-01-17 01:24:48 -08:00
commit 5fe7ca30fd
8 changed files with 747 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/messages.db
/config.json

110
assets/code.js Normal file
View file

@ -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()
})

58
assets/index.html Normal file
View file

@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CENTCOM</title>
<link rel="stylesheet" href="style.css" />
<script src="code.js" defer></script>
</head>
<body>
<div class="header">
<div class="bgWrap">
<pre class="logo">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;◤◢█
&nbsp;🬕🬂 🬴🬰 ▙▐ ▜▛ 🬕🬂 🬕🬨 🬺🬻 ◢◤◢
&nbsp;▙▄ ▙▄ ▌🬨 ▐▌ ▙▄ ▙▟ ▌▐ ◢◤◢
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;◢◤◢</pre>
</div>
<pre class="licenseHeader">
LICENSE OWNER:&nbsp;
RELEASE:
TRIAL EXPIRY:
COMPCON BUILD: </pre>
<div class="licenseValues">
<div class="yellow">TODD (EVALUATION COPY)</div>
<div class="yellow">234-22b</div>
<div class="syserror">NaN</div>
<div id="glitch">ERR</div>
</div>
<pre class="trailerSlide">
████◤
███◤
██◤
█◤
</pre>
<div class="trailer"></div>
</div>
<div class="resources">
CENTCOM RESOURCES ARE AVAILABLE AT: <a href="https://spaghetti.top/share/DND%20stuff/Lancer/Lancer%20-%20Core%20Book.pdf" target="_blank">HANDBOOK</a>
<a href="https://spaghetti.top/share/DND%20stuff/Lancer/Lancer%20-%20Field%20Guide%20to%20the%20Karrakin%20Trade%20Baronies.pdf" target="_blank">BARONIES</a>
<a href="https://spaghetti.top/share/DND%20stuff/Lancer/" target="_blank">MORE</a>
</div>
<div class="display">
<div class="padder"></div>
<table id="displayTable">
<tbody id="displayBody">
<tr>
<td>*</td>
<td>Please enter your callsign. It will be remembered.</td>
</tr>
</tbody>
</table>
</div>
<div class="cmdinput">
<span>&gt;</span>
<span id="input" contenteditable="true"></span>
</div>
</body>
</html>

218
assets/style.css Normal file
View file

@ -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;
}

BIN
assets/unscii-16.woff Normal file

Binary file not shown.

338
centcom.go Normal file
View file

@ -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))
}

13
config.example.json Normal file
View file

@ -0,0 +1,13 @@
{
"listen": "0.0.0.0:7305",
"webhook": "",
"cef": {
"channel": "#test",
"username": "Centcom",
"password": "Password"
},
"users": [
"COMP/CON",
"Guest"
]
}

8
go.mod Normal file
View file

@ -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
)