found this old project, not sure why it contains rust stuff but I just want to html
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -140,6 +140,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"serde",
|
||||
"tokio",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
||||
@@ -6,4 +6,5 @@ edition = "2024"
|
||||
[dependencies]
|
||||
axum = { version = "0.8.3", features = ["http2", "ws"] }
|
||||
serde = "1.0.219"
|
||||
tokio = { version = "1.44.2", features = ["rt-multi-thread"] }
|
||||
uuid = "1.16.0"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
fn main() {
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
|
||||
7
static/chat.css
Normal file
7
static/chat.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.user-mod {
|
||||
background-color: #70C070;
|
||||
}
|
||||
|
||||
.user-vip {
|
||||
background-color: #ffc6f8;
|
||||
}
|
||||
548
static/chat.html
Normal file
548
static/chat.html
Normal 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>
|
||||
Reference in New Issue
Block a user