diff --git a/Dockerfile b/Dockerfile index f65bc00..76d4f28 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,7 @@ ENV PORT 3000 ENV HOSTNAME "0.0.0.0" HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/healthz || exit 1 + CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/api/healthz || exit 1 EXPOSE 3000 CMD [ "node", "build" ] diff --git a/README.md b/README.md index 5f79a7a..51ddc5d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Events that thrive anywhere. Like the cactus, great events bloom under any condition when managed with care. Cactoide(ae) helps you streamline RSVPs, simplify coordination, and keep every detail efficient—so your gatherings are resilient, vibrant, and unforgettable.

- + actoide diff --git a/federation.config.js b/federation.config.js new file mode 100644 index 0000000..20faf77 --- /dev/null +++ b/federation.config.js @@ -0,0 +1,16 @@ +const config = { + name: 'Cactoide Genesis', + instances: [ + { + url: 'cactoide.org' + }, + { + url: 'cactoide.dalev.hu' + }, + { + url: 'localhost:5174' + } + ] +}; + +export default config; diff --git a/src/lib/fetchFederatedEvents.ts b/src/lib/fetchFederatedEvents.ts new file mode 100644 index 0000000..bd2c721 --- /dev/null +++ b/src/lib/fetchFederatedEvents.ts @@ -0,0 +1,146 @@ +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { logger } from '$lib/logger'; +import type { Event } from '$lib/types'; + +import config from '../../federation.config.js'; + +console.log(config.instances); + +interface FederationConfig { + name: string; + instances: Array<{ url: string }>; +} + +interface FederationEventsResponse { + events: Array; + count?: number; +} + +/** + * Reads the federation config file + */ +async function readFederationConfig(): Promise { + try { + const configPath = join(process.cwd(), 'federation.config.js'); + + // Use dynamic import to load the config file as a module + // This is safer than eval and works with ES modules + const configModule = await import(configPath + '?t=' + Date.now()); + const config = (configModule.default || configModule.config) as FederationConfig; + + if (config && config.instances && Array.isArray(config.instances)) { + return config; + } + + logger.warn('Invalid federation config structure'); + return null; + } catch (error) { + // If dynamic import fails, try reading as text and parsing + try { + const configPath = join(process.cwd(), 'federation.config.js'); + const configContent = readFileSync(configPath, 'utf-8'); + + // Try to extract JSON-like structure + const configMatch = configContent.match(/instances:\s*\[([\s\S]*?)\]/); + if (configMatch) { + // Simple parsing - extract URLs + const urlMatches = configContent.matchAll(/url:\s*['"]([^'"]+)['"]/g); + const instances = Array.from(urlMatches, (match) => ({ url: match[1] })); + + if (instances.length > 0) { + return { + name: 'Federated Instances', + instances + }; + } + } + } catch (fallbackError) { + logger.error({ error: fallbackError }, 'Error parsing federation.config.js as fallback'); + } + + logger.error({ error }, 'Error reading federation.config.js'); + return null; + } +} + +/** + * Fetches events from a single federated instance + */ +async function fetchEventsFromInstance(instanceUrl: string): Promise { + try { + // Ensure URL has protocol and append /api/events + + const apiUrl = `http://${instanceUrl}/api/events`; + + logger.debug({ apiUrl }, 'Fetching events from federated instance'); + + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + Accept: 'application/json' + }, + signal: AbortSignal.timeout(10000) // 10 second timeout + }); + + if (!response.ok) { + logger.warn({ apiUrl, status: response.status }, 'Failed to fetch events from instance'); + return []; + } + + const data = (await response.json()) as FederationEventsResponse; + + if (!data.events || !Array.isArray(data.events)) { + logger.warn({ apiUrl }, 'Invalid events response structure'); + return []; + } + + // Mark events as federated and add source URL + const federatedEvents: Event[] = data.events.map((event) => ({ + ...event, + federation: true, + federation_url: `http://${instanceUrl}` + })); + + logger.info( + { apiUrl, eventCount: federatedEvents.length }, + 'Successfully fetched federated events' + ); + return federatedEvents; + } catch (error) { + logger.error( + { instanceUrl, error: error instanceof Error ? error.message : 'Unknown error' }, + 'Error fetching events from instance' + ); + return []; + } +} + +/** + * Fetches events from all configured federated instances + */ +export async function fetchAllFederatedEvents(): Promise { + const config = await readFederationConfig(); + + if (!config || !config.instances || config.instances.length === 0) { + logger.debug('No federation config or instances found'); + return []; + } + + logger.info( + { instanceCount: config.instances.length }, + 'Fetching events from federated instances' + ); + + // Fetch from all instances in parallel + const fetchPromises = config.instances.map((instance) => fetchEventsFromInstance(instance.url)); + + const results = await Promise.all(fetchPromises); + + // Flatten all events into a single array + const allFederatedEvents = results.flat(); + + logger.info({ totalEvents: allFederatedEvents.length }, 'Completed fetching federated events'); + + return allFederatedEvents; +} diff --git a/src/lib/types.ts b/src/lib/types.ts index ae1fe33..4dab522 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -17,6 +17,8 @@ export interface Event { user_id: string; created_at: string; updated_at: string; + federation?: boolean; // Optional: true if event is from a federated instance + federation_url?: string; // Optional: URL of the federated instance this event came from } export interface RSVP { diff --git a/src/routes/api/events/+server.ts b/src/routes/api/events/+server.ts new file mode 100644 index 0000000..21296a9 --- /dev/null +++ b/src/routes/api/events/+server.ts @@ -0,0 +1,55 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { database } from '$lib/database/db'; +import { events } from '$lib/database/schema'; +import { desc, eq } from 'drizzle-orm'; +import { logger } from '$lib/logger'; + +import { FEDERATION_INSTANCE } from '$env/static/private'; + +export const GET: RequestHandler = async () => { + try { + if (!FEDERATION_INSTANCE) { + return json({ error: 'Federation API is not enabled on this instance' }, { status: 403 }); + } + + // Fetch all public and invite-only events ordered by creation date (newest first) + const publicEvents = await database + .select() + .from(events) + .where(eq(events.visibility, 'public')) + .orderBy(desc(events.createdAt)); + + // Transform events to include federation_event type + const transformedEvents = publicEvents.map((event) => ({ + id: event.id, + name: event.name, + date: event.date, + time: event.time, + location: event.location, + location_type: event.locationType, + location_url: event.locationUrl, + type: event.type, + federation: true, + attendee_limit: event.attendeeLimit, + visibility: event.visibility, + user_id: event.userId, + created_at: event.createdAt?.toISOString() || '', + updated_at: event.updatedAt?.toISOString() || '' + })); + + return json({ + events: transformedEvents, + count: transformedEvents.length + }); + } catch (error) { + logger.error({ error }, 'Error fetching events from API'); + return json( + { + error: 'Failed to fetch events', + message: error instanceof Error ? error.message : 'Unknown error' + }, + { status: 500 } + ); + } +}; diff --git a/src/routes/healthz/+server.ts b/src/routes/api/healthz/+server.ts similarity index 100% rename from src/routes/healthz/+server.ts rename to src/routes/api/healthz/+server.ts diff --git a/src/routes/discover/+page.server.ts b/src/routes/discover/+page.server.ts index 4d78b2d..08fd02e 100644 --- a/src/routes/discover/+page.server.ts +++ b/src/routes/discover/+page.server.ts @@ -3,10 +3,11 @@ import { desc, inArray } from 'drizzle-orm'; import type { PageServerLoad } from './$types'; import { events } from '$lib/database/schema'; import { logger } from '$lib/logger'; +import { fetchAllFederatedEvents } from '$lib/fetchFederatedEvents'; export const load: PageServerLoad = async () => { try { - // Fetch all non-private events (public and invite-only) ordered by creation date (newest first) + // Fetch all non-private events ordered by creation date (newest first) const publicEvents = await database .select() .from(events) @@ -17,24 +18,36 @@ export const load: PageServerLoad = async () => { const transformedEvents = publicEvents.map((event) => ({ id: event.id, name: event.name, - date: event.date, // Already in 'YYYY-MM-DD' format - time: event.time, // Already in 'HH:MM:SS' format + date: event.date, + time: event.time, location: event.location, location_type: event.locationType, location_url: event.locationUrl, type: event.type, - attendee_limit: event.attendeeLimit, // Note: schema uses camelCase + attendee_limit: event.attendeeLimit, visibility: event.visibility, - user_id: event.userId, // Note: schema uses camelCase + user_id: event.userId, created_at: event.createdAt?.toISOString(), - updated_at: event.updatedAt?.toISOString() + updated_at: event.updatedAt?.toISOString(), + federation: false // Add false for local events })); + // Fetch federated events from federation.config.js + let federatedEvents: typeof transformedEvents = []; + try { + federatedEvents = await fetchAllFederatedEvents(); + } catch (error) { + logger.error({ error }, 'Error fetching federated events, continuing with local events only'); + } + + // Merge local and federated events + const allEvents = [...transformedEvents, ...federatedEvents]; + return { - events: transformedEvents + events: allEvents }; } catch (error) { - logger.error({ error }, 'Error loading public events'); + logger.error({ error }, 'Error loading events'); // Return empty array on error to prevent page crash return { diff --git a/src/routes/discover/+page.svelte b/src/routes/discover/+page.svelte index ed18c45..0a46958 100644 --- a/src/routes/discover/+page.svelte +++ b/src/routes/discover/+page.svelte @@ -267,9 +267,15 @@

{#each filteredEvents as event, i (i)} -
-