diff --git a/cloudflare worker.js b/cloudflare worker.js
deleted file mode 100644
index d505e42..0000000
--- a/cloudflare worker.js
+++ /dev/null
@@ -1,2316 +0,0 @@
-export default {
- async fetch(request, env, ctx) {
- try {
- const url = new URL(request.url);
-
- // Acceptable hostnames
- const validHostnames = [
- 'theradicalparty.com',
- 'www.theradicalparty.com',
- 'radical.omar-c29.workers.dev'
- ];
-
- // Check if the hostname is valid
- if (!validHostnames.includes(url.hostname)) {
- console.error(`Unexpected hostname: ${url.hostname}`);
- return new Response('Invalid hostname', { status: 400 });
- }
-
- const path = url.pathname;
- const proposalId = url.searchParams.get('proposal');
-
- console.log(`Processing request: ${url.toString()}`);
- console.log(`User-Agent: ${request.headers.get('User-Agent')}`);
-
- // Handle CORS preflight requests first
- if (request.method === 'OPTIONS') {
- return handleOptions();
- }
-
- // For GET requests, try to serve from cache first
- if (request.method === "GET") {
- // Create a cache key from the request URL
- const cacheKey = new Request(request.url, {
- headers: request.headers
- });
-
- // Try to get from cache first
- let cachedResponse = await caches.default.match(cacheKey);
-
- if (cachedResponse) {
- // Add header to indicate cache hit
- cachedResponse = new Response(cachedResponse.body, cachedResponse);
- cachedResponse.headers.set("CF-Cache-Status", "HIT");
-
- // Ensure CORS headers are present
- Object.keys(corsHeaders).forEach(key => {
- cachedResponse.headers.set(key, corsHeaders[key]);
- });
-
- return cachedResponse;
- }
- }
-
- // Handle styles.css route
- if (path === '/styles.css') {
- try {
- // Fetch CSS from GitHub Pages
- const response = await fetch('https://radical.pages.dev/radical/styles.css', {
- headers: {
- 'User-Agent': request.headers.get('User-Agent') || 'Cloudflare Worker'
- }
- });
-
- if (!response.ok) {
- console.error('Failed to fetch styles.css', response.status, response.statusText);
- return new Response('CSS File Not Found', { status: 404 });
- }
-
- // Get the CSS content
- const cssContent = await response.text();
-
- // Return CSS response with caching headers
- const cssResponse = new Response(cssContent, {
- status: 200,
- headers: {
- 'Content-Type': 'text/css',
- 'Cache-Control': 'public, max-age=86400', // 1 day
- ...corsHeaders // Add CORS headers
- }
- });
-
- // Store in cache if this is a GET request
- if (request.method === "GET") {
- const cacheKey = new Request(request.url, {
- headers: request.headers
- });
- ctx.waitUntil(caches.default.put(cacheKey, cssResponse.clone()));
- }
-
- return cssResponse;
- } catch (error) {
- console.error('Error fetching styles.css:', error);
- return new Response('Internal Server Error', { status: 500 });
- }
- }
-
- const isTestMeta = url.searchParams.get('test_meta') === 'true';
-
- // Handle root path and index.html with potential meta tag testing
- if (path === '/' || path === '/index.html') {
- // If test_meta is true or there's a proposal, use customization
- if (isTestMeta || proposalId) {
- return await serveCustomizedHtml(proposalId || 'test_proposal', request, env);
- }
-
- // Otherwise, serve main page normally
- return await serveMainPage(request, env);
- }
-
- if (path === '/api/petition-svg') {
- return await servePetitionSVG(request, env);
- }
-
- // Meta tag testing
- if (url.searchParams.get('test_meta') === 'true') {
- console.log('Meta tag test detected');
-
- // If there's a proposal during meta test, use customization
- if (proposalId && (path === '/' || path === '/index.html')) {
- console.log(`Detected proposal in URL during meta test: ${proposalId}`);
- return await serveCustomizedHtml(proposalId, request, env);
- }
- }
-
- console.log(`Received ${request.method} request to ${path}`);
-
- // Check if the DB binding exists
- if (!env.DB) {
- console.error('DB binding is missing! Check your wrangler.toml configuration.');
- return createResponse({
- error: 'Database configuration error. Missing DB binding.',
- hint: 'Check your wrangler.toml file and make sure the DB binding is correctly set up.'
- }, 500);
- }
-
- // Handle media files (memes and audio)
- if (path.startsWith('/memes/') || path.startsWith('/audio/')) {
- const mediaResponse = await serveMedia(request, env);
-
- // Cache media responses for GET requests
- if (request.method === "GET") {
- const cacheKey = new Request(request.url, {
- headers: request.headers
- });
-
- // Clone the response before modifying
- const responseToCache = new Response(mediaResponse.body, mediaResponse);
-
- // Cache media files for 7 days
- responseToCache.headers.set('Cache-Control', 'public, max-age=604800');
-
- ctx.waitUntil(caches.default.put(cacheKey, responseToCache.clone()));
- }
-
- return mediaResponse;
- } else if (path === '/api/memes' && request.method === 'POST') {
- return await uploadMeme(request, env);
- }
-
- // Route requests to appropriate handlers
- let response;
-
- if (path === '/api/health') {
- response = await healthCheck(env);
- } else if (path === '/api/debug-meta') {
- response = await debugMetaTags(request, env);
- } else if (path === '/login' || path === '/login.html') {
- response = await serveLoginPage(request, env);
- } else if (path === '/api/login' && request.method === 'POST') {
- response = await validateLogin(request, env);
- } else if (path === '/api/proposals' && request.method === 'GET') {
- response = await getProposals(request, env);
- } else if (path === '/api/proposals' && request.method === 'POST') {
- response = await createProposal(request, env);
- } else if (path.startsWith('/api/proposals/') && request.method === 'GET') {
- response = await getProposalById(request, env);
- } else if (path.startsWith('/api/proposals/') && request.method === 'PUT') {
- const id = path.split('/').pop();
- response = await updateProposal(id, request, env);
- } else if (path === '/api/votes' && request.method === 'POST') {
- response = await createOrUpdateVote(request, env);
- } else if (path === '/api/users' && request.method === 'POST') {
- response = await createOrGetUser(request, env);
- } else if (path === '/api/petition-stats' && request.method === 'GET') {
- response = await getPetitionStats(request, env);
- } else if (path === '/api/comments' && request.method === 'GET') {
- response = await getComments(request, env);
- } else if (path === '/api/comments' && request.method === 'POST') {
- response = await createComment(request, env);
- } else if (path === '/api/comment-votes' && request.method === 'POST') {
- response = await createOrUpdateCommentVote(request, env);
- } else if (path === '/api/comment-votes' && request.method === 'GET') {
- response = await getCommentVotes(request, env);
- } else {
- response = createResponse({
- error: 'Not found',
- path,
- method: request.method,
- availableRoutes: [
- { path: '/api/health', method: 'GET', description: 'Check system health' },
- { path: '/api/proposals', method: 'GET', description: 'Get proposals' },
- { path: '/api/proposals/:id', method: 'GET', description: 'Get a single proposal by ID' },
- { path: '/api/proposals/:id', method: 'PUT', description: 'Update proposal' },
- { path: '/api/votes', method: 'POST', description: 'Create/update vote' },
- { path: '/api/users', method: 'POST', description: 'Create/get user' },
- { path: '/api/petition-stats', method: 'GET', description: 'Get petition statistics' }
- ]
- }, 404);
- }
-
- // Cache certain GET responses
- if (request.method === "GET") {
- const shouldCache = path.startsWith('/api/proposals') ||
- path.startsWith('/api/comments') ||
- path === '/api/petition-stats' ||
- path === '/api/health';
-
- if (shouldCache && response) {
- // We need to clone the response before reading its body
- const responseToCache = new Response(response.body, response);
-
- // Set appropriate cache duration based on endpoint
- let cacheDuration = 300; // 5 minutes default for API
- if (path === '/api/health') {
- cacheDuration = 3600; // 1 hour for health checks
- }
-
- responseToCache.headers.set('Cache-Control', `public, max-age=${cacheDuration}`);
- responseToCache.headers.set('CF-Cache-Status', 'MISS');
-
- // Ensure all CORS headers are present
- Object.keys(corsHeaders).forEach(key => {
- responseToCache.headers.set(key, corsHeaders[key]);
- });
-
- const cacheKey = new Request(request.url, {
- headers: request.headers
- });
-
- ctx.waitUntil(caches.default.put(cacheKey, responseToCache.clone()));
-
- // Make sure the response we return also has CORS headers
- Object.keys(corsHeaders).forEach(key => {
- response.headers.set(key, corsHeaders[key]);
- });
- }
- }
-
- return response;
-
- } catch (error) {
- console.error('Critical error in request handling:', error);
- return new Response(JSON.stringify({
- error: 'Internal Server Error',
- details: error.message
- }), {
- status: 500,
- headers: {
- 'Content-Type': 'application/json',
- ...corsHeaders // Add CORS headers to error responses too
- }
- });
- }
- }
-};
-
-
-async function serveMainPage(request, env) {
- try {
- // Fetch directly from Cloudflare Pages
- const response = await fetch('https://radical.pages.dev/index.html', {
- headers: {
- 'User-Agent': request.headers.get('User-Agent') || 'Cloudflare Worker'
- }
- });
-
- if (!response.ok) {
- console.error('Failed to fetch main page', response.status, response.statusText);
- return new Response('Failed to load page', { status: 500 });
- }
-
- // Read the HTML content
- const html = await response.text();
-
- // Create a new response with the HTML content
- return new Response(html, {
- status: 200,
- headers: {
- 'Content-Type': 'text/html',
- 'Cache-Control': 'public, max-age=86400',
- // Add CORS headers if needed
- 'Access-Control-Allow-Origin': '*'
- }
- });
- } catch (error) {
- console.error('Comprehensive error serving main page:', error);
- return new Response(`Internal Server Error: ${error.message}`, { status: 500 });
- }
-}
-
-async function serveLoginPage(request, env) {
- try {
- // Fetch the login HTML file from your storage
- // You could store this in R2 bucket or GitHub Pages like your other static files
- const response = await fetch('https://radical.pages.dev/radical/login.html', {
- headers: {
- 'User-Agent': request.headers.get('User-Agent') || 'Cloudflare Worker'
- }
- });
-
- if (!response.ok) {
- console.error('Failed to fetch login page', response.status, response.statusText);
- return new Response('Failed to load login page', { status: 500 });
- }
-
- // Read the HTML content
- const html = await response.text();
-
- // Create a new response with the HTML content
- return new Response(html, {
- status: 200,
- headers: {
- 'Content-Type': 'text/html',
- 'Cache-Control': 'public, max-age=86400',
- 'Access-Control-Allow-Origin': '*'
- }
- });
- } catch (error) {
- console.error('Error serving login page:', error);
- return new Response(`Internal Server Error: ${error.message}`, { status: 500 });
- }
-}
-
-// Function to validate user login with emoji combination and selections
-async function validateLogin(request, env) {
- try {
- const data = await request.json();
- const {
- emojiCombination,
- selectedDictator,
- selectedTarget
- } = data;
-
- console.log(`Processing login attempt: ${JSON.stringify({
- emojiCount: emojiCombination?.length || 0,
- dictator: selectedDictator,
- target: selectedTarget
- })}`);
-
- // Basic validation
- if (!emojiCombination || !selectedDictator || !selectedTarget) {
- return createResponse({
- error: 'Missing required fields',
- received: {
- hasEmojis: !!emojiCombination,
- hasDictator: !!selectedDictator,
- hasTarget: !!selectedTarget
- }
- }, 400);
- }
-
- // Convert emoji array to string for storage/comparison
- const emojiCombinationString = Array.isArray(emojiCombination)
- ? emojiCombination.join('')
- : emojiCombination;
-
- // Check if dictator exists
- const dictatorCheck = await env.DB.prepare(
- `SELECT id FROM dictators WHERE id = ?`
- ).bind(selectedDictator).first();
-
- if (!dictatorCheck) {
- return createResponse({
- error: 'Invalid dictator selection',
- dictatorId: selectedDictator
- }, 400);
- }
-
- // Check if target exists
- const targetCheck = await env.DB.prepare(
- `SELECT id FROM time_targets WHERE id = ?`
- ).bind(selectedTarget).first();
-
- if (!targetCheck) {
- return createResponse({
- error: 'Invalid time target selection',
- targetId: selectedTarget
- }, 400);
- }
-
- // Record the login attempt (for security tracking)
- const attemptId = 'login_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
- const timestamp = Date.now();
- const clientIP = request.headers.get('CF-Connecting-IP') || 'unknown';
-
- try {
- // Try to find a user with the exact combination
- const user = await env.DB.prepare(`
- SELECT id, name, emoji_combination, selected_dictator_id, selected_target_id
- FROM users
- WHERE emoji_combination = ?
- AND selected_dictator_id = ?
- AND selected_target_id = ?
- `).bind(
- emojiCombinationString,
- selectedDictator,
- selectedTarget
- ).first();
-
- // Log the attempt, regardless of outcome
- await env.DB.prepare(`
- INSERT INTO login_attempts (
- id,
- user_id,
- emoji_combination,
- dictator_id,
- target_id,
- timestamp,
- ip_address,
- success
- )
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
- `).bind(
- attemptId,
- user ? user.id : null,
- emojiCombinationString,
- selectedDictator,
- selectedTarget,
- timestamp,
- clientIP,
- user ? 1 : 0
- ).run();
-
- if (user) {
- // Login successful - Update last login time
- await env.DB.prepare(`
- UPDATE users
- SET last_login = ?
- WHERE id = ?
- `).bind(timestamp, user.id).run();
-
- console.log(`Successful login for user ${user.id}`);
-
- // Return success with user info (excluding sensitive data)
- return createResponse({
- success: true,
- userId: user.id,
- username: user.name,
- message: "Login successful"
- });
- } else {
- // No matching user found - this could be a new signup
- // Check if this is a unique emoji combination
- const existingCombo = await env.DB.prepare(`
- SELECT COUNT(*) as count
- FROM users
- WHERE emoji_combination = ?
- `).bind(emojiCombinationString).first();
-
- if (existingCombo.count > 0) {
- // Emoji combination exists but other parameters don't match
- return createResponse({
- error: "Invalid credentials",
- message: "The combination you entered is incorrect"
- }, 401);
- }
-
- // This is a new user registration
- const newUserId = 'citizen_' + Math.random().toString(36).substr(2, 9);
- const newUsername = 'Citizen ' + Math.floor(Math.random() * 9000 + 1000);
-
- // Create the new user
- await env.DB.prepare(`
- INSERT INTO users (
- id,
- name,
- created_at,
- emoji_combination,
- selected_dictator_id,
- selected_target_id,
- last_login
- )
- VALUES (?, ?, ?, ?, ?, ?, ?)
- `).bind(
- newUserId,
- newUsername,
- timestamp,
- emojiCombinationString,
- selectedDictator,
- selectedTarget,
- timestamp
- ).run();
-
- console.log(`Created new user ${newUserId} with emoji login`);
-
- // Return success with new user info
- return createResponse({
- success: true,
- isNewUser: true,
- userId: newUserId,
- username: newUsername,
- message: "Account created successfully"
- });
- }
- } catch (dbError) {
- console.error('Database error during login validation:', dbError);
-
- return createResponse({
- error: "Login processing error",
- details: "An error occurred while processing your login"
- }, 500);
- }
- } catch (error) {
- console.error('Error in login validation:', error);
-
- return createResponse({
- error: "Login request error",
- message: "Could not process login request"
- }, 500);
- }
-}
-
-async function createOrUpdateCommentVote(request, env) {
- try {
- const data = await request.json();
- const { commentId, userId, voteType } = data;
-
- console.log(`Processing comment vote: ${JSON.stringify({
- commentId,
- userId,
- voteType
- })}`);
-
- if (!commentId || !userId || !voteType) {
- return createResponse({
- error: 'Missing required fields',
- received: { commentId, userId, voteType: voteType || 'undefined' }
- }, 400);
- }
-
- if (voteType !== 'upvote' && voteType !== 'downvote') {
- return createResponse({
- error: 'Invalid vote type',
- voteType,
- allowedValues: ['upvote', 'downvote']
- }, 400);
- }
-
- // Check if entities exist
- try {
- const userCheck = await env.DB.prepare(`SELECT id FROM users WHERE id = ?`).bind(userId).first();
- if (!userCheck) {
- return createResponse({ error: 'User does not exist', userId }, 404);
- }
-
- const commentCheck = await env.DB.prepare(`SELECT id FROM comments WHERE id = ?`).bind(commentId).first();
- if (!commentCheck) {
- return createResponse({ error: 'Comment does not exist', commentId }, 404);
- }
- } catch (checkError) {
- return createResponse({
- error: 'Error checking user or comment existence',
- details: logError(checkError, { action: 'check_entities', userId, commentId })
- }, 500);
- }
-
- // Check if vote already exists
- let existingVote;
- try {
- const checkQuery = `
- SELECT id, vote_type FROM comment_votes
- WHERE comment_id = ? AND user_id = ?
- `;
-
- existingVote = await env.DB.prepare(checkQuery).bind(commentId, userId).first();
- console.log(`Existing comment vote check: ${existingVote ? JSON.stringify(existingVote) : 'None found'}`);
- } catch (voteCheckError) {
- return createResponse({
- error: 'Error checking existing comment vote',
- details: logError(voteCheckError, { action: 'check_comment_vote', userId, commentId })
- }, 500);
- }
-
- const timestamp = Date.now();
- const voteId = existingVote ? existingVote.id : 'comment_vote_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
-
- try {
- // Process vote
- if (existingVote) {
- // If vote exists and it's the same type, remove it (toggle off)
- if (existingVote.vote_type === voteType) {
- await env.DB.prepare(`
- DELETE FROM comment_votes
- WHERE id = ?
- `).bind(existingVote.id).run();
-
- console.log(`Deleted comment vote ${existingVote.id} (toggle off)`);
-
- return createResponse({
- commentId,
- userId,
- voteType: null,
- action: 'removed'
- });
- } else {
- // If vote exists but is different type, update it
- await env.DB.prepare(`
- UPDATE comment_votes
- SET vote_type = ?, timestamp = ?
- WHERE id = ?
- `).bind(
- voteType,
- timestamp,
- existingVote.id
- ).run();
-
- console.log(`Updated comment vote ${existingVote.id} to ${voteType}`);
-
- return createResponse({
- commentId,
- userId,
- voteType,
- action: 'updated'
- });
- }
- } else {
- // If no vote exists, create new one
- await env.DB.prepare(`
- INSERT INTO comment_votes (id, comment_id, user_id, vote_type, timestamp)
- VALUES (?, ?, ?, ?, ?)
- `).bind(
- voteId,
- commentId,
- userId,
- voteType,
- timestamp
- ).run();
-
- console.log(`Created new comment vote with ID: ${voteId}`);
-
- return createResponse({
- commentId,
- userId,
- voteType,
- action: 'created'
- });
- }
- } catch (dbError) {
- return createResponse({
- error: 'Failed to process comment vote in database',
- details: logError(dbError, {
- action: 'process_comment_vote',
- existingVote: existingVote || null,
- newVote: { commentId, userId, voteType }
- })
- }, 500);
- }
- } catch (error) {
- return createResponse({
- error: 'Failed to process comment vote request',
- details: logError(error, { action: 'comment_vote_outer' })
- }, 500);
- }
-}
-
-// Function to get votes for a specific comment
-async function getCommentVotes(request, env) {
- try {
- const url = new URL(request.url);
- const commentId = url.searchParams.get('commentId');
- const userId = url.searchParams.get('userId');
-
- if (!commentId) {
- return createResponse({ error: 'Missing commentId parameter' }, 400);
- }
-
- // Get vote counts
- const query = `
- SELECT
- (SELECT COUNT(*) FROM comment_votes WHERE comment_id = ? AND vote_type = 'upvote') as upvotes,
- (SELECT COUNT(*) FROM comment_votes WHERE comment_id = ? AND vote_type = 'downvote') as downvotes
- `;
-
- try {
- const voteCounts = await env.DB.prepare(query).bind(commentId, commentId).first();
-
- let userVote = null;
- // If userId is provided, get the user's vote
- if (userId) {
- const userVoteQuery = `
- SELECT vote_type FROM comment_votes
- WHERE comment_id = ? AND user_id = ?
- `;
-
- const userVoteResult = await env.DB.prepare(userVoteQuery).bind(commentId, userId).first();
- if (userVoteResult) {
- userVote = userVoteResult.vote_type;
- }
- }
-
- return createResponse({
- commentId,
- upvotes: voteCounts.upvotes || 0,
- downvotes: voteCounts.downvotes || 0,
- userVote
- });
- } catch (dbError) {
- return createResponse({
- error: 'Failed to retrieve comment votes',
- details: logError(dbError, { action: 'get_comment_votes', commentId })
- }, 500);
- }
- } catch (error) {
- return createResponse({
- error: 'Failed to process comment votes request',
- details: logError(error, { action: 'get_comment_votes_outer' })
- }, 500);
- }
-}
-
-
-
-// Serve meme files
-async function serveMeme(request, env) {
- const url = new URL(request.url);
- const path = url.pathname;
- const key = path.replace('/memes/', '');
-
- // Handle CORS preflight requests
- if (request.method === 'OPTIONS') {
- return handleOptions();
- }
-
- try {
- const object = await env.MEMES_BUCKET.get(key);
-
- if (object === null) {
- return new Response('Meme not found', { status: 404 });
- }
-
- const headers = new Headers();
- object.writeHttpMetadata(headers);
- headers.set('etag', object.httpEtag);
- headers.set('Cache-Control', 'public, max-age=31536000'); // Cache for a year
-
- // Add CORS headers
- Object.keys(corsHeaders).forEach(key => {
- headers.set(key, corsHeaders[key]);
- });
-
- return new Response(object.body, {
- headers
- });
- } catch (error) {
- console.error('Error serving meme:', error);
- return new Response('Internal Server Error', { status: 500 });
- }
-}
-
-// Debug endpoint to help diagnose meta tag issues
-async function debugMetaTags(request, env) {
- try {
- // Create a sample proposal for testing
- const testProposal = {
- id: 'test_proposal',
- text: 'This is a test proposal for meta tags',
- author_name: 'Test User',
- timestamp: Date.now()
- };
-
- // Generate a shareable image URL
- const shareImageUrl = getShareableImageUrl(testProposal);
-
- // Prepare detailed meta tag information
- const metaTagInfo = {
- og_image: shareImageUrl,
- og_title: 'RADICAL Meta Tag Test',
- og_description: 'Testing meta tag generation and injection',
- twitter_image: shareImageUrl,
- twitter_title: 'RADICAL Meta Tag Verification',
- twitter_description: 'Checking social media sharing metadata'
- };
-
- // Return detailed information about the meta tags
- return createResponse({
- status: 'success',
- message: 'Meta tag debug information',
- shareImageUrl: shareImageUrl,
- metaTagDetails: metaTagInfo
- });
- } catch (error) {
- return createResponse({
- error: 'Debug meta tags endpoint error',
- details: logError(error, { action: 'debug_meta_tags' })
- }, 500);
- }
-}
-
-// Handle meme uploads to R2 storage
-async function uploadMeme(request, env) {
- try {
- // Check if R2 binding exists
- if (!env.MEMES_BUCKET) {
- return createResponse({
- error: 'Storage binding missing. Check your settings configuration.'
- }, 500);
- }
-
- // Handle multipart form data
- const formData = await request.formData();
-
- // Get the proposal ID
- const proposalId = formData.get('proposalId');
- if (!proposalId) {
- return createResponse({ error: 'Missing proposalId' }, 400);
- }
-
- // Get the file
- const file = formData.get('meme');
- if (!file || !(file instanceof File)) {
- return createResponse({ error: 'No file uploaded or invalid file' }, 400);
- }
-
- // Validate file type
- const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
- if (!validTypes.includes(file.type)) {
- return createResponse({
- error: 'Invalid file type. Only JPEG, PNG, GIF, and WebP images are allowed.',
- received: file.type
- }, 400);
- }
-
- // Validate file size (limit to 2MB)
- const MAX_SIZE = 2 * 1024 * 1024; // 2MB
- if (file.size > MAX_SIZE) {
- return createResponse({
- error: 'File too large. Maximum size is 2MB.',
- size: file.size,
- maxSize: MAX_SIZE
- }, 400);
- }
-
- // Create a unique filename
- const fileExt = file.name.split('.').pop();
- const uniqueFilename = `${proposalId}_${Date.now()}.${fileExt}`;
-
- // Upload to R2
- await env.MEMES_BUCKET.put(uniqueFilename, file.stream(), {
- httpMetadata: {
- contentType: file.type,
- },
- });
-
- // Generate the public URL
- const memeUrl = `https://radical.omar-c29.workers.dev/memes/${uniqueFilename}`;
-
- // Helper function to fix broken meme URLs
- function fixMemeUrl(url) {
- // Check if the URL contains the problematic domain
- if (url && url.includes('memes.undefined')) {
- // Extract the filename from the URL
- const filename = url.split('/').pop();
- // Replace with the correct domain
- return `https://radical.omar-c29.workers.dev/memes/${filename}`;
- }
- return url;
- }
-
- // Update the proposal with the meme URL
- try {
- await env.DB.prepare(`
- UPDATE proposals
- SET meme_url = ?
- WHERE id = ?
- `).bind(memeUrl, proposalId).run();
-
- console.log(`Added meme ${uniqueFilename} to proposal ${proposalId}`);
-
- return createResponse({
- success: true,
- proposalId,
- memeUrl
- });
- } catch (dbError) {
- // If DB update fails, try to delete the uploaded file
- try {
- await env.MEMES_BUCKET.delete(uniqueFilename);
- } catch (deleteError) {
- console.error('Failed to delete orphaned file:', deleteError);
- }
-
- return createResponse({
- error: 'Failed to update proposal with meme URL',
- details: logError(dbError, { action: 'update_proposal_meme', proposalId })
- }, 500);
- }
- } catch (error) {
- return createResponse({
- error: 'Failed to process meme upload',
- details: logError(error, { action: 'upload_meme' })
- }, 500);
- }
-}// Get petition statistics
-async function getPetitionStats(request, env) {
- try {
- const url = new URL(request.url);
- const proposalId = url.searchParams.get('proposalId');
-
- if (!proposalId) {
- return createResponse({ error: 'Missing proposalId parameter' }, 400);
- }
-
- // Get petition statistics
- const statsQuery = `
- SELECT
- COUNT(DISTINCT pd.user_id) as verified_count,
- (SELECT COUNT(*) FROM votes WHERE proposal_id = ? AND vote_type = 'upvote' AND is_petition = 1) as total_signatures,
- (SELECT text FROM proposals WHERE id = ?) as proposal_text
- FROM petition_details pd
- WHERE pd.proposal_id = ? AND pd.verified = 1
- `;
-
- const stats = await env.DB.prepare(statsQuery).bind(proposalId, proposalId, proposalId).first();
-
- if (!stats) {
- return createResponse({ error: 'Proposal not found' }, 404);
- }
-
- // Get top postcodes for this petition
- const postcodesQuery = `
- SELECT postcode, COUNT(*) as count
- FROM petition_details
- WHERE proposal_id = ? AND verified = 1
- GROUP BY postcode
- ORDER BY count DESC
- LIMIT 5
- `;
-
- const postcodes = await env.DB.prepare(postcodesQuery).bind(proposalId).all();
-
- return createResponse({
- proposalId,
- proposalText: stats.proposal_text,
- verifiedSignatures: stats.verified_count,
- totalSignatures: stats.total_signatures,
- topPostcodes: postcodes.results
- });
- } catch (error) {
- return createResponse({
- error: 'Failed to process petition stats request',
- details: logError(error, { action: 'petition_stats', proposalId })
- }, 500);
- }
-}// Define CORS headers
-const corsHeaders = {
- 'Access-Control-Allow-Origin': '*',
- 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
- 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
-};
-
-// Helper to log detailed errors
-function logError(error, context = {}) {
- const errorDetails = {
- message: error.message,
- stack: error.stack,
- name: error.name,
- context: context
- };
-
- console.error(JSON.stringify(errorDetails));
- return errorDetails;
-}
-
-// Handle OPTIONS request for CORS preflight
-function handleOptions() {
- return new Response(null, {
- headers: corsHeaders
- });
-}
-
-// Helper to create standardized responses
-function createResponse(body, status = 200, cacheDuration = 60*60*24) {
- const headers = {
- 'Content-Type': 'application/json',
- ...corsHeaders
- };
-
- if (cacheDuration > 0) {
- headers['Cache-Control'] = `public, max-age=${cacheDuration}`;
- headers['CDN-Cache-Control'] = `public, max-age=${cacheDuration}`;
- } else {
- headers['Cache-Control'] = 'no-store';
- }
-
- return new Response(JSON.stringify(body), {
- status,
- headers
- });
-}
-
-
-async function verifyTemplateImage(env) {
- try {
- const key = "petition-template.png";
- const object = await env.MEMES_BUCKET.get(key);
-
- if (object === null) {
- console.error("❌ Template image not found in R2 bucket!");
- return false;
- }
-
- console.log("✅ Template image found in R2 bucket");
- return true;
- } catch (error) {
- console.error("Error verifying template image:", error);
- return false;
- }
-}
-
-
-/**
- * Handle requests for dynamic SVG images for petitions
- * @param {Request} request - The incoming request
- * @param {Object} env - Environment bindings
- * @returns {Response} - SVG response
- */
-async function servePetitionSVG(request, env) {
- try {
- const url = new URL(request.url);
- const proposalId = url.searchParams.get('id');
-
- if (!proposalId) {
- return new Response('Missing proposal ID', { status: 400 });
- }
-
- console.log(`Generating SVG image for proposal: ${proposalId}`);
-
- // Fetch the proposal data from the database
- const proposal = await env.DB.prepare(`
- SELECT
- p.id, p.text, p.timestamp,
- u.name as author_name,
- (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote') as upvotes,
- (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'downvote') as downvotes
- FROM proposals p
- JOIN users u ON p.author_id = u.id
- WHERE p.id = ?
- `).bind(proposalId).first();
-
- if (!proposal) {
- return new Response('Proposal not found', { status: 404 });
- }
-
- // Generate the SVG
- const svgMarkup = generatePetitionSVG(proposal);
-
- // Return the SVG with appropriate headers
- return new Response(svgMarkup, {
- headers: {
- 'Content-Type': 'image/svg+xml',
- 'Cache-Control': 'public, max-age=360000', // Cache for 1 hour
- 'Access-Control-Allow-Origin': '*'
- }
- });
- } catch (error) {
- console.error('Error generating petition SVG:', error);
- return new Response('Internal Server Error', { status: 500 });
- }
-}
-
-/**
- * Generates a dynamic SVG for a petition with the given data
- * @param {Object} proposal - The petition/proposal data
- * @returns {string} - SVG markup as a string
- */
-function generatePetitionSVG(proposal) {
- // Get data with fallbacks
- const petitionText = proposal.text || "Unknown petition";
- const upvotes = proposal.upvotes || 0;
- const downvotes = proposal.downvotes || 0;
- const netVotes = upvotes - downvotes;
- const netVotesDisplay = netVotes > 0 ? `+${netVotes}` : netVotes;
- const netVotesColor = netVotes > 0 ? "#11cc77" : (netVotes < 0 ? "#ff0099" : "#ffffff");
- const proposalId = proposal.id || "unknown";
-
- // Format timestamp
- const timestamp = new Date(proposal.timestamp || Date.now()).toLocaleDateString('en-US', {
- year: 'numeric',
- month: 'short',
- day: 'numeric'
- });
-
- // Base SVG template
- const svgTemplate = ``;
-
- return svgTemplate;
-}
-
-/**
- * Generates SVG text elements with proper line wrapping
- * @param {string} text - The text to wrap
- * @param {number} x - Starting x position
- * @param {number} y - Starting y position
- * @param {number} width - Maximum width for text wrapping
- * @returns {string} - SVG text elements
- */
-function generateSVGTextLines(text, x, y, width) {
- // Simple word wrapping algorithm
- const words = text.split(' ');
- const lines = [];
- let currentLine = '';
-
- // Estimate characters per line based on width and average character width
- const avgCharWidth = 22; // Rough estimate for font size ~36px
- const charsPerLine = Math.floor(width / avgCharWidth);
-
- for (const word of words) {
- const testLine = currentLine ? `${currentLine} ${word}` : word;
- if (testLine.length <= charsPerLine) {
- currentLine = testLine;
- } else {
- lines.push(currentLine);
- currentLine = word;
- }
- }
-
- if (currentLine) {
- lines.push(currentLine);
- }
-
- // Limit to 6 lines to fit in the container
- const limitedLines = lines.slice(0, 6);
- if (lines.length > 6) {
- // Add ellipsis to the last line if truncated
- limitedLines[5] = limitedLines[5].slice(0, -3) + '...';
- }
-
- // Generate SVG text elements
- const lineHeight = 46;
- let svgTextElements = '';
-
- limitedLines.forEach((line, index) => {
- const lineY = y + index * lineHeight;
- svgTextElements += `${escapeHTML(line)}\n`;
- });
-
- return svgTextElements;
-}
-
-/**
- * Escapes HTML special characters in a string
- * @param {string} text - The text to escape
- * @returns {string} - Escaped text
- */
-function escapeHTML(text) {
- return text
- .replace(/&/g, '&')
- .replace(//g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, ''');
-}
-
-// Update the getShareableImageUrl function to use the SVG endpoint
-function getShareableImageUrl(proposal) {
- // Use the dynamic SVG endpoint with the proposal ID
- return `https://${new URL(request.url).hostname}/api/petition-svg?id=${proposal.id}`;
-}
-
-// Update the serveCustomizedHtml function to use the SVG URL for meta tags
-async function serveCustomizedHtml(proposalId, request, env) {
- console.log(`===== BEGIN serveCustomizedHtml for proposal ${proposalId} =====`);
-
- try {
- const url = new URL(request.url);
- const isTestMeta = url.searchParams.get('test_meta') === 'true';
-
- // Determine which proposal to use
- let proposal;
- let shareImageUrl;
-
- if (isTestMeta) {
- // Use test proposal data
- proposal = {
- id: 'test_proposal',
- text: 'This is a test proposal for meta tags',
- timestamp: Date.now(),
- author_name: 'Test User'
- };
- shareImageUrl = `https://${url.hostname}/api/petition-svg?id=test_proposal`;
- } else {
- // Fetch the actual proposal from the database
- proposal = await env.DB.prepare(`
- SELECT
- p.id, p.text, p.timestamp,
- u.name as author_name
- FROM proposals p
- JOIN users u ON p.author_id = u.id
- WHERE p.id = ?
- `).bind(proposalId).first();
-
- if (!proposal) {
- console.error(`Proposal not found: ${proposalId}`);
- return await fetch(request);
- }
-
- // Generate SVG URL
- shareImageUrl = `https://${url.hostname}/api/petition-svg?id=${proposalId}`;
- }
-
- console.log(`Found proposal: ${JSON.stringify(proposal)}`);
- console.log(`Using share image URL: ${shareImageUrl}`);
-
- // Get the full URL for this proposal
- const fullProposalUrl = `https://${url.hostname}/?proposal=${proposalId}`;
-
- // Fetch the original HTML
- const response = await fetch(request);
- const html = await response.text();
-
- // First, remove all existing og:image and twitter:image tags
- let modifiedHtml = html.replace(/]*>/g, '');
- modifiedHtml = modifiedHtml.replace(/]*>/g, '');
- modifiedHtml = modifiedHtml.replace(/]*>/g, '');
-
- // Find the head end tag
- const headEndIndex = modifiedHtml.indexOf('');
- if (headEndIndex === -1) {
- console.error("Could not find tag in HTML");
- return new Response(modifiedHtml, {
- headers: response.headers
- });
- }
-
- // Create comprehensive meta tags
- const customMetaTags = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- // Insert our meta tags right before
- let customizedHtml = modifiedHtml.substring(0, headEndIndex) + customMetaTags + modifiedHtml.substring(headEndIndex);
-
- console.log(`===== END serveCustomizedHtml for proposal ${proposalId} =====`);
-
- // Return the customized HTML with appropriate headers
- const originalHeaders = new Headers(response.headers);
- originalHeaders.set('Cache-Control', 'no-store, max-age=0');
- originalHeaders.set('X-Custom-Meta', isTestMeta ? 'test' : 'proposal');
- originalHeaders.set('X-Share-Image-Url', shareImageUrl); // Debug header
-
- return new Response(customizedHtml, {
- headers: originalHeaders
- });
- } catch (error) {
- console.error('Error generating customized HTML:', error);
- // Fall back to regular page if something goes wrong
- return await fetch(request);
- }
-}
-
-
-
-
-// Function to check if a table exists
-async function tableExists(env, tableName) {
- try {
- const query = "SELECT name FROM sqlite_master WHERE type='table' AND name = ?";
- const result = await env.DB.prepare(query).bind(tableName).first();
- return !!result;
- } catch (error) {
- logError(error, { action: 'check_table_exists', tableName });
- return false;
- }
-}
-
-// Simple health check endpoint
-async function healthCheck(env) {
- try {
- // Check if DB binding exists
- if (!env.DB) {
- return createResponse({
- status: 'error',
- error: 'Database binding missing. Check wrangler.toml configuration.'
- }, 500);
- }
-
- // Check if we can execute a simple query
- try {
- const result = await env.DB.prepare("SELECT 1 as db_check").first();
- if (!result || result.db_check !== 1) {
- return createResponse({
- status: 'error',
- error: 'Database query failed to return expected result'
- }, 500);
- }
- } catch (dbError) {
- return createResponse({
- status: 'error',
- error: 'Database query execution failed',
- details: logError(dbError, { action: 'health_check_query' })
- }, 500);
- }
-
- // Check if required tables exist
- const tables = ['users', 'proposals', 'votes', 'petition_details'];
- const tableStatus = {};
-
- for (const table of tables) {
- tableStatus[table] = await tableExists(env, table);
- }
-
- const missingTables = Object.keys(tableStatus).filter(table => !tableStatus[table]);
-
- if (missingTables.length > 0) {
- return createResponse({
- status: 'warning',
- message: 'Database connected but some tables are missing',
- missingTables,
- tableStatus
- }, 200);
- }
-
- return createResponse({
- status: 'ok',
- message: 'Database connection and tables verified',
- database: env.DB.name || 'unknown',
- tableStatus
- }, 200);
- } catch (error) {
- return createResponse({
- status: 'error',
- error: 'Health check failed',
- details: logError(error, { action: 'health_check' })
- }, 500);
- }
-}
-
-// Get proposals with optional sorting
-async function getProposals(request, env) {
- try {
- const url = new URL(request.url);
- const sortBy = url.searchParams.get('sortBy') || 'newest';
-
- // Log request details
- console.log(`Getting proposals with sort: ${sortBy}`);
-
- let query = `
- SELECT
- p.id, p.text, p.timestamp, p.trending, p.meme_url,
- u.name as author_name, u.id as author_id,
- (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote') as upvotes,
- (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'downvote') as downvotes,
- (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote' AND is_petition = 1) as petition_signatures,
- (SELECT COUNT(DISTINCT pd.user_id) FROM petition_details pd WHERE pd.proposal_id = p.id AND pd.verified = 1) as verified_petitioners
- FROM proposals p
- JOIN users u ON p.author_id = u.id
- `;
- // Add sorting
- if (sortBy === 'newest') {
- query += ' ORDER BY p.timestamp DESC';
- } else if (sortBy === 'popular') {
- query += `
- ORDER BY (
- (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote') -
- (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'downvote')
- ) DESC
- `;
- } else if (sortBy === 'controversial') {
- query += `
- ORDER BY (
- (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote') +
- (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'downvote')
- ) DESC
- `;
- } else if (sortBy === 'petitions') {
- // New sorting option for most petitioned
- query += `
- ORDER BY (
- (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote' AND is_petition = 1)
- ) DESC
- `;
- }
-
- console.log(`Executing query: ${query}`);
-
- // Get user ID for checking existing votes
- const userId = url.searchParams.get('userId');
- let proposals;
-
- try {
- proposals = await env.DB.prepare(query).all();
- console.log(`Retrieved ${proposals.results.length} proposals`);
- } catch (dbError) {
- return createResponse({
- error: 'Failed to retrieve proposals',
- details: logError(dbError, { action: 'get_proposals', query })
- }, 500);
- }
-
- // If we have a userId, get their votes for these proposals
- if (userId && proposals.results.length > 0) {
- const proposalIds = proposals.results.map(p => p.id);
-
- try {
- // Use parameter binding with placeholders
- let placeholderStr = '';
- for (let i = 0; i < proposalIds.length; i++) {
- placeholderStr += (i === 0 ? '?' : ',?');
- }
-
- const votesQuery = `
- SELECT proposal_id, vote_type, is_petition
- FROM votes
- WHERE user_id = ? AND proposal_id IN (${placeholderStr})
- `;
-
- console.log(`Executing votes query for user ${userId} with ${proposalIds.length} proposals`);
-
- const bindParams = [userId, ...proposalIds];
- const votes = await env.DB.prepare(votesQuery).bind(...bindParams).all();
-
- console.log(`Retrieved ${votes.results.length} votes for user ${userId}`);
-
- // Create a map of proposal_id to vote info
- const voteMap = {};
- for (const vote of votes.results) {
- voteMap[vote.proposal_id] = {
- voteType: vote.vote_type,
- isPetition: vote.is_petition === 1
- };
- }
-
- // Also get petition details status
- const petitionQuery = `
- SELECT proposal_id, verified
- FROM petition_details
- WHERE user_id = ? AND proposal_id IN (${placeholderStr})
- `;
-
- const petitionDetails = await env.DB.prepare(petitionQuery).bind(...bindParams).all();
- const petitionMap = {};
-
- for (const petition of petitionDetails.results) {
- petitionMap[petition.proposal_id] = {
- verified: petition.verified === 1
- };
- }
-
- // Add user's vote and petition status to each proposal
- for (const proposal of proposals.results) {
- const voteInfo = voteMap[proposal.id];
- proposal.userVote = voteInfo ? voteInfo.voteType : null;
- proposal.userPetitionSigned = voteInfo ? voteInfo.isPetition : false;
- proposal.userPetitionVerified = petitionMap[proposal.id] ? petitionMap[proposal.id].verified : false;
- }
- } catch (voteError) {
- // Just log the error but continue - we'll return proposals without vote data
- logError(voteError, {
- action: 'get_user_votes',
- userId,
- proposalCount: proposalIds.length
- });
- console.log('Warning: Failed to retrieve user votes, continuing without vote data');
- }
- }
-
- return createResponse(proposals.results);
- } catch (error) {
- return createResponse({
- error: 'Failed to process proposals request',
- details: logError(error, { action: 'get_proposals_outer' })
- }, 500);
- }
-}
-
-// Get a single proposal by ID
-async function getProposalById(request, env) {
- try {
- const url = new URL(request.url);
- const proposalId = url.pathname.split('/api/proposals/')[1];
- const userId = url.searchParams.get('userId');
-
- if (!proposalId) {
- return createResponse({ error: 'Missing proposal ID' }, 400);
- }
-
- console.log(`Getting proposal with ID: ${proposalId}`);
-
- let query = `
- SELECT
- p.id, p.text, p.timestamp, p.trending, p.meme_url,
- u.name as author_name, u.id as author_id,
- (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote') as upvotes,
- (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'downvote') as downvotes,
- (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote' AND is_petition = 1) as petition_signatures,
- (SELECT COUNT(DISTINCT pd.user_id) FROM petition_details pd WHERE pd.proposal_id = p.id AND pd.verified = 1) as verified_petitioners
- FROM proposals p
- JOIN users u ON p.author_id = u.id
- WHERE p.id = ?
- `;
-
- try {
- const proposal = await env.DB.prepare(query).bind(proposalId).first();
-
- if (!proposal) {
- return createResponse({ error: 'Proposal not found' }, 404);
- }
-
- // If we have a userId, get their votes for this proposal
- if (userId) {
- try {
- const votesQuery = `
- SELECT proposal_id, vote_type, is_petition
- FROM votes
- WHERE user_id = ? AND proposal_id = ?
- `;
-
- const vote = await env.DB.prepare(votesQuery).bind(userId, proposalId).first();
-
- if (vote) {
- proposal.userVote = vote.vote_type;
- proposal.userPetitionSigned = vote.is_petition === 1;
- } else {
- proposal.userVote = null;
- proposal.userPetitionSigned = false;
- }
-
- // Also get petition details status
- const petitionQuery = `
- SELECT proposal_id, verified
- FROM petition_details
- WHERE user_id = ? AND proposal_id = ?
- `;
-
- const petition = await env.DB.prepare(petitionQuery).bind(userId, proposalId).first();
-
- if (petition) {
- proposal.userPetitionVerified = petition.verified === 1;
- } else {
- proposal.userPetitionVerified = false;
- }
- } catch (voteError) {
- // Just log the error but continue - we'll return the proposal without vote data
- logError(voteError, {
- action: 'get_user_votes',
- userId,
- proposalId
- });
- console.log('Warning: Failed to retrieve user votes, continuing without vote data');
- }
- }
-
- return createResponse(proposal);
- } catch (dbError) {
- return createResponse({
- error: 'Failed to retrieve proposal',
- details: logError(dbError, { action: 'get_proposal_by_id', proposalId, query })
- }, 500);
- }
- } catch (error) {
- return createResponse({
- error: 'Failed to process proposal request',
- details: logError(error, { action: 'get_proposal_by_id_outer' })
- }, 500);
- }
-}
-
-async function generateAndStoreShareImage(proposal, env) {
- try {
- // Get base text for the image
- const text = proposal.text || "Unknown petition";
- const authorName = proposal.author_name || "Anonymous";
-
- // Create Cloudinary URL for transformation
- const cloudinaryUrl = "https://res.cloudinary.com/dh8apmjya/image/fetch";
-
- // Encode text for URL parameters
- const encodedText = encodeURIComponent(text.substring(0, 80))
- .replace(/'/g, '%27')
- .replace(/"/g, '%22');
-
- // Create transformation parameters
- const params = `b_black,w_1200,h_630,c_fill/l_text:Arial_32_bold:${encodedText},co_white,w_1000,c_fit,g_center`;
-
- // Template image URL
- const templateUrl = encodeURIComponent("https://radical.omar-c29.workers.dev/memes/petition-template.png");
-
- // Create a unique identifier for this image
- const imageId = `share_${proposal.id}_${Date.now()}`;
-
- // Final Cloudinary URL
- const cloudinaryImageUrl = `${cloudinaryUrl}/${params}/${templateUrl}`;
-
- console.log(`Generating share image: ${cloudinaryImageUrl}`);
-
- // Fetch the image from Cloudinary
- const imageResponse = await fetch(cloudinaryImageUrl);
-
- if (!imageResponse.ok) {
- throw new Error(`Failed to fetch image: ${imageResponse.status}`);
- }
-
- // Get the image as a blob
- const imageBlob = await imageResponse.blob();
-
- // Upload the image to your R2 bucket
- await env.MEMES_BUCKET.put(imageId, imageBlob, {
- httpMetadata: {
- contentType: 'image/png',
- },
- });
-
- // Generate the public URL for the stored image
- const shareImageUrl = `https://radical.omar-c29.workers.dev/memes/${imageId}`;
-
- console.log(`Stored share image at: ${shareImageUrl}`);
-
- // Update the proposal in the database with the share image URL
- await env.DB.prepare(`
- UPDATE proposals
- SET share_image_url = ?
- WHERE id = ?
- `).bind(shareImageUrl, proposal.id).run();
-
- return shareImageUrl;
- } catch (error) {
- console.error('Error generating share image:', error);
- // Return a fallback image URL in case of failure
- return "https://radical.omar-c29.workers.dev/memes/mickeymeta.PNG";
- }
-}
-
-// Create a new proposal
-async function createProposal(request, env) {
- try {
- const data = await request.json();
- const { text, authorId, trending = false } = data;
-
- console.log(`Creating proposal: ${JSON.stringify(data)}`);
-
- if (!text || !authorId) {
- return createResponse({ error: 'Missing required fields', received: { text, authorId } }, 400);
- }
-
- // Check if user exists first
- try {
- const userCheck = await env.DB.prepare(`SELECT id, name FROM users WHERE id = ?`).bind(authorId).first();
- if (!userCheck) {
- return createResponse({
- error: 'Author does not exist',
- authorId,
- suggestion: 'Create the user first before posting a proposal'
- }, 400);
- }
-
- // Create a temporary proposal object with author name for image generation
- const tempProposal = {
- id: 'proposal_' + Date.now(),
- text: text,
- author_name: userCheck.name
- };
-
- const id = tempProposal.id;
- const timestamp = Date.now();
-
- // Insert the proposal first
- const query = `
- INSERT INTO proposals (id, author_id, text, timestamp, trending)
- VALUES (?, ?, ?, ?, ?)
- `;
-
- await env.DB.prepare(query).bind(id, authorId, text, timestamp, trending ? 1 : 0).run();
-
- // Generate and store the share image
- const shareImageUrl = await generateAndStoreShareImage(tempProposal, env);
-
- // Update the proposal with the share image URL
- if (shareImageUrl) {
- await env.DB.prepare(`
- UPDATE proposals
- SET share_image_url = ?
- WHERE id = ?
- `).bind(shareImageUrl, id).run();
- }
-
- console.log(`Successfully created proposal with ID: ${id}`);
-
- return createResponse({
- id,
- authorId,
- text,
- timestamp,
- trending,
- shareImageUrl
- });
- } catch (dbError) {
- return createResponse({
- error: 'Failed to create proposal in database',
- details: logError(dbError, {
- action: 'insert_proposal',
- proposal: { id, authorId, timestamp, trending }
- })
- }, 500);
- }
- } catch (error) {
- return createResponse({
- error: 'Failed to process proposal creation',
- details: logError(error, { action: 'create_proposal_outer' })
- }, 500);
- }
-}
-// Update a proposal (e.g., mark as trending)
-async function updateProposal(id, request, env) {
- try {
- const data = await request.json();
- const { trending } = data;
-
- console.log(`Updating proposal ${id}: ${JSON.stringify(data)}`);
-
- if (trending === undefined) {
- return createResponse({ error: 'No fields to update', received: data }, 400);
- }
-
- // Check if proposal exists
- try {
- const proposalCheck = await env.DB.prepare(`SELECT id FROM proposals WHERE id = ?`).bind(id).first();
- if (!proposalCheck) {
- return createResponse({ error: 'Proposal does not exist', id }, 404);
- }
- } catch (checkError) {
- return createResponse({
- error: 'Error checking proposal existence',
- details: logError(checkError, { action: 'check_proposal', id })
- }, 500);
- }
-
- const query = `
- UPDATE proposals
- SET trending = ?
- WHERE id = ?
- `;
-
- try {
- await env.DB.prepare(query).bind(trending ? 1 : 0, id).run();
- console.log(`Successfully updated proposal ${id} trending status to ${trending}`);
-
- return createResponse({ id, trending });
- } catch (dbError) {
- return createResponse({
- error: 'Failed to update proposal',
- details: logError(dbError, { action: 'update_proposal', id, trending })
- }, 500);
- }
- } catch (error) {
- return createResponse({
- error: 'Failed to process proposal update',
- details: logError(error, { action: 'update_proposal_outer', id })
- }, 500);
- }
-}
-
-
- // to serve audio files from R2
-
- async function serveMedia(request, env) {
- const url = new URL(request.url);
- const path = url.pathname;
-
- // Determine if this is a meme or audio file request
- let key;
- if (path.startsWith('/memes/')) {
- key = path.replace('/memes/', '');
- } else if (path.startsWith('/audio/')) {
- key = path.replace('/audio/', '');
- } else {
- return new Response('Not found', { status: 404 });
- }
-
- // Handle CORS preflight requests
- if (request.method === 'OPTIONS') {
- return handleOptions();
- }
-
- try {
- const object = await env.MEMES_BUCKET.get(key);
-
- if (object === null) {
- return new Response('File not found', { status: 404 });
- }
-
- const headers = new Headers();
- object.writeHttpMetadata(headers);
- headers.set('etag', object.httpEtag);
- headers.set('Cache-Control', 'public, max-age=31536000'); // Cache for a year
-
- // Set the appropriate content-type based on file extension
- if (key.endsWith('.mp3')) {
- headers.set('Content-Type', 'audio/mpeg');
- } else if (key.endsWith('.wav')) {
- headers.set('Content-Type', 'audio/wav');
- } else if (key.endsWith('.ogg')) {
- headers.set('Content-Type', 'audio/ogg');
- }
-
- // Add CORS headers
- Object.keys(corsHeaders).forEach(key => {
- headers.set(key, corsHeaders[key]);
- });
-
- return new Response(object.body, {
- headers
- });
- } catch (error) {
- console.error('Error serving media:', error);
- return new Response('Internal Server Error', { status: 500 });
- }
- }
-
-
-
-// Create or update a vote and handle petition data
-async function createOrUpdateVote(request, env) {
- try {
- const data = await request.json();
- const { proposalId, userId, voteType, isPetition = false, petitionDetails = null } = data;
-
- console.log(`Processing vote/petition: ${JSON.stringify({
- proposalId,
- userId,
- voteType,
- isPetition,
- hasDetails: petitionDetails !== null
- })}`);
-
- if (!proposalId || !userId || !voteType) {
- return createResponse({
- error: 'Missing required fields',
- received: { proposalId, userId, voteType: voteType || 'undefined' }
- }, 400);
- }
-
- if (voteType !== 'upvote' && voteType !== 'downvote') {
- return createResponse({
- error: 'Invalid vote type',
- voteType,
- allowedValues: ['upvote', 'downvote']
- }, 400);
- }
-
- // Check if entities exist
- try {
- const userCheck = await env.DB.prepare(`SELECT id FROM users WHERE id = ?`).bind(userId).first();
- if (!userCheck) {
- return createResponse({ error: 'User does not exist', userId }, 404);
- }
-
- const proposalCheck = await env.DB.prepare(`SELECT id FROM proposals WHERE id = ?`).bind(proposalId).first();
- if (!proposalCheck) {
- return createResponse({ error: 'Proposal does not exist', proposalId }, 404);
- }
- } catch (checkError) {
- return createResponse({
- error: 'Error checking user or proposal existence',
- details: logError(checkError, { action: 'check_entities', userId, proposalId })
- }, 500);
- }
-
- // Check if vote already exists
- let existingVote;
- try {
- const checkQuery = `
- SELECT id, vote_type, is_petition FROM votes
- WHERE proposal_id = ? AND user_id = ?
- `;
-
- existingVote = await env.DB.prepare(checkQuery).bind(proposalId, userId).first();
- console.log(`Existing vote check: ${existingVote ? JSON.stringify(existingVote) : 'None found'}`);
- } catch (voteCheckError) {
- return createResponse({
- error: 'Error checking existing vote',
- details: logError(voteCheckError, { action: 'check_vote', userId, proposalId })
- }, 500);
- }
-
- const timestamp = Date.now();
-
- try {
- // Process vote
- if (existingVote) {
- // If vote exists and it's the same type, remove it (toggle off)
- if (existingVote.vote_type === voteType) {
- await env.DB.prepare(`
- DELETE FROM votes
- WHERE id = ?
- `).bind(existingVote.id).run();
-
- console.log(`Deleted vote ${existingVote.id} (toggle off)`);
-
- return createResponse({ proposalId, userId, voteType: null });
- } else {
- // If vote exists but is different type, update it
- await env.DB.prepare(`
- UPDATE votes
- SET vote_type = ?, timestamp = ?, is_petition = ?
- WHERE id = ?
- `).bind(
- voteType,
- timestamp,
- voteType === 'upvote' && isPetition ? 1 : 0,
- existingVote.id
- ).run();
-
- console.log(`Updated vote ${existingVote.id} to ${voteType}`);
- }
- } else {
- // If no vote exists, create new one
- const result = await env.DB.prepare(`
- INSERT INTO votes (proposal_id, user_id, vote_type, timestamp, is_petition)
- VALUES (?, ?, ?, ?, ?)
- `).bind(
- proposalId,
- userId,
- voteType,
- timestamp,
- voteType === 'upvote' && isPetition ? 1 : 0
- ).run();
-
- console.log(`Created new vote with ID: ${result.meta?.last_row_id || 'unknown'}`);
- }
-
- // Process petition details if provided
- if (voteType === 'upvote' && isPetition && petitionDetails) {
- try {
- // Validate petition details
- const { fullName, address, postcode, dob, email } = petitionDetails;
-
- if (!fullName || !address || !postcode || !dob || !email) {
- return createResponse({ error: 'Missing required petition details' }, 400);
- }
-
- // Check if petition details already exist
- const petitionCheck = await env.DB.prepare(`
- SELECT id FROM petition_details
- WHERE user_id = ? AND proposal_id = ?
- `).bind(userId, proposalId).first();
-
- if (!petitionCheck) {
- // Store petition details
- await env.DB.prepare(`
- INSERT INTO petition_details
- (user_id, proposal_id, full_name, address, postcode, dob, email, timestamp, verified)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
- `).bind(
- userId,
- proposalId,
- fullName,
- address,
- postcode,
- dob,
- email,
- timestamp,
- 1 // Assuming verification was successful
- ).run();
-
- console.log(`Stored petition details for user ${userId} on proposal ${proposalId}`);
- } else {
- // Update existing petition details
- await env.DB.prepare(`
- UPDATE petition_details
- SET full_name = ?, address = ?, postcode = ?, dob = ?, email = ?, timestamp = ?, verified = ?
- WHERE user_id = ? AND proposal_id = ?
- `).bind(
- fullName,
- address,
- postcode,
- dob,
- email,
- timestamp,
- 1, // Assuming verification was successful
- userId,
- proposalId
- ).run();
-
- console.log(`Updated petition details for user ${userId} on proposal ${proposalId}`);
- }
- } catch (petitionError) {
- return createResponse({
- error: 'Failed to process petition details',
- details: logError(petitionError, { action: 'store_petition', userId, proposalId })
- }, 500);
- }
- }
-
- return createResponse({
- proposalId,
- userId,
- voteType,
- isPetition: voteType === 'upvote' && isPetition,
- petitionVerified: voteType === 'upvote' && isPetition
- });
- } catch (dbError) {
- return createResponse({
- error: 'Failed to process vote/petition in database',
- details: logError(dbError, {
- action: 'process_vote_petition',
- existingVote: existingVote || null,
- newVote: { proposalId, userId, voteType, isPetition }
- })
- }, 500);
- }
- } catch (error) {
- return createResponse({
- error: 'Failed to process vote/petition request',
- details: logError(error, { action: 'vote_petition_outer' })
- }, 500);
- }
-}
-
-// 3. Update the getComments function to include vote counts
-async function getComments(request, env) {
- try {
- const url = new URL(request.url);
- const proposalId = url.searchParams.get('proposalId');
- const userId = url.searchParams.get('userId');
-
- if (!proposalId) {
- return createResponse({ error: 'Missing proposalId parameter' }, 400);
- }
-
- const query = `
- SELECT
- c.id, c.user_id, c.comment_text, c.timestamp,
- u.name as user_name,
- (SELECT COUNT(*) FROM comment_votes WHERE comment_id = c.id AND vote_type = 'upvote') as upvotes,
- (SELECT COUNT(*) FROM comment_votes WHERE comment_id = c.id AND vote_type = 'downvote') as downvotes
- FROM comments c
- JOIN users u ON c.user_id = u.id
- WHERE c.proposal_id = ?
- ORDER BY c.timestamp DESC
- `;
-
- try {
- const comments = await env.DB.prepare(query).bind(proposalId).all();
- console.log(`Retrieved ${comments.results.length} comments for proposal ${proposalId}`);
-
- // If userId is provided, get the user's votes for these comments
- if (userId && comments.results.length > 0) {
- const commentIds = comments.results.map(c => c.id);
-
- // Build placeholders for the SQL query
- let placeholders = commentIds.map(() => '?').join(',');
-
- const votesQuery = `
- SELECT comment_id, vote_type
- FROM comment_votes
- WHERE user_id = ? AND comment_id IN (${placeholders})
- `;
-
- // Prepare bind parameters
- const bindParams = [userId, ...commentIds];
-
- const votes = await env.DB.prepare(votesQuery).bind(...bindParams).all();
-
- // Create a map of commentId to vote type
- const voteMap = {};
- votes.results.forEach(vote => {
- voteMap[vote.comment_id] = vote.vote_type;
- });
-
- // Add user's vote to each comment
- comments.results.forEach(comment => {
- comment.userVote = voteMap[comment.id] || null;
- });
- }
-
- return createResponse(comments.results);
- } catch (dbError) {
- return createResponse({
- error: 'Failed to retrieve comments',
- details: logError(dbError, { action: 'get_comments', proposalId })
- }, 500);
- }
- } catch (error) {
- return createResponse({
- error: 'Failed to process comments request',
- details: logError(error, { action: 'get_comments_outer' })
- }, 500);
- }
-}
-
-// Function to create a new comment
-async function createComment(request, env) {
- try {
- const data = await request.json();
- const { proposalId, userId, commentText } = data;
-
- console.log(`Creating comment: ${JSON.stringify(data)}`);
-
- if (!proposalId || !userId || !commentText) {
- return createResponse({
- error: 'Missing required fields',
- received: { proposalId, userId, commentText: commentText ? '[present]' : '[missing]' }
- }, 400);
- }
-
- // Check if proposal exists
- try {
- const proposalCheck = await env.DB.prepare(
- `SELECT id FROM proposals WHERE id = ?`
- ).bind(proposalId).first();
-
- if (!proposalCheck) {
- return createResponse({ error: 'Proposal does not exist', proposalId }, 404);
- }
- } catch (checkError) {
- return createResponse({
- error: 'Error checking proposal existence',
- details: logError(checkError, { action: 'check_proposal', proposalId })
- }, 500);
- }
-
- // Check if user exists
- try {
- const userCheck = await env.DB.prepare(
- `SELECT id FROM users WHERE id = ?`
- ).bind(userId).first();
-
- if (!userCheck) {
- return createResponse({ error: 'User does not exist', userId }, 404);
- }
- } catch (userError) {
- return createResponse({
- error: 'Error checking user existence',
- details: logError(userError, { action: 'check_user', userId })
- }, 500);
- }
-
- const id = 'comment_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
- const timestamp = Date.now();
-
- try {
- await env.DB.prepare(`
- INSERT INTO comments (id, proposal_id, user_id, comment_text, timestamp)
- VALUES (?, ?, ?, ?, ?)
- `).bind(id, proposalId, userId, commentText, timestamp).run();
-
- console.log(`Successfully created comment with ID: ${id}`);
-
- // Get user name for response
- const userQuery = await env.DB.prepare(
- `SELECT name FROM users WHERE id = ?`
- ).bind(userId).first();
-
- return createResponse({
- id,
- proposalId,
- userId,
- userName: userQuery.name,
- commentText,
- timestamp
- });
- } catch (dbError) {
- return createResponse({
- error: 'Failed to create comment in database',
- details: logError(dbError, {
- action: 'insert_comment',
- comment: { id, proposalId, userId, timestamp }
- })
- }, 500);
- }
- } catch (error) {
- return createResponse({
- error: 'Failed to process comment creation',
- details: logError(error, { action: 'create_comment_outer' })
- }, 500);
- }
-}
-
-// Create or get a user
-async function createOrGetUser(request, env) {
- try {
- const data = await request.json();
- const { id, name } = data;
-
- console.log(`Processing user request: ${JSON.stringify(data)}`);
-
- if (!id || !name) {
- return createResponse({
- error: 'Missing required fields',
- received: { id: id || 'undefined', name: name || 'undefined' }
- }, 400);
- }
-
- // Check if user exists
- let existingUser;
- try {
- const checkQuery = `
- SELECT id, name FROM users
- WHERE id = ?
- `;
-
- existingUser = await env.DB.prepare(checkQuery).bind(id).first();
- console.log(`Existing user check: ${existingUser ? JSON.stringify(existingUser) : 'None found'}`);
- } catch (userCheckError) {
- return createResponse({
- error: 'Error checking user existence',
- details: logError(userCheckError, { action: 'check_user', id })
- }, 500);
- }
-
- if (!existingUser) {
- try {
- // Create new user if doesn't exist
- const timestamp = Date.now();
- const insertQuery = `
- INSERT INTO users (id, name, created_at)
- VALUES (?, ?, ?)
- `;
- await env.DB.prepare(insertQuery).bind(id, name, timestamp).run();
- console.log(`Created new user with ID: ${id}`);
- } catch (insertError) {
- return createResponse({
- error: 'Failed to create user',
- details: logError(insertError, { action: 'insert_user', user: { id, name } })
- }, 500);
- }
- } else {
- console.log(`Found existing user: ${existingUser.id}`);
- }
-
- return createResponse({ id, name });
- } catch (error) {
- return createResponse({
- error: 'Failed to process user request',
- details: logError(error, { action: 'user_outer' })
- }, 500);
- }
-
-}
\ No newline at end of file
diff --git a/index.html b/index.html
index e9bf7d6..4540872 100644
--- a/index.html
+++ b/index.html
@@ -596,7 +596,7 @@ async function loadUserData() {
}, 3000);
}
- // Improved fetch implementation
+ // Improved safeFetch implementation with Cloudflare error handling
async function safeFetch(url, options = {}) {
try {
// Ensure URL is a string
@@ -607,7 +607,6 @@ async function safeFetch(url, options = {}) {
method: 'GET',
headers: {
'Content-Type': 'application/json',
- // Add any additional default headers
},
mode: 'cors',
credentials: 'same-origin'
@@ -642,14 +641,26 @@ async function safeFetch(url, options = {}) {
// Check for successful response
if (!response.ok) {
- const errorBody = await response.text();
- console.error('Fetch Error Response:', {
- status: response.status,
- statusText: response.statusText,
- body: errorBody
- });
+ const errorText = await response.text();
- throw new Error(`HTTP error! status: ${response.status}, message: ${errorBody}`);
+ // Check if this is a Cloudflare error
+ if (errorText.includes('Cloudflare') || errorText.includes('cf-error')) {
+ console.error('Cloudflare Error:', {
+ status: response.status,
+ statusText: response.statusText,
+ body: errorText
+ });
+
+ // Return a special error object for Cloudflare errors
+ return {
+ error: true,
+ cloudflareError: true,
+ status: response.status,
+ message: 'The server is temporarily unavailable. Please try again later.'
+ };
+ }
+
+ throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
}
return await response.json();
@@ -671,7 +682,7 @@ async function safeFetch(url, options = {}) {
- // Fetch proposals from API
+ // Updated fetchProposals with fallback mechanism
async function fetchProposals() {
try {
// Show loading indicator
@@ -680,14 +691,38 @@ async function safeFetch(url, options = {}) {
const sortSelect = document.getElementById('sort-select');
const sortBy = sortSelect ? sortSelect.value : 'newest';
- const data = await safeFetch(
- `${API_BASE_URL}/proposals?sortBy=${sortBy}&userId=${currentUser.id}`
- );
+ // Log the full request URL and parameters
+ const requestUrl = `${API_BASE_URL}/proposals?sortBy=${sortBy}&userId=${currentUser.id}`;
+ console.log('Fetching proposals from:', requestUrl);
+ console.log('Current user:', currentUser);
+
+ const response = await fetch(requestUrl, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'
+ },
+ mode: 'cors',
+ credentials: 'same-origin'
+ });
+
+ // Log the response details
+ console.log('Response status:', response.status);
+ console.log('Response headers:', Object.fromEntries(response.headers.entries()));
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ console.error('Error response:', errorText);
+ throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
+ }
+
+ const data = await response.json();
+ console.log('Proposals data:', data);
renderProposals(data);
} catch (error) {
console.error('Error fetching proposals:', error);
- showErrorMessage('Oops! Failed to load petitions. Try refreshing the page.');
+ showErrorMessage('Failed to load petitions. Please check the console for details.', true);
}
}
@@ -2566,6 +2601,90 @@ function submitComment(proposalId, commentText) {
});
}
+// Add these new functions for comment management
+const commentRequestQueue = new Set();
+let commentRequestTimeout = null;
+const MAX_BATCH_SIZE = 10;
+const COMMENT_REQUEST_DELAY = 1000; // 1 second delay between batches
+
+async function batchLoadComments() {
+ if (commentRequestQueue.size === 0) return;
+
+ const batch = Array.from(commentRequestQueue).slice(0, MAX_BATCH_SIZE);
+ commentRequestQueue.clear();
+
+ try {
+ const responses = await Promise.all(
+ batch.map(proposalId =>
+ fetch(`${API_BASE_URL}/comments?proposalId=${proposalId}&userId=${currentUser.id}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'
+ },
+ mode: 'cors',
+ credentials: 'same-origin'
+ }).then(response => {
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ return response.json().then(data => ({ proposalId, data }));
+ })
+ )
+ );
+
+ responses.forEach(({ proposalId, data }) => {
+ const commentsContainer = document.querySelector(`.comments-container[data-proposal-id="${proposalId}"]`);
+ if (commentsContainer) {
+ renderComments(commentsContainer.querySelector('.comments-content'), data);
+ }
+ });
+ } catch (error) {
+ console.error('Error loading comments batch:', error);
+ // Retry failed requests
+ batch.forEach(proposalId => commentRequestQueue.add(proposalId));
+ }
+
+ // Schedule next batch if there are more requests
+ if (commentRequestQueue.size > 0) {
+ commentRequestTimeout = setTimeout(batchLoadComments, COMMENT_REQUEST_DELAY);
+ }
+}
+
+function queueCommentLoad(proposalId) {
+ commentRequestQueue.add(proposalId);
+
+ if (!commentRequestTimeout) {
+ commentRequestTimeout = setTimeout(batchLoadComments, COMMENT_REQUEST_DELAY);
+ }
+}
+
+// Update the loadComments function to use the queue
+function loadComments(proposalId) {
+ const commentsContainer = document.querySelector(`.comments-container[data-proposal-id="${proposalId}"]`);
+ const commentsContent = commentsContainer?.querySelector('.comments-content');
+
+ if (!commentsContent) return;
+
+ // Show loading state
+ commentsContent.innerHTML = `
Loading comments...
`;
+
+ // Queue the comment load
+ queueCommentLoad(proposalId);
+}
+
+// Update the loadModalComments function to use the queue
+async function loadModalComments(proposalId) {
+ const commentsContent = document.getElementById(`modal-comments-content-${proposalId}`);
+ if (!commentsContent) return;
+
+ // Show loading state
+ commentsContent.innerHTML = `