Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3dc09d55fd | ||
|
|
079fb1b0f5 | ||
|
|
17b2aa8111 | ||
|
|
78d484cfe0 | ||
|
|
182e12817d |
@@ -121,8 +121,8 @@ fuser -k 8888/tcp
|
|||||||
- Event filtering done at C level, not SQL level for NIP-40 expiration
|
- Event filtering done at C level, not SQL level for NIP-40 expiration
|
||||||
|
|
||||||
### Configuration Override Behavior
|
### Configuration Override Behavior
|
||||||
- CLI port override only affects first-time startup
|
- CLI port override applies during first-time startup and existing relay restarts
|
||||||
- After database creation, all config comes from events
|
- After database creation, all config comes from events (but CLI overrides can still be applied)
|
||||||
- Database path cannot be changed after initialization
|
- Database path cannot be changed after initialization
|
||||||
|
|
||||||
## Non-Obvious Pitfalls
|
## Non-Obvious Pitfalls
|
||||||
|
|||||||
124
api/index.css
124
api/index.css
@@ -950,10 +950,8 @@ button:disabled {
|
|||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
max-width: 200px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sql-results-table th {
|
.sql-results-table th {
|
||||||
@@ -1109,3 +1107,123 @@ body.dark-mode .sql-results-table tbody tr:nth-child(even) {
|
|||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ================================
|
||||||
|
SIDE NAVIGATION MENU
|
||||||
|
================================ */
|
||||||
|
|
||||||
|
.side-nav {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: -300px;
|
||||||
|
width: 280px;
|
||||||
|
height: 100vh;
|
||||||
|
background: var(--secondary-color);
|
||||||
|
border-right: var(--border-width) solid var(--border-color);
|
||||||
|
z-index: 1000;
|
||||||
|
transition: left 0.3s ease;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-top: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav.open {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 999;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav-overlay.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu li {
|
||||||
|
border-bottom: var(--border-width) solid var(--muted-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: block;
|
||||||
|
padding: 15px 20px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid var(--secondary-color);
|
||||||
|
background: none;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
border: 2px solid var(--secondary-color);
|
||||||
|
background:var(--muted-color);
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
text-decoration: underline;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-footer-btn {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 20px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-footer-btn:hover {
|
||||||
|
background:var(--muted-color);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-footer-btn:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title.clickable:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,11 +9,30 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<!-- Side Navigation Menu -->
|
||||||
|
<nav class="side-nav" id="side-nav">
|
||||||
|
<ul class="nav-menu">
|
||||||
|
<li><button class="nav-item" data-page="statistics">Statistics</button></li>
|
||||||
|
<li><button class="nav-item" data-page="subscriptions">Subscriptions</button></li>
|
||||||
|
<li><button class="nav-item" data-page="configuration">Configuration</button></li>
|
||||||
|
<li><button class="nav-item" data-page="authorization">Authorization</button></li>
|
||||||
|
<li><button class="nav-item" data-page="dm">DM</button></li>
|
||||||
|
<li><button class="nav-item" data-page="database">Database Query</button></li>
|
||||||
|
</ul>
|
||||||
|
<div class="nav-footer">
|
||||||
|
<button class="nav-footer-btn" id="nav-dark-mode-btn">DARK MODE</button>
|
||||||
|
<button class="nav-footer-btn" id="nav-logout-btn">LOGOUT</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Side Navigation Overlay -->
|
||||||
|
<div class="side-nav-overlay" id="side-nav-overlay"></div>
|
||||||
|
|
||||||
<!-- Header with title and profile display -->
|
<!-- Header with title and profile display -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
|
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<div class="header-title">
|
<div class="header-title clickable" id="header-title">
|
||||||
<span class="relay-letter" data-letter="R">R</span>
|
<span class="relay-letter" data-letter="R">R</span>
|
||||||
<span class="relay-letter" data-letter="E">E</span>
|
<span class="relay-letter" data-letter="E">E</span>
|
||||||
<span class="relay-letter" data-letter="L">L</span>
|
<span class="relay-letter" data-letter="L">L</span>
|
||||||
@@ -34,10 +53,7 @@
|
|||||||
<span id="header-user-name" class="header-user-name">Loading...</span>
|
<span id="header-user-name" class="header-user-name">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- Logout dropdown -->
|
<!-- Logout dropdown -->
|
||||||
<div class="logout-dropdown" id="logout-dropdown" style="display: none;">
|
<!-- Dropdown menu removed - buttons moved to sidebar -->
|
||||||
<button type="button" id="dark-mode-btn" class="logout-btn">🌙 DARK MODE</button>
|
|
||||||
<button type="button" id="logout-btn" class="logout-btn">LOGOUT</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
318
api/index.js
318
api/index.js
@@ -35,6 +35,10 @@ let statsAutoRefreshInterval = null;
|
|||||||
let countdownInterval = null;
|
let countdownInterval = null;
|
||||||
let countdownSeconds = 10;
|
let countdownSeconds = 10;
|
||||||
|
|
||||||
|
// Side navigation state
|
||||||
|
let currentPage = 'statistics'; // Default page
|
||||||
|
let sideNavOpen = false;
|
||||||
|
|
||||||
// SQL Query state
|
// SQL Query state
|
||||||
let pendingSqlQueries = new Map();
|
let pendingSqlQueries = new Map();
|
||||||
|
|
||||||
@@ -48,8 +52,6 @@ const loginModalContainer = document.getElementById('login-modal-container');
|
|||||||
const profileArea = document.getElementById('profile-area');
|
const profileArea = document.getElementById('profile-area');
|
||||||
const headerUserImage = document.getElementById('header-user-image');
|
const headerUserImage = document.getElementById('header-user-image');
|
||||||
const headerUserName = document.getElementById('header-user-name');
|
const headerUserName = document.getElementById('header-user-name');
|
||||||
const logoutDropdown = document.getElementById('logout-dropdown');
|
|
||||||
const logoutBtn = document.getElementById('logout-btn');
|
|
||||||
|
|
||||||
// Legacy elements (kept for backward compatibility)
|
// Legacy elements (kept for backward compatibility)
|
||||||
const persistentUserName = document.getElementById('persistent-user-name');
|
const persistentUserName = document.getElementById('persistent-user-name');
|
||||||
@@ -484,36 +486,33 @@ function handleLogoutEvent() {
|
|||||||
|
|
||||||
// Update visibility of admin sections based on login and relay connection status
|
// Update visibility of admin sections based on login and relay connection status
|
||||||
function updateAdminSectionsVisibility() {
|
function updateAdminSectionsVisibility() {
|
||||||
const divConfig = document.getElementById('div_config');
|
|
||||||
const authRulesSection = document.getElementById('authRulesSection');
|
|
||||||
const databaseStatisticsSection = document.getElementById('databaseStatisticsSection');
|
|
||||||
const subscriptionDetailsSection = document.getElementById('subscriptionDetailsSection');
|
|
||||||
const nip17DMSection = document.getElementById('nip17DMSection');
|
|
||||||
const sqlQuerySection = document.getElementById('sqlQuerySection');
|
|
||||||
const shouldShow = isLoggedIn && isRelayConnected;
|
const shouldShow = isLoggedIn && isRelayConnected;
|
||||||
|
|
||||||
if (divConfig) divConfig.style.display = shouldShow ? 'block' : 'none';
|
// If logged in and connected, show the current page, otherwise hide all sections
|
||||||
if (authRulesSection) authRulesSection.style.display = shouldShow ? 'block' : 'none';
|
if (shouldShow) {
|
||||||
if (databaseStatisticsSection) databaseStatisticsSection.style.display = shouldShow ? 'block' : 'none';
|
// Show the current page
|
||||||
if (subscriptionDetailsSection) subscriptionDetailsSection.style.display = shouldShow ? 'block' : 'none';
|
switchPage(currentPage);
|
||||||
if (nip17DMSection) nip17DMSection.style.display = shouldShow ? 'block' : 'none';
|
|
||||||
if (sqlQuerySection) sqlQuerySection.style.display = shouldShow ? 'block' : 'none';
|
|
||||||
|
|
||||||
// Start/stop auto-refresh based on visibility
|
// Load data for the current page
|
||||||
if (shouldShow && databaseStatisticsSection && databaseStatisticsSection.style.display === 'block') {
|
loadCurrentPageData();
|
||||||
// Load statistics immediately (no auto-refresh - using real-time monitoring events)
|
|
||||||
sendStatsQuery().catch(error => {
|
|
||||||
console.log('Auto-fetch statistics failed: ' + error.message);
|
|
||||||
});
|
|
||||||
// startStatsAutoRefresh(); // DISABLED - using real-time monitoring events instead
|
|
||||||
// Also load configuration and auth rules automatically when sections become visible
|
|
||||||
fetchConfiguration().catch(error => {
|
|
||||||
console.log('Auto-fetch configuration failed: ' + error.message);
|
|
||||||
});
|
|
||||||
loadAuthRules().catch(error => {
|
|
||||||
console.log('Auto-load auth rules failed: ' + error.message);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
|
// Hide all sections when not logged in or not connected
|
||||||
|
const sections = [
|
||||||
|
'databaseStatisticsSection',
|
||||||
|
'subscriptionDetailsSection',
|
||||||
|
'div_config',
|
||||||
|
'authRulesSection',
|
||||||
|
'nip17DMSection',
|
||||||
|
'sqlQuerySection'
|
||||||
|
];
|
||||||
|
|
||||||
|
sections.forEach(sectionId => {
|
||||||
|
const section = document.getElementById(sectionId);
|
||||||
|
if (section) {
|
||||||
|
section.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
stopStatsAutoRefresh();
|
stopStatsAutoRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,6 +520,31 @@ function updateAdminSectionsVisibility() {
|
|||||||
updateCountdownDisplay();
|
updateCountdownDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load data for the current page
|
||||||
|
function loadCurrentPageData() {
|
||||||
|
switch (currentPage) {
|
||||||
|
case 'statistics':
|
||||||
|
// Load statistics immediately (no auto-refresh - using real-time monitoring events)
|
||||||
|
sendStatsQuery().catch(error => {
|
||||||
|
console.log('Auto-fetch statistics failed: ' + error.message);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'configuration':
|
||||||
|
// Load configuration
|
||||||
|
fetchConfiguration().catch(error => {
|
||||||
|
console.log('Auto-fetch configuration failed: ' + error.message);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'authorization':
|
||||||
|
// Load auth rules
|
||||||
|
loadAuthRules().catch(error => {
|
||||||
|
console.log('Auto-load auth rules failed: ' + error.message);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
// Other pages don't need initial data loading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show login modal
|
// Show login modal
|
||||||
function showLoginModal() {
|
function showLoginModal() {
|
||||||
if (loginModal && loginModalContainer) {
|
if (loginModal && loginModalContainer) {
|
||||||
@@ -553,10 +577,6 @@ function hideProfileFromHeader() {
|
|||||||
if (profileArea) {
|
if (profileArea) {
|
||||||
profileArea.style.display = 'none';
|
profileArea.style.display = 'none';
|
||||||
}
|
}
|
||||||
// Also hide logout dropdown if visible
|
|
||||||
if (logoutDropdown) {
|
|
||||||
logoutDropdown.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update login/logout UI visibility (legacy function - kept for backward compatibility)
|
// Update login/logout UI visibility (legacy function - kept for backward compatibility)
|
||||||
@@ -1044,7 +1064,7 @@ async function subscribeToConfiguration() {
|
|||||||
"#p": [userPubkey], // Only DMs directed to this user
|
"#p": [userPubkey], // Only DMs directed to this user
|
||||||
limit: 50
|
limit: 50
|
||||||
}, {
|
}, {
|
||||||
since: Math.floor(Date.now() / 1000), // Start from current time
|
since: Math.floor(Date.now() / 1000) - (2 * 24 * 60 * 60), // Look back 2 days for NIP-59 randomized timestamps
|
||||||
kinds: [1059], // NIP-17 GiftWrap events
|
kinds: [1059], // NIP-17 GiftWrap events
|
||||||
"#p": [userPubkey], // Only GiftWrap events addressed to this user
|
"#p": [userPubkey], // Only GiftWrap events addressed to this user
|
||||||
limit: 50
|
limit: 50
|
||||||
@@ -1060,7 +1080,7 @@ async function subscribeToConfiguration() {
|
|||||||
if (event.kind === 24567) {
|
if (event.kind === 24567) {
|
||||||
const dTag = event.tags.find(tag => tag[0] === 'd');
|
const dTag = event.tags.find(tag => tag[0] === 'd');
|
||||||
const dataType = dTag ? dTag[1] : 'unknown';
|
const dataType = dTag ? dTag[1] : 'unknown';
|
||||||
console.log(`📊 Monitoring event: ${dataType}`);
|
// console.log(`📊 Monitoring event: ${dataType}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`📨 Event received: kind ${event.kind}`);
|
console.log(`📨 Event received: kind ${event.kind}`);
|
||||||
}
|
}
|
||||||
@@ -1091,20 +1111,32 @@ async function subscribeToConfiguration() {
|
|||||||
|
|
||||||
// Handle NIP-17 GiftWrap DMs
|
// Handle NIP-17 GiftWrap DMs
|
||||||
if (event.kind === 1059) {
|
if (event.kind === 1059) {
|
||||||
|
console.log(`📨 RECEIVED KIND 1059 EVENT:`, {
|
||||||
|
id: event.id,
|
||||||
|
pubkey: event.pubkey,
|
||||||
|
created_at: event.created_at,
|
||||||
|
content: event.content.substring(0, 100) + '...',
|
||||||
|
tags: event.tags
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Step 1: Unwrap gift wrap to get seal
|
// Step 1: Unwrap gift wrap to get seal
|
||||||
const sealJson = await window.nostr.nip44.decrypt(event.pubkey, event.content);
|
const sealJson = await window.nostr.nip44.decrypt(event.pubkey, event.content);
|
||||||
|
console.log(`🔓 STEP 1 - Unwrapped gift wrap:`, sealJson.substring(0, 100) + '...');
|
||||||
const seal = safeJsonParse(sealJson);
|
const seal = safeJsonParse(sealJson);
|
||||||
if (!seal || seal.kind !== 13) {
|
if (!seal || seal.kind !== 13) {
|
||||||
throw new Error('Unwrapped content is not a valid seal (kind 13)');
|
throw new Error('Unwrapped content is not a valid seal (kind 13)');
|
||||||
}
|
}
|
||||||
|
console.log(`✅ Seal validated:`, { kind: seal.kind, pubkey: seal.pubkey.substring(0, 16) + '...' });
|
||||||
|
|
||||||
// Step 2: Unseal to get rumor
|
// Step 2: Unseal to get rumor
|
||||||
const rumorJson = await window.nostr.nip44.decrypt(seal.pubkey, seal.content);
|
const rumorJson = await window.nostr.nip44.decrypt(seal.pubkey, seal.content);
|
||||||
|
console.log(`🔓 STEP 2 - Unsealed rumor:`, rumorJson.substring(0, 100) + '...');
|
||||||
const rumor = safeJsonParse(rumorJson);
|
const rumor = safeJsonParse(rumorJson);
|
||||||
if (!rumor || rumor.kind !== 14) {
|
if (!rumor || rumor.kind !== 14) {
|
||||||
throw new Error('Unsealed content is not a valid rumor (kind 14)');
|
throw new Error('Unsealed content is not a valid rumor (kind 14)');
|
||||||
}
|
}
|
||||||
|
console.log(`✅ Rumor validated:`, { kind: rumor.kind, pubkey: rumor.pubkey.substring(0, 16) + '...', content: rumor.content.substring(0, 50) + '...' });
|
||||||
|
|
||||||
log(`Received NIP-17 DM from relay: ${rumor.content.substring(0, 50)}...`, 'INFO');
|
log(`Received NIP-17 DM from relay: ${rumor.content.substring(0, 50)}...`, 'INFO');
|
||||||
|
|
||||||
@@ -1117,6 +1149,7 @@ async function subscribeToConfiguration() {
|
|||||||
logTestEvent('RECV', `NIP-17 DM: ${rumor.content}`, 'DM');
|
logTestEvent('RECV', `NIP-17 DM: ${rumor.content}`, 'DM');
|
||||||
}
|
}
|
||||||
} catch (unwrapError) {
|
} catch (unwrapError) {
|
||||||
|
console.error(`❌ NIP-17 DM UNWRAP FAILED:`, unwrapError);
|
||||||
log(`Failed to unwrap NIP-17 DM: ${unwrapError.message}`, 'ERROR');
|
log(`Failed to unwrap NIP-17 DM: ${unwrapError.message}`, 'ERROR');
|
||||||
if (typeof logTestEvent === 'function') {
|
if (typeof logTestEvent === 'function') {
|
||||||
logTestEvent('ERROR', `Failed to unwrap DM: ${unwrapError.message}`, 'DM');
|
logTestEvent('ERROR', `Failed to unwrap DM: ${unwrapError.message}`, 'DM');
|
||||||
@@ -2076,45 +2109,8 @@ async function sendConfigUpdateCommand(configObjects) {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Profile area click handler for logout dropdown
|
// Profile area click handler removed - dropdown moved to sidebar
|
||||||
function toggleLogoutDropdown(event) {
|
// Logout and dark mode buttons are now in the sidebar footer
|
||||||
if (!logoutDropdown) return;
|
|
||||||
|
|
||||||
// Only toggle if clicking on the image, not the text or container
|
|
||||||
if (event.target === headerUserImage) {
|
|
||||||
const isVisible = logoutDropdown.style.display === 'block';
|
|
||||||
logoutDropdown.style.display = isVisible ? 'none' : 'block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close logout dropdown when clicking outside
|
|
||||||
document.addEventListener('click', function(event) {
|
|
||||||
if (profileArea && logoutDropdown && !profileArea.contains(event.target)) {
|
|
||||||
logoutDropdown.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize profile area click handler
|
|
||||||
if (profileArea) {
|
|
||||||
profileArea.addEventListener('click', toggleLogoutDropdown);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize logout button handler
|
|
||||||
if (logoutBtn) {
|
|
||||||
logoutBtn.addEventListener('click', function(e) {
|
|
||||||
e.stopPropagation(); // Prevent profile area click
|
|
||||||
logout();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize dark mode button handler
|
|
||||||
const darkModeBtn = document.getElementById('dark-mode-btn');
|
|
||||||
if (darkModeBtn) {
|
|
||||||
darkModeBtn.addEventListener('click', function(e) {
|
|
||||||
e.stopPropagation(); // Prevent profile area click
|
|
||||||
toggleDarkMode();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize relay pubkey container click handler for clipboard copy
|
// Initialize relay pubkey container click handler for clipboard copy
|
||||||
const relayPubkeyContainer = document.getElementById('relay-pubkey-container');
|
const relayPubkeyContainer = document.getElementById('relay-pubkey-container');
|
||||||
@@ -2123,8 +2119,8 @@ if (relayPubkeyContainer) {
|
|||||||
const relayPubkeyElement = document.getElementById('relay-pubkey');
|
const relayPubkeyElement = document.getElementById('relay-pubkey');
|
||||||
if (relayPubkeyElement && relayPubkeyElement.textContent !== 'Loading...') {
|
if (relayPubkeyElement && relayPubkeyElement.textContent !== 'Loading...') {
|
||||||
try {
|
try {
|
||||||
// Get the full npub (remove line breaks for clipboard)
|
// Get the full npub (remove all whitespace for continuous string)
|
||||||
const fullNpub = relayPubkeyElement.textContent.replace(/\n/g, '');
|
const fullNpub = relayPubkeyElement.textContent.replace(/\s/g, '');
|
||||||
|
|
||||||
await navigator.clipboard.writeText(fullNpub);
|
await navigator.clipboard.writeText(fullNpub);
|
||||||
|
|
||||||
@@ -3553,6 +3549,13 @@ async function sendNIP17DM() {
|
|||||||
throw new Error('Failed to sign gift wrap event');
|
throw new Error('Failed to sign gift wrap event');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DEBUG: Log NIP-17 event details when created
|
||||||
|
console.log('=== NIP-17 EVENT CREATED ===');
|
||||||
|
console.log('Full event:', JSON.stringify(signedGiftWrap, null, 2));
|
||||||
|
console.log('Timestamp:', signedGiftWrap.created_at);
|
||||||
|
console.log('Local date time:', new Date(signedGiftWrap.created_at * 1000).toLocaleString());
|
||||||
|
console.log('=== END NIP-17 EVENT DEBUG ===');
|
||||||
|
|
||||||
log('NIP-17 DM event created and signed with ephemeral key, publishing...', 'INFO');
|
log('NIP-17 DM event created and signed with ephemeral key, publishing...', 'INFO');
|
||||||
|
|
||||||
// Publish via SimplePool
|
// Publish via SimplePool
|
||||||
@@ -3988,9 +3991,9 @@ function updateStatsFromTimeMonitoringEvent(monitoringData) {
|
|||||||
|
|
||||||
// Extract values from periods array
|
// Extract values from periods array
|
||||||
monitoringData.periods.forEach(period => {
|
monitoringData.periods.forEach(period => {
|
||||||
if (period.period === '24h') timeStats.last_24h = period.event_count;
|
if (period.period === '24h') timeStats.last_24h = period.count;
|
||||||
else if (period.period === '7d') timeStats.last_7d = period.event_count;
|
else if (period.period === '7d') timeStats.last_7d = period.count;
|
||||||
else if (period.period === '30d') timeStats.last_30d = period.event_count;
|
else if (period.period === '30d') timeStats.last_30d = period.count;
|
||||||
});
|
});
|
||||||
|
|
||||||
populateStatsTime({ time_stats: timeStats });
|
populateStatsTime({ time_stats: timeStats });
|
||||||
@@ -4537,9 +4540,9 @@ function toggleDarkMode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateDarkModeButton(isDarkMode) {
|
function updateDarkModeButton(isDarkMode) {
|
||||||
const darkModeBtn = document.getElementById('dark-mode-btn');
|
const navDarkModeBtn = document.getElementById('nav-dark-mode-btn');
|
||||||
if (darkModeBtn) {
|
if (navDarkModeBtn) {
|
||||||
darkModeBtn.textContent = isDarkMode ? 'LIGHT MODE' : 'DARK MODE';
|
navDarkModeBtn.textContent = isDarkMode ? 'LIGHT MODE' : 'DARK MODE';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4556,6 +4559,97 @@ function initializeDarkMode() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Side navigation functions
|
||||||
|
function toggleSideNav() {
|
||||||
|
const sideNav = document.getElementById('side-nav');
|
||||||
|
const overlay = document.getElementById('side-nav-overlay');
|
||||||
|
|
||||||
|
if (sideNavOpen) {
|
||||||
|
sideNav.classList.remove('open');
|
||||||
|
overlay.classList.remove('show');
|
||||||
|
sideNavOpen = false;
|
||||||
|
} else {
|
||||||
|
sideNav.classList.add('open');
|
||||||
|
overlay.classList.add('show');
|
||||||
|
sideNavOpen = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSideNav() {
|
||||||
|
const sideNav = document.getElementById('side-nav');
|
||||||
|
const overlay = document.getElementById('side-nav-overlay');
|
||||||
|
|
||||||
|
sideNav.classList.remove('open');
|
||||||
|
overlay.classList.remove('show');
|
||||||
|
sideNavOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchPage(pageName) {
|
||||||
|
// Update current page
|
||||||
|
currentPage = pageName;
|
||||||
|
|
||||||
|
// Update navigation active state
|
||||||
|
const navItems = document.querySelectorAll('.nav-item');
|
||||||
|
navItems.forEach(item => {
|
||||||
|
item.classList.remove('active');
|
||||||
|
if (item.getAttribute('data-page') === pageName) {
|
||||||
|
item.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide all sections
|
||||||
|
const sections = [
|
||||||
|
'databaseStatisticsSection',
|
||||||
|
'subscriptionDetailsSection',
|
||||||
|
'div_config',
|
||||||
|
'authRulesSection',
|
||||||
|
'nip17DMSection',
|
||||||
|
'sqlQuerySection'
|
||||||
|
];
|
||||||
|
|
||||||
|
sections.forEach(sectionId => {
|
||||||
|
const section = document.getElementById(sectionId);
|
||||||
|
if (section) {
|
||||||
|
section.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show selected section
|
||||||
|
const pageMap = {
|
||||||
|
'statistics': 'databaseStatisticsSection',
|
||||||
|
'subscriptions': 'subscriptionDetailsSection',
|
||||||
|
'configuration': 'div_config',
|
||||||
|
'authorization': 'authRulesSection',
|
||||||
|
'dm': 'nip17DMSection',
|
||||||
|
'database': 'sqlQuerySection'
|
||||||
|
};
|
||||||
|
|
||||||
|
const targetSectionId = pageMap[pageName];
|
||||||
|
if (targetSectionId) {
|
||||||
|
const targetSection = document.getElementById(targetSectionId);
|
||||||
|
if (targetSection) {
|
||||||
|
targetSection.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special handling for configuration page - ensure config-display is visible and refresh data
|
||||||
|
if (pageName === 'configuration') {
|
||||||
|
const configDisplay = document.getElementById('config-display');
|
||||||
|
if (configDisplay) {
|
||||||
|
configDisplay.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
// Always refresh configuration data when navigating to config page
|
||||||
|
fetchConfiguration().catch(error => {
|
||||||
|
console.log('Failed to refresh configuration on page switch: ' + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close side navigation
|
||||||
|
closeSideNav();
|
||||||
|
|
||||||
|
log(`Switched to page: ${pageName}`, 'INFO');
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the app
|
// Initialize the app
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
console.log('C-Relay Admin API interface loaded');
|
console.log('C-Relay Admin API interface loaded');
|
||||||
@@ -4563,6 +4657,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// Initialize dark mode
|
// Initialize dark mode
|
||||||
initializeDarkMode();
|
initializeDarkMode();
|
||||||
|
|
||||||
|
// Initialize sidebar button text
|
||||||
|
const navDarkModeBtn = document.getElementById('nav-dark-mode-btn');
|
||||||
|
if (navDarkModeBtn) {
|
||||||
|
navDarkModeBtn.textContent = document.body.classList.contains('dark-mode') ? 'LIGHT MODE' : 'DARK MODE';
|
||||||
|
}
|
||||||
|
|
||||||
// Start RELAY letter animation
|
// Start RELAY letter animation
|
||||||
startRelayAnimation();
|
startRelayAnimation();
|
||||||
|
|
||||||
@@ -4571,6 +4671,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
initializeEventRateChart();
|
initializeEventRateChart();
|
||||||
}, 1000); // Delay to ensure text_graph.js is loaded
|
}, 1000); // Delay to ensure text_graph.js is loaded
|
||||||
|
|
||||||
|
// Initialize side navigation
|
||||||
|
initializeSideNavigation();
|
||||||
|
|
||||||
// Ensure admin sections are hidden by default on page load
|
// Ensure admin sections are hidden by default on page load
|
||||||
updateAdminSectionsVisibility();
|
updateAdminSectionsVisibility();
|
||||||
|
|
||||||
@@ -4581,6 +4684,59 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize side navigation event handlers
|
||||||
|
function initializeSideNavigation() {
|
||||||
|
// Header title click handler
|
||||||
|
const headerTitle = document.getElementById('header-title');
|
||||||
|
if (headerTitle) {
|
||||||
|
headerTitle.addEventListener('click', toggleSideNav);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overlay click handler
|
||||||
|
const overlay = document.getElementById('side-nav-overlay');
|
||||||
|
if (overlay) {
|
||||||
|
overlay.addEventListener('click', closeSideNav);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation item click handlers
|
||||||
|
const navItems = document.querySelectorAll('.nav-item');
|
||||||
|
navItems.forEach(item => {
|
||||||
|
item.addEventListener('click', (e) => {
|
||||||
|
const pageName = e.target.getAttribute('data-page');
|
||||||
|
if (pageName) {
|
||||||
|
switchPage(pageName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Footer button handlers
|
||||||
|
const navDarkModeBtn = document.getElementById('nav-dark-mode-btn');
|
||||||
|
const navLogoutBtn = document.getElementById('nav-logout-btn');
|
||||||
|
|
||||||
|
if (navDarkModeBtn) {
|
||||||
|
navDarkModeBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleDarkMode();
|
||||||
|
// Update button text after toggle
|
||||||
|
setTimeout(() => {
|
||||||
|
navDarkModeBtn.textContent = document.body.classList.contains('dark-mode') ? 'LIGHT MODE' : 'DARK MODE';
|
||||||
|
}, 10);
|
||||||
|
closeSideNav();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navLogoutBtn) {
|
||||||
|
navLogoutBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
logout();
|
||||||
|
closeSideNav();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set initial page
|
||||||
|
switchPage(currentPage);
|
||||||
|
}
|
||||||
|
|
||||||
// ================================
|
// ================================
|
||||||
// SQL QUERY FUNCTIONS
|
// SQL QUERY FUNCTIONS
|
||||||
// ================================
|
// ================================
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Copy the binary to the deployment location
|
||||||
cp build/c_relay_x86 ~/Storage/c_relay/crelay
|
cp build/c_relay_x86 ~/Storage/c_relay/crelay
|
||||||
|
|
||||||
|
# Copy the local service file to systemd
|
||||||
|
sudo cp systemd/c-relay-local.service /etc/systemd/system/
|
||||||
|
|
||||||
|
# Reload systemd daemon to pick up the new service
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
|
||||||
|
# Enable the service (if not already enabled)
|
||||||
|
sudo systemctl enable c-relay-local.service
|
||||||
|
|
||||||
|
# Restart the service
|
||||||
|
sudo systemctl restart c-relay-local.service
|
||||||
|
|
||||||
|
# Show service status
|
||||||
|
sudo systemctl status c-relay-local.service --no-pager -l
|
||||||
|
|||||||
209
docs/subscription_matching_debug_plan.md
Normal file
209
docs/subscription_matching_debug_plan.md
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
# Subscription Matching Debug Plan
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
The relay is not matching kind 1059 (NIP-17 gift wrap) events to subscriptions, even though a subscription exists with `kinds:[1059]` filter. The log shows:
|
||||||
|
```
|
||||||
|
Event broadcast complete: 0 subscriptions matched
|
||||||
|
```
|
||||||
|
|
||||||
|
But we have this subscription:
|
||||||
|
```
|
||||||
|
sub:3 146.70.187.119 0x78edc9b43210 8m 27s kinds:[1059], since:10/23/2025, 4:27:59 PM, limit:50
|
||||||
|
```
|
||||||
|
|
||||||
|
## Investigation Strategy
|
||||||
|
|
||||||
|
### 1. Add Debug Output to `event_matches_filter()` (lines 386-564)
|
||||||
|
Add debug logging at each filter check to trace the matching logic:
|
||||||
|
|
||||||
|
- **Entry point**: Log the event kind and filter being tested
|
||||||
|
- **Kinds filter check** (lines 392-415): Log whether kinds filter exists, the event kind value, and each filter kind being compared
|
||||||
|
- **Authors filter check** (lines 417-442): Log if authors filter exists and matching results
|
||||||
|
- **IDs filter check** (lines 444-469): Log if IDs filter exists and matching results
|
||||||
|
- **Since filter check** (lines 471-482): Log the event timestamp vs filter since value
|
||||||
|
- **Until filter check** (lines 484-495): Log the event timestamp vs filter until value
|
||||||
|
- **Tag filters check** (lines 497-561): Log tag filter matching details
|
||||||
|
- **Exit point**: Log whether the overall filter matched
|
||||||
|
|
||||||
|
### 2. Add Debug Output to `event_matches_subscription()` (lines 567-581)
|
||||||
|
Add logging to show:
|
||||||
|
- How many filters are in the subscription
|
||||||
|
- Which filter (if any) matched
|
||||||
|
- Overall subscription match result
|
||||||
|
|
||||||
|
### 3. Add Debug Output to `broadcast_event_to_subscriptions()` (lines 584-726)
|
||||||
|
Add logging to show:
|
||||||
|
- The event being broadcast (kind, id, created_at)
|
||||||
|
- Total number of active subscriptions being checked
|
||||||
|
- How many subscriptions matched after the first pass
|
||||||
|
|
||||||
|
### 4. Key Areas to Focus On
|
||||||
|
|
||||||
|
Based on the code analysis, the most likely issues are:
|
||||||
|
|
||||||
|
1. **Kind matching logic** (lines 392-415): The event kind might not be extracted correctly, or the comparison might be failing
|
||||||
|
2. **Since timestamp** (lines 471-482): The subscription has a `since` filter - if the event timestamp is before this, it won't match
|
||||||
|
3. **Event structure**: The event JSON might not have the expected structure
|
||||||
|
|
||||||
|
### 5. Specific Debug Additions
|
||||||
|
|
||||||
|
#### In `event_matches_filter()` at line 386:
|
||||||
|
```c
|
||||||
|
// Add at start of function
|
||||||
|
cJSON* event_kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||||
|
cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
|
||||||
|
cJSON* event_created_at_obj = cJSON_GetObjectItem(event, "created_at");
|
||||||
|
|
||||||
|
DEBUG_TRACE("FILTER_MATCH: Testing event kind=%d id=%.8s created_at=%ld",
|
||||||
|
event_kind_obj ? (int)cJSON_GetNumberValue(event_kind_obj) : -1,
|
||||||
|
event_id_obj && cJSON_IsString(event_id_obj) ? cJSON_GetStringValue(event_id_obj) : "null",
|
||||||
|
event_created_at_obj ? (long)cJSON_GetNumberValue(event_created_at_obj) : 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### In kinds filter check (after line 392):
|
||||||
|
```c
|
||||||
|
if (filter->kinds && cJSON_IsArray(filter->kinds)) {
|
||||||
|
DEBUG_TRACE("FILTER_MATCH: Checking kinds filter with %d kinds", cJSON_GetArraySize(filter->kinds));
|
||||||
|
|
||||||
|
cJSON* event_kind = cJSON_GetObjectItem(event, "kind");
|
||||||
|
if (!event_kind || !cJSON_IsNumber(event_kind)) {
|
||||||
|
DEBUG_WARN("FILTER_MATCH: Event has no valid kind field");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int event_kind_val = (int)cJSON_GetNumberValue(event_kind);
|
||||||
|
DEBUG_TRACE("FILTER_MATCH: Event kind=%d", event_kind_val);
|
||||||
|
|
||||||
|
int kind_match = 0;
|
||||||
|
cJSON* kind_item = NULL;
|
||||||
|
cJSON_ArrayForEach(kind_item, filter->kinds) {
|
||||||
|
if (cJSON_IsNumber(kind_item)) {
|
||||||
|
int filter_kind = (int)cJSON_GetNumberValue(kind_item);
|
||||||
|
DEBUG_TRACE("FILTER_MATCH: Comparing event kind %d with filter kind %d", event_kind_val, filter_kind);
|
||||||
|
if (filter_kind == event_kind_val) {
|
||||||
|
kind_match = 1;
|
||||||
|
DEBUG_TRACE("FILTER_MATCH: Kind matched!");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!kind_match) {
|
||||||
|
DEBUG_TRACE("FILTER_MATCH: No kind match, filter rejected");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
DEBUG_TRACE("FILTER_MATCH: Kinds filter passed");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### In since filter check (after line 472):
|
||||||
|
```c
|
||||||
|
if (filter->since > 0) {
|
||||||
|
cJSON* event_created_at = cJSON_GetObjectItem(event, "created_at");
|
||||||
|
if (!event_created_at || !cJSON_IsNumber(event_created_at)) {
|
||||||
|
DEBUG_WARN("FILTER_MATCH: Event has no valid created_at field");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
long event_timestamp = (long)cJSON_GetNumberValue(event_created_at);
|
||||||
|
DEBUG_TRACE("FILTER_MATCH: Checking since filter: event_ts=%ld filter_since=%ld",
|
||||||
|
event_timestamp, filter->since);
|
||||||
|
|
||||||
|
if (event_timestamp < filter->since) {
|
||||||
|
DEBUG_TRACE("FILTER_MATCH: Event too old (before since), filter rejected");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
DEBUG_TRACE("FILTER_MATCH: Since filter passed");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### At end of `event_matches_filter()` (before line 563):
|
||||||
|
```c
|
||||||
|
DEBUG_TRACE("FILTER_MATCH: All filters passed, event matches!");
|
||||||
|
return 1; // All filters passed
|
||||||
|
```
|
||||||
|
|
||||||
|
#### In `event_matches_subscription()` at line 567:
|
||||||
|
```c
|
||||||
|
int event_matches_subscription(cJSON* event, subscription_t* subscription) {
|
||||||
|
if (!event || !subscription || !subscription->filters) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_TRACE("SUB_MATCH: Testing subscription '%s'", subscription->id);
|
||||||
|
|
||||||
|
int filter_num = 0;
|
||||||
|
subscription_filter_t* filter = subscription->filters;
|
||||||
|
while (filter) {
|
||||||
|
filter_num++;
|
||||||
|
DEBUG_TRACE("SUB_MATCH: Testing filter #%d", filter_num);
|
||||||
|
|
||||||
|
if (event_matches_filter(event, filter)) {
|
||||||
|
DEBUG_TRACE("SUB_MATCH: Filter #%d matched! Subscription '%s' matches",
|
||||||
|
filter_num, subscription->id);
|
||||||
|
return 1; // Match found (OR logic)
|
||||||
|
}
|
||||||
|
filter = filter->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_TRACE("SUB_MATCH: No filters matched for subscription '%s'", subscription->id);
|
||||||
|
return 0; // No filters matched
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### In `broadcast_event_to_subscriptions()` at line 584:
|
||||||
|
```c
|
||||||
|
int broadcast_event_to_subscriptions(cJSON* event) {
|
||||||
|
if (!event) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log event details
|
||||||
|
cJSON* event_kind = cJSON_GetObjectItem(event, "kind");
|
||||||
|
cJSON* event_id = cJSON_GetObjectItem(event, "id");
|
||||||
|
cJSON* event_created_at = cJSON_GetObjectItem(event, "created_at");
|
||||||
|
|
||||||
|
DEBUG_TRACE("BROADCAST: Event kind=%d id=%.8s created_at=%ld",
|
||||||
|
event_kind ? (int)cJSON_GetNumberValue(event_kind) : -1,
|
||||||
|
event_id && cJSON_IsString(event_id) ? cJSON_GetStringValue(event_id) : "null",
|
||||||
|
event_created_at ? (long)cJSON_GetNumberValue(event_created_at) : 0);
|
||||||
|
|
||||||
|
// ... existing expiration check code ...
|
||||||
|
|
||||||
|
// After line 611 (before pthread_mutex_lock):
|
||||||
|
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||||||
|
|
||||||
|
int total_subs = 0;
|
||||||
|
subscription_t* count_sub = g_subscription_manager.active_subscriptions;
|
||||||
|
while (count_sub) {
|
||||||
|
total_subs++;
|
||||||
|
count_sub = count_sub->next;
|
||||||
|
}
|
||||||
|
DEBUG_TRACE("BROADCAST: Checking %d active subscriptions", total_subs);
|
||||||
|
|
||||||
|
subscription_t* sub = g_subscription_manager.active_subscriptions;
|
||||||
|
// ... rest of matching logic ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expected Outcome
|
||||||
|
|
||||||
|
With these debug additions, we should see output like:
|
||||||
|
```
|
||||||
|
BROADCAST: Event kind=1059 id=abc12345 created_at=1729712279
|
||||||
|
BROADCAST: Checking 1 active subscriptions
|
||||||
|
SUB_MATCH: Testing subscription 'sub:3'
|
||||||
|
SUB_MATCH: Testing filter #1
|
||||||
|
FILTER_MATCH: Testing event kind=1059 id=abc12345 created_at=1729712279
|
||||||
|
FILTER_MATCH: Checking kinds filter with 1 kinds
|
||||||
|
FILTER_MATCH: Event kind=1059
|
||||||
|
FILTER_MATCH: Comparing event kind 1059 with filter kind 1059
|
||||||
|
FILTER_MATCH: Kind matched!
|
||||||
|
FILTER_MATCH: Kinds filter passed
|
||||||
|
FILTER_MATCH: Checking since filter: event_ts=1729712279 filter_since=1729708079
|
||||||
|
FILTER_MATCH: Since filter passed
|
||||||
|
FILTER_MATCH: All filters passed, event matches!
|
||||||
|
SUB_MATCH: Filter #1 matched! Subscription 'sub:3' matches
|
||||||
|
Event broadcast complete: 1 subscriptions matched
|
||||||
|
```
|
||||||
|
|
||||||
|
This will help us identify exactly where the matching is failing.
|
||||||
@@ -133,6 +133,11 @@ if [ -n "$PORT_OVERRIDE" ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Validate strict port flag (only makes sense with port override)
|
||||||
|
if [ "$USE_TEST_KEYS" = true ] && [ -z "$PORT_OVERRIDE" ]; then
|
||||||
|
echo "WARNING: --strict-port is always used with test keys. Consider specifying a custom port with -p."
|
||||||
|
fi
|
||||||
|
|
||||||
# Validate debug level if provided
|
# Validate debug level if provided
|
||||||
if [ -n "$DEBUG_LEVEL" ]; then
|
if [ -n "$DEBUG_LEVEL" ]; then
|
||||||
if ! [[ "$DEBUG_LEVEL" =~ ^[0-5]$ ]]; then
|
if ! [[ "$DEBUG_LEVEL" =~ ^[0-5]$ ]]; then
|
||||||
@@ -163,6 +168,8 @@ if [ "$HELP" = true ]; then
|
|||||||
echo " $0 # Fresh start with random keys"
|
echo " $0 # Fresh start with random keys"
|
||||||
echo " $0 -a <admin-hex> -r <relay-hex> # Use custom keys"
|
echo " $0 -a <admin-hex> -r <relay-hex> # Use custom keys"
|
||||||
echo " $0 -a <admin-hex> -p 9000 # Custom admin key on port 9000"
|
echo " $0 -a <admin-hex> -p 9000 # Custom admin key on port 9000"
|
||||||
|
echo " $0 -p 7777 --strict-port # Fail if port 7777 unavailable (no fallback)"
|
||||||
|
echo " $0 -p 8080 --strict-port -d=3 # Custom port with strict binding and debug"
|
||||||
echo " $0 --debug-level=3 # Start with debug level 3 (info)"
|
echo " $0 --debug-level=3 # Start with debug level 3 (info)"
|
||||||
echo " $0 -d=5 # Start with debug level 5 (trace)"
|
echo " $0 -d=5 # Start with debug level 5 (trace)"
|
||||||
echo " $0 --preserve-database # Preserve existing database and keys"
|
echo " $0 --preserve-database # Preserve existing database and keys"
|
||||||
|
|||||||
@@ -85,3 +85,7 @@ sudo -u c-relay ./c_relay --debug-level=5 -r 85d0b37e2ae822966dcadd06b2dc9368cde
|
|||||||
|
|
||||||
sudo ufw allow 8888/tcp
|
sudo ufw allow 8888/tcp
|
||||||
sudo ufw delete allow 8888/tcp
|
sudo ufw delete allow 8888/tcp
|
||||||
|
|
||||||
|
lsof -i :7777
|
||||||
|
kill $(lsof -t -i :7777)
|
||||||
|
kill -9 $(lsof -t -i :7777)
|
||||||
63
src/api.c
63
src/api.c
@@ -377,8 +377,8 @@ cJSON* query_subscription_details(void) {
|
|||||||
return subscriptions_data;
|
return subscriptions_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate and broadcast monitoring event
|
// Generate event-driven monitoring events (triggered by event storage)
|
||||||
int generate_monitoring_event(void) {
|
int generate_event_driven_monitoring(void) {
|
||||||
// Generate event_kinds monitoring event
|
// Generate event_kinds monitoring event
|
||||||
if (generate_monitoring_event_for_type("event_kinds", query_event_kind_distribution) != 0) {
|
if (generate_monitoring_event_for_type("event_kinds", query_event_kind_distribution) != 0) {
|
||||||
DEBUG_ERROR("Failed to generate event_kinds monitoring event");
|
DEBUG_ERROR("Failed to generate event_kinds monitoring event");
|
||||||
@@ -403,22 +403,45 @@ int generate_monitoring_event(void) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate CPU metrics monitoring event (also triggered by event storage)
|
||||||
|
if (generate_monitoring_event_for_type("cpu_metrics", query_cpu_metrics) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to generate cpu_metrics monitoring event");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_INFO("Generated and broadcast event-driven monitoring events");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate subscription-driven monitoring events (triggered by subscription changes)
|
||||||
|
int generate_subscription_driven_monitoring(void) {
|
||||||
|
// Generate active_subscriptions monitoring event (subscription changes affect this)
|
||||||
|
if (generate_monitoring_event_for_type("active_subscriptions", query_active_subscriptions) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to generate active_subscriptions monitoring event");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
// Generate subscription_details monitoring event (admin-only)
|
// Generate subscription_details monitoring event (admin-only)
|
||||||
if (generate_monitoring_event_for_type("subscription_details", query_subscription_details) != 0) {
|
if (generate_monitoring_event_for_type("subscription_details", query_subscription_details) != 0) {
|
||||||
DEBUG_ERROR("Failed to generate subscription_details monitoring event");
|
DEBUG_ERROR("Failed to generate subscription_details monitoring event");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate CPU metrics monitoring event
|
// Generate CPU metrics monitoring event (also triggered by subscription changes)
|
||||||
if (generate_monitoring_event_for_type("cpu_metrics", query_cpu_metrics) != 0) {
|
if (generate_monitoring_event_for_type("cpu_metrics", query_cpu_metrics) != 0) {
|
||||||
DEBUG_ERROR("Failed to generate cpu_metrics monitoring event");
|
DEBUG_ERROR("Failed to generate cpu_metrics monitoring event");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
DEBUG_INFO("Generated and broadcast all monitoring events");
|
DEBUG_INFO("Generated and broadcast subscription-driven monitoring events");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate and broadcast monitoring event (legacy function - now calls event-driven version)
|
||||||
|
int generate_monitoring_event(void) {
|
||||||
|
return generate_event_driven_monitoring();
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to generate monitoring event for a specific type
|
// Helper function to generate monitoring event for a specific type
|
||||||
int generate_monitoring_event_for_type(const char* d_tag_value, cJSON* (*query_func)(void)) {
|
int generate_monitoring_event_for_type(const char* d_tag_value, cJSON* (*query_func)(void)) {
|
||||||
// Query the monitoring data
|
// Query the monitoring data
|
||||||
@@ -511,20 +534,42 @@ void monitoring_on_event_stored(void) {
|
|||||||
static time_t last_monitoring_time = 0;
|
static time_t last_monitoring_time = 0;
|
||||||
time_t current_time = time(NULL);
|
time_t current_time = time(NULL);
|
||||||
int throttle_seconds = get_monitoring_throttle_seconds();
|
int throttle_seconds = get_monitoring_throttle_seconds();
|
||||||
|
|
||||||
if (current_time - last_monitoring_time < throttle_seconds) {
|
if (current_time - last_monitoring_time < throttle_seconds) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if anyone is subscribed to monitoring events (kind 24567)
|
// Check if anyone is subscribed to monitoring events (kind 24567)
|
||||||
// This is the ONLY activation check needed - if someone subscribes, they want monitoring
|
// This is the ONLY activation check needed - if someone subscribes, they want monitoring
|
||||||
if (!has_subscriptions_for_kind(24567)) {
|
if (!has_subscriptions_for_kind(24567)) {
|
||||||
return; // No subscribers = no expensive operations
|
return; // No subscribers = no expensive operations
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate monitoring events only when someone is listening
|
// Generate event-driven monitoring events only when someone is listening
|
||||||
last_monitoring_time = current_time;
|
last_monitoring_time = current_time;
|
||||||
generate_monitoring_event();
|
generate_event_driven_monitoring();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitoring hook called when subscriptions change (create/close)
|
||||||
|
void monitoring_on_subscription_change(void) {
|
||||||
|
// Check throttling first (cheapest check)
|
||||||
|
static time_t last_monitoring_time = 0;
|
||||||
|
time_t current_time = time(NULL);
|
||||||
|
int throttle_seconds = get_monitoring_throttle_seconds();
|
||||||
|
|
||||||
|
if (current_time - last_monitoring_time < throttle_seconds) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if anyone is subscribed to monitoring events (kind 24567)
|
||||||
|
// This is the ONLY activation check needed - if someone subscribes, they want monitoring
|
||||||
|
if (!has_subscriptions_for_kind(24567)) {
|
||||||
|
return; // No subscribers = no expensive operations
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate subscription-driven monitoring events only when someone is listening
|
||||||
|
last_monitoring_time = current_time;
|
||||||
|
generate_subscription_driven_monitoring();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forward declaration for known_configs (defined in config.c)
|
// Forward declaration for known_configs (defined in config.c)
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ int handle_sql_query_unified(cJSON* event, const char* query, char* error_messag
|
|||||||
|
|
||||||
// Monitoring system functions
|
// Monitoring system functions
|
||||||
void monitoring_on_event_stored(void);
|
void monitoring_on_event_stored(void);
|
||||||
|
void monitoring_on_subscription_change(void);
|
||||||
int get_monitoring_throttle_seconds(void);
|
int get_monitoring_throttle_seconds(void);
|
||||||
|
|
||||||
#endif // API_H
|
#endif // API_H
|
||||||
23
src/config.c
23
src/config.c
@@ -814,7 +814,7 @@ int first_time_startup_sequence(const cli_options_t* cli_options, char* admin_pu
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int startup_existing_relay(const char* relay_pubkey, const cli_options_t* cli_options) {
|
int startup_existing_relay(const char* relay_pubkey, const cli_options_t* cli_options __attribute__((unused))) {
|
||||||
if (!relay_pubkey) {
|
if (!relay_pubkey) {
|
||||||
DEBUG_ERROR("Invalid relay pubkey for existing relay startup");
|
DEBUG_ERROR("Invalid relay pubkey for existing relay startup");
|
||||||
return -1;
|
return -1;
|
||||||
@@ -837,26 +837,7 @@ int startup_existing_relay(const char* relay_pubkey, const cli_options_t* cli_op
|
|||||||
|
|
||||||
// NOTE: Database is already initialized in main.c before calling this function
|
// NOTE: Database is already initialized in main.c before calling this function
|
||||||
// Config table should already exist with complete configuration
|
// Config table should already exist with complete configuration
|
||||||
|
// CLI overrides will be applied after this function returns in main.c
|
||||||
// Check if CLI overrides need to be applied
|
|
||||||
int has_overrides = 0;
|
|
||||||
if (cli_options) {
|
|
||||||
if (cli_options->port_override > 0) has_overrides = 1;
|
|
||||||
if (cli_options->admin_pubkey_override[0] != '\0') has_overrides = 1;
|
|
||||||
if (cli_options->relay_privkey_override[0] != '\0') has_overrides = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (has_overrides) {
|
|
||||||
// Apply CLI overrides to existing database
|
|
||||||
DEBUG_INFO("Applying CLI overrides to existing database");
|
|
||||||
if (apply_cli_overrides_atomic(cli_options) != 0) {
|
|
||||||
DEBUG_ERROR("Failed to apply CLI overrides to existing database");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No CLI overrides - config table is already available
|
|
||||||
DEBUG_INFO("No CLI overrides - config table is already available");
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
111
src/main.c
111
src/main.c
@@ -209,23 +209,21 @@ void signal_handler(int sig) {
|
|||||||
// Send NOTICE message to client (NIP-01)
|
// Send NOTICE message to client (NIP-01)
|
||||||
void send_notice_message(struct lws* wsi, const char* message) {
|
void send_notice_message(struct lws* wsi, const char* message) {
|
||||||
if (!wsi || !message) return;
|
if (!wsi || !message) return;
|
||||||
|
|
||||||
cJSON* notice_msg = cJSON_CreateArray();
|
cJSON* notice_msg = cJSON_CreateArray();
|
||||||
cJSON_AddItemToArray(notice_msg, cJSON_CreateString("NOTICE"));
|
cJSON_AddItemToArray(notice_msg, cJSON_CreateString("NOTICE"));
|
||||||
cJSON_AddItemToArray(notice_msg, cJSON_CreateString(message));
|
cJSON_AddItemToArray(notice_msg, cJSON_CreateString(message));
|
||||||
|
|
||||||
char* msg_str = cJSON_Print(notice_msg);
|
char* msg_str = cJSON_Print(notice_msg);
|
||||||
if (msg_str) {
|
if (msg_str) {
|
||||||
size_t msg_len = strlen(msg_str);
|
size_t msg_len = strlen(msg_str);
|
||||||
unsigned char* buf = malloc(LWS_PRE + msg_len);
|
// Use proper message queue system instead of direct lws_write
|
||||||
if (buf) {
|
if (queue_message(wsi, NULL, msg_str, msg_len, LWS_WRITE_TEXT) != 0) {
|
||||||
memcpy(buf + LWS_PRE, msg_str, msg_len);
|
DEBUG_ERROR("Failed to queue NOTICE message");
|
||||||
lws_write(wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
|
|
||||||
free(buf);
|
|
||||||
}
|
}
|
||||||
free(msg_str);
|
free(msg_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
cJSON_Delete(notice_msg);
|
cJSON_Delete(notice_msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,14 +313,35 @@ int init_database(const char* database_path_override) {
|
|||||||
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
|
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
|
||||||
// Check config table row count immediately after database open
|
// Check config table row count immediately after database open
|
||||||
sqlite3_stmt* stmt;
|
sqlite3_stmt* stmt;
|
||||||
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) {
|
int rc = sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL);
|
||||||
|
if (rc == SQLITE_OK) {
|
||||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||||
int row_count = sqlite3_column_int(stmt, 0);
|
int row_count = sqlite3_column_int(stmt, 0);
|
||||||
DEBUG_LOG("Config table row count immediately after sqlite3_open(): %d", row_count);
|
DEBUG_LOG("Config table row count immediately after sqlite3_open(): %d", row_count);
|
||||||
}
|
}
|
||||||
sqlite3_finalize(stmt);
|
sqlite3_finalize(stmt);
|
||||||
} else {
|
} else {
|
||||||
DEBUG_LOG("Config table does not exist yet (first-time startup)");
|
// Capture and log the actual SQLite error instead of assuming table doesn't exist
|
||||||
|
const char* err_msg = sqlite3_errmsg(g_db);
|
||||||
|
DEBUG_LOG("Failed to prepare config table query: %s (error code: %d)", err_msg, rc);
|
||||||
|
|
||||||
|
// Check if it's actually a missing table vs other error
|
||||||
|
if (rc == SQLITE_ERROR) {
|
||||||
|
// Try to check if config table exists
|
||||||
|
sqlite3_stmt* check_stmt;
|
||||||
|
int check_rc = sqlite3_prepare_v2(g_db, "SELECT name FROM sqlite_master WHERE type='table' AND name='config'", -1, &check_stmt, NULL);
|
||||||
|
if (check_rc == SQLITE_OK) {
|
||||||
|
int has_table = (sqlite3_step(check_stmt) == SQLITE_ROW);
|
||||||
|
sqlite3_finalize(check_stmt);
|
||||||
|
if (has_table) {
|
||||||
|
DEBUG_LOG("Config table EXISTS but query failed - possible database corruption or locking issue");
|
||||||
|
} else {
|
||||||
|
DEBUG_LOG("Config table does not exist yet (first-time startup)");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DEBUG_LOG("Failed to check table existence: %s (error code: %d)", sqlite3_errmsg(g_db), check_rc);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// DEBUG_GUARD_END
|
// DEBUG_GUARD_END
|
||||||
@@ -914,12 +933,11 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
|
|||||||
char* msg_str = cJSON_Print(event_msg);
|
char* msg_str = cJSON_Print(event_msg);
|
||||||
if (msg_str) {
|
if (msg_str) {
|
||||||
size_t msg_len = strlen(msg_str);
|
size_t msg_len = strlen(msg_str);
|
||||||
unsigned char* buf = malloc(LWS_PRE + msg_len);
|
// Use proper message queue system instead of direct lws_write
|
||||||
if (buf) {
|
if (queue_message(wsi, NULL, msg_str, msg_len, LWS_WRITE_TEXT) != 0) {
|
||||||
memcpy(buf + LWS_PRE, msg_str, msg_len);
|
DEBUG_ERROR("Failed to queue config EVENT message");
|
||||||
lws_write(wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
|
} else {
|
||||||
config_events_sent++;
|
config_events_sent++;
|
||||||
free(buf);
|
|
||||||
}
|
}
|
||||||
free(msg_str);
|
free(msg_str);
|
||||||
}
|
}
|
||||||
@@ -957,11 +975,9 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
|
|||||||
char* closed_str = cJSON_Print(closed_msg);
|
char* closed_str = cJSON_Print(closed_msg);
|
||||||
if (closed_str) {
|
if (closed_str) {
|
||||||
size_t closed_len = strlen(closed_str);
|
size_t closed_len = strlen(closed_str);
|
||||||
unsigned char* buf = malloc(LWS_PRE + closed_len);
|
// Use proper message queue system instead of direct lws_write
|
||||||
if (buf) {
|
if (queue_message(wsi, pss, closed_str, closed_len, LWS_WRITE_TEXT) != 0) {
|
||||||
memcpy(buf + LWS_PRE, closed_str, closed_len);
|
DEBUG_ERROR("Failed to queue CLOSED message");
|
||||||
lws_write(wsi, buf + LWS_PRE, closed_len, LWS_WRITE_TEXT);
|
|
||||||
free(buf);
|
|
||||||
}
|
}
|
||||||
free(closed_str);
|
free(closed_str);
|
||||||
}
|
}
|
||||||
@@ -1287,19 +1303,17 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
|
|||||||
cJSON_AddItemToArray(event_msg, cJSON_CreateString("EVENT"));
|
cJSON_AddItemToArray(event_msg, cJSON_CreateString("EVENT"));
|
||||||
cJSON_AddItemToArray(event_msg, cJSON_CreateString(sub_id));
|
cJSON_AddItemToArray(event_msg, cJSON_CreateString(sub_id));
|
||||||
cJSON_AddItemToArray(event_msg, event);
|
cJSON_AddItemToArray(event_msg, event);
|
||||||
|
|
||||||
char* msg_str = cJSON_Print(event_msg);
|
char* msg_str = cJSON_Print(event_msg);
|
||||||
if (msg_str) {
|
if (msg_str) {
|
||||||
size_t msg_len = strlen(msg_str);
|
size_t msg_len = strlen(msg_str);
|
||||||
unsigned char* buf = malloc(LWS_PRE + msg_len);
|
// Use proper message queue system instead of direct lws_write
|
||||||
if (buf) {
|
if (queue_message(wsi, pss, msg_str, msg_len, LWS_WRITE_TEXT) != 0) {
|
||||||
memcpy(buf + LWS_PRE, msg_str, msg_len);
|
DEBUG_ERROR("Failed to queue EVENT message for sub=%s", sub_id);
|
||||||
lws_write(wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
|
|
||||||
free(buf);
|
|
||||||
}
|
}
|
||||||
free(msg_str);
|
free(msg_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
cJSON_Delete(event_msg);
|
cJSON_Delete(event_msg);
|
||||||
events_sent++;
|
events_sent++;
|
||||||
}
|
}
|
||||||
@@ -1435,7 +1449,7 @@ void print_usage(const char* program_name) {
|
|||||||
printf("Options:\n");
|
printf("Options:\n");
|
||||||
printf(" -h, --help Show this help message\n");
|
printf(" -h, --help Show this help message\n");
|
||||||
printf(" -v, --version Show version information\n");
|
printf(" -v, --version Show version information\n");
|
||||||
printf(" -p, --port PORT Override relay port (first-time startup only)\n");
|
printf(" -p, --port PORT Override relay port (first-time startup and existing relay restarts)\n");
|
||||||
printf(" --strict-port Fail if exact port is unavailable (no port increment)\n");
|
printf(" --strict-port Fail if exact port is unavailable (no port increment)\n");
|
||||||
printf(" -a, --admin-pubkey KEY Override admin public key (64-char hex or npub)\n");
|
printf(" -a, --admin-pubkey KEY Override admin public key (64-char hex or npub)\n");
|
||||||
printf(" -r, --relay-privkey KEY Override relay private key (64-char hex or nsec)\n");
|
printf(" -r, --relay-privkey KEY Override relay private key (64-char hex or nsec)\n");
|
||||||
@@ -1445,13 +1459,14 @@ void print_usage(const char* program_name) {
|
|||||||
printf("Configuration:\n");
|
printf("Configuration:\n");
|
||||||
printf(" This relay uses event-based configuration stored in the database.\n");
|
printf(" This relay uses event-based configuration stored in the database.\n");
|
||||||
printf(" On first startup, keys are automatically generated and printed once.\n");
|
printf(" On first startup, keys are automatically generated and printed once.\n");
|
||||||
printf(" Command line options like --port only apply during first-time setup.\n");
|
printf(" Command line options like --port apply during first-time setup and existing relay restarts.\n");
|
||||||
printf(" After initial setup, all configuration is managed via database events.\n");
|
printf(" After initial setup, all configuration is managed via database events.\n");
|
||||||
printf(" Database file: <relay_pubkey>.db (created automatically)\n");
|
printf(" Database file: <relay_pubkey>.db (created automatically)\n");
|
||||||
printf("\n");
|
printf("\n");
|
||||||
printf("Port Binding:\n");
|
printf("Port Binding:\n");
|
||||||
printf(" Default: Try up to 10 consecutive ports if requested port is busy\n");
|
printf(" Default: Try up to 10 consecutive ports if requested port is busy\n");
|
||||||
printf(" --strict-port: Fail immediately if exact requested port is unavailable\n");
|
printf(" --strict-port: Fail immediately if exact requested port is unavailable\n");
|
||||||
|
printf(" --strict-port works with any custom port specified via -p or --port\n");
|
||||||
printf("\n");
|
printf("\n");
|
||||||
printf("Examples:\n");
|
printf("Examples:\n");
|
||||||
printf(" %s # Start relay (auto-configure on first run)\n", program_name);
|
printf(" %s # Start relay (auto-configure on first run)\n", program_name);
|
||||||
@@ -1798,7 +1813,7 @@ int main(int argc, char* argv[]) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup existing relay (sets database path and loads config)
|
// Setup existing relay FIRST (sets database path)
|
||||||
if (startup_existing_relay(relay_pubkey, &cli_options) != 0) {
|
if (startup_existing_relay(relay_pubkey, &cli_options) != 0) {
|
||||||
DEBUG_ERROR("Failed to setup existing relay");
|
DEBUG_ERROR("Failed to setup existing relay");
|
||||||
cleanup_configuration_system();
|
cleanup_configuration_system();
|
||||||
@@ -1811,23 +1826,7 @@ int main(int argc, char* argv[]) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check config table row count before database initialization
|
// Initialize database with the database path set by startup_existing_relay()
|
||||||
{
|
|
||||||
sqlite3* temp_db = NULL;
|
|
||||||
if (sqlite3_open(g_database_path, &temp_db) == SQLITE_OK) {
|
|
||||||
sqlite3_stmt* stmt;
|
|
||||||
if (sqlite3_prepare_v2(temp_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) {
|
|
||||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
||||||
int row_count = sqlite3_column_int(stmt, 0);
|
|
||||||
printf(" Config table row count before database initialization: %d\n", row_count);
|
|
||||||
}
|
|
||||||
sqlite3_finalize(stmt);
|
|
||||||
}
|
|
||||||
sqlite3_close(temp_db);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize database with existing database path
|
|
||||||
DEBUG_TRACE("Initializing existing database");
|
DEBUG_TRACE("Initializing existing database");
|
||||||
if (init_database(g_database_path) != 0) {
|
if (init_database(g_database_path) != 0) {
|
||||||
DEBUG_ERROR("Failed to initialize existing database");
|
DEBUG_ERROR("Failed to initialize existing database");
|
||||||
@@ -1842,6 +1841,20 @@ int main(int argc, char* argv[]) {
|
|||||||
}
|
}
|
||||||
DEBUG_LOG("Existing database initialized");
|
DEBUG_LOG("Existing database initialized");
|
||||||
|
|
||||||
|
// Apply CLI overrides atomically (now that database is initialized)
|
||||||
|
if (apply_cli_overrides_atomic(&cli_options) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to apply CLI overrides for existing relay");
|
||||||
|
cleanup_configuration_system();
|
||||||
|
free(relay_pubkey);
|
||||||
|
for (int i = 0; existing_files[i]; i++) {
|
||||||
|
free(existing_files[i]);
|
||||||
|
}
|
||||||
|
free(existing_files);
|
||||||
|
nostr_cleanup();
|
||||||
|
close_database();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
// DEBUG_GUARD_START
|
// DEBUG_GUARD_START
|
||||||
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
|
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
|
||||||
sqlite3_stmt* stmt;
|
sqlite3_stmt* stmt;
|
||||||
@@ -2010,8 +2023,8 @@ int main(int argc, char* argv[]) {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Start WebSocket Nostr relay server (port from configuration)
|
// Start WebSocket Nostr relay server (port from CLI override or configuration)
|
||||||
int result = start_websocket_relay(-1, cli_options.strict_port); // Let config system determine port, pass strict_port flag
|
int result = start_websocket_relay(cli_options.port_override, cli_options.strict_port); // Use CLI port override if specified, otherwise config
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
cleanup_relay_info();
|
cleanup_relay_info();
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
#define MAIN_H
|
#define MAIN_H
|
||||||
|
|
||||||
// Version information (auto-updated by build system)
|
// Version information (auto-updated by build system)
|
||||||
#define VERSION "v0.7.34"
|
#define VERSION "v0.7.39"
|
||||||
#define VERSION_MAJOR 0
|
#define VERSION_MAJOR 0
|
||||||
#define VERSION_MINOR 7
|
#define VERSION_MINOR 7
|
||||||
#define VERSION_PATCH 34
|
#define VERSION_PATCH 39
|
||||||
|
|
||||||
// Relay metadata (authoritative source for NIP-11 information)
|
// Relay metadata (authoritative source for NIP-11 information)
|
||||||
#define RELAY_NAME "C-Relay"
|
#define RELAY_NAME "C-Relay"
|
||||||
|
|||||||
@@ -70,11 +70,9 @@ void send_nip42_auth_challenge(struct lws* wsi, struct per_session_data* pss) {
|
|||||||
char* msg_str = cJSON_Print(auth_msg);
|
char* msg_str = cJSON_Print(auth_msg);
|
||||||
if (msg_str) {
|
if (msg_str) {
|
||||||
size_t msg_len = strlen(msg_str);
|
size_t msg_len = strlen(msg_str);
|
||||||
unsigned char* buf = malloc(LWS_PRE + msg_len);
|
// Use proper message queue system instead of direct lws_write
|
||||||
if (buf) {
|
if (queue_message(wsi, pss, msg_str, msg_len, LWS_WRITE_TEXT) != 0) {
|
||||||
memcpy(buf + LWS_PRE, msg_str, msg_len);
|
DEBUG_ERROR("Failed to queue AUTH challenge message");
|
||||||
lws_write(wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
|
|
||||||
free(buf);
|
|
||||||
}
|
}
|
||||||
free(msg_str);
|
free(msg_str);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ int validate_timestamp_range(long since, long until, char* error_message, size_t
|
|||||||
int validate_numeric_limits(int limit, char* error_message, size_t error_size);
|
int validate_numeric_limits(int limit, char* error_message, size_t error_size);
|
||||||
int validate_search_term(const char* search_term, char* error_message, size_t error_size);
|
int validate_search_term(const char* search_term, char* error_message, size_t error_size);
|
||||||
|
|
||||||
|
// Forward declaration for monitoring function
|
||||||
|
void monitoring_on_subscription_change(void);
|
||||||
|
|
||||||
// Global database variable
|
// Global database variable
|
||||||
extern sqlite3* g_db;
|
extern sqlite3* g_db;
|
||||||
|
|
||||||
@@ -310,6 +313,9 @@ int add_subscription_to_manager(subscription_t* sub) {
|
|||||||
// Log subscription creation to database (INSERT OR REPLACE handles duplicates)
|
// Log subscription creation to database (INSERT OR REPLACE handles duplicates)
|
||||||
log_subscription_created(sub);
|
log_subscription_created(sub);
|
||||||
|
|
||||||
|
// Trigger monitoring update for subscription changes
|
||||||
|
monitoring_on_subscription_change();
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,6 +363,9 @@ int remove_subscription_from_manager(const char* sub_id, struct lws* wsi) {
|
|||||||
// Update events sent counter before freeing
|
// Update events sent counter before freeing
|
||||||
update_subscription_events_sent(sub_id_copy, events_sent_copy);
|
update_subscription_events_sent(sub_id_copy, events_sent_copy);
|
||||||
|
|
||||||
|
// Trigger monitoring update for subscription changes
|
||||||
|
monitoring_on_subscription_change();
|
||||||
|
|
||||||
free_subscription(sub);
|
free_subscription(sub);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -379,30 +388,48 @@ int event_matches_filter(cJSON* event, subscription_filter_t* filter) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug: Log event details being tested
|
||||||
|
cJSON* event_kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||||
|
cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
|
||||||
|
cJSON* event_created_at_obj = cJSON_GetObjectItem(event, "created_at");
|
||||||
|
|
||||||
|
DEBUG_TRACE("FILTER_MATCH: Testing event kind=%d id=%.8s created_at=%ld",
|
||||||
|
event_kind_obj ? (int)cJSON_GetNumberValue(event_kind_obj) : -1,
|
||||||
|
event_id_obj && cJSON_IsString(event_id_obj) ? cJSON_GetStringValue(event_id_obj) : "null",
|
||||||
|
event_created_at_obj ? (long)cJSON_GetNumberValue(event_created_at_obj) : 0);
|
||||||
|
|
||||||
// Check kinds filter
|
// Check kinds filter
|
||||||
if (filter->kinds && cJSON_IsArray(filter->kinds)) {
|
if (filter->kinds && cJSON_IsArray(filter->kinds)) {
|
||||||
|
DEBUG_TRACE("FILTER_MATCH: Checking kinds filter with %d kinds", cJSON_GetArraySize(filter->kinds));
|
||||||
|
|
||||||
cJSON* event_kind = cJSON_GetObjectItem(event, "kind");
|
cJSON* event_kind = cJSON_GetObjectItem(event, "kind");
|
||||||
if (!event_kind || !cJSON_IsNumber(event_kind)) {
|
if (!event_kind || !cJSON_IsNumber(event_kind)) {
|
||||||
|
DEBUG_WARN("FILTER_MATCH: Event has no valid kind field");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int event_kind_val = (int)cJSON_GetNumberValue(event_kind);
|
int event_kind_val = (int)cJSON_GetNumberValue(event_kind);
|
||||||
int kind_match = 0;
|
DEBUG_TRACE("FILTER_MATCH: Event kind=%d", event_kind_val);
|
||||||
|
|
||||||
|
int kind_match = 0;
|
||||||
cJSON* kind_item = NULL;
|
cJSON* kind_item = NULL;
|
||||||
cJSON_ArrayForEach(kind_item, filter->kinds) {
|
cJSON_ArrayForEach(kind_item, filter->kinds) {
|
||||||
if (cJSON_IsNumber(kind_item)) {
|
if (cJSON_IsNumber(kind_item)) {
|
||||||
int filter_kind = (int)cJSON_GetNumberValue(kind_item);
|
int filter_kind = (int)cJSON_GetNumberValue(kind_item);
|
||||||
|
DEBUG_TRACE("FILTER_MATCH: Comparing event kind %d with filter kind %d", event_kind_val, filter_kind);
|
||||||
if (filter_kind == event_kind_val) {
|
if (filter_kind == event_kind_val) {
|
||||||
kind_match = 1;
|
kind_match = 1;
|
||||||
|
DEBUG_TRACE("FILTER_MATCH: Kind matched!");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!kind_match) {
|
if (!kind_match) {
|
||||||
|
DEBUG_TRACE("FILTER_MATCH: No kind match, filter rejected");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
DEBUG_TRACE("FILTER_MATCH: Kinds filter passed");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check authors filter
|
// Check authors filter
|
||||||
@@ -463,13 +490,19 @@ int event_matches_filter(cJSON* event, subscription_filter_t* filter) {
|
|||||||
if (filter->since > 0) {
|
if (filter->since > 0) {
|
||||||
cJSON* event_created_at = cJSON_GetObjectItem(event, "created_at");
|
cJSON* event_created_at = cJSON_GetObjectItem(event, "created_at");
|
||||||
if (!event_created_at || !cJSON_IsNumber(event_created_at)) {
|
if (!event_created_at || !cJSON_IsNumber(event_created_at)) {
|
||||||
|
DEBUG_WARN("FILTER_MATCH: Event has no valid created_at field");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
long event_timestamp = (long)cJSON_GetNumberValue(event_created_at);
|
long event_timestamp = (long)cJSON_GetNumberValue(event_created_at);
|
||||||
|
DEBUG_TRACE("FILTER_MATCH: Checking since filter: event_ts=%ld filter_since=%ld",
|
||||||
|
event_timestamp, filter->since);
|
||||||
|
|
||||||
if (event_timestamp < filter->since) {
|
if (event_timestamp < filter->since) {
|
||||||
|
DEBUG_TRACE("FILTER_MATCH: Event too old (before since), filter rejected");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
DEBUG_TRACE("FILTER_MATCH: Since filter passed");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check until filter
|
// Check until filter
|
||||||
@@ -551,6 +584,7 @@ int event_matches_filter(cJSON* event, subscription_filter_t* filter) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DEBUG_TRACE("FILTER_MATCH: All filters passed, event matches!");
|
||||||
return 1; // All filters passed
|
return 1; // All filters passed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,14 +594,23 @@ int event_matches_subscription(cJSON* event, subscription_t* subscription) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DEBUG_TRACE("SUB_MATCH: Testing subscription '%s'", subscription->id);
|
||||||
|
|
||||||
|
int filter_num = 0;
|
||||||
subscription_filter_t* filter = subscription->filters;
|
subscription_filter_t* filter = subscription->filters;
|
||||||
while (filter) {
|
while (filter) {
|
||||||
|
filter_num++;
|
||||||
|
DEBUG_TRACE("SUB_MATCH: Testing filter #%d", filter_num);
|
||||||
|
|
||||||
if (event_matches_filter(event, filter)) {
|
if (event_matches_filter(event, filter)) {
|
||||||
|
DEBUG_TRACE("SUB_MATCH: Filter #%d matched! Subscription '%s' matches",
|
||||||
|
filter_num, subscription->id);
|
||||||
return 1; // Match found (OR logic)
|
return 1; // Match found (OR logic)
|
||||||
}
|
}
|
||||||
filter = filter->next;
|
filter = filter->next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DEBUG_TRACE("SUB_MATCH: No filters matched for subscription '%s'", subscription->id);
|
||||||
return 0; // No filters matched
|
return 0; // No filters matched
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -589,7 +632,17 @@ int broadcast_event_to_subscriptions(cJSON* event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int broadcasts = 0;
|
int broadcasts = 0;
|
||||||
|
|
||||||
|
// Log event details
|
||||||
|
cJSON* event_kind = cJSON_GetObjectItem(event, "kind");
|
||||||
|
cJSON* event_id = cJSON_GetObjectItem(event, "id");
|
||||||
|
cJSON* event_created_at = cJSON_GetObjectItem(event, "created_at");
|
||||||
|
|
||||||
|
DEBUG_TRACE("BROADCAST: Event kind=%d id=%.8s created_at=%ld",
|
||||||
|
event_kind ? (int)cJSON_GetNumberValue(event_kind) : -1,
|
||||||
|
event_id && cJSON_IsString(event_id) ? cJSON_GetStringValue(event_id) : "null",
|
||||||
|
event_created_at ? (long)cJSON_GetNumberValue(event_created_at) : 0);
|
||||||
|
|
||||||
// Create a temporary list of matching subscriptions to avoid holding lock during I/O
|
// Create a temporary list of matching subscriptions to avoid holding lock during I/O
|
||||||
typedef struct temp_sub {
|
typedef struct temp_sub {
|
||||||
struct lws* wsi;
|
struct lws* wsi;
|
||||||
@@ -597,13 +650,21 @@ int broadcast_event_to_subscriptions(cJSON* event) {
|
|||||||
char client_ip[CLIENT_IP_MAX_LENGTH];
|
char client_ip[CLIENT_IP_MAX_LENGTH];
|
||||||
struct temp_sub* next;
|
struct temp_sub* next;
|
||||||
} temp_sub_t;
|
} temp_sub_t;
|
||||||
|
|
||||||
temp_sub_t* matching_subs = NULL;
|
temp_sub_t* matching_subs = NULL;
|
||||||
int matching_count = 0;
|
int matching_count = 0;
|
||||||
|
|
||||||
// First pass: collect matching subscriptions while holding lock
|
// First pass: collect matching subscriptions while holding lock
|
||||||
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||||||
|
|
||||||
|
int total_subs = 0;
|
||||||
|
subscription_t* count_sub = g_subscription_manager.active_subscriptions;
|
||||||
|
while (count_sub) {
|
||||||
|
total_subs++;
|
||||||
|
count_sub = count_sub->next;
|
||||||
|
}
|
||||||
|
DEBUG_TRACE("BROADCAST: Checking %d active subscriptions", total_subs);
|
||||||
|
|
||||||
subscription_t* sub = g_subscription_manager.active_subscriptions;
|
subscription_t* sub = g_subscription_manager.active_subscriptions;
|
||||||
while (sub) {
|
while (sub) {
|
||||||
if (sub->active && sub->wsi && event_matches_subscription(event, sub)) {
|
if (sub->active && sub->wsi && event_matches_subscription(event, sub)) {
|
||||||
|
|||||||
@@ -562,11 +562,9 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
|||||||
char *error_str = cJSON_Print(error_response);
|
char *error_str = cJSON_Print(error_response);
|
||||||
if (error_str) {
|
if (error_str) {
|
||||||
size_t error_len = strlen(error_str);
|
size_t error_len = strlen(error_str);
|
||||||
unsigned char *buf = malloc(LWS_PRE + error_len);
|
// Use proper message queue system instead of direct lws_write
|
||||||
if (buf) {
|
if (queue_message(wsi, pss, error_str, error_len, LWS_WRITE_TEXT) != 0) {
|
||||||
memcpy(buf + LWS_PRE, error_str, error_len);
|
DEBUG_ERROR("Failed to queue error response message");
|
||||||
lws_write(wsi, buf + LWS_PRE, error_len, LWS_WRITE_TEXT);
|
|
||||||
free(buf);
|
|
||||||
}
|
}
|
||||||
free(error_str);
|
free(error_str);
|
||||||
}
|
}
|
||||||
|
|||||||
40
systemd/c-relay-local.service
Normal file
40
systemd/c-relay-local.service
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=C Nostr Relay Server (Local Development)
|
||||||
|
Documentation=https://github.com/your-repo/c-relay
|
||||||
|
After=network.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=teknari
|
||||||
|
WorkingDirectory=/home/teknari/Storage/c_relay
|
||||||
|
Environment=DEBUG_LEVEL=0
|
||||||
|
ExecStart=/home/teknari/Storage/c_relay/crelay --port 7777 --debug-level=$DEBUG_LEVEL
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=c-relay-local
|
||||||
|
|
||||||
|
# Security settings (relaxed for local development)
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ReadWritePaths=/home/teknari/Storage/c_relay
|
||||||
|
PrivateTmp=true
|
||||||
|
|
||||||
|
# Network security
|
||||||
|
PrivateNetwork=false
|
||||||
|
RestrictAddressFamilies=AF_INET AF_INET6
|
||||||
|
|
||||||
|
# Resource limits
|
||||||
|
LimitNOFILE=65536
|
||||||
|
LimitNPROC=4096
|
||||||
|
|
||||||
|
# Event-based configuration system
|
||||||
|
# No environment variables needed - all configuration is stored as Nostr events
|
||||||
|
# Database files (<relay_pubkey>.db) are created automatically in WorkingDirectory
|
||||||
|
# Admin keys are generated and displayed only during first startup
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Reference in New Issue
Block a user