Skip to main content
Glama
user-feed.ts5.85 kB
// ============================================================================= // kivv - User Feed Endpoint Handlers // ============================================================================= // Handles RSS and Atom feed generation for user paper libraries // Public endpoints (no authentication required) // ============================================================================= import { Context } from 'hono'; import { PaperWithStatus } from '../../../shared/types'; import { generateRSS, generateAtom } from './rss-generator'; import { HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_OK } from '../../../shared/constants'; // ============================================================================= // Username Validation // ============================================================================= /** * Validate username to prevent SQL injection and path traversal attacks * Only allows alphanumeric characters and underscores * * @param username - Username to validate * @returns true if username is valid, false otherwise */ function isValidUsername(username: string): boolean { if (!username || username.length === 0) return false; if (username.length > 50) return false; // Reasonable max length return /^[a-zA-Z0-9_]+$/.test(username); } // ============================================================================= // RSS Feed Endpoint // ============================================================================= /** * GET /feeds/:username/rss.xml * * Generate RSS 2.0 feed for a user's paper library * Public endpoint - no authentication required * * @param c - Hono context * @returns RSS 2.0 XML response */ export async function getUserRSSFeed(c: Context) { const username = c.req.param('username'); // Validate username to prevent injection attacks if (!isValidUsername(username)) { return c.text('Invalid username format. Only alphanumeric characters and underscores allowed.', HTTP_BAD_REQUEST); } try { // Find user by username const user = await c.env.DB .prepare('SELECT id FROM users WHERE username = ?') .bind(username) .first<{ id: number }>(); if (!user) { return c.text('User not found', HTTP_NOT_FOUND); } // Fetch user's papers (latest 50, newest first) const result = await c.env.DB .prepare(` SELECT p.id, p.arxiv_id, p.title, p.authors, p.abstract, p.categories, p.published_date, p.pdf_url, p.r2_key, p.summary, p.summary_generated_at, p.summary_model, p.relevance_score, p.content_hash, p.collected_for_user_id, p.created_at, ups.explored, ups.bookmarked, ups.notes FROM papers p LEFT JOIN user_paper_status ups ON p.id = ups.paper_id AND ups.user_id = ? WHERE p.collected_for_user_id = ? ORDER BY p.published_date DESC LIMIT 50 `) .bind(user.id, user.id) .all<PaperWithStatus>(); const papers = result.results || []; // Generate RSS XML const rssXml = generateRSS(username, papers); // Return RSS feed with correct MIME type return c.text(rssXml, HTTP_OK, { 'Content-Type': 'application/rss+xml; charset=utf-8', 'Cache-Control': 'public, max-age=300', // Cache for 5 minutes }); } catch (error) { console.error('Error generating RSS feed:', error); return c.text('Internal server error', 500); } } // ============================================================================= // Atom Feed Endpoint // ============================================================================= /** * GET /feeds/:username/atom.xml * * Generate Atom 1.0 feed for a user's paper library * Public endpoint - no authentication required * * @param c - Hono context * @returns Atom 1.0 XML response */ export async function getUserAtomFeed(c: Context) { const username = c.req.param('username'); // Validate username to prevent injection attacks if (!isValidUsername(username)) { return c.text('Invalid username format. Only alphanumeric characters and underscores allowed.', HTTP_BAD_REQUEST); } try { // Find user by username const user = await c.env.DB .prepare('SELECT id FROM users WHERE username = ?') .bind(username) .first<{ id: number }>(); if (!user) { return c.text('User not found', HTTP_NOT_FOUND); } // Fetch user's papers (latest 50, newest first) const result = await c.env.DB .prepare(` SELECT p.id, p.arxiv_id, p.title, p.authors, p.abstract, p.categories, p.published_date, p.pdf_url, p.r2_key, p.summary, p.summary_generated_at, p.summary_model, p.relevance_score, p.content_hash, p.collected_for_user_id, p.created_at, ups.explored, ups.bookmarked, ups.notes FROM papers p LEFT JOIN user_paper_status ups ON p.id = ups.paper_id AND ups.user_id = ? WHERE p.collected_for_user_id = ? ORDER BY p.published_date DESC LIMIT 50 `) .bind(user.id, user.id) .all<PaperWithStatus>(); const papers = result.results || []; // Generate Atom XML const atomXml = generateAtom(username, papers); // Return Atom feed with correct MIME type return c.text(atomXml, HTTP_OK, { 'Content-Type': 'application/atom+xml; charset=utf-8', 'Cache-Control': 'public, max-age=300', // Cache for 5 minutes }); } catch (error) { console.error('Error generating Atom feed:', error); return c.text('Internal server error', 500); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jeffaf/kivv'

If you have feedback or need assistance with the MCP directory API, please join our Discord server