The Directory comes from the directory service (#205)
ipx asks ipx-directory on its people's behalf, with the token an admin set in the new Admin, Directory tab, so the page never talks to it and never sees the token or a feed's address. /api/directory, /api/popular and a feed's Directory page answer from it; subscribing takes the address from the directory and goes through Add feed's steps. With no directory set up, the Directory says so. The local directory code is gone; its privacy checks are the snapshot builder's now, and their tests moved with them. With sharing on, ipx sends its snapshot after every scan, whenever the config changes (set_cfg) and every half hour: each public feed and how many here subscribe, never an OPML or Patreon creator itself or anything private. A feed can be marked private in its settings, for a paid feed whose address does not give it away; anyone subscribed may mark it, only an admin unmark it. The admin's category for a feed that names none goes with it, which the directory uses only while the feed names none, so the category an admin gives a blog still reaches the Directory. The directory lists only feeds it has read as a feed: the first snapshot after a start went before the first scan, when an OPML was not yet known to be one, and the directory listed the OPML. It now fetches what a snapshot brings in at once instead of within the minute. The browser suite runs its own ipx-directory on SQLite; the Directory's tests match feeds by title, since the directory names them by address. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
let me: {name: string} | null = null;
|
||||
api('/api/me').then(u => { me = u; }).catch(() => {});
|
||||
|
||||
const SECTIONS: Record<string, () => void> = {server: drawServer, accounts: drawAccounts, log: drawLogView};
|
||||
const SECTIONS: Record<string, () => void> = {server: drawServer, accounts: drawAccounts, directory: drawDirectorySettings, log: drawLogView};
|
||||
|
||||
function showSection(){
|
||||
const t = SECTIONS[location.hash.slice(1)] ? location.hash.slice(1) : 'server';
|
||||
@@ -70,6 +70,67 @@ async function drawServer(){
|
||||
};
|
||||
}
|
||||
|
||||
/* ---------------- directory ---------------- */
|
||||
// The directory service the Directory shows (#205): where it is, this server's token for it, and
|
||||
// whether to share. The token is sent, never shown: the server says only whether one is set.
|
||||
const ACCESS: Record<string, string> = {ro: 'browse', rw: 'browse and share', rwa: 'browse, share and administer the directory'};
|
||||
|
||||
async function drawDirectorySettings(){
|
||||
const box = $('#directory');
|
||||
const g = await api('/api/settings');
|
||||
box.innerHTML = `<h2>Directory</h2>
|
||||
<p class="hint">The Directory shows podcasts from a directory service that iPX servers share.
|
||||
Without one, people add feeds by their address.</p>
|
||||
<div class="field"><label for="durl">Directory address</label>
|
||||
<input type="text" inputmode="url" id="durl" value="${esc(g.directory_url)}" placeholder="https://ipx-dir.example.net" spellcheck="false"></div>
|
||||
<div class="field"><label for="dtoken">Access token</label>
|
||||
<div class="inline"><input type="password" id="dtoken" autocomplete="off" spellcheck="false"
|
||||
placeholder="${g.directory_token_set ? 'Set. Paste a new one to replace it' : 'Paste the token the directory gave this server'}">
|
||||
${g.directory_token_set ? `<button type="button" class="btn" id="dclear">Clear</button>` : ''}</div>
|
||||
<span class="hint">Whoever runs the directory makes one for each server, and can revoke it.</span></div>
|
||||
<div class="field"><label class="check"><input type="checkbox" id="dshare"${g.directory_share ? ' checked' : ''}> Share this server's public feeds</label>
|
||||
<span class="hint">Sends the directory the feeds people here subscribe to, and how many of them do, every
|
||||
half hour. Never a private feed: one marked private, one with a login, or one whose address carries a key.</span></div>
|
||||
<div class="field"><label>Status</label><p class="hint" id="dstatus">Asking the directory…</p></div>
|
||||
<div class="cardacts"><button class="btn primary" id="dsave">${ICON.check} Save</button></div>`;
|
||||
const save = async (patch: Record<string, unknown>, said: string) => {
|
||||
try{
|
||||
await api('/api/settings', {method: 'PATCH', body: JSON.stringify(patch)});
|
||||
toast(said);
|
||||
drawDirectorySettings();
|
||||
}catch(e){ toast(e.message, true); }
|
||||
};
|
||||
$('#dsave').onclick = () => {
|
||||
const patch: Record<string, unknown> = {directory_url: $('#durl').value.trim(), directory_share: $('#dshare').checked};
|
||||
// Blank leaves the token as it is; Clear is the way to remove it.
|
||||
if($('#dtoken').value.trim()) patch.directory_token = $('#dtoken').value.trim();
|
||||
save(patch, 'Directory settings saved');
|
||||
};
|
||||
const clear = $('#dclear');
|
||||
if(clear) clear.onclick = () => save({directory_token: ''}, 'Token cleared');
|
||||
drawDirectoryStatus();
|
||||
}
|
||||
|
||||
async function drawDirectoryStatus(){
|
||||
const el = $('#dstatus');
|
||||
let s;
|
||||
try{ s = await api('/api/directory-status'); }catch(e){ el.textContent = e.message; return; }
|
||||
if(!el.isConnected) return;
|
||||
const lines: string[] = [];
|
||||
if(!s.configured) lines.push('Not set up: it needs an address and a token.');
|
||||
else if(s.error) lines.push(esc(s.error));
|
||||
else lines.push(`Connected as <b>${esc(s.name)}</b>, which may ${esc(ACCESS[s.access] || s.access)}.`);
|
||||
const sh = s.shared;
|
||||
if(sh){
|
||||
const when = new Date(sh.at * 1000).toLocaleString();
|
||||
lines.push(sh.error ? `Sharing failed at ${esc(when)}: ${esc(sh.error)}`
|
||||
: `Shared ${plural(sh.accepted, 'feed')} at ${esc(when)}.` +
|
||||
(sh.refused_private ? ` The directory refused ${plural(sh.refused_private, 'feed')} as private.` : '') +
|
||||
(sh.refused_blocked ? ` ${plural(sh.refused_blocked, 'feed')} the directory has removed.` : ''));
|
||||
}
|
||||
el.innerHTML = lines.join('<br>');
|
||||
}
|
||||
|
||||
/* ---------------- accounts ---------------- */
|
||||
async function drawAccounts(){
|
||||
const box = $('#accounts');
|
||||
|
||||
Reference in New Issue
Block a user