620 lines
No EOL
20 KiB
HTML
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>⚙ AetherChat ⚙</h1>
|
|
<div id="main-container">
|
|
<!-- Contacts Sidebar -->
|
|
<div id="contacts-panel">
|
|
<div id="contacts-header">⚙ 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> |