chat/index.html
pavel b613aeb9d9
All checks were successful
/ upload (release) Successful in 1m14s
refresh
2026-02-07 17:37:51 +01:00

620 lines
No EOL
20 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>AetherChat</title>
<style>
:root {
--brass: #d4af37;
--copper: #b87333;
--leather: #2b1d0e;
--parchment: #f5e6d3;
--steam: #e0e0e0;
--gear-color: rgba(0, 0, 0, 0.2);
--online-green: #4caf50;
}
* {
box-sizing: border-box;
}
body {
font-family: 'Courier New', Courier, monospace;
background-color: #1a1a1a;
background-image:
radial-gradient(circle at 50% 50%, #2b1d0e 0%, #000 100%);
color: var(--parchment);
margin: 0;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
h1 {
color: var(--brass);
text-shadow: 2px 2px 4px #000;
border-bottom: 2px solid var(--copper);
padding-bottom: 10px;
letter-spacing: 2px;
text-transform: uppercase;
margin-bottom: 20px;
}
#main-container {
width: 100%;
max-width: 1000px;
display: flex;
gap: 15px;
height: 600px;
}
/* Contacts Sidebar */
#contacts-panel {
width: 250px;
background: rgba(43, 29, 14, 0.9);
border: 4px solid var(--copper);
border-radius: 10px;
box-shadow:
0 0 15px var(--brass),
inset 0 0 20px #000;
display: flex;
flex-direction: column;
position: relative;
}
#contacts-header {
padding: 15px;
border-bottom: 2px solid var(--copper);
font-weight: bold;
color: var(--brass);
text-transform: uppercase;
letter-spacing: 1px;
}
#contacts-list {
flex: 1;
overflow-y: auto;
padding: 10px;
scrollbar-width: thin;
scrollbar-color: var(--brass) var(--leather);
}
.contact {
padding: 12px 15px;
margin-bottom: 8px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--copper);
border-radius: 5px;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
transition: all 0.2s;
}
.contact:hover {
background: rgba(212, 175, 55, 0.2);
border-color: var(--brass);
}
.contact.selected {
background: rgba(212, 175, 55, 0.3);
border-color: var(--brass);
box-shadow: 0 0 10px rgba(212, 175, 55, 0.3);
}
.contact .status-dot {
width: 10px;
height: 10px;
background: var(--online-green);
border-radius: 50%;
box-shadow: 0 0 5px var(--online-green);
}
.contact .username {
flex: 1;
font-weight: bold;
}
.contact .unread {
background: var(--copper);
color: var(--parchment);
padding: 2px 8px;
border-radius: 10px;
font-size: 0.8em;
font-weight: bold;
}
/* Chat Panel */
#chat-container {
flex: 1;
background: rgba(43, 29, 14, 0.9);
border: 4px solid var(--copper);
border-radius: 10px;
box-shadow:
0 0 15px var(--brass),
inset 0 0 20px #000;
padding: 20px;
display: flex;
flex-direction: column;
gap: 15px;
position: relative;
}
/* Decorative screws */
#chat-container::before,
#chat-container::after {
content: '';
position: absolute;
width: 10px;
height: 10px;
background: var(--brass);
border-radius: 50%;
box-shadow: 1px 1px 2px #000;
}
#chat-container::before {
top: 10px;
left: 10px;
}
#chat-container::after {
top: 10px;
right: 10px;
}
#chat-header {
padding: 10px;
border-bottom: 2px solid var(--copper);
font-weight: bold;
color: var(--brass);
text-transform: uppercase;
letter-spacing: 1px;
}
#no-chat-selected {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: var(--copper);
font-style: italic;
}
#chat {
flex: 1;
overflow-y: auto;
border: 2px inset var(--copper);
background: rgba(0, 0, 0, 0.3);
padding: 15px;
scrollbar-width: thin;
scrollbar-color: var(--brass) var(--leather);
display: none;
}
#chat.active {
display: block;
}
#chat::-webkit-scrollbar {
width: 10px;
}
#chat::-webkit-scrollbar-track {
background: var(--leather);
}
#chat::-webkit-scrollbar-thumb {
background-color: var(--brass);
border: 1px solid var(--copper);
}
.message {
background-color: var(--parchment);
color: #2b1d0e;
padding: 8px 12px;
margin-bottom: 10px;
border-radius: 4px;
border: 1px solid var(--copper);
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.3);
font-weight: bold;
position: relative;
max-width: 80%;
}
.message.sent {
margin-left: auto;
background: linear-gradient(to bottom, var(--brass), var(--copper));
color: #2b1d0e;
}
.message.received {
margin-right: auto;
}
.message .sender {
font-size: 0.8em;
color: var(--copper);
margin-bottom: 4px;
}
.message.sent .sender {
color: #2b1d0e;
opacity: 0.7;
}
.system-msg {
font-style: italic;
color: var(--brass);
text-align: center;
margin: 5px 0;
font-size: 0.9em;
background: none;
border: none;
box-shadow: none;
max-width: 100%;
}
#controls {
display: none;
gap: 10px;
padding: 10px;
background: #1a1a1a;
border: 1px solid var(--copper);
border-radius: 5px;
}
#controls.active {
display: flex;
}
input[type="text"] {
flex-grow: 1;
padding: 10px;
background: var(--parchment);
border: 2px solid var(--leather);
color: #2b1d0e;
font-family: inherit;
font-weight: bold;
}
button {
background: linear-gradient(to bottom, var(--brass), var(--copper));
border: 2px solid #2b1d0e;
color: #2b1d0e;
padding: 10px 25px;
font-family: inherit;
font-weight: bold;
text-transform: uppercase;
cursor: pointer;
box-shadow: 2px 2px 4px #000;
transition: all 0.1s;
}
button:active {
transform: translate(2px, 2px);
box-shadow: none;
}
button:hover {
filter: brightness(1.2);
}
</style>
</head>
<body>
<h1>&#9881; AetherChat &#9881;</h1>
<div id="main-container">
<!-- Contacts Sidebar -->
<div id="contacts-panel">
<div id="contacts-header">&#9881; Contacts</div>
<div id="contacts-list"></div>
</div>
<!-- Chat Area -->
<div id="chat-container">
<div id="chat-header">Select a contact to start chatting</div>
<div id="no-chat-selected">Click on a contact to begin transmission...</div>
<div id="chat"></div>
<div id="controls">
<input type="text" id="message" placeholder="Transmit payload..." autofocus>
<button onclick="sendMessage()">Engage</button>
</div>
</div>
</div>
<script>
const CLIENT_ID = "xLL8a23JBnBvEarjQUY6Jgd4zLnnqIi2y3oB6laM";
const AUTH_URL = "https://idm.flegr.me/application/o/authorize/";
const REDIRECT_URI = window.location.origin + window.location.pathname;
const contactsList = document.getElementById("contacts-list");
const chatDiv = document.getElementById("chat");
const chatHeader = document.getElementById("chat-header");
const noChatSelected = document.getElementById("no-chat-selected");
const controls = document.getElementById("controls");
const messageInput = document.getElementById("message");
// State
let onlineUsers = [];
let selectedUser = null;
let messageHistory = {}; // { username: [messages...] }
let unreadCounts = {}; // { username: count }
let currentUsername = null; // Will be set from welcome message
// Handle OAuth Callback
async function handleAuth() {
const params = new URLSearchParams(window.location.search);
const code = params.get("code");
if (code) {
// Exchange code for token
try {
const response = await fetch("https://idm.flegr.me/application/o/token/", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: CLIENT_ID,
code: code,
redirect_uri: REDIRECT_URI,
})
});
const data = await response.json();
if (data.access_token) {
localStorage.setItem("access_token", data.access_token);
if (data.refresh_token) {
localStorage.setItem("refresh_token", data.refresh_token);
}
window.history.replaceState({}, document.title, window.location.pathname);
} else {
console.error("Token exchange failed:", data);
}
} catch (e) {
console.error("Error exchanging code:", e);
}
}
let accessToken = localStorage.getItem("access_token");
const refreshToken = localStorage.getItem("refresh_token");
// Check if token is expired or about to expire (within 60 seconds)
if (accessToken && isTokenExpired(accessToken)) {
console.log("Access token expired, attempting refresh...");
if (refreshToken) {
accessToken = await performTokenRefresh(refreshToken);
} else {
accessToken = null;
}
}
if (!accessToken) {
// Redirect to Authentik with offline_access scope to get a refresh_token
const url = `${AUTH_URL}?client_id=${CLIENT_ID}&response_type=code&redirect_uri=${encodeURIComponent(REDIRECT_URI)}&scope=openid+profile+email+offline_access`;
window.location.href = url;
return null;
}
return accessToken;
}
function isTokenExpired(token) {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
const now = Math.floor(Date.now() / 1000);
return payload.exp < (now + 60);
} catch (e) {
return true;
}
}
async function performTokenRefresh(refreshToken) {
try {
const response = await fetch("https://idm.flegr.me/application/o/token/", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "refresh_token",
client_id: CLIENT_ID,
refresh_token: refreshToken,
})
});
const data = await response.json();
if (data.access_token) {
localStorage.setItem("access_token", data.access_token);
if (data.refresh_token) {
localStorage.setItem("refresh_token", data.refresh_token);
}
console.log("Token refreshed successfully");
return data.access_token;
}
console.error("Token refresh failed:", data);
return null;
} catch (e) {
console.error("Error during token refresh:", e);
return null;
}
}
(async () => {
const token = await handleAuth();
if (!token) return;
// Connect to WebSocket with token
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
const ws = new WebSocket(`${proto}://${window.location.host}/ws?token=${token}`);
ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
handleMessage(msg);
} catch (e) {
console.error("Failed to parse message:", e);
// For debugging: show what we received
addSystemMessage("Error parsing: " + event.data);
}
};
ws.onopen = () => {
addSystemMessage("Connected to the Aether-Net.");
};
ws.onclose = () => {
addSystemMessage("Connection severed. Check steam pressure.");
};
function handleMessage(msg) {
switch (msg.type) {
case "user_list":
onlineUsers = msg.users.filter(u => u !== currentUsername);
renderContacts();
break;
case "user_joined":
if (msg.username !== currentUsername && !onlineUsers.includes(msg.username)) {
onlineUsers.push(msg.username);
renderContacts();
}
break;
case "user_left":
onlineUsers = onlineUsers.filter(u => u !== msg.username);
renderContacts();
if (selectedUser === msg.username) {
addSystemMessage(`${msg.username} has disconnected.`);
}
break;
case "private_message":
handlePrivateMessage(msg);
break;
case "system":
// Extract username from welcome message
if (msg.content.startsWith("Welcome, ")) {
currentUsername = msg.content.replace("Welcome, ", "").replace("!", "");
// Re-filter contacts now that we know our username
onlineUsers = onlineUsers.filter(u => u !== currentUsername);
renderContacts();
}
addSystemMessage(msg.content);
break;
}
}
function handlePrivateMessage(msg) {
const otherUser = msg.from === currentUsername ? msg.to : msg.from;
const isSent = msg.from === currentUsername;
// Initialize history if needed
if (!messageHistory[otherUser]) {
messageHistory[otherUser] = [];
}
// Store message
messageHistory[otherUser].push({
from: msg.from,
content: msg.content,
isSent: isSent
});
// If this chat is open, render it
if (selectedUser === otherUser) {
renderChat();
} else if (!isSent) {
// Increment unread count for received messages
unreadCounts[otherUser] = (unreadCounts[otherUser] || 0) + 1;
renderContacts();
}
}
function renderContacts() {
contactsList.innerHTML = "";
onlineUsers.forEach(username => {
const div = document.createElement("div");
div.className = "contact" + (selectedUser === username ? " selected" : "");
div.onclick = () => selectUser(username);
const statusDot = document.createElement("span");
statusDot.className = "status-dot";
const nameSpan = document.createElement("span");
nameSpan.className = "username";
nameSpan.textContent = username;
div.appendChild(statusDot);
div.appendChild(nameSpan);
// Show unread badge
if (unreadCounts[username] && unreadCounts[username] > 0) {
const badge = document.createElement("span");
badge.className = "unread";
badge.textContent = unreadCounts[username];
div.appendChild(badge);
}
contactsList.appendChild(div);
});
}
function selectUser(username) {
selectedUser = username;
unreadCounts[username] = 0; // Clear unread
chatHeader.textContent = `Chat with ${username}`;
noChatSelected.style.display = "none";
chatDiv.classList.add("active");
controls.classList.add("active");
renderContacts();
renderChat();
messageInput.focus();
}
function renderChat() {
chatDiv.innerHTML = "";
const messages = messageHistory[selectedUser] || [];
messages.forEach(msg => {
const div = document.createElement("div");
div.className = "message " + (msg.isSent ? "sent" : "received");
const sender = document.createElement("div");
sender.className = "sender";
sender.textContent = msg.from;
const content = document.createElement("div");
content.textContent = msg.content;
div.appendChild(sender);
div.appendChild(content);
chatDiv.appendChild(div);
});
chatDiv.scrollTop = chatDiv.scrollHeight;
}
function addSystemMessage(text) {
const div = document.createElement("div");
div.textContent = text;
div.className = "message system-msg";
chatDiv.appendChild(div);
chatDiv.scrollTop = chatDiv.scrollHeight;
}
function sendMessage() {
const content = messageInput.value.trim();
if (content && selectedUser) {
ws.send(JSON.stringify({
to: selectedUser,
content: content
}));
messageInput.value = "";
}
}
messageInput.addEventListener("keypress", (e) => {
if (e.key === "Enter") sendMessage();
});
})();
</script>
</body>
</html>