From 3c5fe0e8b84a07aa5c7722725b446e55a4887bd7 Mon Sep 17 00:00:00 2001 From: MrPiglr <31398225+MrPiglr@users.noreply.github.com> Date: Wed, 3 Dec 2025 03:26:57 +0000 Subject: [PATCH 1/2] moved feed --- .replit | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.replit b/.replit index 6196281d..07348546 100644 --- a/.replit +++ b/.replit @@ -60,10 +60,6 @@ externalPort = 3000 localPort = 40437 externalPort = 3001 -[[ports]] -localPort = 45189 -externalPort = 3002 - [deployment] deploymentTarget = "autoscale" run = ["node", "dist/server/production.mjs"] From be7ed554cd87906770db98b1e62fc30e93d22e1a Mon Sep 17 00:00:00 2001 From: sirpiglr <49359077-sirpiglr@users.noreply.replit.com> Date: Wed, 3 Dec 2025 03:36:05 +0000 Subject: [PATCH 2/2] Implement bidirectional Discord and feed channel synchronization Add an HTTP endpoint to the Discord bot to receive posts from the main server and call the Discord API to send these posts as rich embeds to the configured feed channel. Also, update the main server to call this new Discord bot endpoint when a new post is created. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9203795e-937a-4306-b81d-b4d5c78c240e Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 82d93ef8-d6c2-4d69-96c4-6fa5da4ec508 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7c94b7a0-29c7-4f2e-94ef-44b2153872b7/9203795e-937a-4306-b81d-b4d5c78c240e/duiWnI1 Replit-Helium-Checkpoint-Created: true --- .replit | 4 + discord-bot/bot.js | 51 ++++++++++ discord-bot/listeners/feedSync.js | 150 ++++++++++++++++++++++++++++++ replit.md | 9 +- server/index.ts | 41 ++++++++ 5 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 discord-bot/listeners/feedSync.js diff --git a/.replit b/.replit index 07348546..f32b9f73 100644 --- a/.replit +++ b/.replit @@ -60,6 +60,10 @@ externalPort = 3000 localPort = 40437 externalPort = 3001 +[[ports]] +localPort = 43741 +externalPort = 3002 + [deployment] deploymentTarget = "autoscale" run = ["node", "dist/server/production.mjs"] diff --git a/discord-bot/bot.js b/discord-bot/bot.js index e015b6d5..8e2d0cec 100644 --- a/discord-bot/bot.js +++ b/discord-bot/bot.js @@ -12,6 +12,8 @@ const fs = require("fs"); const path = require("path"); require("dotenv").config(); +const { setupFeedListener, sendPostToDiscord, getFeedChannelId } = require("./listeners/feedSync"); + // Validate environment variables const requiredEnvVars = [ "DISCORD_BOT_TOKEN", @@ -290,6 +292,52 @@ http return; } + // POST /send-to-discord - Send a post from AeThex to Discord channel + if (req.url === "/send-to-discord" && req.method === "POST") { + let body = ""; + req.on("data", (chunk) => { + body += chunk.toString(); + }); + req.on("end", async () => { + try { + // Simple auth check + const authHeader = req.headers.authorization; + const expectedToken = process.env.DISCORD_BRIDGE_TOKEN || "aethex-bridge"; + if (authHeader !== `Bearer ${expectedToken}`) { + res.writeHead(401); + res.end(JSON.stringify({ error: "Unauthorized" })); + return; + } + + const post = JSON.parse(body); + console.log("[API] Received post to send to Discord:", post.id); + + const result = await sendPostToDiscord(post, post.author); + res.writeHead(result.success ? 200 : 500); + res.end(JSON.stringify(result)); + } catch (error) { + console.error("[API] Error processing send-to-discord:", error); + res.writeHead(500); + res.end(JSON.stringify({ error: error.message })); + } + }); + return; + } + + // GET /bridge-status - Check if bridge is configured + if (req.url === "/bridge-status") { + const channelId = getFeedChannelId(); + res.writeHead(200); + res.end( + JSON.stringify({ + enabled: !!channelId, + channelId: channelId, + botReady: client.isReady(), + }), + ); + return; + } + if (req.url === "/register-commands") { if (req.method === "GET") { // Show HTML form with button @@ -507,6 +555,9 @@ client.once("ready", () => { client.user.setActivity("/verify to link your AeThex account", { type: "LISTENING", }); + + // Setup bidirectional feed bridge (AeThex → Discord) + setupFeedListener(client); }); // Error handling diff --git a/discord-bot/listeners/feedSync.js b/discord-bot/listeners/feedSync.js new file mode 100644 index 00000000..a6c822f9 --- /dev/null +++ b/discord-bot/listeners/feedSync.js @@ -0,0 +1,150 @@ +const { EmbedBuilder } = require("discord.js"); +const { createClient } = require("@supabase/supabase-js"); + +const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_SERVICE_ROLE, +); + +const FEED_CHANNEL_ID = process.env.DISCORD_MAIN_CHAT_CHANNELS + ? process.env.DISCORD_MAIN_CHAT_CHANNELS.split(",")[0].trim() + : null; + +let discordClient = null; + +function getArmColor(arm) { + const colors = { + labs: 0x00d4ff, + gameforge: 0xff6b00, + corp: 0x9945ff, + foundation: 0x14f195, + devlink: 0xf7931a, + nexus: 0xff00ff, + staff: 0xffd700, + }; + return colors[arm] || 0x5865f2; +} + +function getArmEmoji(arm) { + const emojis = { + labs: "🔬", + gameforge: "🎮", + corp: "🏢", + foundation: "🎓", + devlink: "🔗", + nexus: "🌐", + staff: "⭐", + }; + return emojis[arm] || "📝"; +} + +async function sendPostToDiscord(post, authorInfo = null) { + if (!discordClient || !FEED_CHANNEL_ID) { + console.log("[Feed Bridge] No Discord client or channel configured"); + return { success: false, error: "No Discord client or channel configured" }; + } + + try { + const channel = await discordClient.channels.fetch(FEED_CHANNEL_ID); + if (!channel || !channel.isTextBased()) { + console.error("[Feed Bridge] Could not find text channel:", FEED_CHANNEL_ID); + return { success: false, error: "Could not find text channel" }; + } + + let content = {}; + try { + content = typeof post.content === "string" ? JSON.parse(post.content) : post.content; + } catch { + content = { text: post.content }; + } + + if (content.source === "discord") { + console.log("[Feed Bridge] Skipping Discord-sourced post to prevent loop"); + return { success: true, skipped: true, reason: "Discord-sourced post" }; + } + + let author = authorInfo; + if (!author && post.author_id) { + const { data } = await supabase + .from("user_profiles") + .select("username, full_name, avatar_url") + .eq("id", post.author_id) + .single(); + author = data; + } + + const authorName = author?.full_name || author?.username || "AeThex User"; + const authorAvatar = author?.avatar_url || "https://aethex.dev/logo.png"; + const arm = post.arm_affiliation || "labs"; + + const embed = new EmbedBuilder() + .setColor(getArmColor(arm)) + .setAuthor({ + name: `${getArmEmoji(arm)} ${authorName}`, + iconURL: authorAvatar, + url: `https://aethex.dev/creators/${author?.username || post.author_id}`, + }) + .setDescription(content.text || post.title || "New post") + .setTimestamp(post.created_at ? new Date(post.created_at) : new Date()) + .setFooter({ + text: `Posted from AeThex • ${arm.charAt(0).toUpperCase() + arm.slice(1)}`, + iconURL: "https://aethex.dev/logo.png", + }); + + if (content.mediaUrl) { + if (content.mediaType === "image") { + embed.setImage(content.mediaUrl); + } else if (content.mediaType === "video") { + embed.addFields({ + name: "🎬 Video", + value: `[Watch Video](${content.mediaUrl})`, + }); + } + } + + if (post.tags && post.tags.length > 0) { + const tagString = post.tags + .filter((t) => t !== "discord" && t !== "main-chat") + .map((t) => `#${t}`) + .join(" "); + if (tagString) { + embed.addFields({ name: "Tags", value: tagString, inline: true }); + } + } + + const postUrl = `https://aethex.dev/community/feed?post=${post.id}`; + embed.addFields({ + name: "🔗 View on AeThex", + value: `[Open Post](${postUrl})`, + inline: true, + }); + + await channel.send({ embeds: [embed] }); + console.log(`[Feed Bridge] ✅ Sent post ${post.id} to Discord`); + return { success: true }; + } catch (error) { + console.error("[Feed Bridge] Error sending to Discord:", error); + return { success: false, error: error.message }; + } +} + +function setupFeedListener(client) { + discordClient = client; + + if (!FEED_CHANNEL_ID) { + console.log("[Feed Bridge] No DISCORD_MAIN_CHAT_CHANNELS configured - bridge disabled"); + return; + } + + console.log("[Feed Bridge] ✅ Feed bridge ready (channel: " + FEED_CHANNEL_ID + ")"); +} + +function getDiscordClient() { + return discordClient; +} + +function getFeedChannelId() { + return FEED_CHANNEL_ID; +} + +module.exports = { setupFeedListener, sendPostToDiscord, getDiscordClient, getFeedChannelId }; diff --git a/replit.md b/replit.md index c15522ee..d9f15573 100644 --- a/replit.md +++ b/replit.md @@ -143,11 +143,14 @@ https://supabase.aethex.tech/auth/v1/callback - `https://supabase.aethex.tech/auth/v1/callback` ## Recent Changes (December 3, 2025) -- ✅ **Discord-to-Feed Integration**: Messages from Discord FEED channel sync to AeThex community feed +- ✅ **Bidirectional Discord-Feed Bridge**: Full two-way sync between Discord and AeThex feed + - **Discord → AeThex**: Messages from Discord FEED channel sync to community feed + - **AeThex → Discord**: Posts created in AeThex appear in Discord with rich embeds - Bot listens to configured channel (DISCORD_MAIN_CHAT_CHANNELS env var) - Posts display with purple Discord badge and channel name - - Supports images/videos from Discord messages - - Real-time updates via Supabase subscriptions + - Supports images/videos from both platforms + - Loop prevention: Discord-sourced posts don't re-post back to Discord + - API endpoint: Discord bot exposes `/send-to-discord` for main server to call - ✅ **Moved /feed to /community/feed**: Feed is now a tab within the Community page - Old /feed URL redirects to /community/feed - Added redirect in vercel.json for production diff --git a/server/index.ts b/server/index.ts index 74b595d7..5d682884 100644 --- a/server/index.ts +++ b/server/index.ts @@ -2523,6 +2523,47 @@ export function createServer() { .json({ error: error.message || "Failed to create post" }); } + // Send post to Discord feed channel (fire and forget) + try { + const contentParsed = JSON.parse(String(payload.content).trim()); + // Only sync to Discord if this is NOT a Discord-sourced post + if (contentParsed.source !== "discord") { + // Get author info for the Discord embed + const { data: authorProfile } = await adminSupabase + .from("user_profiles") + .select("username, full_name, avatar_url") + .eq("id", payload.author_id) + .single(); + + const discordBotPort = process.env.DISCORD_BOT_PORT || "8044"; + const discordBridgeToken = process.env.DISCORD_BRIDGE_TOKEN || "aethex-bridge"; + + fetch(`http://localhost:${discordBotPort}/send-to-discord`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${discordBridgeToken}`, + }, + body: JSON.stringify({ + ...data, + author: authorProfile, + }), + }).then(async (resp) => { + if (resp.ok) { + console.log("[Feed Bridge] ✅ Post sent to Discord"); + } else { + const errText = await resp.text(); + console.log("[Feed Bridge] Discord sync response:", resp.status, errText); + } + }).catch((err) => { + console.log("[Feed Bridge] Could not sync to Discord:", err.message); + }); + } + } catch (syncErr: any) { + // Non-blocking - just log it + console.log("[Feed Bridge] Sync error:", syncErr?.message); + } + res.json(data); } catch (e: any) { console.error("[API] /api/posts exception:", e?.message || String(e));