found this old project, not sure why it contains rust stuff but I just want to html

This commit is contained in:
2025-09-23 10:18:37 -05:00
parent ee48ed5d07
commit 4fab74efe5
5 changed files with 559 additions and 1 deletions

1
Cargo.lock generated
View File

@@ -140,6 +140,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"axum", "axum",
"serde", "serde",
"tokio",
"uuid", "uuid",
] ]

View File

@@ -6,4 +6,5 @@ edition = "2024"
[dependencies] [dependencies]
axum = { version = "0.8.3", features = ["http2", "ws"] } axum = { version = "0.8.3", features = ["http2", "ws"] }
serde = "1.0.219" serde = "1.0.219"
tokio = { version = "1.44.2", features = ["rt-multi-thread"] }
uuid = "1.16.0" uuid = "1.16.0"

View File

@@ -1,3 +1,4 @@
fn main() { #[tokio::main]
async fn main() {
println!("Hello, world!"); println!("Hello, world!");
} }

7
static/chat.css Normal file
View File

@@ -0,0 +1,7 @@
.user-mod {
background-color: #70C070;
}
.user-vip {
background-color: #ffc6f8;
}

548
static/chat.html Normal file
View File

@@ -0,0 +1,548 @@
<!DOCTYPE html>
<head>
<script>
channel = "youraveragebo";
//if this isn't set, external emotes can't be loaded until
//after the first chat message is received
channelId = undefined;
messageLimit = 20;
enable7tv = true;
deleteMessages = true;
</script>
<style>
.wrapper,
html,
body {
height: 100%;
margin: 0;
}
.ChatBody {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: flex-end;
height: 100%;
}
.MessageBox {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
}
.Username {
padding-right: 10px;
}
.Message {
padding-bottom: 10px;
}
</style>
<link rel="stylesheet" href="chat.css">
</head>
<body>
<div id="ChatBody" class="ChatBody">
</div>
<template id="Template-MessageBox">
<div class="MessageBox" data-userid="">
<div class="Badges"></div>
<div class="Username">Username</div>
<div class="Message">Message</div>
</div>
</template>
<template id="Template-BadgeBox">
<div class="BadgeBox">
<img class="Badge" />
</div>
</template>
<template id="Template-Emote">
<img class="Emote" />
</template>
<template id="Template-MessageFragment">
<span class="MessageFragment"></span>
</template>
<script>
class Mutex {
constructor() {
this.locked = false;
}
async lock() {
while (this.locked) {
await new Promise(resolve => setTimeout(resolve, 50));
}
this.locked = true;
}
unlock() {
this.locked = false;
}
}
function escapeRegex(text) {
if (typeof RegExp.escape === typeof undefined) {
return text.replace("\\").replace("?", "\\?").replace(")", "\\)")
.replace("[", "\\[").replace("]", "\\]").replace("^", "\\^")
.replace("$", "\\$").replace("|", "\\|");
}
return RegExp.escape(text);
}
class ExternEmote {
constructor(text, url) {
this.text = text;
this.url = url;
// this.reg = new RegExp(`(?<=\\s|^)${RegExp.escape(text)}(?=\\s|$)`);
this.reg = new RegExp(`(?<=\\s|^)${escapeRegex(text)}(?=\\s|$)`);
}
}
const mutex7tv = new Mutex();
var emotes7tv = null;
async function get7tvEmotes(id) {
try {
mutex7tv.lock();
if (emotes7tv !== null) {
return emotes7tv;
}
const url = `https://api.7tv.app/v3/users/twitch/${id}`;
const resp = await fetch(url);
if (!resp.ok) {
throw new Error(`failed to load 7tv: ${resp.status}`);
}
const json = await resp.json();
const emotes = [];
for (const emote of json['emote_set']['emotes']) {
emotes.push(new ExternEmote(emote['name'], `https://cdn.7tv.app/emote/${emote['id']}/1x.webp`));
}
emotes7tv = emotes;
return emotes;
} finally {
mutex7tv.unlock();
}
}
const mutexBttv = new Mutex();
var emotesBttv = null;
async function getBttvEmotes(id) {
try {
mutexBttv.lock();
const url = `https://api.betterttv.net/3/cached/users/twitch/${id}`;
} finally {
mutexBttv.unlock();
}
}
const templateEmote = document.getElementById("Template-Emote");
const templateMessageFrag = document.getElementById("Template-MessageFragment");
function createMessageFragment(text) {
return document.createTextNode(text);
}
async function applyExternalEmotes(text, channelId) {
console.assert(typeof text === 'string', `text expected string, got: ${typeof text}`);
const emotesFor7tv = await get7tvEmotes(channelId);
var textParts = [text];
var emoteParts = [];
for (const emote of emotesFor7tv) {
let newTextParts = [];
let newEmoteParts = [];
for (const ind in emoteParts) {
const spl = textParts[ind].split(emote.reg);
const len = spl.length;
newTextParts = newTextParts.concat(spl);
if (len > 1) {
newEmoteParts = newEmoteParts.concat(Array(len - 1).fill(createEmote(emote.url, emote.text)));
}
newEmoteParts.push(emoteParts[ind]);
}
// console.log(textParts);
const spl = textParts[textParts.length - 1].split(emote.reg);
// console.log(spl);
const len = spl.length;
newTextParts = newTextParts.concat(spl);
if (len > 1) {
newEmoteParts = newEmoteParts.concat(Array(len - 1).fill(createEmote(emote.url, emote.text)));
}
textParts = newTextParts;
emoteParts = newEmoteParts;
console.assert(textParts.length == emoteParts.length + 1, `text: ${textParts.length}, emotes: ${emoteParts.length}`);
}
console.assert(textParts.length == emoteParts.length + 1, `text: ${textParts.length}, emotes: ${emoteParts.length}`);
const res = [];
for (const ind in emoteParts) {
res.push(createMessageFragment(textParts[ind]));
res.push(emoteParts[ind]);
}
res.push(createMessageFragment(textParts.at(-1)));
return res;
}
function createEmote(url, title) {
const img = templateEmote.content.cloneNode(true).firstElementChild;
img.src = url;
img.title = title;
return img;
}
async function createEmotedMessage(message) {
let nodes = [];
var text = message.parameters[1];
const emotes = message.emotes();
emotes.sort((em) => em.end);
// console.log(emotes);
for (const emote of emotes) {
const start = text.substring(0, emote.start);
const end = text.substring(emote.end + 1);
const img = createEmote(emote.url(1), text.substring(emote.start, emote.end+1));
const extern = await applyExternalEmotes(end, message.channelId());
extern.reverse();
nodes = nodes.concat(extern);
// nodes.push(createMessageFragment(end));
nodes.push(img);
text = start;
// text = start + img.outerHTML + end;
}
const extern = await applyExternalEmotes(text, message.channelId());
extern.reverse();
nodes = nodes.concat(extern);
nodes.reverse();
return nodes;
}
function messageTags(s) {
s = s.trimStart("@");
let key = "";
let value = "";
const FINDING_KEY = 0;
const FINDING_VALUE = 1;
const VALUE_ESCAPED = 2;
let state = FINDING_KEY;
tags = {};
for (const c of s) {
switch (state) {
case FINDING_KEY:
if (c === "=") {
state = FINDING_VALUE;
} else if (c === ";") {
state = FINDING_KEY;
tags[key] = "";
key = "";
} else if (c === " ") {
tags[key] = value;
return tags;
} else {
key += c;
}
break;
case FINDING_VALUE:
if (c === "\\") {
state = VALUE_ESCAPED;
} else if (c === ";") {
tags[key] = value;
key = "";
value = "";
state = FINDING_KEY;
} else if (c === " ") {
tags[key] = value;
return tags;
} else if (c === "\n" || c === "\r" || c === "\0") {
throw new Error("found invalid character while parsing message tags");
} else {
value += c;
}
break;
case VALUE_ESCAPED:
// if (c === ":") {
// value += ":"
// state = FINDING_VALUE
// }
if (c === "s") {
value += " "
state = FINDING_VALUE;
} else if (c === "r") {
value += "\r";
state = FINDING_VALUE;
} else if (c === "n") {
value += "\n"
state = FINDING_VALUE;
} else if (c === ";") {
tags[key] = value;
key = "";
value = "";
state = FINDING_KEY;
} else if (c === " ") {
tags[key] = value;
return tags;
} else if (c === "\n" || c === "\r" || c === "\0") {
throw new Error("found invalid character while parsing message tags");
} else {
value += c;
state = FINDING_VALUE;
}
break;
default:
throw new Error("invalid enum while parsing message tags");
}
}
if (key.length > 0) {
tags[key] = value;
}
return tags;
}
function Emote(id, start, end) {
this.id = id.trim();
this.start = parseInt(start);
this.end = parseInt(end);
this.url = (scale) => {
return `https://static-cdn.jtvnw.net/emoticons/v2/${this.id}/static/dark/${scale}.0`
}
}
function parseEmotes(s) {
s = s.trim();
if (s.length === 0) {
return [];
}
res = [];
for (const emote of s.split("/")) {
const spl = emote.split(":");
const id = spl[0];
for (const indices of spl[1].split(",")) {
const splInd = indices.split("-");
res.push(new Emote(id, splInd[0], splInd[1]));
}
}
return res;
}
function Badge(badge, version) {
this.badge = badge;
this.version = version;
}
function parseBadges(s) {
s = s.trim();
res = [];
for (const b of s.split(",")) {
const spl = b.split("/");
res.push(new Badge(spl[0], spl[1]));
}
return res;
}
class IrcMessage {
constructor(s) {
s = s.trim();
this.rawMessage = s;
this.messageTags = undefined;
if (s.startsWith("@")) { //mressage tags
s = s.substring(1);
const split = s.split(" ");
this.messageTags = messageTags(split[0]);
s = split.slice(1).join(" ");
}
this.prefix = undefined;
if (s.startsWith(":")) { //prefix
s = s.substring(1);
const split = s.split(" ");
this.prefix = split[0];
s = split.slice(1).join(" ").trimStart(" ");
}
const splCommand = s.split(" ");
this.messageType = splCommand[0];
this.rawParameters = undefined;
this.parameters = [];
if (splCommand.length >= 2) { //parameters
s = splCommand.slice(1).join(" ");
this.rawParameters = s;
if (s.startsWith(":")) {
this.parameters.push(s.substring(1));
} else {
const spl = s.split(" :");
const splFinal = spl.slice(1).join(" :");
for (let x of spl[0].split(" ")) {
x = x.trim(" ");
if (x.length > 0) {
this.parameters.push(x);
}
}
this.parameters.push(splFinal);
}
}
}
emotes() {
const emotes = this.messageTags["emotes"];
if (typeof emotes === 'string') {
return parseEmotes(emotes);
}
return [];
}
badges() {
const badges = this.messageTags["badges"];
if (typeof badges === 'string') {
return parseBadges(badges);
}
return [];
}
id() {
const id = this.messageTags["id"];
if (typeof id === 'string') {
return id;
}
return "";
}
userid() {
const userid = this.messageTags["user-id"];
if (typeof userid === 'string') {
return userid;
}
return "";
}
mod() {
const mod = this.messageTags["mod"];
return mod === "1";
}
turbo() {
const turbo = this.messageTags["turbo"];
if (typeof turbo === 'string') {
return turbo;
}
return false;
}
admin() {
const admin = this.messageTags["user-type"];
if (typeof admin === 'string') {
return userType === "admin";
}
return false;
}
globalMod() {
const globalMod = this.messageTags["user-type"];
if (typeof globalMod === 'string') {
return userType === "global_mod";
}
return false;
}
staff() {
const staff = this.messageTags["user-type"];
if (typeof staff === 'string') {
return userType === "staff";
}
return false;
}
vip() {
const vip = this.messageTags["vip"];
return typeof vip !== 'undefined';
}
displayName() {
const displayName = this.messageTags['display-name'];
if (typeof displayName === 'string') {
return displayName;
}
return "";
}
bits() {
const bits = this.messageTags['bits'];
if (typeof bits === 'string') {
return parseInt(bits);
}
return 0;
}
color() {
const color = this.messageTags['color'];
if (typeof color === 'string') {
return color;
}
return "black";
}
subscriber() {
const subscriber = this.messageTags['subscriber'];
if (typeof subscriber === 'string') {
return subscriber;
}
return false;
}
channelId() {
const channelId = this.messageTags['room-id'];
if (typeof channelId === 'string') {
return channelId;
}
return null;
}
}
const chatBody = document.getElementById("ChatBody");
const chatTemplate = document.getElementById("Template-MessageBox");
async function pushMessage(message) {
// const text = message.parameters[1];
const newElem = chatTemplate.content.cloneNode(true).firstElementChild;
newElem.id = message.id();
newElem.dataset.userid = message.userid();
const messageElem = newElem.querySelector(".Message");
const emoted = await createEmotedMessage(message);
messageElem.textContent = "";
messageElem.append(...emoted);
const userElem = newElem.querySelector(".Username");
userElem.textContent = message.displayName();
const userColor = message.color();
userElem.style.color = userColor;
if (message.vip()) {
newElem.classList.add("user-vip");
}
if (message.mod()) {
newElem.classList.add("user-mod");
}
if (message.subscriber()) {
newElem.classList.add("user-sub");
}
const chatElems = chatBody.children;
if (chatElems.length >= messageLimit) {
chatElems[0].remove();
}
chatBody.appendChild(newElem);
console.log(message);
}
async function deleteMessage(message) {
if (!deleteMessages) {
return;
}
const target = message.messageTags['target-msg-id'];
if (typeof target === 'string') {
const del = document.getElementById(target);
del.remove();
}
}
async function clearUser(message) {
if (!deleteMessages) {
return;
}
const target = message.messageTags['target-user-id'];
if (typeof target === 'string') {
const dels = document.querySelectorAll(`[data-userid='${target}']`);
for (const del of dels) {
del.remove();
}
}
}
websocket = new WebSocket("wss://irc-ws.chat.twitch.tv:443");
websocket.onopen = (event) => {
websocket.send("PASS password\n");
websocket.send("NICK justinfan1234\n");
websocket.send("CAP REQ :twitch.tv/membership twitch.tv/tags\n");
websocket.send(`JOIN #${channel}\n`);
};
websocket.onmessage = async (event) => {
console.log(event.data);
for (const line of event.data.split('\n')) {
const tr = line.trim();
if (tr.length > 0) {
const message = new IrcMessage(line);
if (message.messageType === "PRIVMSG") {
await pushMessage(message);
} else if (message.messageType === "PING") {
const pong = tr.replace("PING", "PONG");
websocket.send(pong);
} else if (message.messageType === "CLEARMSG") {
await deleteMessage(message);
} else if (message.messageType === "CLEARCHAT") {
await clearUser(message);
}
}
}
};
</script>
</body>