From 748f41b22225efcb63b3650479c7961469b497df Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Thu, 13 Nov 2025 08:54:22 +0000 Subject: [PATCH] Discord Feed Sync - Post to Discord webhook cgen-d3c9a84bab384df6bbaaae65b9f5f7f3 --- api/discord/feed-sync.ts | 134 +++++++++++ api/integrations/fourthwall.ts | 337 ++++++++++++++++++++++++++++ discord-bot/events/messageCreate.js | 173 ++++++++++++++ 3 files changed, 644 insertions(+) create mode 100644 api/discord/feed-sync.ts create mode 100644 api/integrations/fourthwall.ts create mode 100644 discord-bot/events/messageCreate.js diff --git a/api/discord/feed-sync.ts b/api/discord/feed-sync.ts new file mode 100644 index 00000000..6ff46d5f --- /dev/null +++ b/api/discord/feed-sync.ts @@ -0,0 +1,134 @@ +export const config = { + runtime: "nodejs", +}; + +const webhookUrl = process.env.DISCORD_FEED_WEBHOOK_URL; + +interface FeedPost { + id: string; + title: string; + content: string; + author_name: string; + author_avatar?: string | null; + arm_affiliation: string; + likes_count: number; + comments_count: number; + created_at: string; +} + +export default async function handler(req: any, res: any) { + // Only accept POST requests + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + try { + // Validate webhook is configured + if (!webhookUrl) { + console.warn( + "[Discord Feed Sync] No webhook URL configured. Skipping Discord post.", + ); + return res.status(200).json({ + success: true, + message: "Discord webhook not configured, post skipped", + }); + } + + const post: FeedPost = req.body; + + // Validate required fields + if ( + !post.id || + !post.title || + !post.content || + !post.author_name || + !post.arm_affiliation + ) { + return res.status(400).json({ + error: + "Missing required fields: id, title, content, author_name, arm_affiliation", + }); + } + + // Truncate content if too long (Discord has limits) + const description = + post.content.length > 1024 + ? post.content.substring(0, 1021) + "..." + : post.content; + + // Build Discord embed + const armColors: Record = { + labs: 0xfbbf24, // yellow + gameforge: 0x22c55e, // green + corp: 0x3b82f6, // blue + foundation: 0xef4444, // red + devlink: 0x06b6d4, // cyan + nexus: 0xa855f7, // purple + staff: 0x6366f1, // indigo + }; + + const embed = { + title: post.title, + description: description, + color: armColors[post.arm_affiliation] || 0x8b5cf6, + author: { + name: post.author_name, + icon_url: post.author_avatar || undefined, + }, + fields: [ + { + name: "Arm", + value: post.arm_affiliation.charAt(0).toUpperCase() + + post.arm_affiliation.slice(1), + inline: true, + }, + { + name: "Engagement", + value: `👍 ${post.likes_count} • 💬 ${post.comments_count}`, + inline: true, + }, + ], + footer: { + text: "AeThex Community Feed", + }, + timestamp: post.created_at, + }; + + // Send to Discord webhook + const response = await fetch(webhookUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: "AeThex Community Feed", + avatar_url: + "https://aethex.dev/logo.png", // Update with your logo URL + embeds: [embed], + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error( + "[Discord Feed Sync] Webhook failed:", + response.status, + errorText, + ); + return res.status(500).json({ + success: false, + error: "Failed to post to Discord", + }); + } + + return res.status(200).json({ + success: true, + message: "Post sent to Discord feed", + }); + } catch (error: any) { + console.error("[Discord Feed Sync] Error:", error); + return res.status(500).json({ + error: error.message || "Internal server error", + }); + } +} diff --git a/api/integrations/fourthwall.ts b/api/integrations/fourthwall.ts new file mode 100644 index 00000000..f69e668b --- /dev/null +++ b/api/integrations/fourthwall.ts @@ -0,0 +1,337 @@ +export const config = { + runtime: "nodejs", +}; + +import { createClient } from "@supabase/supabase-js"; + +const supabaseUrl = process.env.VITE_SUPABASE_URL; +const supabaseServiceRole = process.env.SUPABASE_SERVICE_ROLE; + +if (!supabaseUrl || !supabaseServiceRole) { + throw new Error("Missing Supabase configuration"); +} + +const supabase = createClient(supabaseUrl, supabaseServiceRole); + +const FOURTHWALL_API_EMAIL = process.env.FOURTHWALL_API_EMAIL; +const FOURTHWALL_API_PASSWORD = process.env.FOURTHWALL_API_PASSWORD; +const FOURTHWALL_STOREFRONT_TOKEN = process.env.FOURTHWALL_STOREFRONT_TOKEN; + +const FOURTHWALL_API_BASE = "https://api.fourthwall.com"; + +interface FourthwallAuthResponse { + token: string; + expires_in: number; +} + +interface FourthwallProduct { + id: string; + name: string; + description: string; + price: number; + currency: string; + image_url?: string; + category: string; +} + +// Get Fourthwall auth token +async function getFourthwallToken(): Promise { + try { + const response = await fetch(`${FOURTHWALL_API_BASE}/auth/login`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email: FOURTHWALL_API_EMAIL, + password: FOURTHWALL_API_PASSWORD, + }), + }); + + if (!response.ok) { + throw new Error(`Fourthwall auth failed: ${response.statusText}`); + } + + const data: FourthwallAuthResponse = await response.json(); + return data.token; + } catch (error) { + console.error("[Fourthwall] Auth error:", error); + throw error; + } +} + +export default async function handler(req: any, res: any) { + const action = req.query.action || ""; + + try { + switch (action) { + case "products": + return await handleGetProducts(req, res); + case "sync-products": + return await handleSyncProducts(req, res); + case "store-settings": + return await handleGetStoreSettings(req, res); + case "webhook": + return await handleWebhook(req, res); + default: + return res.status(400).json({ error: "Invalid action" }); + } + } catch (error: any) { + console.error("[Fourthwall API] Error:", error); + return res.status(500).json({ + error: error.message || "Internal server error", + }); + } +} + +// Get products from Fourthwall storefront +async function handleGetProducts(req: any, res: any) { + try { + const token = await getFourthwallToken(); + + const response = await fetch( + `${FOURTHWALL_API_BASE}/storefront/products?storefront_token=${FOURTHWALL_STOREFRONT_TOKEN}`, + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }, + ); + + if (!response.ok) { + throw new Error(`Failed to fetch products: ${response.statusText}`); + } + + const data = await response.json(); + + return res.status(200).json({ + success: true, + products: data.products || [], + }); + } catch (error: any) { + console.error("[Fourthwall] Get products error:", error); + return res.status(500).json({ + error: error.message || "Failed to fetch products", + }); + } +} + +// Sync Fourthwall products to AeThex database +async function handleSyncProducts(req: any, res: any) { + try { + const token = await getFourthwallToken(); + + const response = await fetch( + `${FOURTHWALL_API_BASE}/storefront/products?storefront_token=${FOURTHWALL_STOREFRONT_TOKEN}`, + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }, + ); + + if (!response.ok) { + throw new Error(`Failed to fetch products: ${response.statusText}`); + } + + const data = await response.json(); + const products: FourthwallProduct[] = data.products || []; + + // Sync products to Supabase + const syncResults = []; + + for (const product of products) { + const { error } = await supabase + .from("fourthwall_products") + .upsert( + { + fourthwall_id: product.id, + name: product.name, + description: product.description, + price: product.price, + currency: product.currency, + image_url: product.image_url, + category: product.category, + synced_at: new Date().toISOString(), + }, + { + onConflict: "fourthwall_id", + }, + ); + + syncResults.push({ + product_id: product.id, + product_name: product.name, + success: !error, + error: error?.message, + }); + } + + return res.status(200).json({ + success: true, + message: `Synced ${products.length} products`, + results: syncResults, + }); + } catch (error: any) { + console.error("[Fourthwall] Sync products error:", error); + return res.status(500).json({ + error: error.message || "Failed to sync products", + }); + } +} + +// Get store settings +async function handleGetStoreSettings(req: any, res: any) { + try { + const token = await getFourthwallToken(); + + const response = await fetch(`${FOURTHWALL_API_BASE}/store/settings`, { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch store settings: ${response.statusText}`); + } + + const data = await response.json(); + + return res.status(200).json({ + success: true, + settings: data, + }); + } catch (error: any) { + console.error("[Fourthwall] Get settings error:", error); + return res.status(500).json({ + error: error.message || "Failed to fetch store settings", + }); + } +} + +// Handle Fourthwall webhooks (order events, etc) +async function handleWebhook(req: any, res: any) { + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + try { + const { event_type, data } = req.body; + + if (!event_type) { + return res.status(400).json({ error: "Missing event_type" }); + } + + // Log webhook event + const { error } = await supabase.from("fourthwall_webhook_logs").insert({ + event_type, + payload: data, + received_at: new Date().toISOString(), + }); + + if (error) { + console.error("[Fourthwall] Webhook log error:", error); + } + + // Handle specific events + switch (event_type) { + case "order.created": + await handleOrderCreated(data); + break; + case "order.paid": + await handleOrderPaid(data); + break; + case "product.updated": + await handleProductUpdated(data); + break; + } + + return res.status(200).json({ + success: true, + message: "Webhook processed", + }); + } catch (error: any) { + console.error("[Fourthwall] Webhook error:", error); + return res.status(500).json({ + error: error.message || "Failed to process webhook", + }); + } +} + +// Handle Fourthwall order created event +async function handleOrderCreated(data: any) { + try { + const { order_id, customer_email, items, total_amount } = data; + + // Store order in database for later processing + const { error } = await supabase + .from("fourthwall_orders") + .insert({ + fourthwall_order_id: order_id, + customer_email, + items: items || [], + total_amount, + status: "pending", + created_at: new Date().toISOString(), + }); + + if (error) { + console.error("[Fourthwall] Failed to store order:", error); + } + + console.log(`[Fourthwall] Order created: ${order_id}`); + } catch (error) { + console.error("[Fourthwall] Order creation error:", error); + } +} + +// Handle Fourthwall order paid event +async function handleOrderPaid(data: any) { + try { + const { order_id } = data; + + // Update order status + const { error } = await supabase + .from("fourthwall_orders") + .update({ + status: "paid", + paid_at: new Date().toISOString(), + }) + .eq("fourthwall_order_id", order_id); + + if (error) { + console.error("[Fourthwall] Failed to update order:", error); + } + + console.log(`[Fourthwall] Order paid: ${order_id}`); + } catch (error) { + console.error("[Fourthwall] Order payment error:", error); + } +} + +// Handle Fourthwall product updated event +async function handleProductUpdated(data: any) { + try { + const { product_id, ...updates } = data; + + // Update product in database + const { error } = await supabase + .from("fourthwall_products") + .update({ + ...updates, + synced_at: new Date().toISOString(), + }) + .eq("fourthwall_id", product_id); + + if (error) { + console.error("[Fourthwall] Failed to update product:", error); + } + + console.log(`[Fourthwall] Product updated: ${product_id}`); + } catch (error) { + console.error("[Fourthwall] Product update error:", error); + } +} diff --git a/discord-bot/events/messageCreate.js b/discord-bot/events/messageCreate.js new file mode 100644 index 00000000..1eb0bcad --- /dev/null +++ b/discord-bot/events/messageCreate.js @@ -0,0 +1,173 @@ +const { createClient } = require("@supabase/supabase-js"); + +// Initialize Supabase +const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_SERVICE_ROLE, +); + +const FEED_CHANNEL_ID = process.env.DISCORD_FEED_CHANNEL_ID; +const FEED_GUILD_ID = process.env.DISCORD_FEED_GUILD_ID; + +module.exports = { + name: "messageCreate", + async execute(message, client) { + // Ignore bot messages + if (message.author.bot) return; + + // Only listen to messages in the feed channel + if ( + FEED_CHANNEL_ID && + message.channelId !== FEED_CHANNEL_ID + ) { + return; + } + + // Only listen to the correct guild + if (FEED_GUILD_ID && message.guildId !== FEED_GUILD_ID) { + return; + } + + try { + // Get user's linked AeThex account + const { data: linkedAccount, error } = await supabase + .from("discord_links") + .select("user_id") + .eq("discord_user_id", message.author.id) + .single(); + + if (error || !linkedAccount) { + // Optionally, send a DM asking them to link their account + try { + await message.author.send( + "To have your message posted to AeThex, please link your Discord account! Use `/verify` command.", + ); + } catch (dmError) { + console.warn("[Feed Sync] Could not send DM to user:", dmError); + } + return; + } + + // Get user profile for author info + const { data: userProfile, error: profileError } = await supabase + .from("user_profiles") + .select("id, username, full_name, avatar_url") + .eq("id", linkedAccount.user_id) + .single(); + + if (profileError || !userProfile) { + console.error("[Feed Sync] Could not fetch user profile:", profileError); + return; + } + + // Prepare message content and media + let content = message.content || "Shared a message on Discord"; + + // Handle embeds and attachments + let mediaUrl = null; + let mediaType = "none"; + + // Check for attachments (images, videos) + if (message.attachments.size > 0) { + const attachment = message.attachments.first(); + if (attachment) { + mediaUrl = attachment.url; + + // Detect media type + const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp"]; + const videoExtensions = [".mp4", ".webm", ".mov", ".avi"]; + + const attachmentLower = attachment.name.toLowerCase(); + + if (imageExtensions.some((ext) => attachmentLower.endsWith(ext))) { + mediaType = "image"; + } else if (videoExtensions.some((ext) => + attachmentLower.endsWith(ext), + )) { + mediaType = "video"; + } + } + } + + // Prepare post content JSON + const postContent = JSON.stringify({ + text: content, + mediaUrl: mediaUrl, + mediaType: mediaType, + }); + + // Determine arm affiliation from guild name or default to 'labs' + let armAffiliation = "labs"; + const guild = message.guild; + if (guild) { + const guildNameLower = guild.name.toLowerCase(); + + if (guildNameLower.includes("gameforge")) armAffiliation = "gameforge"; + else if (guildNameLower.includes("corp")) armAffiliation = "corp"; + else if (guildNameLower.includes("foundation")) + armAffiliation = "foundation"; + else if (guildNameLower.includes("devlink")) + armAffiliation = "devlink"; + else if (guildNameLower.includes("nexus")) armAffiliation = "nexus"; + else if (guildNameLower.includes("staff")) armAffiliation = "staff"; + } + + // Create post in AeThex + const { data: createdPost, error: insertError } = await supabase + .from("community_posts") + .insert({ + title: content.substring(0, 100) || "Discord Shared Message", + content: postContent, + arm_affiliation: armAffiliation, + author_id: userProfile.id, + tags: ["discord"], + category: null, + is_published: true, + likes_count: 0, + comments_count: 0, + }) + .select( + `id, title, content, arm_affiliation, author_id, created_at, updated_at, likes_count, comments_count, + user_profiles!community_posts_author_id_fkey (id, username, full_name, avatar_url)`, + ); + + if (insertError) { + console.error("[Feed Sync] Failed to create post:", insertError); + try { + await message.react("❌"); + } catch (reactionError) { + console.warn("[Feed Sync] Could not add reaction:", reactionError); + } + return; + } + + console.log( + `[Feed Sync] ✅ Posted message from ${message.author.tag} to AeThex`, + ); + + // React with success emoji + try { + await message.react("✅"); + } catch (reactionError) { + console.warn("[Feed Sync] Could not add success reaction:", reactionError); + } + + // Send confirmation DM + try { + await message.author.send( + `✅ Your message was posted to AeThex Community Feed! Check it out at https://aethex.dev/feed`, + ); + } catch (dmError) { + console.warn("[Feed Sync] Could not send confirmation DM:", dmError); + } + } catch (error) { + console.error("[Feed Sync] Unexpected error:", error); + + try { + await message.react("⚠️"); + } catch (reactionError) { + console.warn("[Feed Sync] Could not add warning reaction:", reactionError); + } + } + }, +};