Update favicon paths and enhance chat functionality with public message fetching

This commit is contained in:
2026-03-07 09:16:57 +11:00
parent 288b48ddab
commit 3d987dced1
2 changed files with 229 additions and 12 deletions

View File

@@ -7,13 +7,13 @@
<meta name="description" content="Interactive terminal interface for Lit.ruv.wtf - A retro-styled command line experience with chat, documentation, and more.">
<!-- Favicons -->
<link rel="icon" type="image/png" sizes="16x16" href="/logos/16px.png">
<link rel="icon" type="image/png" sizes="32x32" href="/logos/32px.png">
<link rel="icon" type="image/png" sizes="48x48" href="/logos/48px.png">
<link rel="icon" type="image/png" sizes="64x64" href="/logos/64px.png">
<link rel="icon" type="image/png" sizes="128x128" href="/logos/128px.png">
<link rel="icon" type="image/png" sizes="256x256" href="/logos/256px.png">
<link rel="apple-touch-icon" sizes="180x180" href="/logos/256px.png">
<link rel="icon" type="image/png" sizes="16x16" href="logos/16px.png">
<link rel="icon" type="image/png" sizes="32x32" href="logos/32px.png">
<link rel="icon" type="image/png" sizes="48x48" href="logos/48px.png">
<link rel="icon" type="image/png" sizes="64x64" href="logos/64px.png">
<link rel="icon" type="image/png" sizes="128x128" href="logos/128px.png">
<link rel="icon" type="image/png" sizes="256x256" href="logos/256px.png">
<link rel="apple-touch-icon" sizes="180x180" href="logos/256px.png">
<!-- Open Graph / Social Media -->
<meta property="og:type" content="website">

View File

@@ -223,10 +223,12 @@ let chatMode = {
displayNames: {} // Cache for display names
};
const MATRIX_PUBLIC_READ_TOKEN = 'syt_Z2VuZXJhbGNoYXQtcmVhZG9ubHk_sikLltUtfbHlztnanEVm_2icJ1o';
// Matrix API helper
const matrixApi = async (endpoint, method = 'GET', body = null) => {
if (!window.matrixSession) return null;
const homeserver = 'https://b.ruv.wtf';
const homeserver = 'https://chat.ruv.wtf';
const url = `${homeserver}/_matrix/client/r0${endpoint}`;
const headers = {
'Content-Type': 'application/json'
@@ -243,6 +245,154 @@ const matrixApi = async (endpoint, method = 'GET', body = null) => {
return response.json();
};
/**
* Fetch the latest public chat message using the read-only public token.
* @param {string} homeserver - Matrix homeserver URL
* @param {string} roomAlias - Room alias (e.g. #generalchat:example.org)
* @returns {Promise<{time: string, sender: string, text: string, timestamp: number} | null>} Last message summary or null
*/
async function fetchPublicLastMessage(homeserver, roomAlias) {
const resolvedAlias = encodeURIComponent(roomAlias);
const authHeaders = {
Authorization: `Bearer ${MATRIX_PUBLIC_READ_TOKEN}`
};
const roomResponse = await fetch(`${homeserver}/_matrix/client/r0/directory/room/${resolvedAlias}`, {
headers: authHeaders
});
if (!roomResponse.ok) {
return null;
}
const roomData = await roomResponse.json();
if (!roomData || !roomData.room_id) {
return null;
}
const resolvedRoomId = encodeURIComponent(roomData.room_id);
const messagesResponse = await fetch(`${homeserver}/_matrix/client/r0/rooms/${resolvedRoomId}/messages?dir=b&limit=30`, {
headers: authHeaders
});
if (!messagesResponse.ok) {
return null;
}
const messagesData = await messagesResponse.json();
if (!messagesData || !Array.isArray(messagesData.chunk)) {
return null;
}
const lastTextEvent = messagesData.chunk.find((event) => {
return event && event.type === 'm.room.message' && event.content && event.content.msgtype === 'm.text';
});
if (!lastTextEvent) {
return null;
}
const timestamp = typeof lastTextEvent.origin_server_ts === 'number'
? new Date(lastTextEvent.origin_server_ts)
: new Date();
const time = timestamp.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
});
const sender = typeof lastTextEvent.sender === 'string'
? lastTextEvent.sender.split(':')[0].replace(/^@/, '')
: 'unknown';
const text = lastTextEvent.content.body || '';
const timestampMs = typeof lastTextEvent.origin_server_ts === 'number'
? lastTextEvent.origin_server_ts
: Date.now();
return { time, sender, text, timestamp: timestampMs };
}
/**
* Fetch Matrix presence for a user using the read-only public token.
* @param {string} homeserver - Matrix homeserver URL
* @param {string} userId - Full Matrix user ID
* @returns {Promise<string | null>} Presence state (online/offline/unavailable) or null when unavailable
*/
async function fetchPublicPresence(homeserver, userId) {
const authHeaders = {
Authorization: `Bearer ${MATRIX_PUBLIC_READ_TOKEN}`
};
const encodedUserId = encodeURIComponent(userId);
const response = await fetch(`${homeserver}/_matrix/client/r0/presence/${encodedUserId}/status`, {
headers: authHeaders
});
if (!response.ok) {
return null;
}
const data = await response.json();
return data && typeof data.presence === 'string' ? data.presence : null;
}
/**
* Format a unix-millisecond timestamp as relative age text.
* @param {number} timestampMs - Epoch timestamp in milliseconds
* @returns {string} Relative time label
*/
function formatTimeAgo(timestampMs) {
if (typeof timestampMs !== 'number') {
return 'unknown';
}
const elapsedSeconds = Math.max(0, Math.floor((Date.now() - timestampMs) / 1000));
if (elapsedSeconds < 60) {
return 'just now';
}
if (elapsedSeconds < 3600) {
return `${Math.floor(elapsedSeconds / 60)}m ago`;
}
if (elapsedSeconds < 86400) {
return `${Math.floor(elapsedSeconds / 3600)}h ago`;
}
return `${Math.floor(elapsedSeconds / 86400)}d ago`;
}
/**
* Write a startup MOTD line with the latest public chat message.
* Uses a short timeout so startup stays responsive.
* @returns {Promise<void>}
*/
async function writeStartupChatMotd() {
try {
const result = await Promise.race([
(async () => {
const [latest, litruvPresence] = await Promise.all([
fetchPublicLastMessage('https://chat.ruv.wtf', '#generalchat:b.ruv.wtf'),
fetchPublicPresence('https://chat.ruv.wtf', '@litruv:b.ruv.wtf')
]);
return { latest, litruvPresence };
})(),
new Promise((resolve) => setTimeout(() => resolve({ latest: null, litruvPresence: null }), 2500))
]);
if (!result.latest && !result.litruvPresence) {
return;
}
const lastMessageAge = result.latest
? formatTimeAgo(result.latest.timestamp)
: 'unknown';
const onlineText = result.litruvPresence === 'online'
? 'online'
: 'offline';
term.writeln(` #generalchat · last message ${lastMessageAge} · @litruv:b.ruv.wtf ${onlineText}`);
term.writeln('');
} catch (error) {
// Ignore MOTD fetch issues to avoid blocking terminal startup.
}
}
// Get display name for a user (with caching)
async function getDisplayName(userId) {
// Check cache first
@@ -310,6 +460,47 @@ async function isDisplayNameTaken(newName) {
}
}
/**
* Determine if a Matrix display name appears unchanged/default.
* @param {string} displayName - Current display name
* @returns {boolean} True when name looks like an auto-generated default
*/
function isDefaultDisplayName(displayName) {
if (!displayName) {
return true;
}
const trimmedName = displayName.trim();
if (!trimmedName) {
return true;
}
const uuidV4Pattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidV4Pattern.test(trimmedName);
}
/**
* Check whether to show a nickname setup hint for the current user.
* @returns {Promise<boolean>} True when the user should be prompted to change nickname
*/
async function shouldShowNicknameHint() {
if (!window.matrixSession || !window.matrixSession.userId) {
return false;
}
try {
const encodedUserId = encodeURIComponent(window.matrixSession.userId);
const profileData = await matrixApi(`/profile/${encodedUserId}/displayname`, 'GET');
const displayName = profileData && typeof profileData.displayname === 'string'
? profileData.displayname
: '';
return isDefaultDisplayName(displayName);
} catch (error) {
return false;
}
}
// Enter chat mode
async function enterChatMode() {
if (!window.matrixSession || !window.matrixSession.roomId) {
@@ -331,6 +522,16 @@ async function enterChatMode() {
// Fetch initial messages
await syncChatMessages();
// Show nickname setup hint in message history when display name is still default
const showNicknameHint = await shouldShowNicknameHint();
if (showNicknameHint) {
const hintTime = new Date().toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
});
term.writeln(`\x1b[90m[${hintTime}]\x1b[0m \x1b[93mSystem:\x1b[0m You are using a default name. Use /nick [name] to change it.`);
}
// Start polling for new messages
chatMode.pollInterval = setInterval(async () => {
await syncChatMessages(true);
@@ -597,7 +798,7 @@ const commands = {
chat: {
description: 'Connect to chat room',
execute: async (args) => {
const homeserver = 'https://b.ruv.wtf';
const homeserver = 'https://chat.ruv.wtf';
const roomAlias = '#generalchat:b.ruv.wtf';
// Generate UUID
@@ -615,8 +816,22 @@ const commands = {
let subcommand = args[0];
let subcommandArgs = args.slice(1);
// Fetch the latest message without logging in
if (subcommand === 'last') {
try {
const latest = await fetchPublicLastMessage(homeserver, roomAlias);
if (!latest) {
return '\r\n Could not read latest message without login (room may not be public).\r\n';
}
return `\r\n #generalchat latest:\r\n [${latest.time}] ${latest.sender}: ${latest.text}\r\n`;
} catch (error) {
return '\r\n Error: Failed to fetch latest message without login.\r\n';
}
}
// If first arg is not a known subcommand and second arg exists, treat as credentials
if (args[0] && args[1] && args[0] !== 'send' && args[0] !== 'disconnect' && isNaN(parseInt(args[0]))) {
if (args[0] && args[1] && args[0] !== 'send' && args[0] !== 'disconnect' && args[0] !== 'last' && isNaN(parseInt(args[0]))) {
username = args[0];
password = args[1];
subcommand = args[2];
@@ -681,7 +896,7 @@ const commands = {
localStorage.setItem('matrix_username', username);
localStorage.setItem('matrix_password', password);
term.writeln(` Registered as: @${username}:b.ruv.wtf\r\n`);
term.writeln(` Registered as: ${regData.user_id}\r\n`);
} else if (regData.errcode === 'M_USER_IN_USE') {
// Username exists, try to login
term.writeln(' Username exists, logging in...\r\n');
@@ -1023,7 +1238,7 @@ async function submitInlineInput() {
}
// Initialize terminal
function init() {
async function init() {
// Display welcome banner with clickable commands
const cols = term.cols;
if (cols >= 78) {
@@ -1041,6 +1256,8 @@ function init() {
term.writeln('');
}
await writeStartupChatMotd();
term.write(promptColored);
// Add inline input after a short delay to ensure terminal is rendered