Discord Feed Sync - Post to Discord webhook

cgen-d3c9a84bab384df6bbaaae65b9f5f7f3
This commit is contained in:
Builder.io 2025-11-13 08:54:22 +00:00
parent 466ce9ba83
commit 748f41b222
3 changed files with 644 additions and 0 deletions

134
api/discord/feed-sync.ts Normal file
View file

@ -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<string, number> = {
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",
});
}
}

View file

@ -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<string> {
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);
}
}

View file

@ -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);
}
}
},
};