mirror of
https://github.com/litruv/lit.ruv.wtf.git
synced 2026-07-24 10:46:03 +10:00
Update favicon paths and enhance chat functionality with public message fetching
This commit is contained in:
@@ -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.">
|
<meta name="description" content="Interactive terminal interface for Lit.ruv.wtf - A retro-styled command line experience with chat, documentation, and more.">
|
||||||
|
|
||||||
<!-- Favicons -->
|
<!-- Favicons -->
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/logos/16px.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="32x32" href="logos/32px.png">
|
||||||
<link rel="icon" type="image/png" sizes="48x48" href="/logos/48px.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="64x64" href="logos/64px.png">
|
||||||
<link rel="icon" type="image/png" sizes="128x128" href="/logos/128px.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="icon" type="image/png" sizes="256x256" href="logos/256px.png">
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/logos/256px.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="logos/256px.png">
|
||||||
|
|
||||||
<!-- Open Graph / Social Media -->
|
<!-- Open Graph / Social Media -->
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
|
|||||||
@@ -223,10 +223,12 @@ let chatMode = {
|
|||||||
displayNames: {} // Cache for display names
|
displayNames: {} // Cache for display names
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MATRIX_PUBLIC_READ_TOKEN = 'syt_Z2VuZXJhbGNoYXQtcmVhZG9ubHk_sikLltUtfbHlztnanEVm_2icJ1o';
|
||||||
|
|
||||||
// Matrix API helper
|
// Matrix API helper
|
||||||
const matrixApi = async (endpoint, method = 'GET', body = null) => {
|
const matrixApi = async (endpoint, method = 'GET', body = null) => {
|
||||||
if (!window.matrixSession) return 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 url = `${homeserver}/_matrix/client/r0${endpoint}`;
|
||||||
const headers = {
|
const headers = {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -243,6 +245,154 @@ const matrixApi = async (endpoint, method = 'GET', body = null) => {
|
|||||||
return response.json();
|
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)
|
// Get display name for a user (with caching)
|
||||||
async function getDisplayName(userId) {
|
async function getDisplayName(userId) {
|
||||||
// Check cache first
|
// 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
|
// Enter chat mode
|
||||||
async function enterChatMode() {
|
async function enterChatMode() {
|
||||||
if (!window.matrixSession || !window.matrixSession.roomId) {
|
if (!window.matrixSession || !window.matrixSession.roomId) {
|
||||||
@@ -331,6 +522,16 @@ async function enterChatMode() {
|
|||||||
// Fetch initial messages
|
// Fetch initial messages
|
||||||
await syncChatMessages();
|
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
|
// Start polling for new messages
|
||||||
chatMode.pollInterval = setInterval(async () => {
|
chatMode.pollInterval = setInterval(async () => {
|
||||||
await syncChatMessages(true);
|
await syncChatMessages(true);
|
||||||
@@ -597,7 +798,7 @@ const commands = {
|
|||||||
chat: {
|
chat: {
|
||||||
description: 'Connect to chat room',
|
description: 'Connect to chat room',
|
||||||
execute: async (args) => {
|
execute: async (args) => {
|
||||||
const homeserver = 'https://b.ruv.wtf';
|
const homeserver = 'https://chat.ruv.wtf';
|
||||||
const roomAlias = '#generalchat:b.ruv.wtf';
|
const roomAlias = '#generalchat:b.ruv.wtf';
|
||||||
|
|
||||||
// Generate UUID
|
// Generate UUID
|
||||||
@@ -615,8 +816,22 @@ const commands = {
|
|||||||
let subcommand = args[0];
|
let subcommand = args[0];
|
||||||
let subcommandArgs = args.slice(1);
|
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 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];
|
username = args[0];
|
||||||
password = args[1];
|
password = args[1];
|
||||||
subcommand = args[2];
|
subcommand = args[2];
|
||||||
@@ -681,7 +896,7 @@ const commands = {
|
|||||||
localStorage.setItem('matrix_username', username);
|
localStorage.setItem('matrix_username', username);
|
||||||
localStorage.setItem('matrix_password', password);
|
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') {
|
} else if (regData.errcode === 'M_USER_IN_USE') {
|
||||||
// Username exists, try to login
|
// Username exists, try to login
|
||||||
term.writeln(' Username exists, logging in...\r\n');
|
term.writeln(' Username exists, logging in...\r\n');
|
||||||
@@ -1023,7 +1238,7 @@ async function submitInlineInput() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize terminal
|
// Initialize terminal
|
||||||
function init() {
|
async function init() {
|
||||||
// Display welcome banner with clickable commands
|
// Display welcome banner with clickable commands
|
||||||
const cols = term.cols;
|
const cols = term.cols;
|
||||||
if (cols >= 78) {
|
if (cols >= 78) {
|
||||||
@@ -1041,6 +1256,8 @@ function init() {
|
|||||||
term.writeln('');
|
term.writeln('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await writeStartupChatMotd();
|
||||||
|
|
||||||
term.write(promptColored);
|
term.write(promptColored);
|
||||||
|
|
||||||
// Add inline input after a short delay to ensure terminal is rendered
|
// Add inline input after a short delay to ensure terminal is rendered
|
||||||
|
|||||||
Reference in New Issue
Block a user